설정
Target은 설정 없이도 돌아가야 하므로 모든 필드는 선택이고, init은 파일을 {}로 만듭니다. 이 파일은 Target만 아는 것을 적는 곳입니다. 어떤 명령이 코드가 제대로 돈다는 걸 증명하는지, CI가 얼마나 걸리는지, Check가 한 머신을 함께 써도 되는지, 라벨을 뭐라고 부르는지 같은 것들이죠. Target이 아니라 Host에 속하는 것은 run의 플래그입니다. --lanes는 lanes를 덮어쓰고, --api-key는 Stage 비용을 어디로 청구할지 고릅니다. 이건 어떤 필드로도 정하지 않습니다.
파일은 Target 루트의 ticket-runner.json입니다. 평범한 JSON이라 주석을 넣을 수 없습니다. Run은 시작할 때 main checkout의 working tree에서 이 파일을 한 번 읽으니, 고친 내용은 다음 Run부터 적용됩니다.
| 필드 | 기본값 | 바꾸는 것 |
|---|---|---|
baseBranch | GitHub의 기본 branch | Ticket을 따 오고, rebase하고, merge하는 branch |
lanes | 1 | Run이 동시에 맡는 Ticket 수 |
checks | package.json에서 추론 | 파이프라인이 Ticket을 거르려고 직접 돌리는 명령 |
checkTimeoutMinutes | 15 | Check 명령 하나당 wall-clock 한도 |
gates.checks | true | Check가 하나도 없어도 Run을 시작할지 |
gates.ci | true | CI check가 없는 pull request를 merge할지 |
stages | 아래 참고 | Stage별 모델, effort, 한도, 추가 지시 |
permissionMode | "auto" | 모든 Stage 세션이 묻지 않고 할 수 있는 일 |
ciTimeoutMinutes | 30 | pull request의 CI를 기다리는 시간 |
ciGraceMinutes | 5 | "아직 등록된 check 없음"을 대기 중으로 봐 주는 시간 |
labels | 기본 이름 여섯 개 | 파이프라인이 읽고 쓰는 라벨 이름 |
여러 필드를 설정한 예:
{
"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" }
}읽히지 않는 파일
스키마는 엄격합니다. 모르는 키, 잘못된 타입, 범위를 벗어난 값은 이름을 짚어 거절하고, JSON이 아닌 파일도 마찬가지입니다. 파일을 읽는 명령(init, run, remove)은 이런 파일을 만나면 모두 종료 코드 2로 끝납니다. stop은 파일을 아예 읽지 않습니다.
Invalid ticket-runner.json: stages: Unrecognized key: "implment". Refused by ticket-runner 0.5.2, so the key may be newer than this install.두 번째 문장은 모르는 키일 때만 붙습니다. 설치가 오래된 머신에서는 새 버전이 추가한 키와 오타가 똑같아 보이니, 철자를 확인하고 그다음 업그레이드하세요.
baseBranch
baseBranch: 문자열
base branch는 모든 Ticket이 출발하고 merge되어 들어가는 branch입니다. Run은 Ticket을 이 branch 위로 rebase하고, Ticket의 pull request도 이 branch를 대상으로 열며, Ticket이 merge되면 main checkout을 이 branch로 pull합니다. 이 필드가 없으면 GitHub가 저장소의 기본 branch라고 부르는 것을 쓰니, master를 쓰는 Target도 따로 설정할 필요가 없습니다. Run이 시작할 때 한 번 정합니다.
lanes
lanes: 1 이상의 정수
Run이 동시에 맡는 Ticket 수입니다. Lane 하나에 Ticket 하나. run --lanes <n>이 한 Run에 한해 덮어씁니다. Lane들은 각자의 worktree에서 동시에 Check를 돌리니, 포트나 데이터베이스처럼 Check끼리 겹쳐 쓰게 되는 것이 있는 Target은 1로 두세요. Landing(rebase부터 merge까지)에는 한 번에 Lane 하나만 들어가므로, Lane을 늘리면 implement와 verify가 빨라질 뿐 merge가 빨라지지는 않습니다(ADR-0005).
checks
checks: 문자열 배열checkTimeoutMinutes: 0보다 큰 수
Check는 파이프라인이 직접 돌리는 명령입니다. 각 명령은 Ticket의 worktree에서 셸을 통해 돌고, implement Stage 뒤, fix Stage 뒤, rebase 충돌을 해결한 뒤에 실행됩니다. 0이 아닌 코드로 끝나면 게이트를 통과하지 못하고, Ticket은 fix 기회를 씁니다. 단, base branch도 같은 명령에 실패하면 예외입니다(base branch도 함께 실패할 때). CI가 실패했을 때도 base branch가 함께 실패하는지 이 명령들로 따지므로, CI가 돌리는 명령과 같은 것을 적어 두세요.
이 필드가 없으면 package.json에 test, typecheck 스크립트 중 정의된 것을 보고 npm test와 npm run typecheck를 추론합니다.
checkTimeoutMinutes는 모든 명령에 하나로 적용되는 한도이고, 명령마다 이 시간을 통째로 받습니다. merge-base에서 돌릴 때도 같습니다. 한도에서 kill된 명령은 실패한 Check이고 다른 실패처럼 fix 기회를 씁니다.
gates
gates.checks: 불리언gates.ci: 불리언
| 설정 | 켜져 있을 때(기본) | false일 때 |
|---|---|---|
gates.checks | 설정하거나 추론한 Check 명령이 하나도 없으면 Run이 시작을 거절 | 경고와 함께 그대로 시작. 있는 Check 명령은 그대로 실행되고, 하나도 없으면 Ticket의 checks 줄이 ⚠️ no checks |
gates.ci | CI check가 하나도 없는 pull request는 Hand-off | 시작할 때 경고하고, merge하면서 Ticket에 ⚠️ no checks 줄을 남김 |
gates.ci를 꺼도 봐주는 것은 check가 하나도 없는 pull request뿐입니다. CI 실패, CI 시간 초과, base branch와 충돌하는 pull request는 gates 설정과 상관없이 Ticket을 끝냅니다.
gates.checks가 켜져 있으면 Run은 이렇게 거절합니다.
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: 객체
Stage 하나는 claude -p 세션 하나입니다. stages는 Stage마다 객체를 받고, 안의 키는 모두 선택입니다.
| Stage | 하는 일 | maxTurns | maxMinutes |
|---|---|---|---|
implement | Ticket에 /mattpocock-skills:implement를 돌림 | 300 | 60 |
verify | Acceptance Criteria가 충족되지 않았음을 증명하려 하고, 판정을 돌려줌 | 130 | 30 |
fix | fix 기회를 쓰게 만든 실패 하나를 고침 | 150 | 40 |
conflict | rebase가 멈춘 자리에서 충돌을 해결함 | 120 | 30 |
| 키 | 타입 | 기본값 | 넘기는 방식 |
|---|---|---|---|
model | 문자열. sonnet처럼 claude --model이 받는 값이면 무엇이든 | "claude-opus-5-5" | --model |
effort | "low", "medium", "high", "xhigh", "max" | "high" | --effort |
maxTurns | 1 이상의 정수 | 위의 Stage별 값 | --max-turns |
maxMinutes | 0보다 큰 수 | 위의 Stage별 값 | 세션을 kill하는 wall-clock 한도 |
extraPrompt | 문자열 | "" | Stage 프롬프트의 마지막 섹션으로 덧붙임 |
모델과 effort는 언제나 넘기므로, 머신에 있는 claude의 기본값은 쓰이지 않습니다. 턴이나 시간을 다 쓴 Stage가 Ticket에 무엇을 치르게 하는지는 Ticket 하나가 merge되기까지에 있습니다.
permissionMode
permissionMode:"auto","acceptEdits","bypassPermissions"
모든 Stage에 --permission-mode로 넘깁니다. 모든 Stage는 --permission-prompts none으로도 실행됩니다. 지켜보는 사람이 없으니, 물어봐야 할 일은 묻는 대신 거부됩니다.
CI
ciTimeoutMinutes: 0보다 큰 수ciGraceMinutes: 0 이상인 수
Landing이 pull request를 연 뒤, Run은 최대 ciTimeoutMinutes 동안 check를 기다립니다. 시간 안에 끝나지 않는 CI는 fix 기회를 쓰지 않는 Hand-off입니다.
ciGraceMinutes는 GitHub가 check run을 등록하기 전의 틈을 메웁니다. 이 틈이 3분을 넘긴 적도 있습니다. 이 시간이 지나기 전까지는 check가 아직 없는 pull request를 "없음"이 아니라 대기 중으로 봅니다. ciTimeoutMinutes보다 길어지지는 않습니다. Actions 대기열이 느린 Target이라면 늘리세요. CI가 아예 없는 Target은 0으로 두세요. 그러면 첫 확인에서 바로 답이 나옵니다. 기본값 그대로면 merge할 때마다 이 시간을 한 번씩 기다리고, Landing에는 한 번에 Lane 하나만 들어가니 그동안 다른 Lane의 merge도 함께 기다립니다. CI가 있는 Target은 유예 시간을 남겨 두세요. GitHub의 답으로는 CI가 없는 것과 아직 등록되지 않은 것을 구별할 수 없어서, 0이면 늦게 뜨는 check run도 없는 것으로 읽힙니다.
labels
labels: 문자열 객체
Target이 이미 다른 라벨 이름을 쓰고 있다면 triage 어휘의 이름을 바꾸세요. 준 키만 바뀝니다.
| 키 | 기본값 |
|---|---|
needsTriage | needs-triage |
needsInfo | needs-info |
readyForAgent | ready-for-agent |
readyForHuman | ready-for-human |
wontfix | wontfix |
inProgress | in-progress |
init은 이 이름으로 라벨을 만들고, 하나라도 없는 Target은 Run이 거절합니다. 각 라벨이 파이프라인에 어떤 뜻인지는 일 계획하기에 있습니다.
관련 페이지
- 실행하기:
--lanes, 그리고 이 파일을 읽는 Run - Ticket 하나가 merge되기까지: 각 Stage, Check, 게이트가 Ticket의 생명 주기 어디에 있는지
- 일 계획하기: 라벨과 그 뜻
- 설치와 제거:
init이 빈 파일을 만들고,remove가 맨 마지막에 지움