Configuration
A Target should work with no settings at all, so every field is optional and init writes the file as {}. The file exists for what only the Target can know: which commands prove its code works, how long its CI takes, whether its Checks can share a machine, and what it calls its labels. What belongs to the Host rather than the Target is a flag of run: --lanes overrides lanes, and --api-key chooses what the Stages are billed to, which no field sets.
The file is ticket-runner.json at the Target's root. It is plain JSON, so it cannot hold comments. A Run reads it once, as it starts, from the main checkout's working tree, so an edit applies from the next Run.
| Field | Default | What it changes |
|---|---|---|
baseBranch | GitHub's default branch | The branch Tickets are branched from, rebased onto and merged into |
lanes | 1 | How many Tickets a Run holds at once |
checks | inferred from package.json | The commands the pipeline runs itself to gate a Ticket |
checkTimeoutMinutes | 15 | Wall-clock limit for each Check command |
gates.checks | true | Whether a Run may start with no Check at all |
gates.ci | true | Whether a pull request with no CI checks may merge |
stages | see below | Model, effort, limits and extra instructions per Stage |
permissionMode | "auto" | What every Stage session may do without asking |
ciTimeoutMinutes | 30 | How long a Run waits for a pull request's CI |
ciGraceMinutes | 5 | How long "no checks registered yet" still counts as pending |
labels | the six default names | The label names the pipeline reads and writes |
A file that sets several of them:
{
"baseBranch": "develop",
"lanes": 2,
"checks": ["npm test", "npm run typecheck", "npm run lint"],
"checkTimeoutMinutes": 20,
"stages": {
"implement": { "maxTurns": 400, "extraPrompt": "Run `npm run lint -- --fix` before committing." },
"verify": { "effort": "max" }
},
"ciTimeoutMinutes": 45,
"labels": { "readyForAgent": "agent-ready" }
}A file that will not load
The schema is strict. An unknown key, a wrong type or an out-of-range value is refused by name, and so is a file that is not JSON. Every command that reads the file (init, run, remove) exits 2 over it; stop never reads it.
Invalid ticket-runner.json: stages: Unrecognized key: "implment". Refused by ticket-runner 0.5.2, so the key may be newer than this install.The second sentence is added only for an unknown key: on a stale install, a key a newer version added looks exactly like a typo, so check the spelling, then upgrade.
baseBranch
baseBranch: string
The base branch is the one every Ticket starts from and merges into: a Run rebases each Ticket onto it, opens the Ticket's pull request against it and pulls the main checkout to it once the Ticket merges. Without this field it is whatever GitHub calls the repository's default branch, so a Target on master needs no setting. It is resolved once at the start of a Run.
lanes
lanes: whole number ≥ 1
How many Tickets a Run holds at once, one per Lane. run --lanes <n> overrides it for one Run. Lanes run their Checks at the same time, each in its own worktree, so a Target whose Checks need a port, a database or anything else they would share keeps this at 1. Only one Lane at a time is in the Landing (rebase to merge), so more Lanes speed up implement and verify, not merging (ADR-0005).
checks
checks: array of stringscheckTimeoutMinutes: number > 0
A Check is a command the pipeline runs itself. Each one runs through the shell in the Ticket's worktree, after the implement Stage, after a fix Stage and after a resolved rebase conflict. A non-zero exit fails the gate, and the Ticket spends its fix budget, unless the base branch fails the same command (A failure the base branch shares). That question is asked of a red CI by running these commands too, so they should be ones CI runs.
Without this field the pipeline infers npm test and npm run typecheck from whichever of the test and typecheck scripts package.json defines.
checkTimeoutMinutes is one limit for every command, and each command gets the whole of it, at the merge-base too. A command killed at the limit is a failed Check and spends the fix budget like any other.
gates
gates.checks: booleangates.ci: boolean
| Setting | When on (the default) | When false |
|---|---|---|
gates.checks | A Run with no Check command, configured or inferred, refuses to start | The Run starts anyway, with a warning. Check commands that do exist still run; with none, the Ticket's checks row reads ⚠️ no checks |
gates.ci | A pull request with no CI checks at all is a Hand-off | It merges, with a warning at start and a ⚠️ no checks row on the Ticket |
Turning gates.ci off tolerates only a pull request that has no checks. Red CI, CI that times out, and a pull request that conflicts with the base branch still end the Ticket whatever the gates say.
The refusal gates.checks guards against reads:
No Check commands are configured and none could be inferred from package.json. Add them to `checks` in ticket-runner.json, add `test` and `typecheck` scripts to package.json, or set `gates.checks` to false in ticket-runner.json to run without a net.stages
stages: object
Each Stage is one claude -p session. stages takes an object per Stage, and every key in it is optional:
| Stage | What it does | maxTurns | maxMinutes |
|---|---|---|---|
implement | Drives /mattpocock-skills:implement on the Ticket | 300 | 60 |
verify | Tries to prove the Acceptance Criteria are not met, and returns a verdict | 130 | 30 |
fix | Mends the one failure that spent the fix budget | 150 | 40 |
conflict | Resolves the conflict where a rebase stopped | 120 | 30 |
| Key | Type | Default | Passed as |
|---|---|---|---|
model | string, anything claude --model takes, such as sonnet | "claude-opus-5-5" | --model |
effort | "low", "medium", "high", "xhigh" or "max" | "high" | --effort |
maxTurns | whole number ≥ 1 | per Stage, above | --max-turns |
maxMinutes | number > 0 | per Stage, above | The wall-clock limit the session is killed at |
extraPrompt | string | "" | Appended as the last section of the Stage's prompt |
The model and effort are always passed, so the machine's own claude defaults never apply. What a Stage that runs out of turns or time costs the Ticket is on From Ticket to merge.
permissionMode
permissionMode:"auto","acceptEdits"or"bypassPermissions"
Passed to every Stage as --permission-mode. Every Stage also runs with --permission-prompts none: nobody is watching, so anything that would prompt is denied instead.
CI
ciTimeoutMinutes: number > 0ciGraceMinutes: number ≥ 0
After the Landing opens a pull request, the Run waits up to ciTimeoutMinutes for its checks. CI that does not finish in time is a Hand-off that spends no fix budget.
ciGraceMinutes covers the gap before GitHub registers a check run, which has taken over three minutes. Until it passes, a pull request with no checks yet counts as pending rather than as having none. It never outlasts ciTimeoutMinutes. Raise it on a Target whose Actions queue slowly. A Target with no CI at all sets it to 0, so the wait answers at its first reading; left at the default, it waits the grace out once per merge, and since only one Lane is in the Landing at a time, that wait holds the other Lanes' merges too. A Target with CI keeps a grace: GitHub's answer cannot tell no CI from CI not registered yet, so under 0 a slow check run reads as none.
labels
labels: object of strings
Rename the triage vocabulary when the Target already uses other label strings. Only the keys you give change.
| Key | Default |
|---|---|
needsTriage | needs-triage |
needsInfo | needs-info |
readyForAgent | ready-for-agent |
readyForHuman | ready-for-human |
wontfix | wontfix |
inProgress | in-progress |
init creates the labels under these names, and a Run refuses a Target missing any of them. What each label means to the pipeline is on Planning the work.
Related pages
- Running:
--lanesand the Run that reads this file. - From Ticket to merge: where each Stage, Check and gate sits in a Ticket's life.
- Planning the work: the labels and what they mean.
- Install and remove:
initwrites the empty file;removedeletes it last.