spar-cli 0.4.32

Two AI coding agents alternate implementing and reviewing GitHub issues until a PR converges.
Documentation
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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
# spar

Two coding agents alternate implementing and reviewing GitHub issues until a PR
converges. Neither agent reviews its own most recent edit.

Roles are not fixed. Whoever holds the PR may implement, review, fix, or file
follow-ups, and then hands custody to the other. A reviewer that finds a problem
can either fix it directly and hand back for review of its own fix, or return
notes for the author to address. A reviewer that fixes its own findings reports
on each one, so a point it decided against stays open and reaches the author
with its reason rather than being recorded as fixed.

It also reviews without touching: `spar review <pr>` puts two independent
reviewers on a pull request you cannot push to, including one from a fork, and
reports what they agree on and what they do not.

Ships pairing **Claude Code** with **OpenAI Codex**. Any two CLIs work.

## Why alternate instead of assigning roles

A single model reviewing its own work grades its own homework. Two models with
different training and different failure modes catch different things, and the
disagreements are the useful part. In an early run, Codex flagged that Python's
`\d` matches Unicode digits, so `"Ù¡h"` parsed as a valid duration. The Claude
agent had missed it. On a separate point the Claude agent pushed back and
refused the change with a reason, which is what kept the code from drifting.

Refutation is a first-class outcome. An agent that accepts every review comment
to get approved produces worse code, not better.

## Install

Needs `git`, `gh` (authenticated), and two agent CLIs. Rust 1.85 or newer to
build (`rustup update` if yours is older).

```bash
cargo install spar-cli
spar doctor
```

The package is named `spar-cli` because `spar` is taken on crates.io by an
unrelated config parser. The binary it installs is `spar`, which is what you
type.

For whatever is on main rather than the last release:

```bash
cargo install --git https://github.com/coreyphillips/spar spar-cli
```

From a clone:

```bash
git clone https://github.com/coreyphillips/spar && cd spar
cargo install --path .
```

`spar doctor` checks every prerequisite, resolves each configured agent, and
tells you which is missing. Presets are compiled into the binary, so there is
nothing to copy alongside it.

### After upgrading

`spar init` will not touch a config that already exists, so a release that adds a
setting would otherwise be invisible. `doctor` lists every setting your file does
not mention, with its default, and `init --update` appends them as comments
without changing a line of what is already there:

```bash
spar doctor                 # says what is new since you wrote your config
spar init --update          # appends those settings, commented out
```

## Trying it without letting it do anything

Every command that writes has a way to not write. Worth knowing before the first
run on a repository you care about:

```bash
spar triage 42              # judges the issue, writes plan.json, touches nothing.
                            # Prints the tracker decomposition too, with
                            # decompose_trackers on
spar review 108 --dry-run   # full two agent review, printed, nothing posted
spar checkin 108 --dry-run  # every reply and every change, posted nowhere
spar answer 229 --dry-run   # what it would answer for you, posted nowhere
spar followup --screen-only # judges the queue, writes nothing, files nothing
spar code "Add CSV export" --dry-run # drafts an issue, printed, nothing filed
spar split 10 --dry-run     # the parts it would make, filed and branched nowhere
spar run 42 --no-close-skipped   # will not close an issue it declines
spar listen --dry-run       # every event and what it would trigger, nothing triggered
```

`--dry-run` is on `review`, `checkin`, `answer`, `code`, `split` and `listen`. The first
three are the commands whose whole output is a comment; `split` writes more kinds of thing than
any other command, which is why its read-only half exists. `code --dry-run`
previews the issue before it is filed or implemented. `listen --dry-run` is
different in kind: it calls no agent at all, since what it would have run is the
thing being previewed, and `spar review N --dry-run` is how to see a review's text.
There is no `--dry-run` on `run`, since `run` writes code: `triage` is its
read-only half, and `followup --screen-only` is `followup`'s.

### Write outcomes

On normal completion, a command that attempted GitHub writes prints one summary
for the whole command:

```text
writes: 3 attempted, 2 succeeded, 1 failed
```

One failed write does not stop independent work. Spar finishes the remaining
items, reports every outcome together, and returns a non-zero status if any
write failed. The same holds for a failed triage in a later wave: it ends the
waves, and the run still resumes the pull requests named on the command line and
prints the report, with the waves that did not run named in it. This includes partial failures and cleanup with
`spar clean --pr-state`. `spar post` follows the same policy.

The counts describe logical outcomes, not command invocations. If a GitHub
command reports an error but a read-back proves the change landed, it counts as
one success. Finding an exact duplicate is a no-op, so it counts as no attempt.
If a required check fails before a write can safely start, it counts as one
failed write.

A hard command error can stop before this final summary. The error is printed
directly and returns a non-zero status.

For something persistent rather than per invocation, these live in `spar.toml`:

```toml
[loop]
followups   = "local"   # the default. Records to .spar/followups.md, never the
                        # tracker. "none" drops them entirely.

[style]
pr_comments = "none"    # never comment on a pull request, in any mode.
                        # The standing equivalent of --dry-run.
```

## Quick start

```bash
cd ~/projects/thing
spar init      # detects your installed CLIs, writes spar.toml
spar doctor    # confirms it works
spar run 478   # triage issue 478, and open a PR if it is worth doing
spar code "Add a JSON output option to the status command"
```

## Use

Point it at a number and it works out the rest. Issues and pull requests share
one number sequence per repository, so spar sorts them itself: `spar run 42` does
the right thing whether 42 is an untouched issue, an issue that already has a
pull request open, or a pull request. Omit the numbers and `run` takes every open
issue while `resume` takes every open PR.

Work already on a pull request goes first. Name a pull request alongside some
issues and it is continued before triage runs, rather than after every issue has
been through its own implement and review loop, because finishing what is in
flight beats starting something new. Name both a pull request and the issue it
closes and that is one piece of work, not two: the issue is dropped from the
queue and the pull request carries it.

```bash
spar code "Fix retries when the server returns 429" # draft, file, and work an issue
spar code "Add CSV export" --dry-run   # preview the issue without filing it
spar code "Add CSV export" --file-only # file the issue for a later run

spar run                    # triage every open issue, then work them in order
spar run 42 51 60           # or name the ones you care about
spar triage                 # triage only, write plan.json, touch nothing
spar run --limit 50         # raise the cap on how many open issues to take
spar run --min-number 480   # ignore anything numbered below #480
spar run 42 --auto-merge    # merge when no blocking findings remain
spar run 42 --max-rounds 5  # allow five rounds that can ask for changes
spar run 42                 # if 42 already has an open PR, continues that
spar run 101                # 101 is a PR? it reviews it, no triage
spar run 42 101             # mixed is fine
spar run 42 --first codex   # the other agent implements
spar run 42 --base develop  # base branch, if origin/HEAD is not what you want
spar run 42 --plan-out /tmp/plan.json      # where the triage plan is written
spar run 42 --no-close-skipped   # comment on a declined issue but leave it open
spar run 42 --close-skipped      # close it (the default, so rarely needed)
spar run 42 --absorb 1      # work the follow-ups this run files, in this run

spar followup               # work the follow-ups recorded in .spar/followups.md
spar followup --screen-only # print the verdicts, write nothing
spar followup --file-only   # file the issues, leave them for a later run

spar checkin 108            # answer the unanswered comments on a PR
spar checkin                # every open PR
spar checkin 108 --dry-run  # every reply and every change, posted nowhere
spar checkin 108 --reply-only    # answer in words, never touch the code

spar answer 229             # answer what issue 229 asks of you by name
spar answer                 # every open issue that mentions you
spar answer 229 --dry-run   # print the answer, post nothing

spar split 42               # break an issue into smaller issues, and stop
spar split 108              # break a PR into smaller PRs, leaving it untouched
spar split                  # go through every open issue and PR
spar split 42 --dry-run     # print the proposal, write nothing

spar review 108             # review a PR without touching it, fork or not
spar review 108 --dry-run   # print the review instead of posting it
spar review                 # review every open PR
spar review 108 --verdict   # approve or request changes instead of commenting
spar review 108 --recheck   # rule on the last review's points, review what landed since

spar listen                 # watch the repo: second looks at reviewed PRs that move,
                            # reviews of what is assigned to you or awaiting your review
spar listen 108 42          # watch only these, and stop once they are closed
spar listen --once          # one poll, act on what is due, exit; for cron
spar listen --dry-run       # say what each event would trigger, trigger nothing
spar listen --since 2h      # on the first run, catch up on the last two hours

spar brainstorm novel uses of Bitcoin Script for shared custody
                            # both agents propose, build on and challenge each
                            # other's ideas, then rank; written to a local file
spar brainstorm             # read the repository and brainstorm ideas for it
spar brainstorm --file-issues    # and file each kept idea as an issue
spar brainstorm --from .spar/brainstorms/x.md --file-issues
                            # file a saved session, edited or not, calling no agent

spar resume                 # continue the loop on every open PR
spar resume 108             # or name them
spar resume 108 --next codex     # override whose turn it is

spar run 42 --quiet         # suppress progress; warnings and errors still print
```

`--quiet` works before or after the subcommand.

`spar run`, `spar code`, and `spar followup` print a progress line each time an item
finishes, so a long run says how far along it is:

```
spar: [########............] 2 of 5 done, #123 approved, 3 left, 1h12m elapsed
```

The total starts with any pull requests being continued, grows once triage has
said which issues get worked, and grows again when a wave of follow-ups joins.

With no numbers given, spar takes the 20 lowest numbered open items. It says
which ones it picked, and says so explicitly when there were more than the cap
rather than quietly truncating. Raise it with `--limit`.

Because it takes the **lowest** numbered items, a repository that has been going
a while walks straight into a tail of old issues nobody is going to reach.
`min_number` puts a floor under that:

```bash
spar run --min-number 480      # or min_number = 480 in spar.toml
```

The floor is applied before the cap, which is the part that matters: filtering
afterwards would fill the cap with the oldest items and then discard them all,
leaving nothing. So `--min-number 480 --limit 20` gives you the twenty lowest
numbered items **at or above** #480, and spar says how many it skipped. A number
you name explicitly is always honoured, floor or not, because naming it is the
point.

Works on any GitHub repo you have cloned. It follows `gh` conventions: the repo
comes from the checkout you point at, and the base branch is detected from
`origin/HEAD` rather than assumed to be `main`.

