Skip to content

Running ​

A Run exists so you can start one command and walk away. It takes every Ticket that is ready, carries each to a merge or to a human, and ends on its own when nothing it may take is left. Everything you might want to know afterwards is in its summary, on the board, and in the transcripts it leaves on disk.

CommandWhat it doesExit codes
ticket-runner runDrains the Frontier0 · 1 a Hand-off · 2 nothing taken
ticket-runner run 3 7The same Run, narrowed to #3 and #7as above
ticket-runner run --lanes 2The same Run, two Tickets at onceas above
ticket-runner run --api-keyThe same Run, billed to the ANTHROPIC_API_KEY your shell exportsas above
ticket-runner stopAsks the Run in this Target to finish what it holds and take no more0 sent · 2 nobody to send it to
ticket-runner -vPrints which version this is0
ticket-runner -hPrints the usage0 (2 when no command is given)

init and remove are on Install and remove. An unknown command or option, or a bad argument, is refused with the usage and exit code 2.

/implement-spec or a Run ​

The plugin's /implement-spec also carries a Spec's Tickets to code. It runs in your own session and a Run runs unattended, and the other differences follow from that:

/implement-specA Run
Who watchesYou, in your own session, free to steer itNobody; you read the summary and the board afterwards
Unit of workA whole Spec, in one sessionA Ticket, with its own branch and pull request
GatesThe skill's own steps, then your reviewThe Checks, the verify Stage and CI, for every Ticket
How it endsWhen the session is done, with the Spec's work waiting for your reviewWith each Ticket merged into the base branch or handed to a human
ResumingNothing on the board records how far it got, so what is left is yours to work outA Stranded Ticket resumes in the next Run, on any Host
Mark on the boardNone while it worksin-progress and an assignee while a Run holds a Ticket

The two do not share a Spec: take ready-for-agent off its Tickets before you run /implement-spec on it (Labels). What the skill does step by step, when it opens a pull request and its known rough edges are on its upstream page.

run ​

run drains the Frontier: the open Tickets labelled ready-for-agent that nobody is assigned to and whose native blockers have all closed (Planning). Before it takes anything it can be refused, with exit code 2 and nothing left behind.

It then takes Tickets through its Lanes, one Ticket per Lane, one Lane by default:

  • A Ticket that fails is handed off and its Lane takes the next one; one bad Ticket does not cost the rest of the night.
  • A free Lane looks at the Frontier again while another Lane is busy, five minutes after the Run last listed it. A Ticket you label ready-for-agent, or whose last blocker you close, mid-Run is taken without waiting for a busy Lane to end. A Run with one Lane never does this.
  • A Release stops the filling: the busy Lanes finish, and the next Run is yours or an Operator's to start.
  • The Run ends when no Lane is busy and nothing is left to take: the Frontier is empty, or everything on it is blocked.
  • Leave the main checkout to the Run. It pulls the base branch there before each rebase and after each merge. Uncommitted changes the pull would overwrite hand the Ticket off, and commits on a checked-out base branch that the remote lacks are rebased into the next pull request. Work in a worktree of your own meanwhile.

What happens to each Ticket inside a Lane is on From Ticket to merge. Stranded Tickets and Releases are on Hand-offs, stopping and resuming.

--lanes ​

--lanes <n> gives this one Run n Lanes, over whatever lanes in ticket-runner.json says, because how many Lanes a Host can carry is the Host's business, not the Target's. That page is also where to read what more Lanes speed up and when to keep one. It may go before or after Ticket numbers.

--api-key ​

Every Stage is a claude -p session. Under -p an ANTHROPIC_API_KEY outranks your Claude login without asking, so a Run starts each Stage without that key, whatever your shell exports, and the Stages run on your plan. ANTHROPIC_AUTH_TOKEN is passed on as it is, for a gateway or proxy.

