Run a ticket end to end
Coding agents explains what the pieces are. This page is the operator's path through one ticket: what to write, which calls launch it, what "normal" looks like while it runs, and what to do when it stalls.
The loop you are launching
ticket ──▶ git work ──▶ delivery ──▶ coding session (hosted runner)
│ commit, run required commands
▼
trusted broker publishes the exact head
▼
reviewer actor opens the pull request (labels, provenance)
▼
approval gate: CI green ▸ AI code review ▸ receipt
▼
merge lane squashes, deletes the work branch
▼
release operator advances post-merge, resolves the ticket
Every arrow is driven by the software-delivery controller, a Staff actor that runs every 30 seconds and claims one actionable delivery at a time. You never push, open, review or merge anything yourself. The coding session never holds a git credential; the broker publishes only the exact object the session acknowledged.
Before you start
- The repository is ready for hosted delivery: platform registration, GitHub binding, delivery policy, execution profile, and manifest files on the prepared branch. See Wire a repository for coding agents. For local-only work, Coding agents is enough. The execution profile's dependency kind must be one the runner implements; a
python-uvprofile needs a committeduv.lock. See also Runner groups. - The controller actor's schedule is enabled. Check it in Staff → Staff tree, or with
staff.get-actor. - You know the repository's
repository_uuid, the policy'spolicy_uuid, and the target branch the policy allows (usuallyrefs/heads/master).
1. Write the ticket as a specification
The coding agent treats the ticket body as the contract and implements nothing outside it. Tickets that merge on the first attempt share one shape:
- the exact files to change, by path;
- numbered rules, each verifiable inside the workspace;
- schema rules spelled out when the change touches a workflow — an undeclared output field fails every call of that workflow, so "declare it in
OUTPUT_SCHEMA" is part of the change; - the tests to add, by file and by case;
- a done-when line and "no other files".
Keep the diff inside the policy's approval thresholds (changed files, additions, deletions); the gate will not approve a change that exceeds them.
ticket.open
{
"kind": "task",
"title": "github.pulls.get_exact_pr: expose head_repository and is_fork",
"body": "<the specification>",
"labels": ["pipeline:coding"],
"source_type": "human", "owner_type": "human", "owner_uuid": "<your principal>",
"priority": "routine", "severity": "low"
}
The label marks intent; it does not launch anything. Step 2 does.
2. Bind the work and open the delivery
Delivery policies are serial per target branch: one active piece of work per branch. If a previous delivery on the same branch has merged, close its work first so the lane is free:
ticket.git-work.complete
{ "work_uuid": "<previous work>", "outcome": "merged", "merge_oid": "<its squash oid>" }
ticket.git-work.concurrency reports the lane without claiming anything.
Then bind the ticket to the repository and open the delivery. Pass the current head of the target branch as base_oid; the derived default can be stale.
ticket.git-work.begin
{
"ticket_uuid": "<ticket>",
"repository_uuid": "<repository>",
"runner_group_uuid": "<hosted coding runner group>",
"target_ref": "refs/heads/master",
"base_oid": "<current head of master>"
}
ticket.software-delivery.begin
{ "work_uuid": "<work from above>", "idempotency_key": "delivery-<work8>-r1", "policy_uuid": "<policy>" }
You get a work item in active with a codex/<slug>-<id> branch ref and a delivery in coding. Within 30 seconds ticket.software-delivery.controller-list shows the delivery on start_coding_session, first with the claim available, then leased. The runner group named here is advisory; the coding actor's own config decides where the session runs.
3. Watch it
All reads, no side effects:
| Question | Call |
|---|---|
| Which step is the controller on, how many attempts | ticket.software-delivery.controller-list |
| Is the coding session alive, on which runner | ticket.git-work.get → workspace_leases (heartbeat, session id) |
| What is the agent doing right now | data.agents.session-trace with the session id |
| Did the runner capture the commit | ticket.coding-artifact.list → an artifact in available |
| Is publication moving | ticket.git-delivery.list → attempt claimed → pushed → verified |
| The full ledger | ticket.software-delivery.get with transitions and evidence |
| The provider side | the pull request, its checks, and the review carrying the orkestia-verdict marker |
What a healthy run looks like on a hosted runner, measured from the delivery's creation:
| Stage | Elapsed | Signal |
|---|---|---|
| Runner claims the session | about 1 min | lease running |
| Commit | about 20 min | git.commit in the session trace |
| Publication requested | about 30 min | an attempt appears |
Branch on the provider, attempt verified | about 35 min | delivery published |
| Pull request opened with labels and provenance | about 70 min | delivery checks_pending |
| AI review posted | about 80 min | review with the verdict marker |
merge_authorized | about 95 min | gate receipt recorded as approval evidence |
Merged, delivery completed | about 105 min | squash oid on the merge transition |
A coding turn takes roughly a minute of model time. Reviewer actions run on your agent runner group and take four to eight minutes each. After a session releases its claim, the controller re-dispatches that step about five minutes later.
4. Close the loop
After the merge the fabric resolves the ticket and deletes the work branch. Verify:
- ticket
resolved; deliverycompleted; git workmerged; - the pull request merged with its
model:*,tool:*andcategory:*labels; - the work branch gone; the repository's own release pipeline picked up the merge.
If the ticket is still open, move it yourself. The lifecycle refuses open → resolved; start it first:
ticket.transition { "ticket_uuid": "<ticket>", "intent": "start", "actor_type": "human", "actor_uuid": "<you>" }
ticket.transition { "ticket_uuid": "<ticket>", "intent": "resolve", "resolution": "Merged as PR #<n> (<oid>)", "actor_type": "human", "actor_uuid": "<you>" }
A pass is autonomous when the delivery's transitions show no actor_type: human entry between coding and merged.
When it stalls
Read the ledger before naming a cause: ticket.software-delivery.get with evidence tells you what actually happened; the agent's narration does not.
| Symptom | What it usually is | What to do |
|---|---|---|
controller-list is empty right after software-delivery.begin | controller schedule disabled | staff.manage-actor-schedule with enable on the controller actor |
start_coding_session attempts climb, no lease ever runs | no warm runner, or the execution profile's requirements do not match a warm runner | data.runner.pool-status on the group; adopt a universal profile |
Delivery sits in publish_requested, git-delivery.list is empty | the session transitioned outside publish-request | ticket.software-delivery.recovery-inspect, then publish-request for the acknowledged head, then resume or abandon |
Attempt claimed, branch already at the right oid on the provider | push recorded on the provider, not yet in the ledger | wait for the lease to expire; the broker re-claims and verifies by reading the ref back |
checks_pending for a long time while CI is green | a reviewer session judged CI from a stale snapshot | the next dispatch runs the gate; the gate waits for CI itself |
Reviewer reports a tool call failed with Unknown workflow type: virtual.… | a composition was re-versioned after the session launched | nothing; the next dispatch carries the new version |
| Session dies mid-step with a connection reset | transient control-plane connectivity | the controller re-dispatches within about 90 seconds and resumes the retained worktree |
To stop a delivery: ticket.software-delivery.recovery-inspect returns a snapshot digest and says whether recovery-abandon is allowed (only while no controller claim is live; release it with ticket.software-delivery.controller-claim-release first). Abandon with the cleanup acknowledgement, then ticket.git-work.complete with outcome: abandoned. Nothing merged can be undone from here; revert in the repository.
Where to go next
- Tickets & Software Delivery — the lifecycle model and the evidence chain
- Coding agents — wiring a repository and the provider-blind rules
- Runner groups — agent-eligible capacity for sessions
- Troubleshooting — sessions, seats, tokens and approvals
Wire a repository for coding agents
Prepare a GitHub repository for Staff coding agents — platform registration, manifest files, and preflight before hosted ticket-to-PR delivery
Build a product team of actors
A pattern for running one product with Staff actors. A manager, a product manager, an engineer, a reviewer, QA and a release manager work from tickets, humans approve merges and releases, and a support actor answers people in the product's chat