Without push access, a new branch goes to your fork instead, made if you have
none, and the pull request is opened from there. The fork is reached the way
origin is, the same host alias, user, and port with only `owner/repo` swapped,
and your checkout's remotes are left alone. A branch of the same name already
on the fork is checked before any agent runs: one whose commits a pull request
holds is replaced, and one with work nothing else has stops the run. Set
`fork_when_denied = false` under `[loop]` to have the push fail instead.

`spar resume` and `spar checkin` carry on a pull request from your own fork,
pushing to the fork. One from anybody else's fork is reviewed without being
changed.

```bash
spar run --repo ~/projects/thing 42     # --repo belongs to the subcommand
```

### Opening pull requests as drafts

```toml
[loop]
drafts = "until_approved"   # never | until_approved | always
```

`until_approved` opens the pull request as a draft and marks it ready the moment
the review has no blocking findings left. That is what the draft was saying while
two agents were still arguing about it, so it clears itself rather than becoming
something you have to remember to promote. A run that ends escalated or out of
rounds leaves it a draft, which is correct: it is not ready.

`always` opens a draft and leaves it, for somebody who promotes every pull
request by hand. It cannot be combined with `auto_merge`, and spar refuses the
pair rather than picking a winner, because merging a draft means promoting it
first and that is the one thing `always` asks it not to do.

### Telling it something the config has no setting for

Both agents take extra instructions, standing ones from `[loop] instructions`
and per-run ones from `--instructions`, which adds to them rather than replacing
them:

```bash
spar run 42 51 --instructions "Do not wait for CI. If it is red, pick it up on the next pass."
spar resume 108 --instructions "Prefer the smaller fix; we ship tonight."
```

They arrive after the request and before the schema, under a header saying where
they came from, so they change how the work is done and not what was asked for
or the shape of the answer.

Each CLI already reads its own conventions file, CLAUDE.md or AGENTS.md, and
will keep doing so. What this adds is one place both agents see, since two
agents given different standing instructions are not the pair the design rests
on.

Note that spar itself never waits on CI, and never reads it: an agent that
waits is doing it on its own initiative, which is why that example is an
instruction rather than a setting.

## Starting from a prompt

`spar code` takes a feature request, bug report, or enhancement in your own
words, turns it into one detailed GitHub issue, and runs that issue through the
same pipeline as `spar run`.

```bash
spar code "Add JSON output to status, keeping the current text output as the default"
spar code "Fix the retry loop when the server returns 429" --max-rounds 5
spar code "Add CSV export" --dry-run
spar code "Add CSV export" --file-only
spar code "Add CSV export" --repo ~/projects/thing --config ~/projects/thing/spar.toml
```

The configured first implementor reads the checkout and drafts the title and
body, including the motivation, current and requested behavior, scope,
acceptance criteria, and a test plan. Reproduction steps and file references
must come from the request or the code; assumptions and open questions are
called out. The drafting step only reads the project. Both the config's
instructions and `--instructions` apply to drafting and the subsequent run.

By default, Spar files the issue and immediately hands its number to normal
triage, implementation, and alternating review. Both agents must still agree
that it is worth doing. The usual run settings apply, including the base
branch, worktrees, draft PRs, checks, review budget, follow-ups, and merge policy.
`--limit` and `--min-number` do not filter out the new issue or select other open
issues. The triage plan is written to `plan.json`, or the path in `--plan-out`.

`--dry-run` prints the cleaned title and body without filing or implementing.
`--file-only` files the issue and stops. These two flags cannot be combined.
A nonempty prompt is required. Quote prompts containing command-line flags,
newlines, or shell punctuation, or put `--` before the prompt after any options.

The drafted acceptance criteria are `- [ ]` checkboxes, and the run treats them
as a list: each agent is asked about every one by number, and a box is ticked
when spar merges a pull request the review ruled met it. See "Acceptance
criteria" under "How a run goes".

The issue URL and a `spar run <number>` recovery command print before triage,
even with `--quiet`. If a later step fails, use that issue number to continue.
Running `spar code` again drafts and files a new issue.

Issue drafting adds one structured call before the usual run. Its effort can be
set with `code` under `[loop.effort_schedule]` or an agent's own effort schedule.
When unset, it falls back to `round_1` and then the agent's configured effort.

## How a run goes

**Triage.** Both agents independently judge every issue: is it worth doing, how
complex, what does it depend on, how risky. Complexity and risk are defined in
the prompt in terms an agent can check, files touched and whether a test exists
for the first, blast radius and reversibility for the second, and the agents are
told what the answers are used for and that the larger of the two wins. Where
they land two steps apart on a three point scale, the run says so: that is the
scale not being shared rather than a rounding difference. They run at the same time, and
neither sees the other's answer. Then reconcile mechanically:

- Both say do, it is scheduled.
- Both say skip, the shared reasoning is posted and the issue is closed as not
  planned. Turn that off with `close_skipped = false`.
- They disagree, that issue is parked for you and the run continues. One agent
  never overrules the other. The terminal report lists it with both positions
  and both reasons, and it is remembered in `.spar/state/contested.json`, so
  later runs leave it alone rather than spending two calls asking the same
  question again. `--retriage` asks again once you have acted on it.

Scheduled issues are ordered by dependency, then cheapest first, so blockers
clear early and risky work inherits a healthier base. Every worktree is built
from `origin/<base>`, so a dependency's work is only there once it has merged:
an issue whose dependency has not merged is held back with the reason rather
than built on a base that lacks it, and so is one whose dependency both
reviewers declined or disagreed about. A dependency outside the run is named in
the log and holds nothing, since it may well be merged already. The plan is
written to `plan.json`, and the preview names what each item waits on.

**Per issue.** An isolated git worktree, so a failed issue cannot poison the
next one's base. The first agent implements and opens a PR. With
`first_implementor = "alternate"` that agent swaps from issue to issue, so both
models write first drafts and both review them: roles alternate inside a pull
request either way, and without this one model authors every one of them, blind
spots and all. Who wrote each first draft is recorded in
`.spar/state/implementors.json`, and `--first` still pins one agent for a run. Custody passes to
the other, which reviews with full repo context rather than a bare diff. Each
finding is labelled `blocking`, `non-blocking`, or `nit`.

**Rounds.** `max_rounds` is a budget for one invocation, not a lifetime cap on
the PR, and it bounds the asking rather than the run. A run that escalates after
three rounds and is then resumed gets three more, because a person looked at it
and chose to continue. Round numbers keep counting up (4, 5, 6) so the ledger and
the PR history stay coherent.

**The close.** A round is review and then fix, so the fix comes last and the
budget used to run out on a commit nobody had read. After the last round, one
closing pass by the agent that did not write the head reads what landed and the
points the author says it fixed, and the run ends approved or with the remaining
points named for a person. That is at most one additional review call beyond
the calls used by the rounds, on a run that spends its whole budget and has
something new to read. A run that converges before the budget, or whose last
round changed nothing, buys none.

**Acceptance criteria.** Every unchecked task list item in the issue body is an
acceptance criterion, wherever it sits in the body. The implementor is handed
them numbered and reports done, not done, or needs a person on each, with one
line of reason. "Needs a person" is for what the repository cannot show, such as
a run on a device. That goes in the pull request body under `## Acceptance
criteria`, labelled as the author's account. Each review, and the closing pass,
rules on the same list: met, not met, or needs a person, and what it checked.
The ruling that counts is the one from the pass that signs the pull request off,
so a tick always comes from the agent that did not write the head, never from
the author's claim. A ruling does not gate the merge.

When spar merges the pull request itself, it ticks each criterion ruled met on
the issue, the same way the tracker checklist is ticked: the body is read again
before each write, a line that changed, moved or now appears twice is skipped
with the reason in the log, every other byte is left alone, and a box is never
unchecked. A merge that only joined a merge queue ticks nothing. The outcome
comment names every criterion not ruled met, with the ruling and its reason,
even on an otherwise clean approval. With `auto_merge` off, or
`tick_criteria = false`, nothing is ticked, because a tick means the work
landed, and the outcome comment lists every ruling instead. Ticks count in the
`writes:` line like any other GitHub write. Resuming a pull request that closes
an issue in this repository does the same. A person merging does not tick
anything.

**Follow-ups.** A review that finds something real but out of scope files it as
its own issue. Before filing, spar looks for an issue that already describes the
same defect, comparing titles and bodies rather than matching strings, because
two agents never word one defect the same way. If it finds one it adds whatever
the new pass learned as a comment there instead of opening a second issue, and
says nothing if the new pass learned nothing.

By default those follow-ups wait for the next run. `absorb_new_issues` folds them
back into the current one instead, a wave at a time, each wave triaged like any
other issue so both agents still have to agree it is worth doing. It is off by
default because it multiplies what a run costs.

With the default `followups = "local"` they go to `.spar/followups.md` instead
of the tracker, and `spar followup` is what works that file. See below.

**Convergence.** Only `blocking` findings gate the merge. Everything else is
either filed as a follow-up issue or listed on the PR, and the PR proceeds. This
matters: a competent reviewer can always find something, so "no objections
remaining" is not a stopping condition, but "no blocking objections" is. A real
defect too small to be worth another round is `non-blocking`, and every in-scope
one a reviewer chose not to gate on is listed under "Noted, not blocking", so
downgrading a point is visible rather than a way to delete it. The author is
handed those points in the same response call it is already making, marked
optional: a two line fix lands with the round it was going to spend anyway, and
anything it declines is reported for you exactly as before. Nothing about that
touches the round budget or the merge gate, and the reviewer is told what each
label costs, so it chooses one knowing where the point ends up.

A run that reaches an ending reports unresolved points, deadlocks, unchanged
branches, and unread closing work on the pull request when comments are
enabled. A clean approval stays silent unless an acceptance criterion was not
ruled met, and `pr_comments = "none"` suppresses
all pull request comments. A failure before the closing pass ends in an error
with the reason and posts no final outcome comment. A closing-pass failure
keeps the review record and reports that the last changes remain unread.

## Resuming work someone else started

`spar resume <pr>` picks up any open PR, whether or not spar created it. Human
authored, agent authored, half finished, does not matter: if it has a branch and
a diff, the loop can take custody of it.

`spar run <issue>` does the same thing when that issue already has work open. It
checks its own branch naming first, then falls back to GitHub's issue linkage, so
a pull request somebody else started on a branch called anything at all is found
and continued rather than duplicated. Running spar again on work in progress is
always a resume, never a restart. A lookup that fails is an error for that issue
rather than an answer, since "no pull request exists" is the answer that makes
spar implement over the top of somebody's work. spar also says when the list
filled its page, and when the base is not the default branch, where GitHub does
not record closing links at all.

