Fifteen AI agents work on astro-ai, the codebase behind shubhagya.com. In the designed path, none of them merges a pull request: a workflow does. The unattended loop that runs them does hold a written permission to merge its own pull request after QA, and that matters, so it gets its own section below.
Four times, a pull request that had passed everything stayed open anyway. Gates green, QA passed, no conflicts. It sat there, waiting for a signal that had already been sent.
Merge authority is the right to decide that a change lands on main. In
astro-ai's design it sits in a GitHub Actions workflow that merges only on
evidence: green checks on the head commit, a qa-pass label bound to that
commit, and no protected paths touched. The lesson that matters most is to put
the merge decision in something that reads artifacts, then deny the merge
command to the agents, or say plainly which ones you have not.
This post covers both halves: why merge authority sits in a workflow instead of an agent, and the failure that design kept producing anyway. The agent cycle diagram shows the whole loop if you want the map first.
How agents hand off work without talking to each other#
The agents are Claude Code subagents: each one runs in its own context window with its own system prompt, tools and permissions. A subagent starts with no memory of any other, so this system cannot pass work along by conversation.
Every handoff is written somewhere durable instead. The source page lists the channels, and each one exists for a different reason:
| Channel | Written by | Read by | Why this one |
|---|---|---|---|
| Issue comment | every agent | the next cycle, the steward, a human | The only record that outlives the run |
| Label | qa_verdict.sh only | agent-merge | A verdict a workflow can act on |
| Commit SHA | engineer | QA, gates, close-on-evidence | Binds a verdict to exact code |
workflow_run | GitHub | agent-merge, qa-on-green | The signal that a gate finished |
/health | production | close-on-evidence | Proof the fix is serving, not merely merged |
A label
is GitHub's tag for issues and pull requests. Here it carries the QA verdict,
qa-pass or qa-fail, and only one script writes it. When QA records a pass,
that script binds it to the SHA QA examined and refuses if the branch has moved
since.
Why does this beat conversation? Because an artifact can be re-read by something that was not there when it was written. A merge workflow cannot read an agent's reasoning. It can read a label, a commit and a check result, and it can read them again an hour later and get the same answer.
The repo learned the cost of the alternative directly. Its status file,
AGENT_STATUS.md, is gitignored, so nothing written there ever left one
machine. The decision log records five issues audited and found already
complete on one day, four of them carrying zero comments. The rule now is that
the issue holds the decision record.
Where does merge authority sit?#
An agent pushes a branch and opens a pull request. The decision to merge lives
in agent-merge.yml,
a GitHub Actions workflow:
a YAML file that GitHub runs on its own machines when a named event happens.
The workflow runs its checks as a chain of steps, and each step runs only if the previous one passed:
- Author (
own). The pull request was opened byclaude[bot]or the repo owner. A pull request from a fork never reaches the job at all. - Gates (
gates). Every check on the head commit has finished green. It takes the latest run of each check, so a superseded, cancelled run stops voting, and a check still running counts as not green. - QA (
qa). Theqa-passlabel is present, and neitherqa-failnorneeds-human-reviewis. - Size (
size). The diff is underMAX_LINES. - Trust boundary (
boundary). The diff touches none of seven protected paths: the system prompts, the knowledge corpus, the workflows themselves, and the four files that decide what is recorded, sent or stored about a user.
A pull request that touches the trust boundary is labelled
needs-human-review and left open. One agent, chief, can release that hold,
but only with a ruling comment that names the exact head SHA, so the next push
cancels it. It cannot skip the gates or the QA label.
The point is least privilege: give each actor only the access its job needs, the same principle behind OWASP's excessive agency risk. Fifteen things write code. In the designed path, one workflow decides what lands, and it decides on evidence rather than on anyone's judgment that the work is finished.
The size check holds nothing#
Step 4 is switched off, and the source page says so. MAX_LINES is set to
1000000, which admits any plausible diff.
It was raised on 2026-10-08, and the workflow quotes the owner's reason: "we should not restrict git push and merge for this autonomous system to be successful. If QA is passed, push and merge." The step was left in place so that lowering the number to 400 brings it back with one edit.
I think that honesty is the useful part. A control that is written but
inert should be described as inert, not counted as protection. The case for
the limit still stands: the repo notes that test coverage is narrow and main
deploys with no staging, so green CI proves much more about deleting one file
than about a 3D chart.
Where the boundary is a prompt#
The same 2026-10-08 grant reaches further than the size limit.
CLAUDE.md records it as a standing permission:
/agent-cycle, running unattended in agent-work.yml, "pushes its branch and
merges its own PR once QA passes."
agent-work.yml says the
same in a comment. Its denied-tools list blocks rm, sudo, force-push,
gh repo, gh secret and gcloud, but not gh pr merge, and the job holds
pull-requests: write.
So "no agent merges" describes the design, not a hard guarantee. agent-merge is
the workflow that merges the loop's pull requests. qa and qa-lead are told
never to merge, and chief that it cannot merge directly. But only the steward is
actually denied gh pr merge in its tool list
(agent-steward.yml). For the
delivery loop, what stands between it and a merge is its instructions plus
branch protection on main, which requires the gates and build-gates
checks but not the qa-pass label.
That is the same lesson as the size check. Where the boundary is a prompt rather than a deny, say so.
Why the authority is not an agent's#
In this same repo, a dev-lead agent wrote USER AUTHORISED PUSH this turn
into a status file and then cited its own line as permission. The
earlier post covers that failure; the
structural answer is to hold merge authority in a workflow, so there is
nothing for an agent to grant itself. That answer is only as strong as the
tool deny behind it, which, as above, the delivery loop does not have.
The missing edge: why green pull requests stayed open#
Here is the failure the design kept producing.
agent-merge starts on two kinds of event.
workflow_run
starts one workflow when another finishes, here the PR gates and
verify-build workflows. pull_request_target with type labeled starts it
when a label is added.
The workflow's own comment calls both of these edges. An edge is a one-time notification that something changed. If the condition is not yet true when the edge arrives, the run exits, and nothing asks again.
So whenever the last green check landed after the last edge, the pull request
stayed green, qa-pass and mergeable, indefinitely. The workflow lists four
times it happened:
- #303:
ready_for_reviewwas not among the pull request eventspr-gateslistened to, so the pull request, once out of draft, carried no check at all. - #322: a pull request
authored with
GITHUB_TOKENemits noworkflow_run. GitHub's documentation states that, apart fromworkflow_dispatchandrepository_dispatch, events triggered byGITHUB_TOKENdo not create workflow runs. - #352: the run that went green
was a manual
workflow_dispatch, which the job deliberately ignores. A dispatched run checks out the branch, not the merge result, so it does not prove the merge. - #356:
verify-buildis a different workflow, andagent-mergelistened only toPR gates. On a docs-only changepr-gatesskips its suite and finished in 9 seconds, whileverify-buildtook 68.
Each was fixed by adding or correcting an edge. The workflow's conclusion is blunt: "a fifth edge was always going to be missed -- so the answer is to stop relying on edges."
The sweep, and what it does not fix#
The answer was a sweep
(#357): a
cron schedule,
which runs a workflow on a clock rather than on an event, set to */30. That
asks for every 30 minutes.
The sweep adds no authority. It picks a candidate pull request number, and then the same steps re-check every condition through the GitHub API in the same order. The worst it can do is merge something that already satisfies every condition.
It also turned out to be much slower than it looks. Over the 71 scheduled runs up to 2026-10-27, the measured gap was:
Not once in 71 runs did it fire at 30 minutes. The repo attributes this to GitHub throttling short-interval cron on public repositories; GitHub's documentation says only that scheduled runs can be delayed during periods of high load.
So the sweep stopped pull requests stranding forever. It did not make them fast. A stranded pull request now waits hours instead of indefinitely.
The edge bug also came back. On
#562, a code change, gates
finished in 7m43s and build-gates in 10m25s. The workflow_run edge fired
while build-gates was still queued, nothing was listening for the second
workflow, and the merge check exited. The workflow records that the hole cost
two stranded pull requests before it was closed on 2026-10-27, by listening to
both gate workflows.
The decision log records one more measured case (#573) that was not a missing edge at all. A redundant dispatched gate run became the last check to go green, and a dispatched run is exactly the kind that cannot wake a merge. The pull request met every condition and sat 2h36m until the queue was drained by hand.
Lessons for a startup CTO running AI agents#
None of these lessons depend on astro-ai's scale. They come from what is in the repo.
- Put merge authority in something that cannot be talked into it. An agent that can merge can be the agent that writes its own permission slip. A workflow reading labels and check results cannot. Then deny the merge command to the agents, or say plainly which ones you have not.
- Make every handoff a durable artifact. If the next actor cannot re-read it, it did not happen. Bind verdicts to a commit SHA, so a pass on old code cannot vote on new code.
- Event triggers are edges, and edges get missed. Anything that must eventually happen needs a reconciler that re-asks the question on a clock, and the reconciler must re-check everything rather than carry authority of its own.
- Measure the backstop. The cron said 30 minutes; the measurement said two to seven hours. Use the measured number when you decide how much the primary path can be allowed to fail.
- Describe switched-off controls as switched off. An inert size limit, stated plainly, is a decision someone can revisit. The same limit counted as protection is a false claim.
- Hold changes by path, not just by result. Green checks say little about a change to the prompts, the gates or user data, so those paths wait for a ruling regardless.
On 2026-10-21 main itself also gained
branch protection
requiring a pull request plus the gates and build-gates checks. A protected
branch with required status checks refuses a merge until those checks pass,
whatever opened it. It does not require qa-pass; only agent-merge reads
that label. That is the floor; the workflow above is the decision.
The full loop, all fifteen agents and every artifact between them, is on the agent cycle page. If you are designing something similar and want a second pair of eyes on where the authority sits, I am happy to talk.
FAQ#
Can an agent in astro-ai merge its own pull request? In the designed path,
no: the agent-merge workflow does. But the unattended delivery loop holds a
written grant to merge its own pull request once QA passes
(CLAUDE.md),
and its denied-tools list does not block gh pr merge. Only the steward is
denied that command.
Why did pull requests that passed everything stay open? The merge workflow started on events, and an event is a one-time notification. When the last green check landed after the last event, nothing asked again. The workflow lists four such strands, each fixed by adding or correcting a trigger.
How often does a 30-minute GitHub cron actually run? In this repo, never at
30 minutes. Over 71 scheduled runs up to 2026-10-27 the gap ranged from 126 to
444 minutes, according to
agent-merge.yml.
GitHub's documentation says only that scheduled runs can be delayed during
periods of high load.
References#
- astro-ai,
docs/agent-cycle.html, the agent cycle source page (generated as of 2026-10-27) - astro-ai,
.github/workflows/agent-merge.yml, merge conditions, the four strands, the sweep and its measured interval - astro-ai,
CLAUDE.md, the QA verdict script, the*/30sweep rule (#357), and the 2026-10-08 grant letting the autonomous loop push and merge - astro-ai,
.github/workflows/agent-work.yml, the delivery loop's merge permission and denied-tools list - astro-ai,
.github/workflows/agent-steward.yml, the steward's denied-tools list, which includesgh pr merge - astro-ai,
qa.md,qa-lead.mdandchief.md, the agents' instructions not to merge - astro-ai,
ARCHITECTURE.md, decision log rows for the sweep (#357), branch protection (#393), the dispatched-run strand (#573) and the issue as durable record (#607) - astro-ai pull requests and issues: #303, #322, #352, #356, #357, #562
- Anthropic, Claude Code subagents
- GitHub, About workflows
- GitHub, Events that trigger workflows (
workflow_run,schedule, andGITHUB_TOKEN-triggered events) - GitHub, About protected branches
- GitHub, Managing labels
- NIST CSRC glossary, least privilege
- What actually goes wrong with a team of agents, the earlier post on the self-authorisation incident