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
# 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
= 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
# The concision gate. spar composes every comment itself from structured fields
# and holds each one to a budget, so a model that writes three paragraphs where
# one sentence would do cannot put them on your PR.
= 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"
= 90 # a finding, issue, or PR title
= 200 # a one-line verdict or refutation
= 320 # a blocking finding's explanation, in the thread
= 900 # a PR body
= 4000 # 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.