If a branch carries pushed commits that no open pull request accounts for, spar
refuses rather than force pushing over them, and says how to recover.

## Reviewing what you cannot push to

A pull request from a fork cannot be resumed: spar has to push its fixes back to
the PR's branch, and that branch is not in your repository. Reviewing it is still
the useful thing, and for an outside contribution it is usually the only thing
you wanted, so `spar review` does that and `run` falls into it automatically when
it meets a fork.

```bash
spar review 108             # any PR, fork or not
spar review 108 --dry-run   # see it before it is posted
```

### Agreeing with a dry run

A dry run keeps what it produced, so liking it does not mean paying for a second
review:

```bash
spar review 108 --dry-run   # prints it, and saves it under .spar/reviews/
spar post 108               # posts exactly that, agents not run again
```

`spar post 108 --dry-run` shows what would go up. The saved file is plain
markdown, so editing it before posting is the expected thing rather than a
trick: strike the finding you disagree with, then post. Anything you post still
goes through the style gate, so an edit that reintroduces an em dash is caught
rather than published. `spar post 108 --file notes.md` posts something else
entirely. What `spar post` puts up is the comment alone: findings go on their
lines only in a review that posts itself, since the saved file is yours to edit
and a line comment rebuilt from the record could bring back a finding you
struck.

The same saving happens when `pr_comments = "none"` suppresses a comment, so
nothing spar spent money producing is thrown away.

The loop is different here, and deliberately so. The custody loop converges
because the diff changes between rounds. In review only mode nothing changes, so
more rounds would just re-litigate the same unchanged code. It is three passes:

1. **Independent review.** Both agents review at the same time, neither seeing
   the other. A finding both reach on their own is the strongest signal there is.
2. **Cross-adjudication.** Each reads the other's remaining findings, goes to the
   code, and rules on them. A point one model raised and the other examined and
   rejected is usually a pattern match, and saying so is worth more to you than
   forwarding both.
3. **Rebuttal.** Anything rejected goes back to whoever raised it, to withdraw or
   to substantiate with the line, the input, the failing case.

What comes out is sorted by how well it is attested, so a maintainer can see at a
glance which findings carry two independent signatures:

```
Two independent reviews.

needs changing before merge
- Retry loop never terminates when max_attempts is unset (src/net.rs:88) [both].
  Both reviewers reproduced this: the guard on line 91 compares against Some(0).

the reviewers disagree, your call
- Config loader swallows a parse error (src/config.rs:210). Objection: the
  caller validates against the schema first. Answer: the validation runs after.
```

Nothing is committed, pushed, merged, or closed. `--max-rounds` picks how far it
goes: 1 is two independent reviews with no cross-checking, 2 adds it, 3 adds the
rebuttal.

What goes up is a comment, unless you ask for a verdict:

```bash
spar review 108 --verdict   # approve, or request changes, as GitHub shows them
```

The verdict is derived from the findings and never asked of a model: approve
when nothing blocks, request changes when a blocking finding both reviewers count
stands, and a plain review comment with no verdict when the only thing short of
approval is a blocking finding the two dispute, which is yours to settle. The
review names the commit the reviewers read, so a push that lands while it runs
gets no approval nobody gave it. GitHub refuses an approval from the account
that opened the pull request, so on your own the verdict travels as words in a
comment.

Each finding the comment lists also goes on the line it is about, when the pull
request's diff shows that line, so it can be answered and resolved where it is.
The comment still lists every finding, but names one on a line by its title,
location and tag alone, so nothing is read twice. Its line comment carries the
detail, and for a disputed point the objection and the answer. A finding with
no line, or a line the diff does not show, is in the comment alone and in full,
since GitHub refuses a whole review over one line comment it cannot place. If
GitHub refuses one anyway, the review goes up again without them, with every
finding in full, and spar says so. A review that is saved rather than posted is
saved in full too, since `spar post` sends no line comments. A line comment can
only travel on a review, so without `--verdict` a review that has one goes up
as a plain review comment, which approves nothing and asks for nothing. A reply
in one of those threads is something said since, so a second look reads it,
and the new points a second look raises go on their lines the same way. What
it says about earlier points is unchanged. `inline_comments = false` under
`[style]` keeps every finding in the comment alone, in full.

Every review now leaves a record under `.spar/state/review-N.json`: the commit it
read, when it began, and the points it raised with how well each was attested.
That is what makes a second look possible:

```bash
spar review 108 --recheck   # after the author pushed or replied
```

A second look reads the record, and before any agent runs it checks with `gh`
alone whether anything changed: a new head, or something somebody other than you
said since. Nothing new means nothing spent. Otherwise both agents, independently,
rule on every earlier point by number with the code in front of them, fixed,
rebutted by a quoted reply, or open, and review only the commits that landed
since for anything new. A point is closed only when both close it; one reviewer
still seeing it keeps it open and says so; a reviewer that did not answer closes
nothing. New findings go through the same cross-check as a first review. The
verdict follows the same rule as `--verdict`, earlier points included: a blocker
the first look left disputed is still yours to settle, not a change request,
for as long as it stays open. The comment leads with the
commits it read, then what was addressed, what is still open, what is new, and
what the two disagree about. A branch rewritten since the review is said as
such, and every earlier point is verified from scratch. The record is rewritten
afterwards, so the next look starts from this one.

A review or second look reads a head only once the pull request and its branch
agree on it. GitHub moves a pull request to a pushed commit some time after the
push, sometimes minutes, and until then it names the commit before. A look that
finds the head moved again by the time it is ready to post does not post: what
it wrote is saved for `spar post`, no record is written, and the run ends
`moved` so the next look reads the new head.

This is also the cheapest way to adopt spar. No agent writes a feature from
scratch; it only reviews what already exists.

Resuming needs state that GitHub cannot provide. Spar records accepted changes
and comments through the same git and GitHub identities, so `author` is always
the human who ran spar and authorship reveals nothing about who acted last. Spar
therefore keeps its own state: round number, whose turn is next, and the full
disputed ledger. That lives in `.spar/state/` by default, and can also travel on
the PR as a hidden comment (`state_store = "pr"`) if a run might be picked up
from another machine. That comment is identified by the marker opening the body,
by the payload parsing, and by the author being spar's own account, so a comment
somebody wrote about the block is never patched over or deleted, only reported.

Each checkpoint is bound to the exact published PR head. If the branch moves
without a matching checkpoint, `resume` refuses to guess who may safely review
it. Choose the reviewer explicitly with `--next <agent>`; spar then keeps filed
follow-ups but clears branch-dependent findings and ledger entries before the
new review. State written by an older release has no head binding, so its first
resume preserves the recorded handoff and review state, then binds the migrated
checkpoint to the current head.

A PR with no spar state starts fresh, with the agent that did not implement
taking the first review.

## Brainstorming with the pair

`spar brainstorm` rests on one premise: the pieces of an answer usually exist
already, in the problem's own field or an adjacent one, and what is missing is
the pairing. So the agents are not asked to invent. They are asked to take
inventory of what exists and then to combine, and every idea has to name the
pieces it pairs and the nearest thing that already does something like it.

```bash
spar brainstorm novel ways to use Bitcoin Script for shared custody
spar brainstorm                       # read the repository, brainstorm for it
spar brainstorm --ideas 8 --max-rounds 4
spar brainstorm --out ideas.md        # somewhere you would commit
spar brainstorm --file-issues         # file each kept idea as an issue
spar brainstorm --from .spar/brainstorms/20260912-140307-novel-ways.md --file-issues
```

The subject is words, not a number, and needs no quotes. A word that starts
with a hyphen goes after `--`. None at all means the repository itself: the
agents read it and propose things it could do, or do differently, that would
matter to the people who use it. Open issues are listed in the prompt so what
is already filed is not proposed again.

Nothing is edited between rounds, so this is not the custody loop. It is the
shape `spar review` uses, gated by `max_rounds`:

1. **Diverge.** Both agents propose at the same time, neither seeing the
   other. An idea both reach on their own is the strongest signal there is,
   and it is marked as such and ranks first.
2. **Cross.** Each reads only the ideas the other proposed and, for each,
   builds on it with a further piece, challenges it by naming where it already
   exists or why it cannot work, or keeps it. A build stands beside the
   original as an idea of its own. With the default of three rounds this
   happens once; more rounds repeat it.
3. **Converge.** Whoever proposed a challenged idea defends it with specifics
   or withdraws it, and both rank what is left. The order is the sum of the
   two rankings, and an idea left out of a ranking scores last rather than
   vanishing.

Nothing here lets one agent overrule the other. An idea leaves the set only
when its own proposer withdraws it or never answers the objection, and a
challenge that was defended is kept with both sides of the argument under it.
The first `--ideas` survive, five by default, and the rest are listed with the
reason each was set aside.

The session goes to `.spar/brainstorms/<date>-<subject>.md`, or wherever
`--out` says. It is a header a person reads and then one entry per idea in the
same shape as the follow-up queue: a title, what it combines, how it works, why
it is new, what it enables, its risks, the first experiment, and the objection
and reply where there was one. `spar followup --file` reads it too, so a
brainstorm can be screened and worked like any other queue. Edit the file first
if you like: `--from` files what it says, calls no agent, and the same duplicate
search that guards every other filed issue applies. The agents' names stay in
the file and never reach an issue.

A brainstorm is read only, and it runs both agents at once in a throwaway
checkout of HEAD, the same way triage does. What you have not committed is not
in it.

## Working the follow-up queue

`followups = "local"` is the default, so a review that finds something real but
out of scope writes it to `.spar/followups.md` and leaves your tracker alone.
That file used to be where follow-ups went to die: nothing read it back.

```bash
spar followup                 # screen, file, and work them
spar followup --screen-only   # print the verdicts and write nothing
spar followup --file-only     # file the issues, leave them for a later run
spar followup --limit 3       # take three entries rather than twenty
```

One agent reads every entry against the checkout as it is now and rules on it:
still there, already fixed, not worth it, or a duplicate. Time passes and code
moves, so a note written three weeks ago is often about a defect somebody has
since fixed, and filing it would put a closed question on somebody's queue. What
survives becomes an issue and then goes through ordinary triage, which means
both agents still have to agree before anything is implemented. The screen is
one agent because it is a filter, not a verdict: it is told to say
still_relevant when unsure, because what survives can still be declined and what
it drops is dropped.

