The Agent Delivery Loop — From a Reported Issue to a Verified Deploy
loopctl can take a problem reported on GitHub and carry it through triage, implementation, review, CI, merge and post-deploy verification. Agent sessions do the work on runners, which are dev machines the tenant enrolls. loopctl is the control plane: it holds the queue, the stage of every story, the claims and the gates. It never runs these sessions itself and never pushes to the repository; its only GitHub writes are a resolution comment, labels and closing on the reporter's issue.
The stages
detected → triaged → queued → claimed → worktree → implementing → reviewing
→ pr_open → ci → merged → deployed → verified → done
any gate refusal → escalated (left only by a human's resolution)
A story's stage is read with story_stage. stage: null means the loop has never touched the story.
Who moves a story
- Intake. A signed GitHub webhook on an enrolled repository (an intake source) records each issue. A job turns the record into a stub story at
detected. The reporter's words are stored as untrusted and never reach the implementer. - Triage. A triage session on a runner returns a verdict:
story(queued),escalateorreject. Astoryverdict that fails a delivery gate is escalated instead of queued. - Placement. An operator (
place_dispatch) or the unattended dispatch driver claims a queued story under a fresh custody dispatch and sends it to a runner with a free slot. - The session. The runner reports each stage as it implements, reviews, opens the pull request and watches CI. It can move a story forward, or back to
implementing, but never toverifiedordone. - The merge gate. An orchestrator or operator calls
merge_precondition, which a runner session cannot. It re-checks the real diff: both delivery gates, custody and a hard size bound. Onlyallowlicenses a merge,refuseescalates the story, and a head that moved since CI sends it back toimplementing. - After the merge. Post-deploy verification moves a story from
deployedtoverified, and completion moves it todoneonce the reporter's issue is closed.
The claim: lease and fence
A claim has a lease (claimed_until) and an epoch (claim_epoch). Every report names the epoch, so a session that lost its claim cannot move a story that someone else now holds. A lease nobody renews is reclaimed. A counted release puts the story back in the queue below the retry ceiling and escalates it at the ceiling.
Escalation is the safety valve
Wherever a gate refuses, a budget runs out or a session gives up, the story goes to escalated and waits for a person. resolve_escalation moves it to queued, done or failed. It requires a user key that no dispatch minted, so a session can never resolve its own escalation. This is the same separation the chain of custody enforces for report, review and verify.
Setting it up
- Enroll a runner (
runner_enroll) and start it on the machine with its token file. - Enroll the repository (
intake_source_enroll) with its target epic and base branch, and add the GitHub webhook for the Issues event. - Configure the delivery gates and the dispatch budgets.
- Place one story by hand and watch it with
story_stage. - Turn on the unattended dispatch driver.
The full operator reference, including every refusal and recovery path, is docs/agent-delivery-loop.md in the loopctl repository.