Watch
0
0
Fork
You've already forked hyperhive
0

an agent gets one todo when its pr starts conflicting with the target branch

This commit is contained in:
atlas 2026-11-11 04:36:28 +01:00 • committed by mara
commit 11eb7dac9c

View file

@ -12,14 +12,16 @@ the way it did. An agent can get the same news from both.
## What an agent gets
| Flow | Event | The agent's todo |
| -------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:assignment` |
| `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` |
| `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, then queues `closed` on each agent that held one: `<kind> <repo>#<n> was closed: <title> (<url>)`, key `forge:<repo>#<n>:closed` |
| `ci-failed` | a CI run on a PR finishes with a failed job | the PR's author gets `CI failed on PR <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:ci` |
| `ci-cancelled` | a CI run on a PR gets cancelled | the same key rewritten: `CI was cancelled on PR <repo>#<n>: <title> (<url>)` |
| `ci-cleared` | someone pushes to the PR's branch | the router clears the PR author's `:ci` todo, as the push starts a new run; the author's other todos for the PR stay |
| Flow | Event | The agent's todo |
| ------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:assignment` |
| `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` |
| `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, then queues `closed` on each agent that held one: `<kind> <repo>#<n> was closed: <title> (<url>)`, key `forge:<repo>#<n>:closed` |
| `ci-failed` | a CI run on a PR finishes with a failed job | the PR's author gets `CI failed on PR <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:ci` |
| `ci-cancelled` | a CI run on a PR gets cancelled | the same key rewritten: `CI was cancelled on PR <repo>#<n>: <title> (<url>)` |
| `ci-cleared` | someone pushes to the PR's branch | the router clears the PR author's `:ci` todo, as the push starts a new run; the author's other todos for the PR stay |
| `conflict` | the controller finds that a PR no longer merges into its target branch | the PR's author gets `PR <repo>#<n> no longer merges into its target branch: <title> (<url>)`, key `forge:<repo>#<n>:conflict`, once until the PR merges again |
| `conflict-cleared` | the controller finds that a conflicting PR merges again | the router clears the PR author's `:conflict` todo |
The todos are subsystem `router` in the agent's todo list. A todo under the
same key replaces the one before, so an issue gives an agent one assignment
@ -85,8 +87,8 @@ under a `todo` or a `request_reviewer`. The most is 64 nodes, 16
levels deep, and 100 custom flows. A `todo` node sends to the agents an event
field holds (`assignees_added`, `todo_holders`, …) or to one named agent.
The router ships the six flows above, each a source → match on `action` →
todo, and `head-classifier`: source `pull_request` and `pull_request_sync` →
The router ships the eight flows above, each a source → match on `action` →
todo (`conflict` and `conflict-cleared` also match `mergeability_changed`), and `head-classifier`: source `pull_request` and `pull_request_sync` →
match on `action` (`opened`, `reopened`, `synchronized`) and `draft` = `false`, or on
`undrafted` → `request_reviewer` for `review-classifier` ([below](#requesting-a-review)). The code defines them
(`swarm_wire::notify_flows::builtins`), so a build that changes one reaches
@ -100,7 +102,7 @@ switched off, and the custom flows. Only the controller writes it, through
compare-and-set. The controller answers 400 unless every flow passes the tree
check above and reads only what exists: a source event from `issues`,
`issue_assign`, `pull_request`, `pull_request_assign`, `pull_request_sync` or one of the
three `pull_request_review_*` events, `workflow_run_failure` or `workflow_run_cancelled`, and condition and recipient fields from the event's field list. The same goes for the `{{ field }}`
three `pull_request_review_*` events, `workflow_run_failure`, `workflow_run_cancelled` or `pull_request_mergeability`, and condition and recipient fields from the event's field list. The same goes for the `{{ field }}`
variables in a todo's key and text, which are Jinja templates (see [`hive-template`](../../../hive-template/README.md)). A `PUT` must carry the `revision` it read, `0` for no
document, and gets 409 when the stored revision is another. Swarm UI's
[notification router card on `/swarm`](../web-ui/swarm-ui.md#notification-flows) shows the document and
@ -213,6 +215,35 @@ head commit from the last delivery about the PR that wasn't a run. A run whose
delivery names another head began before the latest push, and the router drops
it. With no head kept yet, it can't tell and sends the todo.
**Merge conflicts as a source.** Forgejo sends no delivery when a push to a
branch changes whether the PRs targeting it merge, and the router has no forge
client. swarm-controller makes the event instead: on each `push` delivery on its
instance-wide `push` hook, for a branch push that didn't delete the branch, it
waits 30 seconds for the forge to recompute, then lists the open PRs that
target the branch (one call per page of 50) and publishes one
`pull_request_mergeability` event per PR to `$SWARM.events.forge`, where
`action` is `conflicting` or `mergeable`. The body is a `pull_request`
delivery, so `repo`, `number`, `title`, `url`, `author` and `assignees` read as
for any PR.
Forgejo's `mergeable` flag is false for a conflict, for an error, for a draft
and while the check still runs. The controller leaves drafts out and counts a
PR as conflicting only when two reads 30 seconds apart both say false. A PR
that reads undecided gets read again, up to four reads in all. One check runs
per branch at a time; a push during it makes it run once more when it ends. A
push to a branch with no open PR costs one list call. With open PRs that all
merge it's still one; each PR that reads false or undecided adds a second read
at least, and four is the most.
The event also carries `mergeability_changed`, which the router sets to `true`
only when the event moves the PR into or out of conflict, judged against the
`conflicting` flag it keeps per PR in its snapshot. `conflict` and
`conflict-cleared` match on it, so a PR that stays in conflict across many
pushes to its target branch gets the one todo, and a PR that merges and never
conflicted gets nothing. The flag resets when the PR closes, so the router tells a reopened PR
that still conflicts again. A PR whose author fixed it stays flagged
until the next push to its target branch finds it merging.
**What the router runs.** It reads the key at most every 10 seconds, when an
event arrives, and runs the built-ins you left on, then the custom flows that
are on and pass the tree check. It logs a custom flow that fails the check,
@ -239,7 +270,7 @@ holding such a node (a custom flow with one) as one it can't read and keeps the
flows it had; it also has no `head-classifier`. A router that has `head-classifier`
but predates the `draft` and `undrafted` fields requests the review of a draft PR
as before. A router that has `request_reviewer`, with a
controller that predates it, gets no answer to the request and logs that. A controller that predates this leaves the key absent, and the
controller that predates it, gets no answer to the request and logs that. A router that predates the `conflict` flows ignores the `pull_request_mergeability` events a newer controller sends, and gives no conflict todos; a controller that predates them sends none, so the new flows never run. A controller that predates this leaves the key absent, and the
router runs the built-ins as before. A router that predates this ignores the
document, so the page and the API show a state the router isn't yet running.