An entry that was filed or ruled out leaves the queue, so the file shrinks as it
is worked, and its text is kept in `.spar/followups.done.md` with the verdict
under it. That archive is not sentiment. It is what stops the next run
rediscovering the same defect and recording it again on top of the issue that
now exists for it, and it means a wrong "already fixed" costs you a re-read
rather than the only copy of a real finding.

The queue is ordinary markdown and editing it by hand is expected. An entry is a
`## Title` line and the text under it. spar writes an invisible marker above each
one it appends, so the boundaries are exact for anything it wrote; a file you
keep yourself works without it. Editing an entry before running `spar followup`
changes what gets filed, which is the point.

An entry is only removed after its issue exists, so a run that dies halfway
files one thing twice rather than losing one. The duplicate is caught by the
same search that catches every other one. An entry the screen retitles is
archived under both wordings, so a later run that rediscovers the defect in the
original words still recognises it, and an entry the screen calls a duplicate of
another entry is resolved after the batch: if the one it points at was not
filed, this one stays in the queue rather than being archived as covered by an
issue that has nothing to do with it.

That search reads open issues, and weighs the title separately from the body, so
a long umbrella issue cannot absorb every short finding about the module it
mentions. A finding whose text matches an issue somebody already closed is filed
again with a reference to it, since a defect that was fixed and has come back is
the finding most worth keeping. Whichever way it goes, the log names the issue
that absorbed it.

## Working a tracking issue's checklist

Both agents recognise a tracker and decline to open one pull request for it,
which is right, and used to be the end of it: every run judged the same umbrella
again, commented again, and none of the work written down in it moved. With
`decompose_trackers = true` the checklist becomes the work.

```toml
[loop]
decompose_trackers   = true
max_tracker_children = 5
```

For each unchecked `- [ ]` item, in order:

- It already names an issue (`#123`, or a link to one here): that is its issue.
- It names none, but an issue already covers it: link it, file nothing.
- Neither: file one, then link it.

`max_tracker_children` caps the issues one run files from a checklist. An item
that already links an issue costs nothing against it, since it needs a tick or
an adoption rather than a filing.

The number is written back beside the item, so the tracker body is the record.
There is no state file to go stale, and you can correct a wrong link by editing
one token in your own issue. An item whose issue is closed gets its box ticked,
whoever closed it and whenever, so a tracker somebody has been working by hand
catches up too.

The children are a wave like any other: dependency order, cheapest first, and
both agents triage each one before anything is implemented. An item that is
stale or already fixed is declined there rather than guessed at here. A child
that is itself a tracker is held rather than decomposed in turn.

Read it before you let it act. With the setting on, `spar triage` prints the
whole decomposition, item by item, with the exact body diff, and writes none of
it. `triage` never calls the acting path, so this is the dry run:

```bash
spar triage 42    # what it would file, link, and tick, and the lines it would write
```

Three restraints worth knowing, because this and ticking an issue's acceptance
criteria after a merge are the only things spar does that rewrite text a person
wrote. Both go through the same line surgery:

- **Only a checklist.** Task list items, never an agent's reading of your prose.
  A tracker with no checklist behaves exactly as it did before. Anything GitHub
  does not render as a checkbox is not an item, so a code block or an HTML
  comment is left alone, and a line spar cannot rewrite unambiguously is skipped
  with a reason.
- **It ticks a box and never unchecks one.** A checked box next to an open issue
  is left alone: every state that produces one is a state where you are right
  and the heuristic is wrong. A tick means the work landed, so an issue closed
  as not planned, which is what `close_skipped` writes when both agents decline
  an item, leaves the box alone and says why. An item linked by a fuzzy match is not ticked in
  the same run that linked it, even when the match is closed, so the link is in
  front of you first.
- **Line surgery, proved.** A reference is appended to one line, or one `[ ]`
  becomes `[x]`. Nothing is reflowed, reordered, or re-emitted from a parsed
  model, every other line is checked byte identical before the write, and the
  body is read again immediately before each one so an edit you make mid run is
  not lost.

A tracker is never closed by this, whatever `close_skipped` says. The parts
landing is not the same as the issue being finished.

## Answering the comments on a pull request

The loop's only input is what the two agents produce. Somebody who leaves a
review comment is talking to nobody. `spar checkin` reads what other people
said, judges it, and acts on it.

```bash
spar checkin 108            # one pull request
spar checkin                # every open PR, up to --limit
spar checkin 108 --dry-run  # every reply and every change, posted nowhere
spar checkin 108 --reply-only    # answer in words, never touch the code
spar checkin 108 --any-author    # act on a comment from outside the repo too
spar checkin 108 --again         # re-read what spar already answered
```

Unanswered means three things: an inline review thread GitHub has not marked
resolved, a review summary, and a top level comment, in each case written by
somebody other than you and not already answered. Comments you wrote are
excluded, and so is everything spar wrote: it posts under your account, so every
comment it composes carries a hidden signature, and neither the state block nor
an outcome or round comment counts as you having answered anybody.

Both agents then judge each one. The first rules on it with the code checked
out; the second reads the same comments and those rulings, goes to the code, and
agrees or does not. Five outcomes:

- **A change they both agree is right and belongs here** is made, committed,
  pushed to the pull request branch, answered in that comment's own thread, and
  the thread marked resolved. Not a reply saying it should be fixed. The fix, on
  the branch, with the thread closed off.
- **Right, but really its own piece of work** is filed as an issue, and the
  reply says where it went.
- **Wrong, or not worth doing** gets the argument as a reply, and **the thread
  is left open**. It is not spar's thread to close: the person who raised it has
  not had their say yet. It also shows up under "disputed" in the terminal
  summary.
- **The two disagree** and nothing is changed. It is answered, left open, and
  handed to you.
- **Nothing asked**, such as praise, an approval, a review that found nothing to
  change, or a bot reporting a build, gets no reply and no line in the summary
  comment. It still counts as answered, so it is not judged again until
  somebody adds to it. It takes both agents: one that sees something asked
  turns it into an answer.

Replies to inline threads go into those threads. A review summary and a top
level comment have no thread to reply into, so they are answered together in one
comment on the pull request rather than five comments in a row.

### What stops a comment from getting arbitrary code pushed

This is the only command whose input is written by a third party and whose
output is a `git push`, so it is worth saying exactly what the layers are.

- `checkin_trust = "write"` by default. Everybody is answered in words; only
  somebody GitHub says can write to this repository can cause a commit. On a
  repository you do not own, the author of a pull request is a `CONTRIBUTOR` on
  their own PR, so set `checkin_trust = "anyone"` or pass `--any-author` if you
  are answering your own contributors. Every comment the gate holds back is
  named in the log, so the setting is never silent. The same setting decides
  whose comments under an issue reach the prompts that implement, triage, and
  review it. Issue comments from the person who filed the issue and the
  authenticated GitHub user are included regardless of repository write access.
- **Both agents have to agree.** A second opinion that never arrived is not
  agreement: if the checking agent and its fallback both fail, nothing is
  implemented that run. Disagreement always resolves toward saying something
  rather than doing something, because getting a decline wrong costs one person
  one read of a thread that stays open, and getting a change wrong costs them a
  commit they did not ask for.
- **Ambiguity is a stop, not a guess.** Either agent saying the comment could be
  read two ways turns it into a reply asking what was meant.
- **A third refusal, with the code open**, and a mechanical check per comment:
  the fix call names the files it changed for each one, and they are checked
  against what the commit actually touched. A reply never claims a fix that is
  not in the diff.
- **The trust gate reads the whole thread**, not its newest message, so waiting
  for somebody with write access to reply is not a way through it.
- Comment bodies reach the agents inside a marked block, as data rather than
  instruction. The marker carries a suffix generated per run, so it cannot be
  named in advance, and any line that reads as one, in any case, with any dash,
  is stripped out of the body, the diff hunk, the pull request title, and the
  first agent's summary of it all before the second agent reads it. A comment
  that tries to redirect the agent is itself grounds to decline.
- The push is `--force-with-lease` onto a worktree built from the pull request's
  own head, and the lease names that exact head rather than the tracking ref, so
  a commit pushed while the round ran refuses the push instead of being
  overwritten. The worst case is a commit you revert, never rewritten history.
  Nothing merges: there is no `--auto-merge` on this command, deliberately.

A pull request from a fork cannot be pushed to, so `checkin` answers it and
files what it finds and changes nothing, and says so. `spar checkin <issue>`
routes to that issue's open pull request when there is one, and otherwise
answers the comments on the issue itself without touching code.

Running it twice does not answer the same thing twice. Threads carry GitHub's
own resolved flag, and what spar answered is recorded in `.spar/state`, which is
what makes leaving a disputed thread open terminate rather than re-arguing it
once a run forever. A fresh clone has no record, so it will answer a previously
declined point once more.

## Answering what an issue asks of you

`checkin` answers everybody on a thread. An issue that names you and asks you
something is different: a design review that mentions five people and asks each
of them their own question is waiting on you for yours, and on nobody for the
rest. `spar answer` answers what is asked of you, and only that.

```bash
spar answer 229             # one issue
spar answer                 # every open issue that mentions you, up to --limit
spar answer 229 --dry-run   # print it, save it for `spar post`, post nothing
spar answer 229 --again     # judge mentions spar already judged
```

It reads the places on the issue that mention you, its body and its comments,
and that nobody has answered. One agent lists the questions they put to you,
by name or as one of a group that names you, and drafts an answer to each with
the code open. The other checks every answer itself. Two outcomes:

- **A question of fact**, one the code, the issue, or what it links settles, is
  answered when both agents agree the answer is right and both call it a
  question of fact. Every one of them goes up in a single comment, each answer
  under the question it answers, quoted.
- **Anything else is yours.** A question that asks you to decide, agree,
  commit, own, schedule, approve, review, or give a view is never answered in
  your name, and neither is one either agent could not check or one they
  disagree about. Spar posts nothing about it. It is listed in the terminal and
  added to `.spar/answers/issue-N.md` with what the agents found and what it
  rests on, for you to write your own reply from.

A mention that asks you nothing, a cc or a thank you, gets no reply. A question
put to somebody else is left out, even one the agents could answer.

`spar post 229` posts what a dry run saved, on the issue, without running the
agents again, the same way it posts a saved review.

### What stops it answering itself

