Delivering work
A finished run leaves a summary comment on the issue. That says what happened; it does not say where anything is. Artifacts are the other half: the pull request, the branch, the document, the preview link.
What Félagi knows about pull requests
As little as it can, and one host by name.
An issue here can be about code, a document or an analysis, and a run can deliver any of
those. So the core stores a link and a state, and an agent opens the pull request itself
with gh or glab — tools already on the runtime and already authenticated — then
reports the URL back. That is still how a run delivers, and it works with any host.
This page used to say there was no GitHub integration and no plan for one. That was wrong for a reason worth writing down: it protected the model and ignored the people. An agent reported its deliveries because Félagi had asked it to. A person opening the identical pull request reported nothing, because nobody had told them to — and no amount of neutrality closes that gap. A product whose claim is that people and agents are one workforce cannot show one of them working and not the other.
So there is now a GitHub connection: Administration → GitHub. GitHub posts what happened, Félagi finds the issue named in the branch, and the pull request appears on it whoever opened it. A branch cut for an issue starts that issue, which is the earliest honest signal there is that somebody has begun.
See GitHub for the whole of it. Listening is all of it unless you ask for
more. Reporting back is a separate step
and optional: install a GitHub App with two permissions and Félagi adds a check to each
pull request naming its issue, and keeps one comment current. The check is deliberately
neutral — Félagi is reporting, not judging, and a red cross would block a merge over a
link.
The generic route is unchanged and unprivileged — hooks/deliveries still takes a URL
and a state from any forge at all.
How an agent reports a delivery
Every run gets an environment variable:
FELAGI_DELIVERY=/…/workdir/.felagi/delivery.json
The agent writes JSON there. The daemon reads it after the run and sends it with the completion.
{
"artifacts": [
{
"type": "pull_request",
"url": "https://github.com/acme/api/pull/482",
"title": "Add a rate limiter to the daemon routes",
"state": "open"
},
{ "type": "branch", "reference": "felagi/ACM-231-rate-limiter" }
]
}
A bare array works too. An agent following a written instruction gets the wrapper wrong about half the time, and refusing a delivery over that would lose the one thing the run was for.
The types
| Type | Needs | Notes |
|---|---|---|
pull_request |
url |
The only type that moves the issue — see below |
branch |
reference |
A name, not an address. Push it, or it is not a delivery |
commit |
reference |
A revision |
document |
url |
Something written |
url |
url |
A preview, a dashboard, a report |
state is optional on any of them: draft, open, merged, closed.
Why a file and not the CLI's output
Every provider formats its output differently. A delivery has to survive Claude Code, Elyra and whatever comes next without an adapter change for each one, and a file is the only thing all of them can write.
It is cleared before every run, so a resumed task cannot report yesterday's link
as today's. It lives outside any checkout, so it never appears in git status
and never ends up in somebody's commit. And custom_env cannot redirect it: a
task that could set that path could report a link to anywhere as this issue's
outcome.
The skill
None of the above happens on its own. The agent has to be told, and that is what a skill is for. Create one in Administration → Skills, and attach it to any agent that works on code.
---
name: Deliver as a pull request
description: Commit, push and open a pull request when the work is done.
---
# Deliver as a pull request
When you have finished the work on an issue, do not stop at describing it.
Hand it over the way a colleague would.
## Branch
Work on a branch named after the issue, never on the default branch:
```
felagi/<ISSUE-IDENTIFIER>-<short-description>
```
For example: `felagi/ACM-231-rate-limiter`.
## Commit
Write a message that says what changed and why. Reference the issue
identifier in the body, not the subject.
## Push and open the pull request
```bash
git push -u origin HEAD
gh pr create --fill --body "Resolves ACM-231"
```
Use `glab mr create` if the remote is GitLab. If neither tool is
authenticated, push the branch anyway and report it as a branch artifact —
a pushed branch somebody can open themselves is still a delivery. A branch
that exists only on the runtime is not.
## Report it
Write the file named by the `FELAGI_DELIVERY` environment variable:
```json
{
"artifacts": [
{
"type": "pull_request",
"url": "<the URL gh printed>",
"title": "<the pull request title>",
"state": "open"
}
]
}
```
Do this last, after the pull request exists. Reporting a URL for something
that has not been created yet is worse than reporting nothing.
It needs gh on the runtime, authenticated
Same rule as git credentials: Félagi holds no token for anybody's forge. Before
attaching this skill, check that a person can run gh auth status on that
machine as the user the daemon runs as.
What happens on the issue
The artifacts appear as chips above the run output, beside the agent's summary comment — a link where a colleague would have put one.
A pull request moves the issue to In review. This is the one place an agent changes an issue's status, and it is narrow on purpose:
- Only for a
pull_request. A summary is not a delivery, and a branch or a link is not something a person has been asked to review. - Only from a status that comes before review. An agent must not reopen work somebody has already closed.
- It is written to the timeline, attributed to the agent, so an issue that moved on its own says who moved it.
That gives the human-in-the-loop gate for free: the agent delivers, a person reviews and merges.
Closing the loop
Nothing polls anybody's API, so Félagi does not find out on its own that a pull request was merged. A forge can tell it:
POST /hooks/deliveries/<FELAGI_DELIVERY_SECRET>
{ "url": "https://github.com/acme/api/pull/482", "state": "merged" }
| State | What happens |
|---|---|
merged |
The issue moves from In review to Done |
closed |
The issue moves back to In progress — the work came back |
| anything else | The artifact's state is updated, the issue is not touched |
Set FELAGI_DELIVERY_SECRET to switch it on. Until then every request is a
404 — and a wrong token gets the same 404 as a missing one, because a 401 tells
somebody they have found the right URL.
Three things it deliberately does not do:
- It only closes a handover. An issue that is in progress, on hold, or already finished by a person is left alone. A stale webhook must not overwrite somebody's work.
- It does not credit the agent. A person merged that pull request. An audit
log that names the wrong actor is worse than a quiet one, so the entry has no
actor and says
via: delivery— the timeline reads "The delivery status changed to done". - It knows nothing about GitHub. A URL and a state, that is all. Any forge, any CI
server, or a person with
curlcan post it. The GitHub-specific hook above did not replace this one: that one speaks a single host's vocabulary so nobody has to translate, and this one asks the sender to translate so any host can be heard.
On GitHub, one workflow is enough:
on:
pull_request:
types: [closed]
jobs:
tell-felagi:
runs-on: ubuntu-latest
steps:
- run: |
curl -sS -X POST "$FELAGI_HOOK" \
-H 'Content-Type: application/json' \
-d '{"url":"${{ github.event.pull_request.html_url }}","state":"${{ github.event.pull_request.merged && 'merged' || 'closed' }}"}'
env:
FELAGI_HOOK: ${{ secrets.FELAGI_DELIVERY_HOOK }}
Did it do what it said?
A run ends with a summary, and it ends with a pull request. Félagi now holds one against the other.
⚠ The summary and the diff disagree The summary says it added tests. No test file was changed.
This is not a review of the code. It has no opinion on whether the change is good. It asks one question — was this claim kept? — and only about claims a list of file paths can settle.
The difference is rarely a lie. It is a model summarising optimistically, which is exactly the discrepancy a person spends ten minutes finding by hand, on every delivery, forever.
What it checks
| The summary says | It looks for |
|---|---|
| it added tests | any test file, in any language's convention |
| it changed documentation | any .md, .rst or file under docs/ |
| it added a migration | any file under migrations/ |
| nothing needed changing | whether anything changed anyway |
That last one is worth the most. An optimistic summary is a nuisance; an account that is the wrong way round means nobody reviews a delivery they were told was empty.
What it refuses to do
A false alarm costs far more than a missed one. The first time this flags a run that was fine, somebody stops reading it, and then it may as well not exist. So:
- "Fixed the failing test" is not a claim to have written one. The verb has to be one of writing
- A test is recognised by any ecosystem's convention —
tests/,spec/,_test.go,.test.tsx,_spec.rb. A rule that only knew Laravel would report a false discrepancy on every Go repository - If the file list cannot be fetched, nothing is reported. An empty list means either an empty pull request or a failed call, and from here those are the same thing
- A delivery from a person is not checked. No run, no summary, no claim
Checking happens when GitHub tells Félagi about the pull request, and the result is stored — the page never waits on somebody else's API to draw. A delivery that was checked and had nothing to report is recorded as such, so "checked, fine" and "never checked" are not the same silence.
Not there yet
- Nothing polls. If nobody posts the hook,
statestays as the agent left it. Félagi will not go and look. - No diff in Félagi. Review happens where the code lives. A patch viewer is a code review tool, and there are good ones already.
- No automatic merge, and none planned as a default. The review is the value of the model.