--api-key bills every Stage of this one Run to the ANTHROPIC_API_KEY your shell exports instead. Whose money a Run spends is the Host's business, so it is a flag and never a field of ticket-runner.json. It is how the monthly API credits of a Max or Team plan are spent: they cover claude -p only on a key. It is refused before the lock where no key is exported and on a cloud Host (Refused before it starts). What the Stages are billed to is chosen once, when the Run starts, and held for the whole Run: a Run the plan's limit stops does not switch to the key by itself.

On an --api-key RunWhat happens
The key's organization is out of credit, or its workspace reached its spend limitThe Ticket is released, and the Run ends Out of API budget.
A 429Released as the API's rate limit, not the plan's: API rate limit

The pipeline keeps no budget of its own. Give the key a workspace of its own in the Console, with a spend limit. The limit is not exact: a Run can spend past it before the API refuses, further with several Lanes at once.

A narrowed Run ​

ticket-runner run 12 13 14 is the same Run in every way (its Lanes, the Landing, the Run lock, a Release, a Stop) except that it takes only #12, #13 and #14, from the Stranded Tickets and the Frontier alike. Every other Ticket is left exactly as it was, stranded ones included.

  • The numbers say which Tickets, not the order: stranded ones first, then the rest lowest number first.
  • Blockers still hold. A named Ticket blocked by another named Ticket is taken once that one merges in the same Run.

Every named Ticket the Run did not take gets a skipped row saying why. Only a guard's reason is also commented on the issue.

RowMeaning
blockedAn open blocker still held it back when the Run ended
claimedSomebody else is assigned to it
not-readyIt is not labelled ready-for-agent, or it has closed
no-issueNo issue has that number
pull-requestThe number is a pull request
spec, no-criteria, body-only-blockersA guard refused it

A named Ticket that a Stop or a Release kept the Run from reaching has no row; the last line of the summary says why the Run stopped.

stop ​

bash
ticket-runner stop

A Stop asks the Run to finish the Tickets its Lanes hold, to merge or Hand-off, and take no more. stop reads the Run lock from GitHub, sends SIGTERM to the process it names, and says which Run it asked:

text
`ticket-runner run` (run 2026-09-17T09-00-00-000, pid 4321) will stop once the Tickets it holds are finished. It claims no more.
Ctrl+C in that Run's own terminal stops it at once instead, at the cost of killing the Stages it is running and leaving their Tickets stranded for the next Run.

It needs nothing of the Target but the lock: no config, no gh login, no readiness.

FindsPrintsExit
a Run on this Hostthe two lines above0
nobody holds the lock, or its Run on this Host is goneNo Run to stop: …. A dead lock is left for the next Run to take over.2
a Run on another Host, which only that Host can signalnames the Host, the Run and its start; Nothing was sent.2
the signal could not be deliveredCould not ask … to stop: <reason>2

A second stop prints the same, and the Run ignores the second signal. What a Stop and a kill each leave behind: Stop and kill.

-v ​

-v, or --version, prints which version this is, such as 0.5.2. The same string is stamped on everything a Run writes (The stamp).

Refused before it starts ​

SituationWhat you see
Another Run holds the Target, on this HostAnother ticket-runner is running on this Host: `ticket-runner run` as run … Wait for it to finish, or ask it to with `ticket-runner stop`.
Another Run holds the Target, on another HostThe Host, the Run and when it started, and how to release the lock if that Run is gone
The shell belongs to a StageRefusing to start: TICKET_RUNNER_STAGE is set to `implement`, …
--api-key with no key exported`--api-key` runs every Stage on the `ANTHROPIC_API_KEY` this shell exports, and it exports none. …
--api-key on a cloud Host`--api-key` needs a workstation Host: a cloud session runs every Stage on the subscription, …
The Target is not set up, still holds local state files, or has no CheckSee Target readiness

One Run at a time per Target, whichever Host it is on. The Run lock says when a lock is taken over by itself and how to release one another Host left.

The summary ​

When a Run ends it prints one row per Ticket, in the order the Tickets ended, then one line saying why it stopped:

text
ticket-runner 0.5.2 run 2026-09-17T09-00-00-000 · 84m · subscription

  merged   #4 Planning guards (PR #12)
  noted    #8 comment · from #4 implement · the CLI help drifts
  handed   #5 Fix Stage with a single retry · verify · 1 unmet
  skipped  #7 no-criteria
  skipped  #9 blocked

Frontier blocked.

The header ends with what the Stages were billed to: subscription, or API key on an --api-key Run. An --api-key Run follows it with its spend, API key · $12.34: what that key was billed for the Stages that ran on it, as the CLI prices them, which can differ from the Console's. It ends +, $12.34+, when a Stage stopped before reporting all of its figure, so the Run spent at least that much. A subscription Run shows no amount, and adds extra usage when a Stage ran on the plan's extra usage. A Run that took nothing prints nothing to do in place of the rows.

RowMeaning
mergedMerged; the pull request is named
handedHanded off to a human: the Stage it stopped at, and why
releasedReleased at the named Stage by the rate limit (API rate limit on an --api-key Run), an API failure, or out of API budget, or at checks or ci by a failing base branch. Nobody has to do anything about the Ticket
skippedPassed over, with the reason (guards, or the table above)
billedA Stage of the Ticket above that said, in its apiKeySource, it was billed to something other than what the header names: an apiKeyHelper in the Host's settings, say, which no environment variable removes. It ends with that Stage's own amount, · $4.15, where it gave one, which the header's leaves out
notedA Note the row above made, from #<n> pipeline when it is the pipeline's own about a failing base branch, and where it went: comment on an issue already there, the Notes issue included, or new for the one Note that opened the Notes issue
Last lineWhy the Run ended
Frontier empty.Nothing left to take
Frontier blocked.Everything left has an open blocker
Rate limited · resets 2026-09-20 03:40 UTC.A Release by the rate limit stopped it. The time is the one the CLI gave for the limit's reset, in UTC; a Run it gave none ends Rate limited.. An --api-key Run says API rate limited
API failure.A Release by a transient API failure stopped it, and the rate limit released no Ticket after it: the rate limit outranks an API failure on the last line
Out of API budget.A Release by the key's credit or spend limit stopped it. It outranks the rate limit and an API failure, since only it needs you: look at the Console
Base branch `main` fails `npm test` at 1a2b3c4.A Release over a failing base branch stopped it: the Check, and the merge-base it failed at. Mend it on the base branch; the Note on the standing Notes issue has its output
Stopped at 22:07 · finishing #4 #9.A Stop arrived at that time (UTC), while those Tickets were in its Lanes

A Run a Stop or a Release ended prints no blocked rows, since it never reached the end of the Frontier.

Exit codes ​

Coderun
0Nothing was handed off. A Release counts as taken, and so does a Run given no numbers that skipped everything
1At least one Ticket was handed off
2Nothing was taken: the Run was refused, or it was given numbers and took none of them

A stopped Run exits with its outcomes' code as usual. The codes of the other commands are in the table at the top of this page, and those of init and remove on Install and remove.

Logs and transcripts ​

The Run's own lines go to the terminal, every line about a Ticket starting with its number, so interleaved Lanes can be read one Ticket at a time:

text
ticket-runner run 2026-09-17T09-00-00-000 · 1 lane
#4 claimed · agent/4-planning-guards
#4 verified · 6 met · 1 unverifiable
#4 merged · PR #12

Nothing is printed while a Stage runs or CI is awaited, which can take an hour; a Stage's own output is in its .stdout file below. A narrowed Run's first line adds the Tickets named, and a resumed Ticket says resumed from <state> in place of claimed. A Stage billed to something other than what the Run bills its Stages to is named as it comes back: #4 implement ran on apiKeySource apiKeyHelper, not the subscription (none). Warnings and refusals go to stderr.

Everything a Stage did goes to disk, under the Target's gitignored .ticket-runner/. The run id is the start time in UTC, such as 2026-09-17T09-00-00-000.

