1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# Copy to spar.toml, or run `spar init` to generate one from the CLIs you
# actually have installed. Each agent inherits a command template from a preset
# compiled into the binary; anything set here overrides it.
[]
= "claude"
= "fable" # fable | opus | sonnet, or a full name like claude-fable-5
= "high" # high | low | medium | xhigh | max
# Omit either line to use the CLI's own default.
# Check the current sets with: claude --help
[]
= "codex"
= "gpt-5.6-sol"
= "ultra" # Codex does not list these in its help. Your
# ~/.codex/config.toml is the source of truth, and effort
# is passed through as -c model_reasoning_effort=...
# Everything a preset supplies can be overridden per agent. The full set:
#
# preset = "claude" # inherit a built in command template
# models = ["a", "b"] # hints only, written into a config generated by
# efforts = ["a", "b"] # `spar init` as comments. Never validated
# options_note = "..." # against, so a value not listed still works.
# model = "fable" # omit to use the CLI's own default
# effort = "high" # omit to use the CLI's own default
# command = [...] # a template of your own, instead of a preset
# output = "text" # text | jsonl | json
# timeout = 1800 # seconds one call may take before spar gives up
# search_paths = ["~/.local/bin"] # extra places to look for the binary
# system_via = "prompt" # prompt | placeholder. Where the style rules go
# message_path = "item.text" # jsonl only: where the answer lives
# [agents.NAME.message_match] # jsonl only: which event carries the answer
#
# Any two agents work. To pair different CLIs, point at other presets:
# [agents.gemini]
# preset = "gemini"
#
# Or declare a command template inline, with no preset at all. An argument group
# whose placeholder is unset is dropped whole, so omitting `model` drops the
# `-m` flag rather than passing an empty string.
#
# [agents.custom]
# command = ["mytool", ["-m", "{model}"], "--prompt", "{prompt}"]
# output = "text"
# timeout = 3600 # a slow model on a large repo
#
# Placeholders: {prompt} {system} {model} {effort} {cwd} {schema} {schema_file}
#
# Include {schema} or {schema_file} and spar uses the CLI's native structured
# output, which is worth doing: without it spar asks for JSON in the prompt and
# parses whatever comes back, and a long answer that hits the model's output
# limit stops mid-object and is lost. {schema} passes the schema as an argument
# (Claude Code), {schema_file} passes a path to it (Codex).
#
# For a CLI that emits an event stream rather than plain text, say where the
# answer lives:
#
# [agents.custom]
# command = ["mytool", "--json", "{prompt}"]
# output = "jsonl"
# message_path = "item.text"
# [agents.custom.message_match]
# type = "item.completed"
# "item.type" = "agent_message"
[]
= 3 # review rounds ONE invocation may spend before
# escalating. Resuming a PR grants a fresh budget,
# so this is not a lifetime cap. Round numbers keep
# counting up so the ledger stays coherent.
= false # off on purpose: two models agreeing is not the
# same as being right, and neither carries the
# consequences of a bad merge
= "claude"
= "main" # only a fallback; origin/HEAD wins when it exists
= true # isolate each issue in its own git worktree
= false # keep them after a run, for inspection
= "" # branches are issue-N and pr-N. Set e.g. "spar/"
# to namespace them
= true # close an issue both agents independently declined,
# after posting the shared reasoning
= 0 # ignore issues and PRs numbered below this when
# spar is picking for itself. 0 is no floor. A long
# lived repo carries a tail of old issues nobody is
# going to reach, and spar takes the LOWEST numbered
# open items, so without a floor it walks straight
# into them. A number you name explicitly is always
# honoured.
= true # both agents triage at once; they only read
= 0 # waves of newly filed follow-ups to fold back into
# the same run rather than leaving them for the
# next one. 0 is off. Each wave is triaged like any
# other issue, so both agents still have to agree.
# Multiplies what a run costs.
= "local" # issues | local | none. Where a follow-up goes.
# local writes .spar/followups.md and leaves the
# tracker alone, which is the default because a
# thorough reviewer always finds something and a
# tracker is somebody's queue.
= false # file a non-blocking finding as a follow-up. Off,
# and this is the setting that stops a run breeding:
# with it on, one issue produced ten follow-ups,
# four of which produced more. Not gating a merge is
# not the same as deserving a triage queue.
= false # nits stay in the PR thread, never a follow-up
= 5 # most one run may record before it stops and says
# what it dropped. A backstop, not a target.
= "local" # local | pr | both. local keeps resume state in
# .spar/state and off the PR
[]
= "ultra" # the deep first review
= "high" # later rounds only see a small delta
[]
= true
= true
# What made a PR thread unreadable was never the length of the findings, it was
# spar narrating itself. So spar composes every comment from structured fields,
# and that is where brevity comes from.
#
# The lengths below are safety valves, not editors. They are sized so real
# content is never touched, and when one does fire it finishes the sentence in
# progress rather than stopping mid thought. A reader who cannot act on a
# finding has been given nothing, and the characters saved bought nothing.
= true
# How much of its own working spar narrates into a PR thread. The agents never
# read the thread, so none of this affects the loop.
# outcome one comment at the end, and only if it has something to say
# rounds a comment per review and per response, an audit trail
# none never comment on a PR; everything goes to the terminal
= "outcome"
# Set this to "none" and spar never comments on a
# pull request in any mode, which is the standing
# equivalent of `spar review --dry-run`.
= 140 # a finding, issue, or PR title. Never ellipsised:
# a title ending in "..." reads as broken.
= 1200 # a verdict or a refutation's argument
= 2000 # a blocking finding's explanation, in the thread
= 2000 # a PR body
= 8000 # a filed issue's body. Far larger on purpose: a
# comment is read with the diff in front of you,
# an issue is picked up cold months later. Fenced
# code blocks are never truncated and never count
# against this at all, so steps to reproduce and
# the offending snippet always survive.