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
# 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_file}
# Include {schema_file} and spar uses the CLI's native structured output; leave
# it out and spar asks for JSON in the prompt and parses it back.
#
# 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.
= "issues" # issues | local | none. Where out-of-scope findings
# go. Use local on a repo that is not yours: notes
# land in .spar/followups.md instead of the tracker
= false # nits stay in the PR thread, never the tracker
= "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"
= 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.