Skip to content

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. How many Tickets a machine carries at once is the machine's business, so --lanes overrides the one setting that is really about the Host.

The file is ticket-runner.json at the Target's root. It is plain JSON, read with JSON.parse, so it cannot hold comments.

FieldDefaultWhat it changes
baseBranchGitHub's default branchThe branch Tickets are branched from, rebased onto and merged into
lanes1How many Tickets a Run holds at once
checksinferred from package.jsonThe commands the pipeline runs itself to gate a Ticket
checkTimeoutMinutes15Wall-clock limit for each Check command
gates.checkstrueWhether a Run may start with no Check at all
gates.citrueWhether a pull request with no CI checks may merge
stagessee belowModel, effort, limits and extra instructions per Stage
permissionMode"auto"What every Stage session may do without asking
ciTimeoutMinutes30How long a Run waits for a pull request's CI
ciGraceMinutes5How long "no checks registered yet" still counts as pending
labelsthe six default namesThe label names the pipeline reads and writes

A file that sets several of them:

json
{
  "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.

text
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 machine with a stale install, a key a newer Version added looks exactly like a typo, so the refusal names the Version that refused it. The file itself carries no Version (ADR-0007).

baseBranch ​

  • baseBranch: string

The Base branch is what a Run branches each Ticket from, rebases it onto, targets its pull request at, and pulls the main checkout to after a merge. 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 strings
  • checkTimeoutMinutes: number > 0

A Check is a deterministic command the pipeline runs itself, never an agent's opinion. 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.

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. A command killed at the limit is a failed Check and spends the Fix budget like any other, because a suite that hangs is a defect in the branch's own code. The fix Stage is told that the Check hung rather than failed.

gates ​

  • gates.checks: boolean
  • gates.ci: boolean
SettingWhen on (the default)When false
gates.checksA Run with no Check command, configured or inferred, refuses to startThe Run starts anyway, with a warning. Check commands that do exist still run
gates.ciA pull request with no CI checks at all is a Hand-offIt 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:

text
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:

StageWhat it doesmaxTurnsmaxMinutes
implementDrives /mattpocock-skills:implement on the Ticket30060
verifyTries to prove the Acceptance Criteria are not met, and returns a Verdict8020
fixMends the one failure that spent the Fix budget15040
conflictResolves the conflict where a rebase stopped12030
KeyTypeDefaultPassed as
modelstring"claude-opus-5-5"--model
effort"low", "medium", "high", "xhigh" or "max""high"--effort
maxTurnswhole number ≥ 1per Stage, above--max-turns
maxMinutesnumber > 0per Stage, aboveThe wall-clock limit the session is killed at
extraPromptstring""Appended as the last section of the Stage's prompt

The model and effort are always passed, never left to the machine's own defaults, so a Stage's saved command line says exactly what ran it and reruns the same way. 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. The modes on offer are the ones that can do work unattended; plan and the prompting modes would guarantee a Stage that does nothing.

CI ​

  • ciTimeoutMinutes: number > 0
  • ciGraceMinutes: 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: it is somebody else's infrastructure, not a defect a fix Stage could mend.

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 workflow waits it out once per merge, and since only one Lane is in the Landing at a time, that wait holds the other Lanes' merges too.

labels ​

  • labels: object of strings

Rename the triage vocabulary when the Target already uses other label strings. Only the keys you give change.

KeyDefault
needsTriageneeds-triage
needsInfoneeds-info
readyForAgentready-for-agent
readyForHumanready-for-human
wontfixwontfix
inProgressin-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.

References ​