`spar listen` runs this unattended, and every comment it posts is under your
account. A thread you are part of keeps the reason it was raised for, so spar's
own answer on a thread that began with a mention comes back as a mention. These
are what keep that from turning into a conversation with itself, or with
somebody else's bot:

- **A mention is judged once.** Every mention judged is recorded in
  `.spar/state/answer-N.json`, whatever became of it, and the comment spar posts
  names the mentions it judged in a hidden line, so a fresh clone or a second
  machine reads the same off the thread. An edit to a mention spar judged, or
  the thread moving under spar's reply, is not a new question. `--again` reads
  them all again, by hand.
- **Only a person asks.** Your own words, a bot's, and anything spar composed,
  on anybody's account, are never taken for a question. Somebody else's spar
  mentioning you does not start one, and yours mentioning them does not either.
- **The reply mentions nobody.** Every `@name` in it, the quoted questions
  included, goes in a code span, where GitHub notifies nobody. The people on
  the thread are notified anyway, since they are subscribed to it.
- **A reply you wrote yourself answers it.** A mention older than something you
  said in your own words on the issue is taken as answered.
- **Nothing asked, nothing said.** A reply saying there is nothing to answer
  invites one back.
- **A cap.** `spar listen` answers on one issue `max_answers` times, 3 by
  default, and then leaves it to you, saying so once. Running `spar answer N`
  yourself sets the count back to zero. A run counts whether or not it posted,
  so a run that fails every time still stops.
- **Two agents or nothing.** A check that never arrived is not agreement:
  nothing is posted, nothing is recorded, and it is tried again later.

## Listening for what happens next

Every command above does one pass and exits, so a review asked for again after
spar posted one waited for you to notice and run spar again. `spar listen` waits
instead, and it waits for free: nothing calls an agent until GitHub says
something happened.

```bash
spar listen                 # watch the repository
spar listen 108 42          # watch only these, and exit once every one is closed
spar listen --once          # one poll and one sweep, act on what is due, exit
spar listen --dry-run       # every event and what it would trigger, nothing triggered
spar listen --since 2h      # on the first run, catch up on the last two hours
```

Two sources feed it, because neither is enough alone. GitHub's notifications for
the repository say why a thread moved, a review request, a mention, a comment,
and spar polls them with a conditional request: when nothing changed GitHub
answers `304 Not Modified`, which costs nothing against the rate limit, and spar
obeys the interval GitHub asks for. But a push raises no notification for a
reviewer, so every `sweep` seconds spar lists the open pull requests and compares
each head with the commit its review record holds. Your own actions raise no new
notification, but they can still move a thread you are part of, and a thread
keeps the reason it was first raised for. A thread that began as a review request
is therefore called one again when spar's own review moves it, so spar checks the
pull request: a review request counts only while it is still open, to you by name
or to a team you are in. A team request asks GitHub whether you are in the team,
which takes the `read:org` scope `gh auth login` asks for, and a team GitHub will
not answer for counts as asking you. A review spar posted as a comment, which is
what `spar review` does without `--verdict`, leaves the request open, so on a
pull request spar reviewed it also checks when the request was made: one made
before that review began was answered by it, and counts as an update.

What an event leads to is a table, not a judgement:

- A pull request spar has reviewed, written by somebody else, gets a **second
  look** only when somebody asks for one: your review requested again, or a
  mention of you made since that review. The earlier points are ruled on, every
  commit and reply since is read, and a verdict is posted. A push or a comment
  alone gets nothing, because the author may still be working and a comment may
  be another reviewer's review rather than an answer to yours. A review request
  made since that review, still open, that arrives with nothing new gets a
  mechanical comment saying so, and no agent call. A request the review already
  answered, or a thread that began with a mention long before it, is only an
  update. A second look lists only points a review showed: one that predates the
  pull request is kept on record, and never posted.
- A pull request spar has not reviewed, written by somebody else, where your
  review was requested or that was assigned to you gets a review with a verdict,
  under `on_review_requested` and `on_assigned`. A mention on it gets a review
  whatever those say. A push or a comment on one gets nothing, unless you named
  it, in which case the first thing that happens to it starts the first review.
- A pull request you wrote, which the same account cannot approve, follows
  `on_own_pr`: `checkin` runs `spar checkin`, which may push a change both agents
  agree on behind `checkin_trust`; `reply` answers in words only; `ignore` does
  nothing. A push to your own branch is never news.
- A pull request in the custody loop's hands, with `.spar/state/pr-N.json` and
  no review record, is left to `spar resume`, and the log says so once.
- A mention on an issue that has an open pull request spar reviewed, made after
  that review, gives that pull request a second look on the same terms as its
  own: none once it has had `max_rechecks`, and none when it is yours.
- A mention on any other issue runs `spar answer` under `on_mention`: `answer`
  posts what both agents agree on and leaves the rest for you, `draft` saves it
  for `spar post`, `ignore` does nothing. Only a mention spar has not judged
  counts, so its own answer moving the thread is an update, and after
  `max_answers` runs on one issue it is left for you. Nothing else on an issue
  asks for anything.
- Anything closed or merged is dropped. Anything below `min_number` is ignored
  unless you named it.

Events settle before anything runs. An item is acted on once it has been quiet
for `settle` seconds, or after `max_wait` seconds however noisy, so a review
request followed by three comments in the same minute is one second look rather
than four. Dispatches run one at a time; what arrives during one is read on the
next tick, and nothing is lost, because the notifications stay on GitHub's side
until spar reads past them. Each pull request gets `max_rechecks` second looks
before spar leaves it to a person, which is what keeps two bots from arguing
forever; `spar review N` resets that.

With numbers, `spar listen 108 42` watches only those: a named pull request gets
a second look on the same terms as any other, is reviewed on the first thing
that happens to it if spar never reviewed it, and the command exits once every
named item is closed. A named issue is watched through its open pull request,
when spar has reviewed one. Nothing else is acted on, including work a listener
over the whole repository left pending, which stays for the next one.

`--once` does one poll and one sweep, acts on whatever is due without waiting
for it to settle, and exits non-zero if a poll or a sweep failed, so cron can run
it instead of a resident process. Only one listener may watch a checkout at a
time; a second refuses, naming the first's pid, for as long as the first is
running, however long one review takes. A lock left by a process that is gone
holds nothing. Between runs the listener keeps where it got to in
`.spar/state/listen.json`, so stopping and starting it loses no event. A dry run
writes none of it, so the run after one acts on everything it showed.

Ctrl-C stops it. An agent call in flight is stopped rather than left running,
the item is reported as interrupted with its worktree kept, and spar exits 130. A
second Ctrl-C ends it at once.

A `gh` failure, a network drop or a rate limit does not end the listener: it
backs off, a minute doubling to fifteen for an ordinary failure and until the
limit resets for a rate limit, says so once, and says when `gh` answers again. A
poll is all or nothing: one that cannot read every page of notifications moves
past none of them. A review or second look that fails, or that GitHub would not
take, is tried again after a quarter of an hour, doubling, and after five
failures it is left until something new happens to it. One that ended `moved`
is tried again once the push settles, under the same five. Left is not
forgotten: what the item was waiting on, a review request say, comes back with
the next thing that happens to it, so a first review held up by GitHub lagging
behind a push runs when the push shows.

## Splitting something too big to work in one piece

The unit of work used to be fixed. One issue became one branch and one pull
request whatever its size, and the only way to change that was to file the
smaller issues by hand. `spar split` is that judgement made executable.

```bash
spar split 10 12        # split these
spar split              # every open issue and PR, split what is worth splitting
spar split 10 --dry-run # print the proposal, write nothing
spar split 10 --again   # split something spar already split
```

It decomposes and stops. Nothing is triaged, implemented, or merged: it produces
smaller units and hands them to the commands that already exist.

```bash
spar split 42
spar run 101 102             # use the child numbers reported by split
```

Or set `decompose_trackers = true` and run `spar run 42`. The checklist added
to the parent is then read as a tracker, and its open child issues become the
next wave in that run.

Two agents, asymmetrically. One proposes the parts with the code open, the other
rules on the proposal: accept, reject, or accept with named parts struck. Not two
independent proposals, because two decompositions of one thing cannot be
reconciled mechanically and reconciling them is a third judgement nobody asked
for. Disagreement resolves toward not splitting, and striking enough parts that
one is left means nothing is split, because a split into one part is not a split.

**An issue.** Each surviving part is filed through the same path a follow-up
takes, so it inherits the similarity check that stops duplicates. The parent is
then rewritten into a tracker: your text untouched, with a checklist appended
where each line carries its `#N`. It is never closed. Its body is now the plan,
which makes it a tracker, and spar already refuses to close one of those.

**A pull request.** Additive, always. An independent slice gets its own branch
off the base; a stacked slice starts from its predecessor. The slice is applied,
an agent is asked to make it stand on its own, and it gets its own pull request.
The original gets one comment naming the parts and whatever is left over, and is
left open and otherwise untouched. **spar never rewrites the branch behind
somebody's pull request:** split branches use create-only pushes that refuse an
existing remote ref unless it already names the same commit, and they never
move it. Nothing is closed, and nothing is rebased under anybody.
These are code-level guards, not requests left to a prompt.

A slice that will not stand alone is not a slice. If the agent cannot make a part
build and pass on its own branch it says so, and the part is dropped rather than
pushed broken. Spar commits a successful stand-alone edit before pushing it. If
the edit or commit fails after changing files, the split stops and keeps that
part's worktree for recovery. The whole value of splitting is that each part can
be reviewed and merged on its own, and a part that does not build has none of it.
If dropping parts leaves only one, nothing was decomposed: the one that was made
is still named on the original, and the run reports the pull request as left whole.

Whether the parts are independent or stacked is a property of the change rather
than of the repository, so it rides on the proposal instead of `spar.toml`.
Independent parts each sit on the base branch and merge in any order. Stacked
parts are created in order, each branched off its predecessor.

The parent head is checked throughout the build and once more before the final
comment. If it moves, spar stops rather than publishing more work from the old
snapshot. A branch already pushed before a later failure is kept with its
worktree and blocks an ordinary rerun. Compare every retained branch with the
current parent, then open a missing child pull request only when the branch is
still valid and post the parts summary by hand. To start over, remove every
retained local worktree and branch, child pull request, and remote split branch.
`--again` starts a new split; it does not resume retained branches.

Once every child pull request exists, successful part worktrees and local
branches are released together unless `[loop] keep_worktrees = true`. A failure
after an editing call changes files or commits, or after a push, keeps them so
the partial result can be inspected and recovered.

