spar-cli 0.1.2

Two AI coding agents alternate implementing and reviewing GitHub issues until a PR converges.
Documentation
# 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.

[agents.claude]
preset = "claude"
model  = "fable"      # fable | opus | sonnet, or a full name like claude-fable-5
effort = "high"       # high | low | medium | xhigh | max
                      # Omit either line to use the CLI's own default.
                      # Check the current sets with: claude --help

[agents.codex]
preset = "codex"
model  = "gpt-5.6-sol"
effort = "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"

[loop]
max_rounds        = 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.
auto_merge        = false    # off on purpose: two models agreeing is not the
                             # same as being right, and neither carries the
                             # consequences of a bad merge
first_implementor = "claude"
base_branch       = "main"   # only a fallback; origin/HEAD wins when it exists
worktrees         = true     # isolate each issue in its own git worktree
keep_worktrees    = false    # keep them after a run, for inspection
branch_prefix     = ""       # branches are issue-N and pr-N. Set e.g. "spar/"
                             # to namespace them
close_skipped     = true     # close an issue both agents independently declined,
                             # after posting the shared reasoning
parallel_triage   = true     # both agents triage at once; they only read
absorb_new_issues = 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.
followups         = "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
file_nits         = false    # nits stay in the PR thread, never the tracker
state_store       = "local"  # local | pr | both. local keeps resume state in
                             # .spar/state and off the PR

[loop.effort_schedule]
round_1 = "ultra"            # the deep first review
rest    = "high"             # later rounds only see a small delta

[style]
ban_em_dash        = true
ban_ai_attribution = 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.
terse              = 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
pr_comments        = "outcome"
max_title_chars    = 90      # a finding, issue, or PR title
max_summary_chars  = 200     # a one-line verdict or refutation
max_detail_chars   = 320     # a blocking finding's explanation, in the thread
max_body_chars     = 900     # a PR body
max_issue_body_chars = 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.