Path under .ticket-runner/runs/<runId>/Holds
version.txtThe version that ran, written before the first Stage
<n>/<stage>.commandThe exact command line, environment included, to rerun the Stage by hand: env -u ANTHROPIC_API_KEY … for a Stage run without the key, ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" … for one run on it, never its value
<n>/<stage>.stdout, <n>/<stage>.stderrOutput as the Stage printed it, appended live
<n>/<stage>.transcript.jsonlThe session's stream-json events
<n>/retry/The fix Stage and the pass it bought, so the failing pass's files survive beside them

The command line is written before the Stage starts and output as it arrives, so a Run killed mid-Stage still leaves what it had reached. Wherever a Stage printed the ANTHROPIC_API_KEY your shell exports, its files say <ANTHROPIC_API_KEY> instead. A handed-off Ticket's command lines and transcripts are also kept on the Target's remote (What stays where).

From the Claude app ​

On a workstation, a Run lasts only while the machine stays on: one that sleeps, reboots or is switched off stops the Frontier draining until you are back, and the Run can be watched or stopped only from the terminal that started it. A Run started from the Claude app goes on with your machine off and can be followed from any device, though its VM can still be reclaimed (below): open a Claude Code cloud session on the Target from the app and say "run it". The session's own Claude is the Operator: it follows the skill init wrote into the Target, .claude/skills/ticket-runner/SKILL.md, because a cloud session carries nothing over but the repository (ADR-0008).

Before the first Run from the app:

  1. Run init on a workstation, and commit and push what it wrote to the base branch. The cloud session sees only the repository.
  2. Let the cloud environment reach npm, GitHub, apt and your Target's own package registry, which readying the Host installs from.

gh needs nothing: the cloud session authenticates it.

You sayThe Operator
"run it"Readies the Host, then starts ticket-runner run in the background
"run 12 and 14"Starts ticket-runner run 12 14
"three at once"Passes --lanes 3, so the cloud can carry a different count from your workstation
"on my API key"Passes --api-key only when you ask, and tells you such a Run needs a workstation: the cloud Host refuses it
"stop"Runs ticket-runner stop and keeps reporting until the Run ends
"release the lock"Commits a free lock.json to ticket-runner/lock, only when no Run of its own is running

Readying the Host installs only what the cloud environment's setup script did not:

  1. The pipeline. A copy the setup script installed is used as it is, whichever version it is. Otherwise the Operator installs the version stamped on the Target's conventions document; with no stamp, it installs nothing and asks you to run init from a workstation.
  2. The mattpocock-skills plugin, from the official marketplace.
  3. gh, from apt.
  4. The Target's own dependencies, in the main checkout, where every worktree's Checks find them.

It reports each Ticket as the Run ends it, then the summary and what the exit code means. Everything else you want of a Ticket still goes through the board, and while its Run holds the Target the Operator leaves the main checkout and the worktrees alone.

A cloud Host differs from a workstation in a few ways the pipeline accounts for:

  • Nothing can be deleted on the remote. That is why init switches on deleting a pull request's branch at merge, and why remove refuses to run on a cloud Host.
  • Every issue body, comment and pull request body gets a "Generated by Claude Code" line, which the proxy appends and nobody can turn off. The squash commit is composed by the pipeline, so none of it reaches the base branch.
  • A reclaimed VM is a kill. It leaves Stranded Tickets, which the next Run on any Host resumes, and a lock no other Host will presume dead: a human releases it, through an Operator or on GitHub.
  • Transcripts go with the VM, except a handed-off Ticket's, which are kept on the state branch.
  • Stages always run on your plan. A cloud session ignores ANTHROPIC_API_KEY, so --api-key is refused there rather than accepted and not honoured.

In everything else a cloud Host runs the same Run against the same Target as a workstation. A claim assigns the same gh user whichever Host made it, so the assignee does not say where a Run ran.

References ​