A pull request from a fork is proposed, not split: spar cannot push to the fork,
and carving somebody's contribution into pull requests of your own without asking
is not something to do automatically. The proposal goes up as a comment and
nothing else happens, which is how `review` already treats a fork.

Given no numbers, `split` is the one command that means both kinds: every open
issue and every open pull request, each list capped by `--limit` on its own so a
shared budget cannot fill up on issues and starve the pull requests. One agent
then rules on the whole list in one call, because two agent calls per item across
a queue is the wrong price for a question whose answer is usually no. Anything
already split is skipped, and `--again` reopens it. An issue that already has a
pull request open routes to that pull request, since splitting an issue whose
work is half done produces children describing work that already exists on a
branch.

A wrong split is expensive to undo: N issues to close, a checklist to strip out
of somebody's body, and branches and pull requests to delete. So there is a hard
cap, no recursion, and `--dry-run`.

```toml
[loop]
max_split_parts = 4     # most parts one issue or PR becomes. A backstop, not a
                        # target, and what it holds back is said out loud.
```

A part is never itself split in the same invocation, however large the proposal
says it is. If a part is still too big, `spar split <part>` is one command away,
typed by somebody who looked at it. There is no setting to turn splitting off,
because it is a command nobody has to run.

## Keeping it readable

The reader of a PR is a person with other work. Model prose defaults to three
paragraphs where one sentence would do, and asking nicely for brevity has the
same reliability problem as asking for no em-dashes.

So spar does not forward what a model wrote. It asks for structured findings,
composes every comment itself, and clips each field to a budget. It also never
narrates its own working: no agent names, no round numbers, no counts of things
listed on the next line. A reader wants to know about the code, not about the
tool. A review with work to do gives the detail only for what blocks, and lists
the rest by title:

```
One real problem, the rest are follow-ups.

blocking
- Retry loop never terminates (src/net.rs:88). Confirmed by running the 429 test
  with max_attempts unset: it spins.

non-blocking
- Timeout is not configurable (src/net.rs)
- Error message does not name the host (src/net.rs)
```

spar is also quiet while it works. The agents never read the PR thread, they get
findings through their prompts, so nothing in the loop depends on any of it being
posted. A run leaves **one** comment, and only when it has something to say:

```
Not signed off: the last round of fixes was pushed but has not been reviewed.

Raised and refuted:
- Config loader swallows a parse error. The caller validates against the schema
  before load_config is reached.

Filed separately: #485, #486
```

A run that converged cleanly and filed nothing posts nothing at all. The absence
of objections is the message, and the diff already records what was fixed. What
survives is what is unrecoverable elsewhere: what is still unresolved, what was
argued down and why, and where the follow-ups went.

Set `pr_comments = "rounds"` for a comment per review and per response, which is
an audit trail at the cost of a thread nobody wants to read, or `"none"` to keep
GitHub out of it entirely.

**A pull request says what it is for.** The body used to be `Closes #42` and one
scraped sentence, which told a reviewer opening the diff cold nothing: not what
was wrong, not what the change does about it, not how to check it. The
implementor is asked for those separately, and spar composes the body from the
fields, so the substance comes from what was asked for and the brevity from spar
rather than from the model:

```markdown
Closes #478

Retry a 429 with exponential backoff instead of failing the request.

A rate limited response was treated as fatal, so a single throttled call ended a
run that had hours of work left in it. The retry path existed but only covered
connection errors.

## What changed

- `send` now retries a 429, honouring `Retry-After` when the server sets it
- the retry budget is bounded at five attempts, so a permanent 429 still ends

## How to test

- `cargo test retries_a_rate_limited_request`, which fakes a 429 and asserts the wait
- point it at a throttled endpoint and watch a run finish

## Notes

Streaming calls do not go through `send` and are unchanged.
```

Everything below the first two paragraphs is optional and disappears when it is
empty, so a one line fix reads as one rather than as a form with most of it left
blank. The lead is two paragraphs rather than two headings, because a heading
over a single sentence is a label on a label.

**A filed issue is written as a bug report.** The agents are asked for the parts
of one separately, and spar assembles them under headings, skipping any that do
not apply:

```markdown
## Problem
What is wrong, with the specifics: the function, the call it does not make.

## Reproduction
Numbered steps, then an Actual result list. If part of what happens is correct
and only part is the defect, which.

## Impact
What an operator or a user can do, or loses, because of this.

## Expected behavior
Requirements specific enough to implement and to test.

Found while working on #400.
```

A finding that stays in the pull request thread carries none of that and is just
its one line, so nothing sprouts empty headings for the sake of a format.

**A filed issue is not a comment, and is not held to a comment's budget.** A
comment is read with the diff in front of you; an issue is picked up cold, months
later, by somebody who was not there. So issues get `max_issue_body_chars`,
several times a comment's allowance, and the agents are asked for what a person
picking one up actually needs: what goes wrong, how to reproduce it, the file and
line, and what would fix it.

Fenced code blocks in an issue are **never truncated and never count against the
budget at all**. A snippet cut in half is broken markdown and a misleading
fragment of the code somebody is being asked to fix. When prose does have to be
dropped, whole blocks go from the end, so what survives is complete.

The lengths in `[style]` 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. Cutting substance was a mistake worth
naming: a reader who cannot act on a finding has been given nothing, and the
characters saved bought nothing. Brevity is asked for in the prompts, which is
free, and enforced only on shape. Set `terse = false` to remove even the valves. To see exactly what spar would post, before spending a
token on it:

```bash
cargo run --example preview            # every comment type, gate on
cargo run --example preview -- --loose # the same input with the gate off
```

## Failure modes it handles

Cross-model review loops break in specific ways. These are handled explicitly.

**The nitpick spiral.** Round 6 findings are worse than round 1 findings and a
naive loop cannot tell. Severity gating means only real defects block. That
needs somewhere for a real defect that is minor to go, or the ladder has a hole
in it and everything real lands on `blocking`, so `non-blocking` is "real, and
smaller than another round" rather than "a genuine improvement".

**No endgame.** A round is review and then fix, so the last thing a long run did
was push a commit nobody had read, and running out of rounds was the only ending
it could reach. Two pull requests on this repository spent three rounds each and
both ended by telling the maintainer the newest code was unreviewed. The budget
now bounds the asking. After it, one closing pass by the agent that did not write
the head is handed what landed and the fixes the author claimed, and asked the
one question a merge turns on.

**Re-litigation.** If A refutes a point and B raises it again next round, the
loop never ends. Refuted points are hashed into a ledger carried across rounds
and injected into the next reviewer's prompt as settled. Re-raising needs new
evidence, and a second re-raise escalates to you.

**Approval drift.** Optimizing for "get approved" pressures the author into
accepting wrong review comments. Refutation is explicitly blessed in the prompt,
disputes are surfaced in the summary, and the merge gate is
blocking-findings-empty rather than reviewer-satisfied.

**A reviewer reading its own commit.** "Neither agent reviews its own most
recent edit" was enforced from what the reviewer said it would do next rather
than from what landed. Custody now follows checked `HEAD` values on both sides
of every call. Spar commits an accepted fix before choosing the next reviewer.
The custody loop rolls back a review pass that writes before it can count as
approval. Detached review, split, and check-in inspections instead stop and keep
the worktree if files or `HEAD` change. Writing a file the project's own ignore
rules cover is not such a change: verifying a finding usually means building the
project and running its tests, and a reviewer held to a byte-identical `dist/`
loses the review it just did. Which paths those are is a question the repository
already answers, so spar asks Git rather than matching a list of directory
names it wrote down in advance. A review died over that difference: a reviewer
ran the project's own binding generator to check a finding, that left a lockfile
beside its own manifest instead of under `target/`, and both agents lost an hour
of work to a path Git had been told to disown.

Only creating such a file is forgiven. Rewriting or deleting one that was
already there still stops the call, because that is where a person's `.env` or
saved log lives, and an ordinary untracked file still stops it too, because a
reviewer is asked to keep its scratch out of the tree. What it was asked to
judge is the tracked tree, and that is compared byte for byte as strictly as
ever, whatever ignore rules appear beside it. A repository cloned into the
output is not such a change either. Git reports it as a nested checkout, spar
records it by identity without reading inside it, and the worktree is kept
afterwards rather than removed. A Git entry Git does not report, such as
`git init` run inside a tracked directory, still stops the call, since every
file written under one is invisible to the outer worktree.

`spar review` is the exception for a new ordinary file. Its two reviewers read
one checkout at the same time, so a file that appears while one of them works
may be the other's, and judging each call alone blamed the reviewer that
finished first for a repro test its partner still had open. A whole review was
thrown away over a file deleted half a minute later. There, new files are
judged once both reviewers have finished: anything still left is moved to
`.spar/scratch/pr-<number>-<seconds>/` under the same path, a warning names it,
and the review goes on. Rewriting a tracked file, or one that was already
there, still stops it.

Probes are held to the same standard. A path Git listed and a build then deleted,
or one that appears between two listings of the same tree, is a fact about a
worktree being built in rather than evidence of damage, and no longer ends the
run.

Successful editing calls leave their files uncommitted, and spar stages and
commits them only after accepting the structured report. A failed editing call
that changed files is not retried or handed to a fallback. If a custom editing
command already left a clean commit that advances the prior tip, spar publishes
and checkpoints it before returning the call failure. A reset or rewritten tip
is kept for recovery instead. Unsafe Git state or uncommitted output stops the
run and keeps the worktree and diagnostic for recovery. A failed initial
implementation that already made durable commits can continue from those
commits. With `--no-worktrees`, spar refuses to reset dirty or unpreserved work
in the shared checkout, including an off-checkout target branch left by an
earlier run.

**Merge authority.** Two models agreeing is not the same as being right, and
neither carries the consequences. `--auto-merge` is off by default; the terminal
state is "approved, ready for a human to merge". Branch protection on the target
repo should be considered required, not optional, if you turn it on.

**A claim nothing else checks.** Every claim an agent makes is checked
mechanically where it can be, and "I ran the tests" was the exception: the
report records it and spar pushed on that word. `[loop] check = "cargo test"`
runs one command in the worktree after an editing call commits and before the
branch is pushed, and fails closed. It is not waiting on CI, which is expensive
and somebody else's machine. On failure the output goes back to the same agent
once; a second failure ends the round with the worktree kept, the output in the
report, and nothing pushed. The pull request body then carries a line saying the
check passed, which is spar's claim rather than the model's. Unset, nothing runs.

**A worktree that can build.** A worktree holds only what git tracks, so a
project that needs `npm ci` or `uv sync` first gets a checkout whose check fails
on modules it cannot find, unless an agent happens to install them. `[loop]
setup = "npm ci"` runs one command in every worktree spar creates, including
the throwaway checkout read-only calls run in, before any agent works there or
the check runs. A failure stops that issue before any agent call, with the
output in the report, and the next run rebuilds the worktree. The shared
checkout used with `--no-worktrees` is never touched. Unset, nothing runs.

**Style rules models forget.** Prompting alone is unreliable over a long run.
Every commit message, PR body, and comment is scrubbed deterministically and
then re-verified. The gate looks for more than the scrub can fix, so a form it
cannot clean is the hard error rather than a leak. Fenced blocks and inline code
spans are left exactly as they are: a snippet somebody is being asked to
reproduce is evidence, and a trailer naming a person rather than a tool is an
ordinary trailer. The scrub is bounded to
the commits spar itself made: on a resumed pull request, or a check-in on
somebody's branch, a message that was already there is reported and left alone,
because rewriting it would change a published sha under the person who wrote it.

**An issue read in part.** An agent handed half an issue judges and builds the
half it saw, and reports it with the confidence of having read all of it.
Nothing about the output looks wrong. So an issue body reaches a prompt entire:
`max_issue_chars` is sized past anything a person writes, a cut is said out loud
in the log and marked in the prompt, and it never lands mid-line or inside a code
fence. Triage reads the whole queue at once, so `max_triage_chars` bounds that
too, and past it whole issues wait for the next run rather than every issue
losing its tail. A verdict is posted on the issue and can close it, so judging
one on part of what it says is worse than not having reached it yet.

Every pass that judges the branch is given the same thing: the review rounds,
the closing pass, the author's response, `spar review`, and `spar checkin` all
carry the issue body under the same rule, alongside the pull request description
labelled as the author's own account of the change, which is a claim to check
rather than a statement of fact. A reviewer asked whether the change resolves
the issue cannot answer that from a title.

The comments under the issue go with the body, read in the same `gh issue view`.
A body is written once, and the thread is where somebody narrows it, widens it,
or finds the real defect under the one they filed. A body that reads as complete
gives no hint that a comment superseded it, so an agent left to follow a link
has no reason to. The comments follow the body oldest first, each headed with
who wrote it and when, and every prompt that carries an issue says a later
comment can change what was asked. The body and its comments share
`max_issue_chars`, and past it the oldest comments go first.

What a comment says can become a commit, so `checkin_trust` also decides whose
issue comments are read: by default somebody GitHub says can write to the
repository, plus the person who filed the issue and the
authenticated GitHub user, regardless of their repository write access. GitHub
identifies the latter with `viewerDidAuthor` on each comment. Anybody passed
over is named in the log. spar's own comments are carried, because one can be
a finding a review added to an issue that already
covered it, and headed as spar's so no agent takes one for somebody deciding what
was asked.

The URL goes with the text, not instead of it: codex runs under
`-s workspace-write`, which has no network at all, so a link alone would leave it
judging the title.

**Two agents that are secretly one.** Config keys are arbitrary, so `alpha` and
`beta` can both be Claude on the same model. spar compares the resolved binary
(by inode, so a symlink does not fool it) and the configured model, and warns
loudly. An approval from a model reviewing itself looks identical to a real one,
which is worse than no review at all.

## Configuration

`spar init` writes a config from the CLIs you actually have:

```
  missing  aider
  found    claude     /Users/you/.local/bin/claude
  found    codex      /Applications/ChatGPT.app/Contents/Resources/codex-cli/bin/codex
  found    cursor     /Users/you/.local/bin/cursor-agent
  missing  gemini

wrote spar.toml
```

```toml
[agents.claude]
preset = "claude"
model  = "opus"         # omit to use the CLI's own default
effort = "high"

[agents.codex]
preset = "codex"
model  = "gpt-6.1-sol"
effort = "high"

[loop]
max_rounds        = 3       # custody rounds, or review-only phases
auto_merge        = false
first_implementor = "claude"  # or "alternate"
close_skipped     = true
worktrees         = true

[loop.effort_schedule]
triage      = "low"         # both agents, over the whole queue
review_1    = "high"        # the deep pass
review_rest = "high"        # later rounds and the closing pass
recheck     = "high"        # the second look after commits or replies

[style]
ban_em_dash        = true
ban_ai_attribution = true
terse              = true
```

See [`spar.example.toml`](spar.example.toml) for every option with its
reasoning.

## Pairing other agents

An agent is a command template plus an output adapter, not a class. Supporting a
new CLI is a preset file, not a code change. Presets ship for claude, codex,
cursor, gemini, and aider; any two can be paired, and agent names are
arbitrary.

To wire up something with no preset, declare it inline:

```toml
[agents.custom]
command = ["mytool", ["-m", "{model}"], "--prompt", "{prompt}"]
output  = "text"            # text | jsonl | json
```

Placeholders: `{prompt}` `{system}` `{model}` `{effort}` `{cwd}` `{schema}`
`{schema_file}`.

An argument group whose placeholder is unset is dropped whole, so omitting
`model` drops the `-m` flag rather than passing an empty string.

Spar does not add writable Git metadata paths to editing commands. They leave
changes in the working tree, then spar stages and commits them after the
structured report is accepted. A command that already runs in a sandbox can
therefore edit a linked worktree without being granted direct write access to
the repository's objects, refs, config, or hooks. Commands without their own
sandbox keep their normal host permissions. Managed commits disable signing and
commit hooks; the eventual merge can still be signed. On macOS and Linux, spar
also stops every process a call started before inspecting the worktree,
whether the direct command exited, timed out, or was interrupted. That includes
processes that moved to a process group or session of their own, which is
where agent CLIs run their shells: a process belongs to the call when it
inherited the call's `SPAR_CALLS` variable, is in a session a descendant
created, or was seen below the call while it ran. A daemon the call started,
such as a Gradle daemon or an ssh control master, is stopped with it. Work
handed to a service that was already running, such as Docker or a tmux server,
is not the call's to stop. On other Unix systems only the call's own process
group is stopped. On other platforms, a command must not return until its
children have finished writing. Spar refuses to stage a changed
`.gitattributes` or gitlink, since attributes can select an external Git
filter and nested repository
objects may exist only in that worktree. If a call leaves uncommitted output or
unsafe Git state, spar keeps the worktree and reports its path for recovery. A
custom editing command that commits before its report fails has its clean,
forward-moving commit published and review custody checkpointed before spar
returns the error. A reset or rewritten tip is kept for recovery. A failed
initial implementation that already made durable commits can continue from
them. Successful editing calls honor the project's Git ignore rules: ignored
files can be created, rebuilt, or removed without blocking a managed commit or
a push. This includes custom output such as `manager/public/assets/`, not just
common directories like `target/` and `dist/`. These files stay outside managed
commits. New non-ignored source files are staged; existing non-ignored untracked
files remain protected. Ignored dependency checkouts also stay outside managed
commits and remain protected from cleanup. Attribute-file checks use the same
Git ignore rules, including nested ignore files, `.git/info/exclude`, and
`core.excludesFile`. Ordinary untracked files are listed with those exclusions
applied immediately, so a build deleting an ignored artifact during the scan
cannot make it appear to be pending source work.

Accepting an edit and deleting its worktree are separate decisions. Ordinary
cleanup still retains ignored files outside recognized build and cache directories,
so accepting custom output does not authorize deleting local data. Failed editing
calls also keep their recovery checks before retry or fallback.

When resuming a PR whose commits are already published and whose source is clean,
custom ignored files do not prevent a fresh checkout. Spar moves the previous
checkout and its branch to a unique `retained-pr-<number>-...` recovery name,
preserving its files and reflog, then checks out the current PR head. It reports
the retained path. Pending source edits and unpublished commits still require
recovery before rebuilding.

A call asked only to read likewise allows changes to project-ignored files.
Tracked changes and ordinary untracked writes still fail that call.

Those calls do not run in your primary checkout. Triage, drafting an issue for
`spar code`, screening follow-ups or split candidates, proposing a split of an
issue, brainstorming, answering an issue, and checking in on an issue each get a
throwaway checkout of HEAD under `.spar-worktrees/read-<pid>-<n>`, set up with
`[loop] setup` like any other worktree. The primary checkout is where you, your
editor, and any other agent are working, and a read-only call is judged on what
changed while it ran: a dependency bump another agent made there during a
seven-minute draft counted as the draft's, and the answer was thrown away.
Nothing works in the throwaway checkout but the calls spar started there, so a
change in it is theirs. It holds what HEAD commits and none of your uncommitted
edits. When the calls finish it is removed, ignored output and all, unless
something in it changed that a read-only call was not allowed to change, in
which case it is kept with a recovery marker. One a killed run never released
is swept by the next run once its process is gone. With `worktrees = false`
these calls run in the shared checkout instead.

The read-only rule is that wide because the call is not the only writer. Builds
and test runs write ignored output wherever a call runs, and in a shared
checkout Finder rewrites `.DS_Store` and an editor rewrites its own state. Triage
asks both agents at once against the same directory, so one ambient write used
to discard two good answers and end the run with nothing scheduled. Nothing is
silent, though: an ignored path that moved and is not recognized build output is
named in a warning, and every error that does stop a call now names the paths
that stopped it. spar leaves its own run state under `.spar/` out of the
comparison entirely, since writing it mid-run is spar's doing rather than an
agent's.

Include `{schema}` or `{schema_file}` and spar uses the CLI's native structured
output. This is worth doing rather than optional: 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, which costs you that agent's whole review.
`{schema}` passes the schema as an argument, `{schema_file}` passes a path to it,
and a CLI that supports either is enough. When a structured answer is unusable
anyway, spar asks once more with the parser's complaint attached before giving
up on that agent.

For a CLI that emits an event stream rather than plain text, say where the
answer lives:

```toml
output       = "jsonl"
message_path = "item.text"

[agents.custom.message_match]
type        = "item.completed"
"item.type" = "agent_message"
```

Binaries are located by walking `SPAR_<NAME>_BIN`, then PATH, then the preset's
`search_paths`. Nothing is hardcoded, and a miss reports every location tried.

### A backup for when one CLI will not answer

A CLI that is down, out of quota, or refusing the request on policy grounds
takes the run with it: the pair is two, and one of them has stopped existing.
Give an agent a stand in and the call goes there instead.

```toml
[agents.codex]
preset = "codex"
model  = "gpt-6.1-sol"

[agents.codex.fallback]
preset = "cursor"
model  = "kimi-k3-max"
```

A fallback is a whole agent, preset and all, and it is not a third opinion. It
never reviews alongside the pair. It answers in place of the agent that failed,
holding that agent's turn, so neither agent ever reviews its own most recent
edit and nothing else about the loop changes.

It fires as soon as another call on the primary would buy nothing. A deadline
never is: the wait is the same length for the same answer. Neither is a failure
the CLI itself reported, a refusal or a quota or a crash, because a different
CLI is a different question while the same one twice is the same refusal at
full price. What still earns a second ask is an answer that arrived and could
not be parsed, which is what the retry was always for and which a model
corrects readily when handed the parser's complaint. With no fallback
configured the retry is the only thing left, so it happens either way.

The scheduled effort is not passed on, because effort words are each CLI's own
vocabulary; the fallback uses whatever its own block asked for.

If both fail you get both reasons, the primary's first. `spar doctor` shows the
fallback under the agent it stands in for, and `SPAR_CODEX_FALLBACK_BIN` points
it somewhere else. One that is not installed warns at startup and is otherwise
ignored, because a missing backup should not stop a run whose pair is fine.

The cursor preset drives [Cursor's CLI](https://cursor.com/docs/cli), which is
installed separately from the editor and serves whichever models your
subscription carries. It has no structured output flag, so spar asks for JSON in
the prompt and parses it back, which is why it makes a better backup than a
primary. It has no effort flag either: effort is a `-low`, `-high` or `-max`
suffix on the model name, so `effort` in a cursor block does nothing.

`cursor-agent models` is the source of truth for what `model` accepts. Prefer a
family neither of your agents already runs, since an uncorrelated backup is the
whole reason the pair is two models rather than one. Cursor also serves Claude
and GPT: pointing a fallback at the same family as your *other* agent means
that when the primary fails you get two of the same model reviewing each other,
which is what the pair exists to prevent, and the correlation warning does not
look at fallbacks.

Then check your plan covers it, because a model being listed is not the same as
being callable. A metered one answers until the month's allowance runs out and
then refuses every call, which is the failure the primary already had. Cursor's
own models are covered by the plan itself, so `composer-2.5` is the safer
choice when a certain reply matters more than an independent one.

Every option a preset supplies can be overridden per agent, including `timeout`
(seconds one call may take before spar gives up), `search_paths`, and
`system_via`. [`spar.example.toml`](spar.example.toml) lists the full set. Every
call is told when its own agent's timeout will stop it, so a model can run the
tests that cover a change rather than start a suite it cannot finish.

Presets are embedded in the binary, and a file on disk of the same name wins
over the built-in copy. So when a CLI's flags drift you can fix it without
waiting for a release:

```bash
mkdir -p ~/.config/spar/presets
spar doctor   # see what it resolves today
$EDITOR ~/.config/spar/presets/codex.toml
```

`SPAR_PRESET_DIR` overrides the search, and `.spar/presets/` in the repo you are
working on is checked first, for an override that should travel with one
project. A bare `presets/` directory is deliberately *not* searched: it is an
ordinary directory name in plenty of projects, and shadowing a built-in preset
by accident produces a baffling failure.

## Housekeeping

```bash
spar followup       # work the follow-ups a run recorded locally
spar checkin        # answer the comments nobody replied to
spar clean          # drop local worktrees, branches, and state for finished PRs
spar clean --all    # drop every local worktree and branch spar recorded
spar clean --pr-state   # also delete state comments left on finished PRs
spar listen         # wait for review requests and mentions, and act
spar doctor         # check prerequisites and resolve each configured agent
spar doctor --config other.toml   # check a config before adopting it

spar init                     # write spar.toml from the CLIs you have
spar init --out other.toml    # somewhere else
spar init --force             # overwrite an existing config
```

`--close-skipped` and `--no-close-skipped` are offered only on `run`, since it is
the only command that triages and so the only one that can decline an issue.

Each issue gets its own git worktree so a failed run cannot poison the next
one's base. When a run reaches a terminal outcome, spar releases its worktree
only after proving that removal will not discard recoverable files or Git
state. An `escalated` or `error` outcome keeps it for inspection. Commands that
expose the run flags accept `--keep-worktrees` to hold on to them regardless,
or `--no-worktrees` to work directly in the main checkout. `split` takes neither
lifecycle flag; set `[loop] keep_worktrees = true` when its successful part
worktrees should remain. The throwaway checkouts read-only calls run in follow
the same two settings: `keep_worktrees` leaves them for inspection until the
next run sweeps them, and `worktrees = false` runs those calls in the main
checkout instead.

Managed commits require the isolated worktree. In `--no-worktrees` mode, an
editing command that leaves files uncommitted stops the run and keeps them in
the shared checkout. Spar cannot safely distinguish those files from edits its
owner made while the command was running. A command that commits directly keeps
the prior shared-checkout behavior.

Releasing matters for more than tidiness: a stranded worktree holds its branch
checked out, which makes a later `gh pr merge --delete-branch` fail to clean up.
Runs also sweep finished worktrees on start, but ordinary cleanup keeps any
worktree with dirty, ignored, or untracked files; changed raw bytes or modes;
initialized submodules or nested repositories; unreadable Git state; detached
or reflog-only commits; per-worktree refs or configuration; or a local commit
that no shared ref preserves. Checkouts with external filters, encodings,
identity expansion, or ambiguous line-ending rules are also retained when exact
reconstruction cannot be proved. It verifies that a candidate path belongs to
this repository before removing it, and ordinary cleanup never forces removal.
`spar clean --all` is the explicit force path for worktrees and branches spar
recorded owning.

`spar review` is the exception, in one direction only. Its checkout is detached,
built by `git worktree add` from `refs/pull/N/head` at the start of every run,
and there is no `--keep-worktrees` to make one outlive the run that wrote it.
Reviewing the same pull request again therefore rebuilds it over whatever ignored
files the last review's build left, because the rebuild replaces the checkout
regardless. Cleanup still keeps it: nothing is deleted until a later review
actually asks for that checkout again. An ordinary untracked file, a local
commit, or a repository cloned into the output still refuses the rebuild, and
says so.

When spar cannot prove what happened to a worktree, it writes the reason into a
`.spar-recovery-needed-<pid>-<n>` file in the directory the call ran in, and
names that path in the error. Nothing removes one, so it is there to read later.
It is spar's own note rather than something a call did, so it is left out of the
comparison a later call is held to, and rebuilding a review checkout reads it out
first rather than being blocked by it.

Cleanup only ever touches local branches spar recorded creating. Branch names
default to `issue-N`, `pr-N`, and `split-N-I`, with a numeric suffix when a split
name is repeated. The first is exactly what a person would call a branch by
hand. The name alone can never establish ownership: `spar clean --all` will not
delete your `issue-9`. It also does not delete a remote branch. Remove a retained
remote split branch only after its child pull request has been handled or
abandoned.

## Cost

Every review round is a repo-aware pass at the configured effort. A full ultra
review of a three-line round-3 delta is money on fire, which is what
`effort_schedule` exists to prevent. It has one key per kind of call, so a value
means what its name says: `code`, `triage`, `implement`, `review_1`, `review_rest`,
`respond`, `close`, `screen`, `checkin`, `split`, `brainstorm`, `recheck`. A call with no key
set falls back to `round_1` or `rest`, which is what the schedule used to be, and
then to the agent's own `effort`. `recheck` reads `review_rest` first, since a
second look is a delta and a list of claims rather than a first reading.

Effort words are each CLI's own vocabulary, so a schedule that has to say
different things to the two agents goes under `[agents.NAME.effort_schedule]`
with the same keys, and that wins over the shared one. A fallback resolves its
own value for the same call rather than inheriting one word from the agent that
just failed. `spar doctor` says when a scheduled value is not one the agent that
would receive it is known to accept.

Both agents bill against their respective subscriptions.

The report ends with what the run spent: calls per agent, wall time, retries and
hand-overs, and tokens where a CLI reports them, with a line under each issue for
its own share. Every call is also named at `logdim` as it happens, and the whole
record is appended to `.spar/state/spend.json`, so a resumed pull request adds to
the count rather than starting it again and anybody charting it has a file.

Rough shape per issue: two calls for triage, one to implement, up to two per
review round, plus at most one closing review call when the run spends its
budget and leaves something new to read. `spar review` is cheaper, four to six
calls for a whole pull request, because nothing is being rewritten between
passes.

`spar followup` is one call for the whole queue, however many entries it holds,
and then the ordinary pipeline for each one it files. `spar checkin` is two
calls per pull request, one to judge and one to check, and a third only when
there is something to implement.

`spar brainstorm` is two calls per round, so six at the default of three rounds
and two with `--max-rounds 1`. Filing from a saved session costs nothing.

`spar listen` costs nothing while it waits: polling is `gh` alone, and a poll
that finds nothing does not even count against GitHub's rate limit. What an
event costs is what the command it dispatches costs: a review's four to six
calls, a check-in's two or three, and a second look's two calls plus the
cross-check when something new turned up, which reads the `recheck` effort key.
`settle` is what keeps a review request and the comments around it to one of
them.

## Caveats

On macOS the Codex CLI usually lives inside `ChatGPT.app`, currently an alpha
build, so its path and flags will drift. The path already has: the app moved the
CLI from `Contents/Resources/codex` to `Contents/Resources/codex-cli/bin/codex`,
and the preset looks in both. Those are only entries in the preset's
`search_paths`, not assumptions: PATH wins, `SPAR_CODEX_BIN` overrides
everything, and a miss reports every location tried rather than degrading
quietly.

Triage runs both agents concurrently in the repo root. They are told to read
only, but they are real agent CLIs with write permissions. Commit or stash
anything you care about before a run, which you would want to do anyway.

## Development

```bash
cargo test          # unit and integration tests, no network needed
cargo clippy --all-targets
cargo fmt
```

The integration tests build real git repositories in a temp directory and
exercise the worktree, branch-ledger, and commit-rewrite paths end to end. They
never touch the network and never call `gh`.

`cargo run --example preview` renders every comment spar can post, from
deliberately verbose model output. It is the fastest way to judge a change to
the concision gate.

## License

MIT.