omh 0.6.0

Launch any coding harness, in a sandbox, with your setup already there.
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
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
5084
5085
5086
5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
5100
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
5117
5118
5119
5120
5121
5122
5123
5124
5125
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
5139
5140
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
5205
5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233
5234
5235
5236
5237
5238
5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
5262
5263
5264
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
5280
5281
5282
5283
5284
5285
5286
5287
5288
5289
5290
5291
5292
5293
5294
5295
5296
5297
5298
5299
5300
5301
5302
5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
5319
5320
5321
5322
5323
5324
5325
5326
5327
5328
5329
5330
5331
5332
5333
5334
5335
5336
5337
5338
5339
5340
5341
5342
5343
5344
5345
5346
5347
5348
5349
5350
5351
5352
5353
5354
5355
5356
5357
5358
5359
5360
5361
5362
5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
5420
5421
5422
5423
5424
5425
5426
5427
5428
5429
5430
5431
5432
5433
5434
5435
5436
5437
5438
5439
5440
5441
5442
5443
5444
5445
5446
5447
5448
5449
5450
5451
5452
5453
5454
5455
5456
5457
5458
5459
5460
5461
5462
5463
5464
5465
5466
5467
5468
5469
5470
5471
5472
5473
5474
5475
5476
5477
5478
5479
5480
5481
5482
5483
5484
5485
5486
5487
5488
5489
5490
5491
5492
5493
5494
5495
5496
5497
5498
5499
5500
5501
5502
5503
5504
5505
5506
5507
5508
5509
5510
5511
5512
5513
5514
5515
5516
5517
5518
5519
5520
5521
5522
5523
5524
5525
5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
5543
5544
5545
5546
5547
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
5588
5589
5590
5591
5592
5593
5594
5595
5596
5597
5598
5599
5600
5601
5602
5603
5604
5605
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
5619
5620
5621
5622
5623
5624
5625
5626
5627
5628
5629
5630
5631
5632
5633
5634
5635
5636
5637
5638
5639
5640
5641
5642
5643
5644
//! The repository the sandbox is allowed to have.
//!
//! A session's worktree carries a `.git` *file* pointing at an admin directory
//! inside the user's checkout, and omh never mounts that checkout — so inside
//! the sandbox the pointer leads nowhere and every git command fails. The agent
//! loses `status`, `diff`, `log`, `stash` and `reset --hard`, and the editor
//! attached over SSH loses its source control panel with them.
//!
//! What it gets instead is a repository of its own: a gitdir omh keeps outside
//! the worktree, seeded with a single commit of the tree the session started
//! from, mounted into the container and pointed at by a `.git` file that exists
//! only inside it. The host's own pointer is never written, so `omh s diff`,
//! `omh s commit` and `omh s push` are untouched.
//!
//! What makes it safe is what is *not* in it. One commit, one branch, no
//! remotes, and no *commit* from the checkout — so an agent reading its own
//! history learns nothing about yours, and there is no `main` here to move.
//!
//! Not "no object": a file whose content matches yours hashes to the same blob,
//! so shared blobs are unavoidable and mean nothing. History is the thing that
//! must not cross, and the guard is written against a commit for that reason.
//!
//! The governing rule for everything downstream: **never trust what the sandbox
//! asserts.** The agent can write in this gitdir, so it can set `user.email`,
//! delete a tag, or force-add a file the exclude list names. Nothing read from
//! here is taken on trust — identity and carried-file policy are enforced on
//! the host, at the moment work crosses back.

use anyhow::{Context, Result};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Where the gitdir is mounted inside the container.
///
/// Outside `/work` on purpose. A gitdir *inside* the worktree would be carried
/// into `omh s commit`'s `git add -A` and land the sandbox's entire scratch
/// history in the user's branch.
pub const GUEST_GITDIR: &str = "/omh/shadow";

/// The pointer file mounted at `/work/.git`, naming the gitdir above.
///
/// This is what makes git work inside the sandbox and nowhere else: it shadows
/// the worktree's real pointer for the container's view only, so the host's
/// file — which names an admin directory in the user's checkout — is never
/// written and every host-side git command is unaffected.
pub fn pointer_file() -> String {
    format!("gitdir: {GUEST_GITDIR}\n")
}

/// One snapshot of the tree, as `omh sNN log --turns` shows it.
///
/// Deliberately **not** a `Checkpoint`, and the missing field is the point.
/// A `Checkpoint` carries a `number`, and that number is the handle
/// `omh sNN diff <n>` and `--keep 1,3-4` take. Rendering snapshots with one
/// meant `log --turns` printed `3 / 2 / 1` and `commit --keep 2` then
/// replanted the *agent's* checkpoint 2 — a different list, no error, and the
/// overlapping range is ordinary. The only mitigation was a sentence in the
/// docs saying the numbers name nothing, which is the kind of guarantee this
/// codebase does not accept.
///
/// So a snapshot has no number. What it has instead is the spelling that does
/// work: `refs/omh/turn~N`, which is what the agent types to get the tree
/// back, and which cannot be confused with anything `--keep` accepts.
///
/// `landed` is absent for the same reason — nothing replays a snapshot, so
/// there is no such thing as one that has been handed over.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Turn {
    /// How far back from the tip: 0 is the newest.
    pub back: usize,
    /// The subject the hook wrote. Constant for omh's own snapshots, which is
    /// exactly why it is shown — anything else on this ref was put there by
    /// the agent.
    pub subject: String,
    pub age: Option<u64>,
    pub touched: Option<Touched>,
}

/// Commits reachable from a ref but not from HEAD — the agent's stranded work.
///
/// Shared by `preflight`, which refuses a harvest over them, and by
/// `checkpoints`, which says so while the user is still reading rather than
/// after they have decided the review is done. One list because they are one
/// question, and two copies of it drifted apart once already elsewhere in this
/// tree.
///
/// The exclusion is the whole reason this is a function. `rev-list --all` is
/// every ref under `refs/`, so omh's own turn snapshots — which are by
/// construction not ancestors of HEAD — are exactly the shape this hunts. Left
/// in, every `--keep` refuses forever and the refusal tells the user to delete
/// omh's commits.
///
/// `--exclude` goes after the subcommand and **before** the `--all` it applies
/// to — it is a `rev-list` option, and put in front of `rev-list` git rejects
/// it outright. Measured 2026-08-24 against git 2.55.0: positioned right it
/// takes the count from two to zero, and it does not reach `--reflog`, which
/// is a separate arm of the query in `unkept`.
fn stranded_args() -> Vec<String> {
    vec![
        "rev-list".into(),
        format!("--exclude={TURN_REF}"),
        "--all".into(),
        "--not".into(),
        "HEAD".into(),
    ]
}

/// Where the sandbox records the tree at the end of each turn.
///
/// One fixed name, never a glob, and the difference matters. The gitdir is a
/// read-write mount — the agent commits into it — so every ref omh teaches its
/// own guards to skip is a place the agent can put commits it would rather
/// those guards did not see. Today `refs/omh/*` does not exist, so writing
/// there only makes `preflight` noisier; an exclusion turns it into somewhere
/// quiet. A single name bounds that to one ref, and `omh sNN log --turns`
/// prints what is in it, so the hiding place is also the display case.
///
/// Under `refs/omh/` rather than `refs/heads/` for two reasons that are both
/// load-bearing. It stays off the branch, so `--keep` curates the agent's
/// commits and not omh's. And measured 2026-08-24: git writes reflogs only for
/// `refs/heads`, `refs/remotes`, `refs/notes` and `HEAD`, so a ref here has
/// none — which is what keeps `unkept`'s `--reflog` arm from finding snapshots
/// a second way, where `--exclude` could not have followed.
pub const TURN_REF: &str = "refs/omh/turn";

/// What the sandbox runs at the end of a turn to photograph the tree.
///
/// Here rather than in `base.rs` for the reason `probe_command` is here: the
/// command and the thing that reads what it writes are one mechanism, and
/// `TURN_REF`'s guarantees are only true if this is the shape that produced
/// it.
///
/// **The index is `mktemp`, fresh every turn.** A fixed path inside the gitdir
/// was three bugs at once, all of them silent. `read-tree` takes an
/// `<index>.lock`, and git never clears a stale one — so a turn killed
/// mid-hook disabled the feature for the rest of the session, with `--turns`
/// showing a frozen timeline and no marker. Two turns at once contended on the
/// same lock. And the file itself was ~32 KB living in the agent-writable
/// mount for the life of the session, larger than everything the snapshots
/// cost, and a directory the agent could create there killed the hook outright.
///
/// **`read-tree HEAD || read-tree --empty`**, because `read-tree` is only
/// there to clear the leftover index and an unborn HEAD is an ordinary
/// `git checkout --orphan` away. Without the fallback that agent gets no
/// snapshots for the rest of the session and is told nothing.
///
/// The other three properties, each load-bearing:
///
/// - **HEAD, the index and the worktree are untouched.** Nothing here moves a
///   branch or stages anything the agent can see. Verified: after a turn the
///   agent's `git status` still lists its work as uncommitted, which is the
///   whole reason this is not `git commit`.
/// - **An unchanged tree writes nothing** — no commit, no ref move. It still
///   costs the time, which the manifest says rather than claiming the turn is
///   free.
/// - **It cannot fail a turn.** Everything is redirected and closed with
///   `|| true`. An earlier version of this paragraph credited
///   `every_hook_runs_quietly_when_its_tool_says_nothing` with enforcing that;
///   it does not — `when` is false on any host, so the body never runs there.
///   `a_hook_that_cannot_do_its_job_still_ends_the_turn_quietly` is the one
///   that actually runs it against a repository it cannot use.
pub fn turn_hook_command(gitdir: &str, worktree: &str) -> String {
    // Both paths named, not a bare `git -C` through the pointer file. Inside
    // the sandbox the pointer resolves the gitdir either way, but naming both
    // is what this design's own invariant asks of every git call omh makes,
    // and it is what lets the shipped command run against a plain repository
    // rather than only inside a container.
    //
    // Not merely tidiness: the shadow is `core.bare=false` with deliberately
    // no `core.worktree`, so `add` with a gitdir and no worktree does not fail
    // — it silently stages the current directory. Naming the worktree is what
    // stops that, and an earlier draft of this comment had the failure mode
    // backwards.
    let g = &[
        "git -C ",
        worktree,
        " --git-dir=",
        gitdir,
        " --work-tree=",
        worktree,
    ]
    .concat();
    [
        "{ i=$(mktemp)",
        "; GIT_INDEX_FILE=$i ",
        g,
        " read-tree HEAD",
        " || GIT_INDEX_FILE=$i ",
        g,
        " read-tree --empty",
        "; GIT_INDEX_FILE=$i ",
        g,
        " add -A",
        " && t=$(GIT_INDEX_FILE=$i ",
        g,
        " write-tree)",
        " && p=$(",
        g,
        " rev-parse -q --verify ",
        TURN_REF,
        " || true)",
        " && if [ -n \"$p\" ] && [ \"$(",
        g,
        " rev-parse \"$p^{tree}\")\" = \"$t\" ]",
        "; then :",
        "; else c=$(",
        g,
        " commit-tree \"$t\" ${p:+-p} ${p:+\"$p\"} -m \"turn end\")",
        " && ",
        g,
        " update-ref ",
        TURN_REF,
        " \"$c\"",
        "; fi; rm -f \"$i\"; } >/dev/null 2>&1 || true",
    ]
    .concat()
}

/// The hook's own arguments, which are the guest's paths.
///
/// Split from the command so a test can run the **shipping** shell against a
/// temporary repository rather than against a copy of it written out again.
/// The first draft of that test re-implemented the plumbing in Rust, which is
/// the trap this project keeps rediscovering: it proved the reimplementation
/// worked, and would have passed against a hook that did nothing at all.
pub fn turn_hook_for_the_sandbox() -> String {
    // Through `container_workdir()` rather than a second literal — a test
    // allows exactly one spelling of the guest workdir in the whole tree, and
    // it found the const the moment it was added. It reads comments too, so
    // this one does not quote the path either.
    turn_hook_command(GUEST_GITDIR, crate::container_workdir())
}

/// The test that stops the hook from running anywhere it would be nonsense.
///
/// `/omh/shadow` exists only inside a sandbox, and the hook is rendered and
/// executed on the host by the base-set suite — where `/work` is somebody's
/// checkout and `git -C /work` would be a real command against a real
/// repository.
pub fn turn_hook_when() -> String {
    format!("[ -d {GUEST_GITDIR} ]")
}

/// Where omh leaves a sentence for the agent to find at its next start.
///
/// In the gitdir rather than under a mount of its own, and that is not
/// laziness: this directory is already mounted, already writable — the agent
/// commits into it — and git ignores a file it does not know. Measured
/// 2026-08-23 against git 2.55.0, in a `--separate-git-dir` layout because
/// that is the shape the shadow has: `status`, `log` and `fsck` say nothing
/// about it and `gc --prune=now` does not reap it.
///
/// Writable is the requirement, not an accident. The hook that reads this
/// **deletes it**, which is what makes the note one-shot; the read-only
/// treatment `config` and the push hook get would leave the same paragraph
/// arriving at every context rebuild for the rest of the session. Nothing here
/// is a control — an agent that deletes its own note has only skipped its own
/// news.
pub const GUEST_NOTE: &str = "/omh/shadow/omh-note";

/// The note's name inside the gitdir, host side.
///
/// The two paths must name one file — a hook reading a path omh never writes
/// fails by saying nothing at all, forever — and that correspondence is
/// asserted rather than derived. Deriving it read better and hid a panic: the
/// first version pulled the basename off `GUEST_NOTE` with an `expect`, and a
/// panic is not an `Err`, so relocating the note under a mount of its own
/// would have aborted at the very end of a *completed* sync — past the merge,
/// past the baseline, past the commit — with a backtrace instead of a report.
/// That is the one outcome the call site's own comment exists to prevent.
pub fn note_file(gitdir: &std::path::Path) -> PathBuf {
    gitdir.join(NOTE_NAME)
}

/// The note's basename, shared by both ends so there is one name to change.
const NOTE_NAME: &str = "omh-note";

/// Where the `pre-push` hook lands inside the container.
///
/// Mounted read-only rather than written into the gitdir, so the agent cannot
/// take it away. Measured: against a read-only mount `rm` gives `Resource
/// busy`, and overwriting or `chmod -x` give `Read-only file system` — the
/// three ways a file-owning agent silently disarms a hook, all closed.
///
/// It is still not a wall. `git push --no-verify` and
/// `git -c core.hooksPath=… push` never consult the file at all, and both were
/// measured pushing to a reachable remote with this mount in place. What the
/// mount buys is that the hook cannot *quietly* stop being there — a bypass now
/// takes a deliberate flag, which is a different thing from a missing file.
pub const GUEST_PRE_PUSH: &str = "/omh/shadow/hooks/pre-push";

/// Where the sandbox's own config lands inside the container.
///
/// Mounted read-only over the copy `ensure` wrote, for the reason the push hook
/// is: the gitdir has to be writable because the agent commits into it, so
/// every file in it is the agent's to change — and this one decides what `git`
/// does. `NEUTRALISED` answers that for calls omh makes on the host; nothing
/// answers it for the agent's own git inside the container.
///
/// Measured inside a real container, because a host-side stand-in got the
/// answer right and the reason wrong. `commit`, `checkout -b` and
/// `reset --hard` never write this file and do not notice it. `git config` and
/// `git remote add` do, and meet:
///
/// ```text
/// error: could not write config file /omh/shadow/config: Resource busy
/// fatal: could not set 'remote.origin.url' to 'https://example.com/x.git'
/// ```
///
/// `Resource busy`, not `Read-only file system` — git replaces this file by
/// renaming a lock over it, and what refuses is the rename onto a mount point
/// rather than the read-only flag. The host stand-in used before this was
/// measured says `Operation not permitted`, and for a different reason again:
/// an immutable flag refusing the rename, not a mount. Same outcome, three
/// different mechanisms, which is exactly how a stand-in gets quoted as if it
/// were the thing.
///
/// That second one is the interesting one. `git push` fails for want of a
/// remote and git's own error suggests `git remote add` — which now fails too,
/// so the route git talks the agent into is closed rather than signposted. The
/// `pre-push` hook keeps its job on the other route: `git push <url> <ref>`
/// needs nothing in config and still meets it.
pub const GUEST_CONFIG: &str = "/omh/shadow/config";

/// The hook's body, so the launcher can stage the same bytes it mounts.
pub fn pre_push_hook() -> String {
    // A quoted heredoc, not `echo '…'`. The message is prose and prose has
    // apostrophes: `the sandbox's own` closed the single quote, and the hook
    // became a script that does not parse. It still exited non-zero, so it
    // still refused and a test asserting failure still passed — but what the
    // agent read was `unexpected EOF while looking for matching \`'\`` with no
    // mention of omh, which is the whole reason it exists rather than letting
    // git fail on its own.
    format!("#!/bin/sh\ncat >&2 <<'OMH'\n{NO_PUSH}\nOMH\nexit 1\n")
}

/// The sandbox's own repository for one session.
pub struct Shadow {
    /// The gitdir, mounted into the container. Agent-writable.
    pub gitdir: PathBuf,
    /// Where the seed commit is recorded. A sibling of the gitdir rather than
    /// anything inside it: the gitdir is mounted, so a tag or a config entry
    /// there is something the agent can delete, and losing the seed is losing
    /// the only fixed point a harvest can replay from.
    pub seed_record: PathBuf,
    /// What the last harvest took, in the sandbox's own commit ids.
    ///
    /// Beside the seed and for the same reason: the gitdir is mounted, so
    /// anything recorded inside it is the agent's to delete, and a replay point
    /// that can be forged is a branch that can be handed work twice.
    ///
    /// Absent until the first harvest, which is why it is an `Option` rather
    /// than a second seed — a session that has never landed anything replays
    /// from the seed, and that is not a missing record, it is the first round.
    pub landed_record: PathBuf,
    /// Named for the session so the user can tell which sandbox an editor
    /// window is showing, and `-scratch` because that is what it is: the
    /// history the user curates before any of it becomes the branch's.
    pub branch: String,
}

/// What a checkpoint touched, when git was willing to count it.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Touched {
    pub files: usize,
    pub added: usize,
    pub removed: usize,
    /// Files git printed `-` for instead of a number.
    ///
    /// Two things reach this and neither is *nothing changed*: a binary file,
    /// and a path the agent gave a `-diff` attribute — `info/attributes` lives
    /// inside the mount and the read-only `config` does not cover it. Measured
    /// 2026-08-23: with `* -diff` in place, a commit adding two lines prints
    /// `-\t-\tc.txt`. Counted as files and never as zero lines, because a
    /// blank churn column beside *1 file* is how a 200MB blob reads as a
    /// mode-bit change.
    pub uncounted: usize,
}

/// One commit the agent made inside the sandbox, as the user reads it.
///
/// `subject` is the agent's own words and is **not** sanitised here — the
/// render boundary owns that, and a value sanitised twice is one nobody can
/// match against git's own output when something goes wrong.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Checkpoint {
    /// 1 is the oldest. Stable as the agent commits more.
    pub number: usize,
    /// The sandbox-side commit id, which is not the id it will have on the
    /// branch: `harvest` replants and stamps, and both rewrite it.
    pub id: String,
    /// The agent's subject line, raw.
    pub subject: String,
    /// Seconds between the commit and now, or `None` when omh could not read a
    /// date from it. Rendered as a question mark rather than as *just now*: a
    /// timestamp omh could not parse is the one case where the strongest
    /// possible claim would be the fallback for having no information.
    pub age: Option<u64>,
    /// What it touched, or `None` for a merge.
    ///
    /// A merge has no diff of its own until you say which parent to compare
    /// against, and omh does not choose one. Measured 2026-08-23: `git log
    /// --numstat` prints the header for a merge and no numstat lines at all,
    /// so counting the absence as zero renders a merge that brought in
    /// hundreds of lines identically to an empty commit.
    pub touched: Option<Touched>,
    /// Already handed to the branch by a previous `--keep`.
    pub landed: bool,
}

/// Everything one read of the sandbox's repository answers.
///
/// More than the list, because two of these states make the list *incomplete*
/// and the list cannot say so about itself. Both are states `harvest` refuses
/// over — a log that showed neither would let a user read a clean review and
/// then be refused by `--keep` citing work they had never been shown.
#[derive(Debug, Clone, Default)]
pub struct Checkpoints {
    pub commits: Vec<Checkpoint>,
    /// Commits reachable from some ref but not from HEAD, and so invisible to
    /// a read of `seed..HEAD`. `preflight`'s stranded check is the same
    /// question, asked where it refuses rather than where it reports.
    pub unreachable: usize,
    /// The replay point names a commit this history no longer reaches.
    ///
    /// Then nothing can be marked as already handed over — not because nothing
    /// was, but because omh cannot tell which. `rev-list seed..landed` still
    /// *succeeds* in this state (measured: exit 0, the ids still resolve),
    /// which is why this is asked separately rather than inferred from a
    /// failure.
    pub replay_point_lost: bool,
    /// Files in the sandbox's worktree that no checkpoint holds.
    ///
    /// Measured the way `harvest` measures it — same gitdir, same
    /// `status --porcelain`, no `-uall`, no rules pathspec — because the
    /// number's whole job is to be the set `--keep` is about to sweep into
    /// *Work in progress*. `Session::uncommitted` answers a different question
    /// (the session worktree against the session *branch*) and would count
    /// work the agent checkpointed an hour ago.
    pub uncommitted: usize,
}

impl Shadow {
    pub fn new(shadow_dir: &Path, session_id: &str) -> Self {
        Self {
            gitdir: shadow_dir.join(format!("{session_id}.git")),
            seed_record: shadow_dir.join(format!("{session_id}.seed")),
            landed_record: shadow_dir.join(format!("{session_id}.landed")),
            branch: format!("{session_id}-scratch"),
        }
    }

    /// Create the repository and seed it with the worktree as it stands.
    ///
    /// Idempotent for a *finished* shadow: relaunching into a running session
    /// must not reset the agent's checkpoints, so one that has a seed recorded
    /// keeps every commit, ref and index it had. The one thing it does not keep
    /// is the exclude list, which is derived from mounts that move between
    /// launches — see the comment on the fast path. One without is the wreckage of a launch that
    /// died partway through, and is rebuilt rather than adopted — see the two
    /// notes in the body for why that cannot lose work.
    pub fn ensure(&self, worktree: &Path, excluded: &[String]) -> Result<()> {
        // Both, not just the directory. Seven subprocess calls stand between
        // `git init` and a usable repository, and a launch killed anywhere in
        // the middle leaves a directory that looks finished.
        //
        // The pair is written seed-then-rename — see the note at the bottom of
        // this function — so a launch killed between them leaves a seed naming
        // a gitdir that is not there, and this condition rebuilds it. The
        // reverse, a gitdir with no seed, is not something a launch produces;
        // `reap` does, when `remove_dir_all` fails and the seed file goes
        // anyway. An earlier version of this comment claimed the record was
        // written last, contradicting the one four lines from the end, and
        // `log_cmd` inherited the mistake.
        if self.gitdir.exists() && self.seed_record.exists() {
            // The repository is left exactly as it is — that is what makes
            // relaunching safe — but the exclude list is not part of "as it
            // is". `container::plan` builds it from the `carry_in` policy and
            // then from the mounts it is about to make, and the second half
            // moves: switch harness, or switch a capability on, and a document
            // lands inside `/work` that this repository has never heard of, so
            // `git add -A` sweeps omh's rendered file — credentials and all —
            // into a history `--keep` replays onto the branch.
            //
            // Written here rather than through a `refresh` the caller has to
            // remember. The tests would catch `plan` forgetting one today; what
            // they cannot catch is the *next* caller of `ensure`, which would
            // be a path they do not cover. `ensure` already takes the list, so
            // the only question was whether it believed it on the second
            // launch.
            //
            // Wholesale, so anything the agent added to this file for its own
            // housekeeping goes with it. That is the trade: merging would keep
            // those, and would also keep an entry omh has since dropped —
            // leaving a path silently untracked in the one repository whose
            // job is to show the agent its own work.
            Self::write_exclude(&self.gitdir, excluded)?;
            Self::write_config(&self.gitdir)?;
            return Ok(());
        }
        let parent = self
            .gitdir
            .parent()
            .context("a shadow gitdir needs somewhere to live")?;
        std::fs::create_dir_all(parent)?;

        // Built beside the real path and moved onto it, so the directory the
        // rest of omh looks for never exists in a half-made state. Same
        // filesystem by construction — both are children of `parent` — which is
        // what makes the move atomic rather than a copy.
        //
        // Anything left over from an attempt that did not finish is removed
        // first, and that is safe precisely because it did not finish: without
        // a seed record it was never mounted, so there is no agent work in it
        // to lose.
        let building = parent.join(format!(
            "{}.building",
            self.gitdir
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
        ));
        let _ = std::fs::remove_dir_all(&building);
        let _ = std::fs::remove_dir_all(&self.gitdir);

        // `init --bare` because the worktree is supplied per-command and is not
        // this directory: the positional non-bare form puts a `.git` *inside*
        // the directory it is given, one level deeper than anything here
        // expects.
        //
        // Positional rather than through the `git()` wrapper because `--bare`
        // and `--work-tree` cannot be combined — git refuses the pair, and
        // confusingly blames the missing `--git-dir` it was in fact given. Not,
        // as this once said, because the repository does not exist yet: `init`
        // through the wrapper is fine, and accepts a `--work-tree` naming a
        // path that is not there at all.
        let made = Command::new("git")
            // `--template=` empty, because `git init` otherwise copies the
            // user's `init.templateDir` into this gitdir — and this gitdir is
            // mounted into the container. That is a host-to-sandbox copy of
            // arbitrary host files, in the one module whose thesis is that
            // safety comes from what is *not* in here; template hooks would
            // then also run inside the sandbox.
            .args(["init", "-q", "--bare", "--template=", "-b", &self.branch])
            .arg(&building)
            .output()
            .context("creating the sandbox's repository")?;
        anyhow::ensure!(
            made.status.success(),
            "git init: {}",
            String::from_utf8_lossy(&made.stderr).trim()
        );
        // Not `--bare` as the repository *behaves*: it has a worktree, it is
        // just one git is told about rather than one it sits in.
        git(&building, worktree, &["config", "core.bare", "false"])?;

        // An identity of its own, because git will not commit without one and
        // the container has no global config to supply it. Without this the
        // agent's first checkpoint dies on `Author identity unknown` — the one
        // thing this repository exists to let it do — on any machine nobody has
        // configured, which is every container and every CI runner.
        //
        // It costs nothing in trust: the agent can rewrite this, and a harvest
        // stamps authorship on the host anyway, per the module's own rule about
        // not believing what the sandbox says about itself. This is here so an
        // unconfigured machine works, not so the name can be relied on.
        Self::write_config(&building)?;

        // And deliberately no `core.worktree`. It looks like the missing half
        // of the line above and it is the opposite: this gitdir is written on
        // the host and read inside a container, so a worktree path recorded
        // here is a host path that does not exist there — and `core.worktree`
        // outranks the directory the `.git` pointer sits in. Setting it made
        // every command in the sandbox fail with `fatal: Invalid path` — the
        // whole list this feature exists to restore.
        //
        // Nothing needs it. Host-side callers pass `--work-tree` themselves,
        // and in the container the pointer file's own directory is the answer,
        // which is `/work` and correct. Guarded by
        // `the_pointer_file_alone_resolves_to_the_worktree_it_sits_in`, which
        // resolves the way the container does rather than the way the tests
        // used to — that blind spot is why this shipped.

        // Inside the gitdir, so nothing omh does appears in the user's tree —
        // the exclude file `carry` writes lives in the *worktree's* git dir and
        // is a different mechanism for a different repository.
        Self::write_exclude(&building, excluded)?;
        Self::write_pre_push(&building)?;

        // And deliberately no `core.hooksPath`, for the same reason as
        // `core.worktree` above and learned the same way — by writing it and
        // watching the container fail.
        //
        // A global `core.hooksPath` does send git looking elsewhere and would
        // leave the hook installed but never consulted. That is a real hazard
        // on the *host*, and it does not exist in the sandbox: the container
        // carries no global git config at all, so `$GIT_DIR/hooks` is where it
        // looks and the hook is right there. Pinning the value wrote a host
        // path into a config only the container reads, which pointed at nothing
        // and let a push through — verified by pushing to a reachable remote
        // and finding two commits on it.
        //
        // If a host-side reader is ever added, it passes `-c core.hooksPath=`
        // itself rather than recording anything here.

        // Everything the worktree holds except what was just excluded. `add -A`
        // rather than a path list: the seed has to be the tree the session
        // actually starts from, or every later diff is against a fiction.
        git(&building, worktree, &["add", "-A", "."])?;
        git(
            &building,
            worktree,
            &[
                // The user's global config governs this commit otherwise, and
                // two ordinary settings turn it into a launch that will not
                // start: `commit.gpgsign = true` fails with `gpg failed to
                // sign the data`, and a `core.hooksPath` pointing at a husky or
                // team hooks directory runs their `pre-commit` against omh's
                // seed. Neither is the user asking for anything — this is a
                // commit they never made, in a repository they cannot see.
                "-c",
                "commit.gpgsign=false",
                "commit",
                "-q",
                "--no-verify",
                "--allow-empty",
                "-m",
                &seed_message(),
            ],
        )?;
        let seed = git(&building, worktree, &["rev-parse", "HEAD"])?;

        // The record before the rename, so the two cannot disagree in the
        // direction that matters. A crash between them leaves a seed naming a
        // gitdir that is not there yet, and the next launch rebuilds both; the
        // reverse — a gitdir with no seed — is the state the fast path above
        // would wave through.
        std::fs::write(&self.seed_record, seed.trim())?;
        std::fs::rename(&building, &self.gitdir)?;
        Ok(())
    }

    /// The commit the session started from, read from the host-side record.
    ///
    /// Not from the shadow itself, at any price. A tag is one `tag -d` away and
    /// the root commit stops being the seed the moment an agent runs
    /// `checkout --orphan` — both leave a harvest replaying from the wrong
    /// point, which is worse than refusing to replay at all.
    pub fn seed(&self) -> Result<String> {
        let seed = std::fs::read_to_string(&self.seed_record).with_context(|| {
            format!(
                "no seed recorded for this session at {}",
                self.seed_record.display()
            )
        })?;
        Ok(seed.trim().to_string())
    }

    /// What the last harvest took, if there has been one.
    ///
    /// A *sandbox-side* commit, deliberately: the range a harvest replays is
    /// computed in the fetched history's ids, and the ids the branch ended up
    /// with are different commits — `replant` rewrites them onto a new parent
    /// and `stamp` rewrites them again. Recording what landed on the branch
    /// would be recording something this range can never mention.
    ///
    /// **Absent and unreadable are different answers.** Only "not there" means
    /// *never harvested*. Every other failure is a record that exists and could
    /// not be read, and reading that as `None` replays from the seed — offering
    /// the branch everything it already has, which is the defect this record
    /// exists to close, reached by a permissions error rather than by a second
    /// run.
    ///
    /// An earlier version collapsed the two and excused it by saying the
    /// ancestry check downstream would catch it. It does not: that check lives
    /// inside the arm where a record *was* read, so the one case being excused
    /// is the one case it never sees. `needles` in this file already states the
    /// rule this now follows — cannot tell must not spell the same as clean.
    pub fn landed(&self) -> Result<Option<String>> {
        let landed = match std::fs::read_to_string(&self.landed_record) {
            Ok(landed) => landed,
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
            Err(e) => {
                return Err(e).with_context(|| {
                    format!(
                        "reading what {} last handed over, at {}",
                        self.branch,
                        self.landed_record.display()
                    )
                })
            }
        };
        let landed = landed.trim();
        // Empty is not absent either, and it is the likelier of the two: the
        // write that produces this record truncates before it writes, so a
        // process killed in that window leaves zero bytes where a commit id
        // was. Read as *never harvested* it would replay from the seed and skip
        // the ancestry check on the way past — the widest failure available,
        // reached by the narrowest accident.
        anyhow::ensure!(
            !landed.is_empty(),
            "{} is empty, which is what an interrupted write leaves behind. omh \
             cannot tell what it last handed over, and will not guess. Take the \
             files as they stand with `omh s commit -m`",
            self.landed_record.display()
        );
        Ok(Some(landed.to_string()))
    }

    /// The sandbox's own commits, numbered, with what each one touched.
    ///
    /// **Numbered from the oldest**, so a number keeps meaning the same commit
    /// as the agent adds more. The numbers are what `diff <n>` (#55) and
    /// `--keep <selection>` (#56) take, and a selection typed against a list one
    /// commit out of date would land a different set of commits than the one
    /// on screen, silently.
    ///
    /// `--topo-order` is what makes that true, and it is not decoration.
    /// Measured 2026-08-23 against git 2.55.0: `--reverse` alone orders by
    /// commit date, so merging a side branch whose commits are older inserts
    /// them into the *middle* of the list and everything after them shifts
    /// down — `Add a` was 1 and became 2. With `--topo-order` the same merge
    /// appends and 1 still names what it named.
    ///
    /// Read on the host, from a gitdir the agent can write. Everything here
    /// goes through `git`, which is what carries `NEUTRALISED` and `GUEST_ENV`
    /// — and for this command those are not about executing anything, they are
    /// about being believed. The subject stays raw, to be sanitised at the
    /// render boundary by whoever prints it.
    pub fn checkpoints(&self, worktree: &Path) -> Result<Checkpoints> {
        let seed = self.seed()?;
        let mut out = Checkpoints {
            // What `--keep` would sweep, measured where `--keep` measures it.
            uncommitted: git(&self.gitdir, worktree, &["status", "--porcelain"])?
                .lines()
                .filter(|l| !l.trim().is_empty())
                .count(),
            // The same question `preflight` refuses over. Asked here so the
            // answer arrives while the user is still reading, rather than as a
            // refusal after they have decided the review is done.
            unreachable: git_owned(&self.gitdir, worktree, &stranded_args())?
                .lines()
                .filter(|l| !l.trim().is_empty())
                .count(),
            ..Checkpoints::default()
        };

        // What the branch already has, in this repository's ids. Read from the
        // replay point rather than compared against the branch, so the line
        // drawn here is the line the next `--keep` will act on — asking the
        // branch instead would answer with commits `stamp` rewrote, which this
        // history cannot mention.
        let handed: BTreeSet<String> = match self.landed()? {
            // Ancestry first. `rev-list seed..landed` succeeds against a
            // replay point the history no longer reaches (measured: exit 0,
            // the ids still resolve), so without this the set simply fails to
            // match anything and every checkpoint reads as new — and the log
            // then offers a `--keep` that `harvest` refuses for this exact
            // reason.
            Some(landed) if self.reaches(worktree, &landed)? => git(
                &self.gitdir,
                worktree,
                &["rev-list", &format!("{seed}..{landed}")],
            )?
            .lines()
            .map(str::to_string)
            .collect(),
            Some(_) => {
                out.replay_point_lost = true;
                BTreeSet::new()
            }
            None => BTreeSet::new(),
        };

        // One call, not one per commit. `--numstat` rather than `--shortstat`
        // because the totals are then arithmetic omh does rather than a
        // sentence omh parses, and because `-` for an uncountable file is a
        // distinction `--shortstat` has already thrown away by the time it
        // reaches this process.
        //
        // The record separator leads each header and the subject comes last,
        // so the agent's own words cannot shift the fields that follow them.
        // Measured: git refuses to write a commit whose message holds a NUL
        // (`error: a NUL byte in commit log message not allowed`), so the
        // separator cannot appear inside the one field that is the agent's.
        //
        // `%ct` and not `%at`: the list is ordered by commit date, and
        // `--amend`, `rebase` and `cherry-pick` — all ordinary agent moves —
        // keep the author date while minting a new commit date. Author dates
        // would run non-monotonically down a list the reader takes as
        // chronological.
        let raw = git(
            &self.gitdir,
            worktree,
            &[
                "log",
                "--topo-order",
                "--reverse",
                "--format=%x00%H%x00%ct%x00%P%x00%s",
                "--numstat",
                &format!("{seed}..HEAD"),
            ],
        )?;

        out.commits = parse_log(&raw, &handed);

        let counted: usize = git(
            &self.gitdir,
            worktree,
            &["rev-list", "--count", &format!("{seed}..HEAD")],
        )?
        .trim()
        .parse()
        .unwrap_or(usize::MAX);
        anyhow::ensure!(
            counted == out.commits.len(),
            "git listed {counted} commits in this sandbox and omh read {}. omh will not \
             number a list it did not fully understand, because the numbers are what \
             `--keep` takes. Read it directly:\n  git --git-dir={} log",
            out.commits.len(),
            self.gitdir.display()
        );
        Ok(out)
    }

    /// One checkpoint, as a summary or as the patch.
    ///
    /// Takes the number the log printed rather than an object id, and resolves
    /// it here — so a caller cannot reach a commit this session never showed.
    /// Not for safety: the store is the agent's own and holds nothing the user
    /// may not see. It is that a command which prints any object you name is a
    /// different command from one that shows you a checkpoint, and the numbers
    /// are the only handle the log ever offered.
    pub fn show(
        &self,
        worktree: &Path,
        number: usize,
        what: crate::session::What,
    ) -> Result<String> {
        let id = self.checkpoint_id(worktree, number)?;
        let args = show_args(what, &id, "auto");
        let args: Vec<&str> = args.iter().map(String::as_str).collect();
        git(&self.gitdir, worktree, &args)
    }

    /// One checkpoint's patch, on the terminal, through the user's pager.
    ///
    /// The pager is the one place this cannot simply hand the terminal to git,
    /// and the reason is omh's own hardening rather than a live threat.
    ///
    /// What git does with a repository's `core.pager` is worth knowing:
    /// measured 2026-08-23 on a pty, a value of `sh -c "echo …; cat"` executes
    /// on a plain `git show`, and only on a tty. That is why `NEUTRALISED`
    /// pins the key to `cat`. It is **not** why the sandbox's config is safe —
    /// `write_config` rewrites that file to a ten-key allowlist on every
    /// launch and `container::plan` mounts it read-only (#52), so a pager key
    /// cannot survive there to begin with. Three layers, and this comment
    /// claimed the outermost was the only one.
    ///
    /// The pin is what would leave `-p` unable to page at all, so the user's
    /// own pager is appended after it and the last `-c` wins (measured).
    ///
    /// One thing this does *not* close: `pager.show` precedes `core.pager` in
    /// git's own order, and it is not in `NEUTRALISED`. The allowlist and the
    /// read-only mount are what make that unreachable, not this line.
    pub fn stream_show(
        &self,
        repo: &Path,
        worktree: &Path,
        number: usize,
        colour: &str,
    ) -> Result<()> {
        let id = self.checkpoint_id(worktree, number)?;
        let args = show_args(crate::session::What::Patch, &id, colour);
        let args: Vec<&str> = args.iter().map(String::as_str).collect();
        let status = Command::new("git")
            .envs(GUEST_ENV)
            .current_dir(worktree)
            .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
            .arg("-c")
            .arg(format!("core.pager={}", user_pager(repo)))
            .arg("--git-dir")
            .arg(&self.gitdir)
            .arg("--work-tree")
            .arg(worktree)
            .args(guarded(&args))
            .status()
            .context("running git show")?;
        // Measured on a pty: a pager that quits early leaves git at 0, and a
        // pager git could not execute leaves it at 128 — so this reports the
        // second without misfiring on the first. What it cannot see is a pager
        // whose *shell* started and then failed, which git reports as 0 with no
        // patch; that is git's own behaviour and identical to running `git
        // show` yourself.
        anyhow::ensure!(status.success(), "git show exited {status}");
        Ok(())
    }

    /// The commit a number names, or a refusal that says what the numbers are.
    fn checkpoint_id(&self, worktree: &Path, number: usize) -> Result<String> {
        let read = self.checkpoints(worktree)?;
        if let Some(found) = read.commits.iter().find(|c| c.number == number) {
            return Ok(found.id.clone());
        }
        anyhow::bail!(
            "there is no checkpoint {number} in this session. {}",
            match read.commits.len() {
                0 => "The agent has not committed anything here yet".to_string(),
                1 => "There is one, numbered 1".to_string(),
                n => format!("They are numbered 1 to {n}"),
            }
        )
    }

    /// The turn snapshots themselves, oldest first, for `omh sNN log --turns`.
    ///
    /// Read through the same parser as the agent's own commits, and returned
    /// as its own list rather than merged into `Checkpoints::commits`. That
    /// separation is load-bearing in three places: `diff <n>` and
    /// `--keep 1,3-4` both index `commits` by number, so a snapshot appended
    /// there would become selectable and then replayable; and the "yours from
    /// here" divider is an index into rendered rows, so an interleaved list
    /// would silently label rows as already on the branch.
    ///
    /// `landed` is meaningless here and is always false — nothing replays a
    /// snapshot.
    pub fn turn_log(&self, worktree: &Path) -> Result<Vec<Turn>> {
        // `None` is *no ref*, which really is an empty list. A failure to read
        // is not, and now propagates — it used to become this same empty
        // vector, which `log --turns` renders as a sentence about what the
        // agent did.
        if self.turns(worktree)?.is_none() {
            return Ok(Vec::new());
        }
        let raw = git(
            &self.gitdir,
            worktree,
            &[
                "log",
                "--topo-order",
                "--reverse",
                "--format=%x00%H%x00%ct%x00%P%x00%s",
                "--numstat",
                TURN_REF,
            ],
        )?;
        // Newest first, and numbered by distance from the tip rather than
        // from the start — `refs/omh/turn~0` is the newest, which is the
        // spelling that gets the tree back.
        let read = parse_log(&raw, &BTreeSet::new());
        let last = read.len().saturating_sub(1);
        Ok(read
            .into_iter()
            .enumerate()
            .map(|(i, c)| Turn {
                back: last - i,
                subject: c.subject,
                age: c.age,
                touched: c.touched,
            })
            .collect())
    }

    /// How many turn snapshots the sandbox is holding, or nothing if it has
    /// never taken one.
    ///
    /// `Option`, not a bare count, and the difference is the one this project
    /// has now removed three times — #46, #63, #64 — and reintroduced here.
    /// The first draft returned `Ok(0)` for *every* failure: a gitdir that is
    /// not there, a git that would not run, a ref pointing at a pruned object,
    /// an unreadable object store, and stdout that would not parse. Its own
    /// comment justified exactly one of those and applied it to all of them.
    ///
    /// What it cost is downstream. `turn_log` gated on the zero, so a sandbox
    /// omh could not read printed *"no turns recorded — the sandbox has not
    /// finished one with anything changed"*, which is a claim about the
    /// agent's behaviour. And `may_remove` took it through `.unwrap_or(0)`, so
    /// the last guard in front of an irreversible delete said nothing at all.
    ///
    /// **`for-each-ref`, not `rev-list --count`.** The two answers have to be
    /// told apart by something other than the exit status, because `rev-list`
    /// exits non-zero for a ref that is simply absent — the ordinary case —
    /// and that is what made the collapse tempting. `for-each-ref` exits 0
    /// either way and prints nothing when the ref is not there, so a non-zero
    /// exit means the question could not be answered. The same shape as #63's
    /// `ps --filter`, for the same reason.
    /// How many turn snapshots the sandbox is holding.
    ///
    /// Its own reader, and its own count, because a snapshot answers a
    /// different question from every other commit in here. It is not work the
    /// agent chose to keep — it is a tree omh photographed at the end of a
    /// turn, which is worth something precisely when the agent has since
    /// thrown that tree away.
    ///
    /// So `rm` names them and does not refuse over them: refusing on every
    /// session that ever had one dirty turn is how a guard teaches people to
    /// type `--force` without reading, and that guard still has the agent's
    /// own commits to protect.
    pub fn turns(&self, worktree: &Path) -> Result<Option<usize>> {
        if !self.gitdir.exists() {
            return Ok(None);
        }
        // Through the module's own helper, so this read is `guarded()` like
        // every other and its failure carries git's words.
        let listed = git(
            &self.gitdir,
            worktree,
            &["for-each-ref", "--format=%(refname)", TURN_REF],
        )?;
        if listed.trim().is_empty() {
            return Ok(None);
        }
        let counted = git(&self.gitdir, worktree, &["rev-list", "--count", TURN_REF])?;
        Ok(Some(counted.trim().parse().with_context(|| {
            format!(
                "counting turn snapshots, which answered `{}`",
                counted.trim()
            )
        })?))
    }

    /// Everything this repository holds that no branch has, counted wide.
    ///
    /// A different question from `checkpoints`, and deliberately a blunter
    /// one. `log` numbers what the user can act on — `seed..HEAD`, in order.
    /// `rm` is about to delete the repository, so what matters is whether
    /// **anything** in it exists nowhere else, however the agent left it.
    ///
    /// `--all --reflog` is the difference, and it is not defensive padding.
    /// Measured 2026-08-23: after a `reset --hard` back to the seed — one of
    /// the four commands this whole feature exists to give the agent —
    /// `seed..HEAD` counts 0 and `--all --not HEAD` counts 0, while three
    /// commits are still in the repository and on no branch anywhere.
    /// `--all --reflog --not <from>` counts all three. It also catches the
    /// side branch the agent wandered off, which `preflight` already refuses a
    /// harvest over.
    ///
    /// Counted from the last handover when there is one that still resolves,
    /// and from the seed otherwise: a replay point the history no longer
    /// reaches means omh cannot tell what was handed over, and counting from
    /// the seed over-counts rather than under-counts. For a question asked
    /// before an irreversible act, that is the direction to be wrong in.
    pub fn unkept(&self, worktree: &Path) -> Result<(usize, usize)> {
        // The last handover, or the seed when there has not been one. A record
        // that no longer names anything this repository has makes the count
        // below fail, which is *omh cannot tell* — the answer the caller
        // refuses over. An earlier version asked `reaches` first and fell back
        // to the seed; that arm could not be made to change any answer, and an
        // untestable branch guarding a case another line already handles is
        // one more thing to be wrong about.
        let from = match self.landed()? {
            Some(landed) => landed,
            None => self.seed()?,
        };
        let lines = |out: String| out.lines().filter(|l| !l.trim().is_empty()).count();
        Ok((
            lines(git(
                &self.gitdir,
                worktree,
                &[
                    "rev-list",
                    // After the subcommand and before the `--all` it applies
                    // to — it is a `rev-list` option, not a `git` one. It does
                    // not reach `--reflog`, which is why the turn ref has to
                    // be one git writes no reflog for. Measured 2026-08-24.
                    &format!("--exclude={TURN_REF}"),
                    "--all",
                    "--reflog",
                    "--not",
                    &from,
                ],
            )?),
            lines(git(&self.gitdir, worktree, &["status", "--porcelain"])?),
        ))
    }

    /// Commit the worktree as it stands, so a sync has a point to undo from.
    ///
    /// Runs **before** anything is written, and it is the reason a sync can be
    /// recovered from at all: `omh sNN log` then shows the state the session
    /// was in, and `git checkout <that>` inside the sandbox takes it back.
    ///
    /// Answers `None` when there was nothing to commit, which is the ordinary
    /// case for a session whose agent has just committed its work — that is not
    /// a failure and must not read as one.
    pub fn checkpoint(&self, worktree: &Path, why: &str) -> Result<Option<String>> {
        if git(&self.gitdir, worktree, &["status", "--porcelain"])?
            .trim()
            .is_empty()
        {
            return Ok(None);
        }
        git(&self.gitdir, worktree, &["add", "-A", "."])?;
        git(
            &self.gitdir,
            worktree,
            &["commit", "-q", "--no-verify", "-m", why],
        )?;
        Ok(Some(
            git(&self.gitdir, worktree, &["rev-parse", "HEAD"])?
                .trim()
                .to_string(),
        ))
    }

    /// Record what moved underneath the agent, as a commit it can read.
    ///
    /// The point of writing anything into the sandbox's repository rather than
    /// only rewriting files: `git show HEAD` is then exactly what changed and
    /// nothing else. Files appearing with no explanation is the confusing
    /// version of a sync; a commit is the legible one.
    ///
    /// **Conflicted paths are left out on purpose.** They stay in the worktree
    /// with their markers, uncommitted, so the sandbox's own `git status` is
    /// the to-do list and `git checkout -- <path>` takes the pre-sync side
    /// back. Committing them would bury the one thing the agent has to act on.
    ///
    /// The base's id goes in the message as **text**, never as a parent: no
    /// commit from the user's checkout may enter this repository, and a sha in
    /// a subject is a string that happens to look like one.
    pub fn record_base_moved(
        &self,
        worktree: &Path,
        onto: &str,
        conflicted: &[String],
    ) -> Result<()> {
        let mut add: Vec<&str> = vec!["add", "-A", "."];
        // `:(exclude)` rather than adding everything and unstaging: an
        // unstage would have to name paths git quotes, and the pathspec does
        // not.
        let keep_out: Vec<String> = conflicted
            .iter()
            .map(|path| format!(":(exclude){path}"))
            .collect();
        add.extend(keep_out.iter().map(String::as_str));
        git(&self.gitdir, worktree, &add)?;

        let message = match conflicted.len() {
            0 => format!("base moved to {onto}"),
            n => format!(
                "base moved to {onto}\n\n{n} file{} need resolving; `git status` names them.\n\
                 `git checkout -- <path>` takes back the version from before this.",
                if n == 1 { "" } else { "s" }
            ),
        };
        // `--allow-empty`, because trunk moving without touching anything this
        // session has is an ordinary outcome and the commit is the *record*.
        // An agent that finds no commit concludes nothing moved.
        git(
            &self.gitdir,
            worktree,
            &[
                "commit",
                "-q",
                "--no-verify",
                "--allow-empty",
                "-m",
                &message,
            ],
        )?;
        Ok(())
    }

    /// Leave a sentence the agent will be given when it next starts.
    ///
    /// Overwrites rather than appends. Two syncs before a single start would
    /// otherwise deliver a stale paragraph above a current one and leave the
    /// agent to work out which describes the tree in front of it.
    ///
    /// The surviving note is not the whole truth in that case, and saying it
    /// is would be the easy sentence to write here. `moved` is counted from
    /// *this* sync's starting point, so a session that took three commits and
    /// then one is told **one**; and `git show HEAD` reaches the second
    /// `base moved to` commit, with the first at `HEAD~1` and nothing pointing
    /// there. Both understate. Neither misleads about the tree the agent is
    /// looking at, which is what the sentence is for — and `git log` in the
    /// sandbox holds the rest, one command away.
    ///
    /// Best-effort by signature, and deliberately so at the call site: a note
    /// that could not be written is worth less than the sync that succeeded,
    /// and failing here would report a completed sync as a failed command.
    pub fn leave_note(&self, text: &str) -> Result<()> {
        let note = note_file(&self.gitdir);
        // Written beside and renamed over, rather than written in place.
        // `fs::write` truncates first, so a disk that fills mid-write leaves a
        // *prefix* of the sentence on disk and returns an error — and the
        // caller, told the note failed, would warn that the agent will find
        // nothing, while the agent is handed half a sentence with the
        // `git show HEAD` clause missing and no sign anything was lost. A
        // rename either replaces the file or does not.
        let part = self.gitdir.join(format!("{NOTE_NAME}.part"));
        let write = std::fs::write(&part, format!("{}\n", text.trim_end()))
            .and_then(|()| std::fs::rename(&part, &note));
        if write.is_err() {
            let _ = std::fs::remove_file(&part);
        }
        write.with_context(|| format!("leaving a note at {}", note.display()))
    }

    /// Whether `commit` is still in the history `HEAD` reaches.
    ///
    /// Its own helper because the answer *no* is not a failure and `git` here
    /// treats every non-zero status as one: `merge-base --is-ancestor` says no
    /// with exit 1 and says *I could not tell* with anything else, and
    /// collapsing those is how a rewound session would report as a broken one.
    fn reaches(&self, worktree: &Path, commit: &str) -> Result<bool> {
        let out = Command::new("git")
            .envs(GUEST_ENV)
            .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
            .arg("--git-dir")
            .arg(&self.gitdir)
            .arg("--work-tree")
            .arg(worktree)
            .args(["merge-base", "--is-ancestor", commit, "HEAD"])
            .output()
            .context("running git")?;
        match out.status.code() {
            Some(0) => Ok(true),
            Some(1) => Ok(false),
            _ => anyhow::bail!(
                "git merge-base --is-ancestor {commit} HEAD: {}",
                crate::out::untrusted(String::from_utf8_lossy(&out.stderr).trim())
            ),
        }
    }

    /// Refuse to harvest from a repository whose history is not all reachable.
    ///
    /// Three states make a harvest succeed while quietly dropping commits, and
    /// all three are ordinary things for an agent to leave behind. Every one is
    /// readable from the gitdir on the host, without entering the sandbox.
    ///
    /// A silent partial harvest is the worst outcome available here: the user
    /// reviews what arrived, sees plausible work, and never learns what did not
    /// come. Refusing costs a command; the alternative costs the thing the
    /// feature exists to protect.
    pub fn preflight(&self, worktree: &Path) -> Result<()> {
        // Absent before anything else, because every check below asks git a
        // question and git's failure to answer is not the same fact as the
        // answer being bad. Without this, a session whose sandbox never ran
        // reported a *detached HEAD* — `symbolic-ref` failing on a directory
        // that is not there, read as "not on a branch".
        anyhow::ensure!(
            self.gitdir.exists(),
            "{} has no sandbox repository — nothing has run in it yet, so there \
             are no commits of its own to keep. `omh s commit -m` takes the \
             files as they are",
            worktree.display()
        );

        // Detached: `git checkout <sha>` to look at an old checkpoint, and the
        // commits after it are no longer reachable from HEAD. Measured: the
        // harvest reported success and left them behind.
        let attached = Command::new("git")
            .arg("--git-dir")
            .arg(&self.gitdir)
            .args(["symbolic-ref", "-q", "HEAD"])
            .output()
            .map(|o| o.status.success())
            .unwrap_or(false);
        anyhow::ensure!(
            attached,
            "the sandbox's repository is on a detached HEAD, so a harvest would \
             take only what that commit can reach. Put it back on a branch \
             first:\n  git --git-dir={} checkout {}",
            self.gitdir.display(),
            self.branch
        );

        // Interrupted: a rebase or merge left halfway leaves HEAD pointing at
        // something that is not the work and not a mistake anyone made.
        for marker in ["rebase-merge", "rebase-apply", "MERGE_HEAD"] {
            anyhow::ensure!(
                !self.gitdir.join(marker).exists(),
                "the sandbox's repository has a {marker} in progress — finish or \
                 abort it before harvesting, or the harvest takes the half of it \
                 that happens to be reachable"
            );
        }

        // Stranded: anything reachable from a ref but not from HEAD. Catches
        // the branch an agent made and wandered off, which neither test above
        // sees.
        let stranded = git_owned(&self.gitdir, worktree, &stranded_args())?;
        let stranded: Vec<&str> = stranded.lines().take(3).collect();
        anyhow::ensure!(
            stranded.is_empty(),
            "the sandbox's repository has commits no branch it is on can reach \
             ({}…) — they would be dropped in silence. Merge or delete them \
             first: git --git-dir={} log --all --not HEAD",
            stranded.join(", "),
            self.gitdir.display()
        );
        Ok(())
    }

    /// Everything the agent committed, replanted onto the session's branch.
    ///
    /// Six steps, and the order of the first two is the whole safety of it.
    ///
    /// **Fetch before replant.** The objects land in the user's own repository
    /// first, so a replant that conflicts leaves the branch untouched and the
    /// work reachable at the fetched ref. Measured: a conflicting replant fails
    /// loudly and loses nothing.
    ///
    /// **Refuse, never strip.** A carried file that reached a commit — by
    /// `git add -f`, by being copied under another name, or by having its
    /// contents pasted into source — is not something omh can quietly remove.
    /// Stripping the path leaves an empty commit with a misleading message and
    /// does nothing about the other two shapes; rewriting the agent's work to
    /// hide a secret is the user's call. So this stops and says which commit.
    ///
    /// Authorship is stamped **after** curation, in a pass of its own. Folding
    /// it into the interactive rebase as `--exec` would put the security step
    /// in the todo list the user is editing, where deleting a line deletes the
    /// guard.
    pub fn harvest(
        &self,
        repo: &Path,
        worktree: &Path,
        branch: &str,
        carried: &[String],
        keep: Keep,
    ) -> Result<usize> {
        self.preflight(worktree)?;

        // Whatever the agent has not checkpointed yet is still its work, and a
        // harvest that drops it is the tail of the session gone. Measured: the
        // uncommitted remainder simply did not arrive.
        //
        // Not for a selection. The numbers were resolved before this ran, so a
        // commit made here is one the user could not have named — it would be
        // swept up, left unapplied, and then recorded as handed over by a
        // replay point that had no way to know it was new. A selection takes
        // exactly what it names, and the uncommitted tail stays where the next
        // `--keep` can still see it.
        if !matches!(keep, Keep::These(_))
            && !git(&self.gitdir, worktree, &["status", "--porcelain"])?
                .trim()
                .is_empty()
        {
            git(&self.gitdir, worktree, &["add", "-A", "."])?;
            git(
                &self.gitdir,
                worktree,
                &["commit", "-q", "--no-verify", "-m", "Work in progress"],
            )?;
        }

        let seed = self.seed()?;
        let scratch = format!("refs/omh/{}/harvest", self.branch);
        // `protocol.file.allow=always`, against `NEUTRALISED`'s blanket `never`.
        //
        // That default exists because the sandbox owns its gitdir and could
        // point a submodule or an alternate at anything on the host. Here the
        // path is omh's own, computed from `paths.shadows()` and never read
        // from anything the agent wrote — so the reason for the ban does not
        // apply, and without the override the fetch dies on `transport 'file'
        // not allowed`. Narrowed to this one call rather than dropped from the
        // list, because every other host-side read still wants it.
        git_in(
            repo,
            &[
                "-c",
                "protocol.file.allow=always",
                "fetch",
                "-q",
                &self.gitdir.to_string_lossy(),
                // Forced. The ref is omh's own scratch namespace, and every
                // failure path leaves it behind — including the carried-secret
                // refusal, whose own advice is "drop that commit in the sandbox
                // and harvest again". Dropping a commit is a rewind, so the next
                // fetch was non-fast-forward and `--keep` died permanently for
                // that session, with a message naming a ref the user has never
                // heard of. Following omh's instructions must not brick omh.
                &format!("+HEAD:{scratch}"),
            ],
        )?;

        // Where to replay from: what the last harvest took, or the seed if
        // this is the first. Replaying from the seed every time is what made
        // `--keep` a one-shot — the second run offered commits the branch had
        // already been given, and whether that duplicated them or died applying
        // them came down to whether the patches still fitted.
        //
        // The record is checked against the history it claims to be part of. An
        // agent that `reset --hard`s below it — one of the four commands this
        // repository exists to give back — leaves a replay point the sandbox no
        // longer reaches. Replaying from the seed instead would hand the branch
        // work it already has, and picking a different point for the user is
        // not omh's to do, so it stops.
        let from = match self.landed()? {
            Some(landed) => {
                // `is_ok` covers two answers and the message names both: *not
                // an ancestor*, which is the agent having rewound, and *git
                // could not tell*, which is a record that no longer reads as a
                // commit at all. Refusing is right for either; blaming the agent
                // for the second would not be.
                let reaches =
                    git_in(repo, &["merge-base", "--is-ancestor", &landed, &scratch]).is_ok();
                anyhow::ensure!(
                    reaches,
                    "the sandbox's history no longer reaches {}, which is what omh last \
                     kept from it — a `reset --hard` or a rebase below that point, or a \
                     record git can no longer read as a commit. omh will not guess which \
                     commits are new. Take the files as they stand with `omh s commit -m`",
                    &landed[..landed.len().min(8)]
                );
                landed
            }
            None => seed.clone(),
        };

        let range = format!("{from}..{scratch}");
        // Not `unwrap_or(0)`. A count that did not parse is a question git did
        // not answer, and zero here means *nothing to keep* — it returns
        // success without ever running the carried-secret scan below.
        let count: usize = git_in(repo, &["rev-list", "--count", &range])?
            .trim()
            .parse()
            .with_context(|| format!("counting what {} has to hand over", self.branch))?;
        if count == 0 {
            git_in(repo, &["update-ref", "-d", &scratch])?;
            return Ok(0);
        }
        self.refuse_carried(repo, &range, carried)?;

        // The guard `Session::commit` makes and this did not: "omh will not
        // commit to a branch it did not open". Without it a session worktree
        // that wandered — `worktree add -b` losing to git's DWIM, or one left
        // detached — still had its branch rewritten by `update-ref`, while the
        // `reset --mixed` below fixed the *worktree's own* unrelated HEAD. The
        // commits landed on a branch nobody was standing on and omh reported
        // success.
        let head = git_in(worktree, &["rev-parse", "--abbrev-ref", "HEAD"])?;
        anyhow::ensure!(
            head.trim() == branch,
            "{} is on {} rather than {branch}; omh will not move a branch the \
             session is not on",
            worktree.display(),
            head.trim()
        );

        let before = git_in(repo, &["rev-parse", branch])?.trim().to_string();

        // Named for the session. One path shared by every harvest meant the
        // first thing a second one did was force-remove the first one's
        // worktree — measured, `worktree remove --force` tears down a live
        // interactive rebase and exits 0, so curation in progress simply
        // vanished. Under the common dir rather than `.git`, which is a *file*
        // in a linked worktree or a submodule and made `worktree add` fail on
        // "could not create leading directories".
        let common = git_in(repo, &["rev-parse", "--git-common-dir"])?;
        let replant = repo
            .join(common.trim())
            .join(format!("omh-harvest-{}", self.branch));
        let _ = git_in(
            repo,
            &["worktree", "remove", "--force", &replant.to_string_lossy()],
        );
        git_in(
            repo,
            &[
                "worktree",
                "add",
                "-q",
                "--detach",
                &replant.to_string_lossy(),
                &scratch,
            ],
        )?;

        let curated = Self::replant(&replant, branch, &from, &keep, &scratch)
            .and_then(|()| Self::stamp(&replant, &before));
        // Counted *after* curation, because the number is reported as what was
        // kept and it is not always what was asked for: `--edit` lets the user
        // drop commits from the todo, a selection can name one git then finds
        // already applied, and `--empty=drop` removes more on every path. Taken from the range that actually landed, this said
        // "kept 3" over a branch that got 1.
        let landed = curated
            .and_then(|()| git_in(&replant, &["rev-parse", "HEAD"]))
            .and_then(|tip| {
                let tip = tip.trim().to_string();
                let n = git_in(
                    &replant,
                    &["rev-list", "--count", &format!("{before}..{tip}")],
                )?;
                // Not `unwrap_or(0)` either — the same rule as the count above.
                // This is the number the user is told was kept, over a branch
                // that has already moved: "kept 0" after landing three is a lie
                // about work they now have.
                let n: usize = n
                    .trim()
                    .parse()
                    .with_context(|| format!("counting what landed on {branch}"))?;
                Ok((tip, n))
            });

        // Removed before any error surfaces, so no message may point at it:
        // `worktree remove --force` deletes a conflicted rebase and all, exit 0.
        // What survives a failure is the fetched ref, and that is what
        // `replant`'s message names.
        let _ = git_in(
            repo,
            &["worktree", "remove", "--force", &replant.to_string_lossy()],
        );

        // Only now. Until the branch has the work, the fetched ref is the copy
        // that survives a failure — and after it, the same ref is the only thing
        // keeping the pre-curation objects reachable in the user's repository.
        let (tip, landed) = landed?;
        // With `before` as the expected old value. `update-ref` will force-move
        // a branch that is checked out in another worktree — `git branch -f`
        // refuses, this does not — so without the third argument a commit made
        // on the session branch while the todo list sat open was discarded, and
        // omh reported a cheerful "kept N".
        git_in(
            repo,
            &["update-ref", &format!("refs/heads/{branch}"), &tip, &before],
        )
        .map_err(|e| {
            anyhow::anyhow!(
                "{e}\n\n{branch} moved while the harvest was running, so nothing was \
                 written. The work is still at {scratch} — run `omh s commit \
                 --keep` again"
            )
        })?;
        // Recorded before the ref goes, because the ref is what names it. From
        // here the next harvest replays from this point rather than from the
        // seed, which is what makes landing work in rounds possible at all.
        //
        // After the branch has it, never before: a record written earlier would
        // claim a handover that a later failure undid, and the next harvest
        // would skip commits nobody ever received. Skipping is the worse of the
        // two, because what it loses goes quietly.
        //
        // **The window on the other side is open and this does not close it.** A
        // ref move and a file write cannot be one operation, so a process killed
        // between them leaves the branch holding work the record does not
        // mention, and the next harvest offers it again — this defect, reached
        // by a crash rather than by a second run. It surfaces as a refusal or a
        // no-op rather than as damage, because the replant drops what is already
        // upstream and fails loudly when it cannot. Closing it means keeping the
        // record as a ref in the user's repository and moving both in one
        // `update-ref --stdin` transaction: a change to what the record *is*,
        // not to when it is written.
        // Everything from here on runs *after* the branch already has the
        // work, and this function's other failures promise the opposite — "the
        // branch is untouched", "nothing was written". A bare git error here
        // would be read the same way and it would be the wrong way round, so
        // each of these says what is already true of the branch.
        let landed_on = |what: &str| {
            format!(
                "{what}, but {branch} already has the work — the harvest itself \
                 succeeded. A later `--keep` may offer those commits again"
            )
        };
        // What was handed over, which for a selection is **not** what was
        // fetched. `scratch` is the sandbox's HEAD; recording it after
        // `--keep 1,3` would file checkpoints 2 and 4 as already delivered —
        // `log` would draw no divider and call them the branch's, the next
        // `--keep` would say *nothing new to keep*, `--keep 2` would refuse by
        // name, and `omh sNN rm` would then delete the only copy. Every screen
        // the user could check would agree the work was safe.
        //
        // The record says *everything up to here has been handed over*, so it
        // may only advance across commits that actually were. A selection that
        // skips one stops there; the skipped commit stays offerable, and a
        // later `--keep` re-offering one that already landed is harmless —
        // measured, git drops it as `patch contents already upstream`.
        let handed_over = match &keep {
            Keep::These(taken) => Self::advanced_past(repo, &from, &scratch, taken)
                .with_context(|| landed_on("omh could not work out what it handed over"))?,
            _ => git_in(repo, &["rev-parse", &scratch])
                .with_context(|| landed_on("omh could not read back what it handed over"))?,
        };
        std::fs::write(&self.landed_record, handed_over.trim())
            .with_context(|| landed_on("omh could not record what it handed over"))?;
        git_in(repo, &["update-ref", "-d", &scratch])
            .with_context(|| landed_on("omh could not clean up its own scratch ref"))?;

        // The branch moved under a live worktree, so its index describes the
        // commit that used to be HEAD. Without this `git status` reports files
        // deleted that are sitting on disk.
        git_in(worktree, &["reset", "-q", "--mixed"])
            .with_context(|| landed_on("omh could not refresh the session's index"))?;
        Ok(landed)
    }

    /// The curation pass: the agent's commits onto the branch, the user's shape.
    fn replant(at: &Path, branch: &str, seed: &str, keep: &Keep, scratch: &str) -> Result<()> {
        // A selection is `cherry-pick`, and everything else is `rebase`.
        //
        // The design said a selection would be a generated rebase todo,
        // delivered through `GIT_SEQUENCE_EDITOR` pointed at omh's own binary.
        // That works — measured, including the quoting it exists to get right:
        // an unquoted path with a space in it dies as *No such file or
        // directory*, and git appends the todo path afterwards as one properly
        // quoted argument even when the repository's path has spaces. It was
        // dropped for something simpler rather than because it failed.
        //
        // `cherry-pick <a> <b>` **is** "these commits, in this order", which is
        // what a selection means. It needs no editor, so no `sh -c`, no
        // quoting, no second entry point into omh, and no `hide = true`
        // subcommand that `RESERVED` then has to know about.
        //
        // The first attempt failed in a way worth recording: `current_exe()`
        // inside a unit test is the *test harness*, so git delivered the todo
        // by running the test binary with `sequence` as a filter — matching
        // nothing, exiting 0, and replaying the unedited list. Both selection
        // tests failed exactly that way.
        //
        // That is a fact about `current_exe()` in-crate and **not** a reason
        // the mechanism was untestable, which is what an earlier version of
        // this comment claimed. `tests/cli.rs` runs the real binary, and
        // `memory::deliver::plan_delivery` in this same repo takes
        // `current_exe` as a parameter for exactly this purpose — "injected
        // rather than probed so the whole decision is a table test". Either
        // would have reached it. The honest reason for the change is the list
        // above: fewer moving parts, not an impossibility.
        //
        // `rebase` stays for `All` and `Edit`: those are "everything in the
        // range, in order", which is what rebase is for, and a merge in that
        // range replays under rebase while `cherry-pick` would need to be told
        // which parent to follow.
        if let Keep::These(ids) = keep {
            // Onto the branch, not onto the fetched tip: this worktree was
            // created at the sandbox's HEAD so `rebase --onto` could move the
            // whole range, and a selection starts from the branch instead.
            git_in(at, &["reset", "-q", "--hard", branch])
                .map_err(|e| anyhow::anyhow!("{e}\n\n{}", Self::replant_failed(scratch)))?;
            let mut args = vec!["cherry-pick", "--empty=drop"];
            args.extend(ids.iter().map(String::as_str));
            let out = Command::new("git")
                .current_dir(at)
                .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
                .args(&args)
                .output()
                .context("running git cherry-pick")?;
            if out.status.success() {
                return Ok(());
            }
            // Both streams, and git's hints removed. Measured on a conflict:
            // `CONFLICT (content): Merge conflict in f.txt` goes to **stdout**
            // — so an error built from stderr alone never names the file — and
            // stderr carries four `hint:` lines telling the user to run
            // `git add`, `cherry-pick --continue`, `--skip` and `--abort`.
            // There is nowhere to run them: `harvest` force-removes this
            // worktree before this error is ever printed. Advice that cannot
            // be followed, printed beside advice that can, is worse than none.
            let said = format!(
                "{}\n{}",
                String::from_utf8_lossy(&out.stdout),
                String::from_utf8_lossy(&out.stderr)
            );
            let said: Vec<&str> = said
                .lines()
                .map(str::trim)
                .filter(|line| !line.is_empty() && !line.starts_with("hint:"))
                .collect();
            return Err(anyhow::anyhow!(
                "{}\n\n{}",
                crate::out::untrusted(&said.join("\n")),
                Self::replant_failed(scratch)
            ));
        }

        let mut args = vec!["rebase", "--onto", branch, seed, "--empty=drop"];
        let curate = *keep == Keep::Edit;
        match keep {
            Keep::Edit => args.push("-i"),
            _ => args.push("-q"),
        }

        if curate {
            let ok = Command::new("git")
                .current_dir(at)
                .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
                .args(&args)
                .status()
                .context("running git rebase -i")?;
            return if ok.success() {
                Ok(())
            } else {
                Err(Self::replant_failed(scratch))
            };
        }

        git_in(at, &args)
            .map(|_| ())
            .map_err(|e| anyhow::anyhow!("{e}\n\n{}", Self::replant_failed(scratch)))
    }

    /// How far the replay point may move, given what was actually taken.
    ///
    /// Walks the replayed range oldest-first and stops at the first commit the
    /// selection did not name. What comes back is a commit id, so the record
    /// keeps meaning one thing — *everything up to here* — rather than
    /// becoming a set, which is a change to what the record **is** and to
    /// every reader of it.
    ///
    /// Returns `from` unchanged when the oldest pending commit was not taken.
    /// That is a no-op write, and it is the right answer: nothing before it
    /// has been handed over.
    fn advanced_past(repo: &Path, from: &str, scratch: &str, taken: &[String]) -> Result<String> {
        let ordered = git_in(
            repo,
            &["rev-list", "--reverse", &format!("{from}..{scratch}")],
        )?;
        let mut point = from.to_string();
        for id in ordered.lines().map(str::trim).filter(|id| !id.is_empty()) {
            if !taken.iter().any(|t| t == id) {
                break;
            }
            point = id.to_string();
        }
        Ok(point)
    }

    /// What is true after a replant that did not finish, whichever way it went.
    fn replant_failed(scratch: &str) -> anyhow::Error {
        anyhow::anyhow!(
            "the branch is untouched and nothing is lost — omh fetched the work \
             before replanting, and it is still at {scratch}. Look at it with \
             `git log {scratch}`, or take the files without the history using \
             `omh s commit -m`"
        )
    }

    /// The enforcement pass. Non-interactive on purpose.
    ///
    /// `--allow-empty` on the amend because `--empty=drop` governs commits that
    /// *become* empty and keeps ones that started that way. An agent's
    /// `git commit --allow-empty` marker then reached here and the amend died
    /// on "doing so would make it empty", taking the whole harvest — and the
    /// curation the user had just done — with it.
    fn stamp(at: &Path, from: &str) -> Result<()> {
        let exec = format!(
            "git -c user.name='{AUTHOR_NAME}' -c user.email='{AUTHOR_EMAIL}' \
             commit -q --amend --no-edit --reset-author --allow-empty"
        );
        git_in(
            at,
            &[
                "rebase", "-q", "--onto", from, from, "HEAD", "--exec", &exec,
            ],
        )
        .map(|_| ())
    }

    /// Stop if anything the user carried in reached a commit.
    ///
    /// Four doors, and a path check closes one of them: `git add -f .env` it
    /// catches, a copy under another name it does not, nor a value pasted into
    /// source, nor one written into a commit message.
    ///
    /// So three searches, not two. `-S` is a pickaxe over diff *content* and
    /// does not read messages at all — measured, it finds nothing for a secret
    /// that appears only in a subject line, while `--grep` finds it. What `-S`
    /// does give, and the reason it is the right tool for the other two, is
    /// that it reports a secret added in one commit and removed in a later one
    /// inside the same range: both change the occurrence count, so both are
    /// named.
    ///
    /// **`-F` on the `--grep`, or the needle is a pattern rather than the bytes
    /// it is.** `--grep` takes a *pattern*, and which language it is written in
    /// is the reader's setting: `grep.patternType` is `basic` unless someone
    /// says otherwise, and people do say otherwise. Measured against git 2.55.0
    /// across all three, on a commit subject quoting the secret verbatim:
    ///
    /// | in the secret | `basic` | `extended` | `perl` |
    /// |---|---|---|---|
    /// | `*` | **missed** | missed | missed |
    /// | `+` | found | **missed** | **missed** |
    /// | `[` | fatal | fatal | fatal |
    ///
    /// So a `*` in a secret goes through on a stock install, a `+` goes through
    /// for anyone who set `extended` or `perl`, and an unbalanced `[` takes the
    /// whole feature down — `git log` exits 128, the harvest fails, and
    /// `--keep` stays dead for that session. A guard whose reach depends on the
    /// user's dotfiles is not a guard. `-F` pins it: fixed strings, whatever
    /// `grep.patternType` says. `-S` needs no such flag — a pickaxe is already
    /// literal unless `--pickaxe-regex` asks otherwise.
    ///
    /// omh is in a position no scanner is — it staged these files, so it knows
    /// the bytes and never has to guess. With one caveat worth stating: it
    /// reads them from the checkout *now*, so a secret rotated mid-session is
    /// matched at its new value and an agent commit holding the old one goes
    /// through.
    fn refuse_carried(&self, repo: &Path, range: &str, carried: &[String]) -> Result<()> {
        for rel in carried {
            let rel = rel.trim().trim_end_matches('/');
            // Sanitised where it is read, not where it is printed. What comes
            // back is a sha and the agent's own subject line, and git quotes
            // neither: measured, `core.quotePath` renders an escape inside a
            // *path* as a literal `\033`, and leaves a **subject** exactly as
            // it was written. The message below is the one that says omh
            // refused to publish a secret, so a subject that can clear the line
            // and answer for it is the forgery that matters most here.
            let found = git_in(repo, &["log", "--oneline", range, "--", rel])?;
            if let Some(line) = found.lines().next().map(crate::out::untrusted) {
                anyhow::bail!(
                    "{rel} is a carried file and {line} has it. omh will not \
                     rewrite your history to hide a secret — drop that commit in \
                     the sandbox and harvest again, or take the files without the \
                     history with `omh s commit -m`"
                );
            }
            for needle in Self::needles(&repo.join(rel))? {
                for (how, args) in [
                    ("contains", vec!["log", "--oneline", "-S", &needle, range]),
                    (
                        "mentions",
                        vec!["log", "--oneline", "-F", "--grep", &needle, range],
                    ),
                ] {
                    let hit = git_in(repo, &args)?;
                    if let Some(line) = hit.lines().next().map(crate::out::untrusted) {
                        anyhow::bail!(
                            "{line} {how} a line from {rel}, which you carried in. \
                             omh will not rewrite your history to hide a secret — \
                             drop that commit in the sandbox and harvest again"
                        );
                    }
                }
            }
        }
        Ok(())
    }

    /// Lines worth searching for: long enough to mean something, and not a
    /// comment or a blank.
    ///
    /// Walks a directory, because `carry_in` takes one and `read_to_string` on
    /// a directory is an `Err` — which, defaulted, meant **zero needles and a
    /// content scan that silently did nothing**. A `certs/` entry got the path
    /// check only, so `cp certs/deploy.key infra/key.pem` and commit put a
    /// private key on the branch through the door this function exists to shut.
    ///
    /// Unreadable is an error, not an empty answer. A file that has been
    /// rotated, renamed or made unreadable since launch is a case where omh
    /// cannot tell whether the harvest is clean, and "cannot tell" must not
    /// spell the same as "clean" in the one module whose subject is the user's
    /// secrets. Binary files are the exception the loop takes deliberately:
    /// they decode-fail, and a byte sequence is not a line to search for.
    fn needles(at: &Path) -> Result<Vec<String>> {
        if at.is_dir() {
            let mut out = Vec::new();
            for entry in std::fs::read_dir(at)
                .with_context(|| format!("reading carried directory {}", at.display()))?
                .flatten()
            {
                out.extend(Self::needles(&entry.path())?);
            }
            return Ok(out);
        }
        if !at.exists() {
            return Ok(Vec::new());
        }
        let body = match std::fs::read_to_string(at) {
            Ok(body) => body,
            // Not text. Nothing to search for line-wise, and the path check
            // still covers it.
            Err(e) if e.kind() == std::io::ErrorKind::InvalidData => return Ok(Vec::new()),
            Err(e) => {
                return Err(e).with_context(|| format!("reading carried file {}", at.display()))
            }
        };
        Ok(body
            .lines()
            .map(str::trim)
            .filter(|l| l.len() >= 12 && !l.starts_with('#') && !l.starts_with("//"))
            .map(str::to_string)
            .collect())
    }

    /// omh's own view of what this repository's config may say.
    ///
    /// Everything `git init` needs to know about the repository, plus an
    /// identity, and nothing else. Anything the agent added is dropped — not
    /// because a relaunch is a boundary, it can set the key again a second
    /// later, but because a key that persists is one omh will still be reading
    /// long after whatever wrote it, and several of them turn `git` into *run
    /// whatever this repository says*.
    ///
    /// Written by asking git rather than by composing the file: `git init`
    /// records `repositoryformatversion` and `filemode` here, and a hand-built
    /// config that forgot either would be a repository git reads differently
    /// from the one it made. So the keys outside the allowlist are unset one by
    /// one and the rest is left exactly as git wrote it.
    ///
    /// The identity is not a claim about who wrote anything — a harvest stamps
    /// authorship on the host — it is there because git will not commit without
    /// one and the container has no global config to supply it.
    fn write_config(gitdir: &Path) -> Result<()> {
        // What git records about the repository itself, as opposed to what it
        // should *do* — and the difference is the whole allowlist. Several of
        // these are detected from the filesystem when the repository is made
        // and differ by platform: `ignorecase` and `precomposeunicode` appear
        // on macOS and not on a case-sensitive Linux box, `symlinks` on
        // Windows. Dropping one leaves a repository git reads differently from
        // the one it created — paths differing only in case become two files,
        // accented filenames flip between NFC and NFD and read as modified.
        //
        // The two `extensions.*` keys git itself sets, by name rather than by
        // prefix. The prefix was wider than it needed to be: git refuses to
        // work in a repository whose extensions it does not recognise, so a
        // preserved `extensions.whatever` from an older shadow is a harvest
        // that can never run again. Losing a real one would be worse, which is
        // why these two are named.
        //
        // Guarded by `refreshing_keeps_what_git_records_about_the_repository`,
        // which measures against a fresh `git init` on the machine running it
        // rather than against this list. A git that starts recording something
        // new turns it red on the platform where that matters, which is the
        // only way a list like this stays true.
        // `core.logallrefupdates` is deliberately **not** here, though it is
        // one of git's own. Left alone it defaults to logging `refs/heads`,
        // `refs/remotes`, `refs/notes` and `HEAD` — and `unkept`'s exclusion
        // of the turn ref works only because a ref outside those gets no
        // reflog. Set to `always`, git logs every ref under `refs/`, the
        // `--reflog` arm sees the snapshots the `--exclude` cannot reach, and
        // `omh sNN rm` refuses for the rest of the session.
        //
        // The agent can write this config, so keeping the key would keep the
        // agent's value. Unsetting is safe — git falls back to exactly the
        // default the design measured.
        const KEEP: [&str; 9] = [
            "core.repositoryformatversion",
            "core.filemode",
            "core.bare",
            "core.worktree",
            "core.ignorecase",
            "core.precomposeunicode",
            "core.symlinks",
            "extensions.objectformat",
            "extensions.refstorage",
        ];

        let listed = Command::new("git")
            .arg("--git-dir")
            .arg(gitdir)
            .args(["config", "--list", "--local", "--name-only"])
            .output()
            .with_context(|| format!("reading the config of {}", gitdir.display()))?;
        // Asked rather than assumed. Empty output means *no keys*; a listing
        // that failed means omh does not know what is in there, and the two
        // must not spell the same — this function's whole job is dropping what
        // it finds, so believing an empty answer it never got would sanitise
        // nothing and report success. The relaunch path is exactly where a key
        // worth dropping would be.
        anyhow::ensure!(
            listed.status.success(),
            "reading the config of {}: {}",
            gitdir.display(),
            crate::out::untrusted(String::from_utf8_lossy(&listed.stderr).trim())
        );
        // A **set**, because `--list --name-only` prints a key once per value.
        // Unsetting takes every value at once, so a multi-valued key arriving
        // twice meant a second `--unset-all` with nothing left to remove, exit
        // 5, empty stderr — and a launch aborted over a key that had already
        // gone. Measured against git 2.55.0.
        let listed = String::from_utf8_lossy(&listed.stdout);
        let keys: std::collections::BTreeSet<&str> = listed
            .lines()
            .map(str::trim)
            .filter(|k| !k.is_empty())
            .collect();
        for key in keys {
            if KEEP.contains(&key) || key == "user.name" || key == "user.email" {
                continue;
            }
            let dropped = Command::new("git")
                .arg("--git-dir")
                .arg(gitdir)
                .args(["config", "--unset-all", key])
                .output()
                .with_context(|| format!("dropping {key} from {}", gitdir.display()))?;
            anyhow::ensure!(
                dropped.status.success(),
                "git config --unset-all {key}: {}",
                String::from_utf8_lossy(&dropped.stderr).trim()
            );
        }

        for (key, value) in [("user.name", AUTHOR_NAME), ("user.email", AUTHOR_EMAIL)] {
            let set = Command::new("git")
                .arg("--git-dir")
                .arg(gitdir)
                .args(["config", key, value])
                .output()
                .with_context(|| format!("setting {key} on {}", gitdir.display()))?;
            anyhow::ensure!(
                set.status.success(),
                "git config {key}: {}",
                String::from_utf8_lossy(&set.stderr).trim()
            );
        }
        Ok(())
    }

    /// Remove the repository and the seed recorded for it.
    ///
    /// Best-effort on purpose: this runs while a session is being torn down,
    /// and a shadow that will not delete is not a reason to fail a removal the
    /// user asked for. What it must not do is leave the *seed* behind without
    /// the gitdir — a seed naming a repository that is gone is exactly the
    /// state `ensure`'s fast path reads as "already built" — so the record goes
    /// last, after the directory it describes.
    pub fn reap(&self) {
        let _ = std::fs::remove_dir_all(&self.gitdir);
        let _ = std::fs::remove_file(&self.seed_record);
        // With the seed, and for the reason the seed goes: session ids come
        // back around, and a replay point inherited by a stranger says a branch
        // has already been given commits it has never seen.
        let _ = std::fs::remove_file(&self.landed_record);
    }

    /// Keep the agent's own `git status` clean at launch.
    ///
    /// Advisory, and known to be: `git add -f` walks straight through it. It is
    /// here so an honest agent is not shown a secret to commit, not to stop a
    /// determined one — what stops the secret reaching the branch is the check
    /// on the host when work crosses back.
    ///
    /// Takes the directory rather than reading `self.gitdir`, because the
    /// first of its two callers runs while the repository is still being built
    /// under another name. The second is the fast path in `ensure`, which
    /// passes the finished gitdir on every relaunch.
    fn write_exclude(gitdir: &Path, excluded: &[String]) -> Result<()> {
        let info = gitdir.join("info");
        // Named, because this is the one write that can fail a launch which
        // would otherwise have been a no-op — the fast path in `ensure` did no
        // I/O at all before. `Permission denied (os error 13)` with no path is
        // not something a user can act on, and the directory it names is one
        // the agent can chmod.
        std::fs::create_dir_all(&info).with_context(|| format!("preparing {}", info.display()))?;
        // Just what the caller names. `container::plan` derives that from the
        // mounts it is about to make, which already covers omh's staged rules —
        // chaining `carry::STAGED_RULES` here as well only made the list
        // disagree with its own source when a capability changed.
        let body: String = excluded.iter().map(|n| format!("{n}\n")).collect();
        let at = info.join("exclude");
        std::fs::write(&at, body).with_context(|| format!("writing {}", at.display()))?;
        Ok(())
    }

    /// A signpost on the accidental path, and **not a wall** — this said "the
    /// one wall left standing" and that was wrong three ways, each one command
    /// long. Measured against git 2.55.0 and a reachable remote:
    ///
    /// | what the agent runs | remote |
    /// |---|---|
    /// | `git push` | refused, 0 commits |
    /// | `git push --no-verify` | **pushed** |
    /// | `git -c core.hooksPath=/dev/null push` | **pushed** |
    /// | `rm hooks/pre-push; git push` | **pushed**, until the mount |
    ///
    /// That last row is why `container::plan` mounts `GUEST_PRE_PUSH` read-only
    /// over this copy: the gitdir is writable because the agent commits into
    /// it, so without the mount the hook is a file the agent can simply take
    /// away. With it, `rm` gives `Resource busy` and overwriting gives
    /// `Read-only file system`.
    ///
    /// The first two rows are unaffected — neither flag reads the file — so
    /// this is still not a wall. Nothing here contains a determined agent, and
    /// nothing ever did: the container has `curl` and unrestricted egress, so a
    /// push was never the narrow path out.
    ///
    /// What it is for is the *honest* path, which is the likely one. git's own
    /// error for a repository with no remote suggests `git remote add`, so git
    /// walks the agent to the edge; this is what meets it there and says why in
    /// omh's words rather than leaving it to read a transport failure.
    ///
    /// git's own hook rather than a pattern over the command line, because git
    /// knows what a push is — though note that argument cuts both ways, and the
    /// `--no-verify` row above is the same "every shape an agent emits" problem
    /// the base set's retired pattern had.
    ///
    /// Worth knowing when it actually fires, because it is not when you would
    /// guess, and the sequence matters. Measured against git 2.55.0:
    ///
    /// - **No remote — the shipped state.** `git push` dies on `fatal: No
    ///   configured push destination`, before any hook runs, and git's advice
    ///   is `git remote add <name> <url>`. So the thing that ends the first
    ///   attempt also hands the agent the recipe for the second.
    /// - **A remote the agent added.** With one configured but no upstream, git
    ///   asks for `--set-upstream`; supply it, or push by name, and the hook
    ///   fires ahead of the transfer. Verified against a reachable remote,
    ///   which stayed at zero commits.
    ///
    /// So this is not what makes a push impossible — having no remote is. It is
    /// what catches the agent after git has talked it into fixing that, which
    /// is the only route by which work could leave the machine.
    fn write_pre_push(gitdir: &Path) -> Result<()> {
        let hooks = gitdir.join("hooks");
        std::fs::create_dir_all(&hooks)?;
        let hook = hooks.join("pre-push");
        // Written here as well as mounted read-only over the top: the mount is
        // what the agent meets, and this is what a host-side reader would see
        // and what makes the gitdir self-contained if it is ever inspected
        // outside a container.
        std::fs::write(&hook, pre_push_hook())?;
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755))?;
        }
        Ok(())
    }
}

/// Who the sandbox's own commits are authored as.
///
/// `.invalid` is reserved by RFC 2606 precisely so it can never resolve, which
/// is what you want on an address nobody should write to. The name says where
/// the commit was made rather than guessing at which harness made it — omh
/// supports several, and the seed is written before any of them starts.
const AUTHOR_NAME: &str = "omh sandbox";
const AUTHOR_EMAIL: &str = "sandbox@omh.invalid";

/// What the arrangement actually is, said once.
///
/// Two surfaces deliver it and they must not drift: the seed commit's message,
/// which every `git log` and editor timeline renders at the moment the agent is
/// working out what this repository is, and the `git-rules` section omh puts in
/// the agent's context every turn. `GIT_ABSENT` was one string for the same
/// reason, when the sentence it carried was the opposite one.
pub const ARRANGEMENT: &str = "This repository is the sandbox's own, and it is not the branch \
     anyone reviews. Commit as often as you like — that is what it is for, and `git reset --hard` \
     back to a checkpoint is yours to use. What reaches the person you are working with is the \
     state of the files, which they read with `omh s diff` and commit with `omh s commit` on the \
     host, pushing with `omh s push` when they are ready. Your commit messages here stay here.\n\n\
     There is nothing to push and no remote to push to, and adding one will not work either — \
     the config is not yours to write. That is the arrangement rather than a fault to repair. Say \
     so rather than offering to push, and do not offer to commit on the host — that is theirs to \
     do.\n\n\
     There is no history here — this starts at one commit holding the files as they were when the \
     session opened, so `git log` and `git blame` have nothing older to tell you. And the branch \
     it came from may move while you work, changing files under you; neither is a fault.";

/// The sentence left for the agent after a sync.
///
/// Three facts and where to look for each, in that order: what changed, how to
/// read it, and what is left to do. Deliberately short — it arrives at the top
/// of a rebuilt context, competing with everything the session is actually
/// about, and it is orientation rather than instruction. Everything it points
/// at is already in the repository; what the agent cannot work out is that any
/// of it happened.
///
/// A function of the three numbers so the wording is asserted rather than
/// typed at the call site, and so neither zero can end up spelled as a count.
///
/// **Both** zeroes, and the second was missed on the first pass. *0 files need
/// resolving* was designed out and *moved 0 commits* was left in — reachable
/// from ordinary input, since `--base` is a user-supplied argument and
/// `sync`'s only guard is that the two ends differ. Pointing an agent at
/// `git show HEAD` under a sentence saying nothing arrived is worse than
/// saying nothing at all.
pub fn note_for(base: &str, moved: usize, conflicted: usize) -> String {
    // `moved` counts `was..onto`, so zero with the two ends differing means
    // `onto` is an ancestor: the base went backwards, to a commit this session
    // already had. Rarer than a fast-forward and not an error — `omh sNN sync
    // --base <an older tag>` is a thing somebody means.
    let arrived = if moved == 0 {
        format!(
            "{base} moved backwards under you while this session was stopped — it now points \
             at a commit this session already had. `git show HEAD` is exactly what changed."
        )
    } else {
        format!(
            "{base} moved {moved} commit{} under you while this session was stopped. \
             `git show HEAD` is exactly what arrived.",
            if moved == 1 { "" } else { "s" }
        )
    };
    match conflicted {
        0 => format!("{arrived} It merged cleanly — nothing needs deciding."),
        n => format!(
            "{arrived} {n} file{} still need{} resolving: `git status` names them, and \
             `git checkout -- <path>` takes back the version from before the merge.",
            if n == 1 { "" } else { "s" },
            if n == 1 { "s" } else { "" }
        ),
    }
}

/// The seed commit's message, which is a delivery surface and not a label.
///
/// Every `git log`, `git show` and editor timeline renders it, at the moment
/// the agent is working out what this repository is — which a rules section,
/// paid for once and then competing with everything after it, cannot reach.
pub fn seed_message() -> String {
    format!("The session starts here.\n\n{ARRANGEMENT}")
}

/// Why a push cannot work, said where the agent is trying to push.
const NO_PUSH: &str =
    "omh: nothing to push from here. This repository is the sandbox's own and has no remote — \
     your work reaches the outside through the host, where `omh s commit` puts it on the branch \
     and `omh s push` sends it. Say that rather than trying to push yourself.";

/// What `--keep` was asked to take.
///
/// A `bool` said *interactive or not*, which is not the question — `--keep`
/// with no selection wants everything and no editor, and the editor is a third
/// thing rather than the absence of a selection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Keep {
    /// Everything since the last handover, in order, with no editor.
    All,
    /// These commits, in this order. Sandbox-side ids, already resolved from
    /// the numbers `log` printed and already checked against the range.
    These(Vec<String>),
    /// The todo, in the user's own editor. The only path that needs a terminal.
    Edit,
}

/// Which checkpoints a `--keep 1,3-4` names, in the order it names them.
///
/// **The user's order, not sorted.** Reordering is half of what curating a
/// history is for, and a selection that quietly sorted itself would land a
/// different history than the one the user read on screen.
///
/// Everything ambiguous is refused rather than resolved, and refused *here* —
/// before a worktree is made, before a fetch, before the branch could move.
/// Each of the refusals below is a plausible thing to type that would
/// otherwise mean something: `4-2` reversed is a guess about intent, `1,1`
/// applies a commit twice, and a number past the end is no commit at all.
/// (Whether the *list* is current is a different question, and not one this
/// can answer — see `checkpoints` for why the numbers are stable.)
pub fn chosen(spec: &str, available: usize) -> Result<Vec<usize>> {
    let range = || match available {
        0 => "this session has no checkpoints to keep".to_string(),
        1 => "there is one, numbered 1".to_string(),
        n => format!("they are numbered 1 to {n}"),
    };
    let number = |raw: &str| -> Result<usize> {
        let n: usize = raw.trim().parse().map_err(|_| {
            anyhow::anyhow!("`{spec}` is not a list of checkpoint numbers — {}", range())
        })?;
        anyhow::ensure!(
            n >= 1 && n <= available,
            "there is no checkpoint {n} in this session — {}",
            range()
        );
        Ok(n)
    };

    let mut out: Vec<usize> = Vec::new();
    anyhow::ensure!(
        !spec.trim().is_empty(),
        "no checkpoints named. `--keep` on its own takes all of them; \
         `--keep 1,3-4` takes those — {}",
        range()
    );
    for part in spec.split(',') {
        // `split_once` rather than `split`, so `1-2-3` is one malformed
        // element rather than quietly becoming `1-2`.
        match part.split_once('-') {
            Some((from, to)) => {
                let (from, to) = (number(from)?, number(to)?);
                anyhow::ensure!(
                    from <= to,
                    "`{spec}` runs backwards at `{}`. omh will not guess whether that means \
                     {to} to {from} or a typo — name them in the order you want them",
                    part.trim()
                );
                out.extend(from..=to);
            }
            None => out.push(number(part)?),
        }
    }
    // After expansion, because `1-2,2` is the same mistake as `2,2` written
    // less obviously.
    for (at, n) in out.iter().enumerate() {
        anyhow::ensure!(
            !out[..at].contains(n),
            "`{spec}` names checkpoint {n} twice. Applying it twice is not what the list \
             showed you"
        );
    }
    Ok(out)
}

/// The options a `git <verb> -h` listing names.
///
/// Pure, so the interesting half is a table rather than a fact about whichever
/// git this machine has — the split `memory::deliver::plan_delivery` makes for
/// the same reason: the part that can be wrong silently is the parse.
///
/// **Parsed as git writes it**, which took a correction. The first version
/// matched an option only at the start of a line, and git puts the short alias
/// first whenever there is one: `-n, --no-commit`, `-e, --[no-]edit`. Against a
/// real listing it answered *no* to eight of nine options cherry-pick has,
/// including the `--no-commit` its own test comment called eternal. `--empty`
/// worked by luck — it has no short form and takes no negation, so it lands
/// first on its line. A future git that gives it either would have been read as
/// a git too old to have it.
///
/// Empty means the text was not a listing at all — a shim that printed
/// something else, a verb git does not know. That is a different answer from
/// *this option is absent*, and the caller has to be able to tell.
pub(crate) fn options_in(help: &str) -> BTreeSet<String> {
    let mut out = BTreeSet::new();
    for line in help.lines() {
        // The option column is what comes before the description, and git
        // separates them by a run of spaces. Descriptions do mention options —
        // `opposite of --no-verify`, `be quiet. implies --no-stat` — and a
        // mention is not a declaration.
        //
        // Belt and braces, honestly labelled: the value split below already
        // stops at the first space, so for every listing git actually prints
        // this line changes nothing. It matters only for a description whose
        // first word after a comma is an option, and no git verb checked
        // produces one — so no test here covers it, and deleting it would go
        // unnoticed. It stays because it makes the rule *about the column*
        // rather than correct by coincidence.
        let column = line.trim_start();
        let column = column.split("  ").next().unwrap_or_default().trim();
        if !column.starts_with('-') {
            continue;
        }
        for token in column.split(',') {
            let token = token.trim();
            // Whatever the option takes, said several ways: `<file>`,
            // `=<n>`, `(stop|drop|keep)`.
            let token = token
                .split([' ', '='])
                .next()
                .unwrap_or_default()
                .trim_end_matches(['[', '<', '(']);
            if token.len() < 2 || !token.starts_with('-') {
                continue;
            }
            out.insert(token.to_string());
            // `--[no-]edit` declares `--edit` and `--no-edit`; a caller asking
            // for either is asking about this line.
            if let Some(rest) = token.strip_prefix("--[no-]") {
                out.insert(format!("--{rest}"));
                out.insert(format!("--no-{rest}"));
            }
        }
    }
    out
}

/// Whether this git knows an option, or whether omh could not ask.
///
/// omh cannot check a version it cannot name. `cherry-pick --empty=` is newer
/// than everything else omh asks of git and #56 made it a dependency of
/// `--keep <selection>`, and the release that introduced it was not verifiable
/// from here. Asking the binary needs no such table and answers for whatever
/// git is on this machine.
///
/// **Three answers, not two.** `Err` is *omh could not ask* — git absent, git
/// unspawnable, a version-manager shim that prints "no version set", a `.git/
/// config` with one bad line (measured: `git --version` still succeeds while
/// `git cherry-pick -h` exits 128 printing nothing). Every one of those was
/// `false` in the first version, so a user with no git on PATH was told their
/// git was too old to name checkpoints.
///
/// Measured 2026-08-23 against git 2.55.0: `git <verb> -h` prints the whole
/// listing on **stdout**, leaves stderr empty, exits **129**, and needs no
/// repository. An earlier note here said the usage line went to stderr; that
/// was zsh's MULTIOS merging the streams in the shell that measured it, not
/// git. The status is still ignored — 129 is the ordinary answer — and both
/// streams are still read, since a shim may use either.
pub fn git_supports(verb: &str, option: &str) -> Result<bool> {
    let out = Command::new("git")
        .args([verb, "-h"])
        .output()
        .with_context(|| format!("asking git whether `{verb}` takes `{option}`"))?;
    let said = format!(
        "{}\n{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let options = options_in(&said);
    anyhow::ensure!(
        !options.is_empty(),
        "`git {verb} -h` listed no options at all, so omh cannot tell what this git \
         supports. It said: {}",
        crate::out::untrusted(said.trim())
    );
    // `--empty=` and `--empty` are one question. Every comment in this tree
    // names the flag with its `=`, both call sites without it, and a listing
    // has neither — so accepting both spellings is what stops an edit that
    // merely aligns prose with code from refusing every git in existence.
    Ok(options.contains(option.trim_end_matches('=')))
}

/// The pager the *user* chose, resolved by git in the user's own checkout.
///
/// git's own resolution order is `GIT_PAGER`, then `core.pager`, then `PAGER`,
/// then a built-in default — and the middle one is a file the agent owns. This
/// skips it: whatever comes back is passed as a later `-c core.pager`, which
/// beats the `cat` in `NEUTRALISED` and is in turn beaten by a `GIT_PAGER` the
/// user exported, exactly as it would be anywhere else.
fn user_pager(repo: &Path) -> String {
    // `git var` resolves the pager the way git resolves it — `GIT_PAGER`, then
    // `core.pager`, then `PAGER`, then the pager git was built with — and run
    // in the *user's own checkout* it reads the user's config and never the
    // sandbox's. Hand-rolling that order skipped `core.pager` altogether, so a
    // user whose pager is `delta` in `~/.gitconfig` got `delta` from
    // `omh sNN diff -p` and bare `less` from `omh sNN diff <n> -p`: the same
    // flag, two renderers, for no reason they could see.
    Command::new("git")
        .current_dir(repo)
        .args(["var", "GIT_PAGER"])
        .output()
        .ok()
        .filter(|out| out.status.success())
        .map(|out| String::from_utf8_lossy(&out.stdout).trim().to_string())
        .filter(|pager| !pager.is_empty())
        .unwrap_or_else(|| "less".to_string())
}

/// What to ask git for, for one checkpoint.
///
/// `--first-parent` is what makes the two answers agree. Measured 2026-08-23
/// against git 2.55.0: `show --stat` on a merge reports the files it brought
/// in, and `show -p` on the same merge prints **nothing at all** — git's
/// default `--cc` collapses a clean merge to an empty diff. So the summary
/// promised a change and the patch showed none, one flag apart, for a commit
/// shape `checkpoints` already models because the agent produces it. On an
/// ordinary commit the flag changes neither answer (measured, both forms
/// byte-identical), which is why it can be unconditional.
///
/// Built as a list rather than passed inline so the pager ordering below is
/// something a test can read: the user's `-c core.pager` has to come *after*
/// the one in `NEUTRALISED` or the sandbox's `cat` wins and nothing pages.
fn show_args(what: crate::session::What, id: &str, colour: &str) -> Vec<String> {
    [
        "show",
        what.flag(),
        "--first-parent",
        &format!("--color={colour}"),
        id,
    ]
    .iter()
    .map(|a| a.to_string())
    .collect()
}

/// Config keys that turn `git` into "run whatever this repository says".
///
/// The gitdir is a read-write mount because the agent commits through it, and
/// the `config` inside it is **not**: `write_config` rewrites that file to a
/// ten-key allowlist on every launch and `container::plan` mounts it read-only
/// (#52). So these keys are omh's second line, not its first — an earlier
/// version of this comment called them all agent-writable, which stopped being
/// true when the mount landed and was still being quoted as a premise
/// afterwards.
///
/// What the agent *can* still write is everything else in the gitdir:
/// `refs/replace/`, `info/grafts`, `info/attributes`. Those are the ones with
/// no config key at all, and they are handled beside this list.
///
/// **These fire.** An earlier version of this comment said nothing host-side
/// read an existing shadow, and that stopped being true twice: `harvest`
/// fetches and replays those commits on the host as the user, and
/// `checkpoints` reads the whole history on every `omh sNN log`. A reader who
/// reached the old sentence first would have concluded the list was inert and
/// could be relaxed.
///
/// Two of them are not about execution at all. `core.useReplaceRefs` and the
/// `GIT_GRAFT_FILE` in `GUEST_ENV` are about the answer being *true*: both let
/// the agent rewrite what a read prints without touching a commit, and the one
/// command that exists to be believed about the agent's work is the one that
/// reads this repository.
const NEUTRALISED: [&str; 8] = [
    // `ensure` has carried this since the seed commit and said why: a global
    // `commit.gpgsign = true` fails with `gpg failed to sign the data`, over a
    // commit the user never made in a repository they cannot see. The harvest
    // then added three more commit sites and read none of that — the
    // checkpoint, the replant and the stamp all died for anyone who signs by
    // default. `--no-verify` covers hooks and nothing of signing.
    "commit.gpgsign=false",
    "core.hooksPath=",
    "core.pager=cat",
    "core.fsmonitor=",
    "core.sshCommand=",
    "protocol.file.allow=never",
    // `git diff` runs an external driver by default; the log family will not
    // without `--ext-diff`, which nothing here passes. Both are the agent's to
    // name — this config lives in a read-write mount — so the key is closed
    // rather than left to depend on which verb a future reader picks.
    "diff.external=",
    // A commit the agent replaced reads as whatever it replaced it with.
    // Measured 2026-08-23 against git 2.55.0, in a gitdir the agent owns:
    // `git replace -f <real> <forged>` and the log prints the forged subject
    // and the forged (empty) numstat beside the *real* commit id, with no
    // warning on either stream. `--no-replace-objects` and this key both stop
    // it; the key is here because this list is the mechanism every host-side
    // read already carries.
    //
    // This one is not about executing anything — it is about being believed.
    // `omh sNN log` exists to tell you what the agent did.
    "core.useReplaceRefs=false",
];

/// What a read has to be told to ignore that no `-c` flag can reach.
///
/// `$GIT_DIR/info/grafts` rewrites ancestry from a file inside the mount, and
/// **the config key does not turn it off**. Measured 2026-08-23 against git
/// 2.55.0, with three commits hidden behind a graft: `-c core.graftsFile=` and
/// `-c core.graftsFile=/nonexistent` both still printed the truncated history,
/// and `--no-replace-objects` did not help either — grafts are not replace
/// refs. `GIT_GRAFT_FILE` pointed away from the gitdir is what restored all
/// three commits.
///
/// A review that has been shortened to one line, over an exit code of 0, is the
/// worst answer this command can give: the user reads *little to review* and
/// runs `rm`. git's only complaint is a deprecation hint on **stderr**, which
/// `git` here discards whenever the status is zero.
const GUEST_ENV: [(&str, &str); 1] = [("GIT_GRAFT_FILE", "/dev/null")];

/// `git` in an ordinary repository — the user's checkout, or a worktree of it.
///
/// Separate from the gitdir/worktree helper below because the harvest works on
/// *both* sides: the sandbox's repository, which needs the explicit pair, and
/// the user's own, which does not. Carries the same neutralising flags, since
/// half of what it touches came from the sandbox.
/// The flags a read has to carry, for the reads that can execute something.
///
/// A textconv driver is two halves and the agent owns both: the driver is a
/// config key in a gitdir mounted read-write, and the `.gitattributes` naming
/// it sits in `/work`. A host-side read that produces a diff then runs whatever
/// the agent chose, as the user, outside the container. Measured against git
/// 2.55.0: `log -S`, `log -p` and `show` all run it; `--no-textconv` stops
/// them; `--grep` never touches it.
///
/// By verb, because these are diff options rather than config: `status`,
/// `commit` and `add` reject them outright, and a blanket flag would break
/// every write this module makes. `rev-list`, `log`, `show` and `diff` accept
/// both — checked, since guessing which verbs take which option is how this
/// kind of guard ends up unarmed.
///
/// `--no-ext-diff` belongs with it even though the log family already ignores
/// external drivers without `--ext-diff`: `git diff` does not, and the next
/// reader added here should not have to know which family its verb is in.
/// Returns the argument list with the flags inserted, or unchanged.
///
/// **After the verb, not before it.** These are subcommand options: put in
/// front they are read as options to `git` itself, and every call dies on
/// `unknown option: --no-textconv` — which is how the first version of this
/// disarmed the whole module rather than just failing to guard it.
fn guarded(args: &[&str]) -> Vec<String> {
    let mut out: Vec<String> = args.iter().map(|a| a.to_string()).collect();
    if matches!(args.first(), Some(&"log" | &"show" | &"diff" | &"rev-list")) {
        out.splice(
            1..1,
            ["--no-textconv".to_string(), "--no-ext-diff".to_string()],
        );
    }
    out
}

/// `git` in an ordinary repository — the user's checkout, or a worktree of it.
///
/// Its stderr goes through `out::untrusted` on the way into the error, because
/// git quotes back the refs and paths it was given and half of what this module
/// hands it came from the sandbox. A branch name carrying an escape sequence
/// would otherwise repaint omh's own output on its way past.
fn git_in(at: &Path, args: &[&str]) -> Result<String> {
    let out = Command::new("git")
        .current_dir(at)
        .envs(GUEST_ENV)
        .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
        .args(guarded(args))
        .output()
        .context("running git")?;
    if !out.status.success() {
        anyhow::bail!(
            "git {}: {}",
            args.join(" "),
            crate::out::untrusted(String::from_utf8_lossy(&out.stderr).trim())
        );
    }
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

/// Turn one `git log --numstat` run into checkpoints.
///
/// Extracted so the turn snapshots can be read by the same parser that reads
/// the agent's own commits, rather than by a second one written to the same
/// format string. Two parsers of one format is how the two drift, and the
/// numstat handling here — a merge told apart from a commit that changed
/// nothing, `-` told apart from zero — is the part that would be quietly
/// wrong in the copy.
///
/// `handed` is empty for turn snapshots: `landed` means *already replayed onto
/// the branch by `--keep`*, and a snapshot never is.
/// No cross-check here, deliberately, and it belongs to the caller.
///
/// `checkpoints` reconciles this against `rev-list --count` and refuses a list
/// it did not fully understand, because its numbers are what `diff <n>` and
/// `--keep 1,3-4` take. `turn_log` does not need to: a snapshot has no number,
/// so a record this dropped would be a missing row rather than a mis-targeted
/// commit.
///
/// An earlier version moved that paragraph *into* this function, where it
/// ended with the words "this does" directly above a bare `commits` — telling
/// the next reader the parser self-checks, while the `ensure!` it described
/// had stayed behind in the caller.
fn parse_log(raw: &str, handed: &BTreeSet<String>) -> Vec<Checkpoint> {
    let mut commits: Vec<Checkpoint> = Vec::new();
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .ok();
    for line in raw.lines() {
        if let Some(header) = line.strip_prefix('\0') {
            let mut fields = header.splitn(4, '\0');
            let id = fields.next().unwrap_or_default().to_string();
            let at: Option<u64> = fields.next().and_then(|at| at.trim().parse().ok());
            // A merge, by parent count. `%P` is space-separated, and the
            // absence of numstat lines below is what has to be told apart
            // from a commit that changed nothing.
            let merge = fields
                .next()
                .is_some_and(|parents| parents.split_whitespace().count() > 1);
            commits.push(Checkpoint {
                number: commits.len() + 1,
                landed: handed.contains(&id),
                id,
                subject: fields.next().unwrap_or_default().to_string(),
                // Saturating: a commit dated in the future is something an
                // agent can produce with one `GIT_COMMITTER_DATE`, and it
                // must read as *just now* rather than panicking on a
                // subtraction or reading as decades old. `None` is the
                // other answer — a date omh could not read at all, which
                // must not borrow the confidence of *just now*.
                age: match (now, at) {
                    (Some(now), Some(at)) => Some(now.saturating_sub(at)),
                    _ => None,
                },
                touched: (!merge).then(Touched::default),
            });
            continue;
        }
        let Some(Some(touched)) = commits.last_mut().map(|c| &mut c.touched) else {
            continue;
        };
        let mut counts = line.split('\t');
        let (Some(added), Some(removed)) = (counts.next(), counts.next()) else {
            continue;
        };
        touched.files += 1;
        // `-` is git's answer for a file it would not count, and it is not
        // a zero. Both halves are checked rather than one: a line with one
        // of each is not a shape git produces, and reading it as counted
        // would put a number omh invented beside a file it did not measure.
        match (added.parse::<usize>(), removed.parse::<usize>()) {
            (Ok(added), Ok(removed)) => {
                touched.added += added;
                touched.removed += removed;
            }
            _ => touched.uncounted += 1,
        }
    }

    commits
}

/// `git`, for a call whose arguments are built rather than written out.
///
/// The same shape `session.rs` keeps for the same reason: `--exclude=<ref>`
/// has to be formatted, and a `Vec<String>` at one call site is not worth a
/// second spelling of the whole helper.
fn git_owned(gitdir: &Path, worktree: &Path, args: &[String]) -> Result<String> {
    let borrowed: Vec<&str> = args.iter().map(String::as_str).collect();
    git(gitdir, worktree, &borrowed)
}

fn git(gitdir: &Path, worktree: &Path, args: &[&str]) -> Result<String> {
    let out = Command::new("git")
        .envs(GUEST_ENV)
        .args(NEUTRALISED.iter().flat_map(|kv| ["-c", kv]))
        // Explicitly, because a child inherits omh's cwd and `add -A .` is
        // resolved against it: invoked from inside the session's worktree, git
        // computes a prefix and seeds only that subtree, silently leaving the
        // rest of the tree out of the commit every later diff is measured from.
        .current_dir(worktree)
        .arg("--git-dir")
        .arg(gitdir)
        .arg("--work-tree")
        .arg(worktree)
        .args(guarded(args))
        .output()
        .context("running git")?;
    if !out.status.success() {
        anyhow::bail!(
            "git {}: {}",
            args.join(" "),
            crate::out::untrusted(String::from_utf8_lossy(&out.stderr).trim())
        );
    }
    Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A checkout with history, a worktree holding a session's starting tree,
    /// and a carried file the user never tracked.
    fn fixture() -> (tempfile::TempDir, PathBuf, PathBuf) {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path().join("checkout");
        std::fs::create_dir_all(&root).unwrap();
        let run = |args: &[&str]| {
            Command::new("git")
                .current_dir(&root)
                .args(args)
                .output()
                .unwrap();
        };
        run(&["init", "-q", "-b", "main"]);
        run(&["config", "user.email", "t@example.com"]);
        run(&["config", "user.name", "t"]);
        std::fs::write(root.join("f.txt"), "base\n").unwrap();
        run(&["add", "-A"]);
        run(&[
            "commit",
            "-q",
            "-m",
            "a commit only the host should know about",
        ]);

        // A real worktree, made the way omh makes one, because the `.git`
        // pointer it writes is the *only* route from here back to the
        // checkout's object store. Built as a plain directory instead, the
        // isolation test could not reach the thing it exists to forbid: a leak
        // that resolves the checkout through that pointer and writes
        // `objects/info/alternates` passed, because there was no pointer to
        // resolve. The guard was correct and the fixture made it decorative.
        let wt = dir.path().join("wt");
        run(&[
            "worktree",
            "add",
            "-q",
            "-b",
            "omh/s01",
            wt.to_str().unwrap(),
        ]);
        std::fs::write(wt.join("f.txt"), "base\n").unwrap();
        std::fs::write(wt.join(".env"), "SECRET=1\n").unwrap();

        let shadow_dir = dir.path().join("shadow");
        std::fs::create_dir_all(&shadow_dir).unwrap();
        (dir, wt, shadow_dir)
    }

    fn head_of(repo: &Path) -> String {
        let out = Command::new("git")
            .current_dir(repo)
            .args(["rev-parse", "HEAD"])
            .output()
            .unwrap();
        String::from_utf8_lossy(&out.stdout).trim().to_string()
    }

    /// The note reads as English in every shape it has, and never claims a
    /// decision is waiting when none is.
    ///
    /// A count is the whole content of this sentence, so a count rendered
    /// wrong is the sentence rendered wrong — and *0 files need resolving* at
    /// the top of a rebuilt context sends an agent looking through a clean
    /// tree for a conflict.
    ///
    /// Its size is asserted too. It arrives where the session's own subject
    /// matter is competing for room, and the manifest claims a figure for it;
    /// a sentence that grows into a paragraph should break the claim rather
    /// than quietly cost more.
    #[test]
    fn the_note_says_what_arrived_and_what_is_left_in_the_words_for_it() {
        let one = note_for("main", 1, 0);
        assert!(
            one.contains("moved 1 commit under"),
            "one commit, not `1 commits`: {one}"
        );
        assert!(
            one.contains("nothing needs deciding"),
            "and nothing is waiting: {one}"
        );
        assert!(
            !one.contains("resolving"),
            "a clean merge does not mention resolving anything: {one}"
        );

        let many = note_for("develop", 12, 2);
        assert!(
            many.contains("develop moved 12 commits"),
            "the branch by name — a session's base is not always `main`: {many}"
        );
        assert!(
            many.contains("2 files still need resolving"),
            "and what is left, in the plural: {many}"
        );

        let single = note_for("main", 3, 1);
        assert!(
            single.contains("1 file still needs resolving"),
            "one file *needs*, not *need*: {single}"
        );

        // The other zero, and the one this test did not have on the first
        // pass. `--base` is a user-supplied argument, so a base that went
        // backwards is ordinary input rather than a corrupted repository.
        let back = note_for("release-1.0", 0, 0);
        assert!(
            !back.contains("0 commit"),
            "a count of nothing is not a sentence: {back}"
        );
        assert!(
            back.contains("moved backwards") && back.contains("release-1.0"),
            "it says what actually happened, by name: {back}"
        );
        assert!(
            back.contains("what changed") && !back.contains("what arrived"),
            "and does not claim something arrived when the base went back: {back}"
        );

        for note in [&one, &many, &single, &back] {
            assert!(
                note.contains("git show HEAD"),
                "every shape says where to read it: {note}"
            );
            // A ceiling, not the cost — the manifest's figure is asserted
            // byte for byte by `the_notes_declared_cost_matches_the_sentence_it_ships`
            // against one named shape, which is the only way a number about an
            // unbounded string can be checked at all. What this catches is the
            // sentence turning into a paragraph, which is a different mistake
            // and the one this arrives in the middle of somebody's context to
            // make.
            assert!(
                note.len() < 320,
                "the note has grown from a sentence into a paragraph: {} B",
                note.len()
            );
            // A run of spaces is a line continuation whose indentation shipped
            // — the same accident `git_checks_from` carries a guard for.
            assert!(!note.contains("  "), "a fold's indentation shipped: {note}");
        }
    }

    /// Commit in the sandbox the way the agent does, and answer with its id.
    /// Run the **shipped** hook body against this fixture.
    ///
    /// The command itself, with the guest's two paths swapped for the
    /// temporary ones — not a re-implementation of it. An earlier draft wrote
    /// the plumbing out again in Rust and asserted on that, which proves the
    /// copy works and would pass against a hook that did nothing.
    fn turn_snapshot(s: &Shadow, wt: &Path) {
        let body = turn_hook_command(s.gitdir.to_str().unwrap(), wt.to_str().unwrap());
        let out = Command::new("sh")
            .arg("-c")
            .arg(&body)
            .env("GIT_CONFIG_GLOBAL", "/dev/null")
            .env("GIT_CONFIG_SYSTEM", "/dev/null")
            .output()
            .unwrap();
        // Deliberately no assertion on the exit status or the streams. The
        // body ends in `>/dev/null 2>&1 || true`, so success and silence are
        // true by construction — an earlier version asserted both and was
        // asserting nothing at all. What the hook did is asserted by the
        // caller, against the repository.
        let _ = out;
    }

    fn checkpoint(s: &Shadow, wt: &Path, file: &str, body: &str, subject: &str) -> String {
        std::fs::write(wt.join(file), body).unwrap();
        git(&s.gitdir, wt, &["add", "-A", "."]).unwrap();
        git(
            &s.gitdir,
            wt,
            &["commit", "-q", "--no-verify", "-m", subject],
        )
        .unwrap();
        git(&s.gitdir, wt, &["rev-parse", "HEAD"])
            .unwrap()
            .trim()
            .to_string()
    }

    /// Two syncs before one start leave one sentence, not two stacked.
    ///
    /// The contract `leave_note`'s own comment argues for, asserted because
    /// switching the write to an append keeps every other test green and the
    /// agent gets a stale paragraph above a current one — with no way to tell
    /// which of the two describes the tree in front of it.
    #[test]
    fn a_second_sync_replaces_the_note_rather_than_stacking_on_it() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        s.leave_note(&note_for("main", 3, 0)).unwrap();
        s.leave_note(&note_for("main", 1, 2)).unwrap();

        let left = std::fs::read_to_string(note_file(&s.gitdir)).unwrap();
        assert!(
            left.contains("moved 1 commit") && !left.contains("moved 3 commits"),
            "the second note is the one that is there: {left}"
        );
        assert_eq!(left.lines().count(), 1, "one sentence, not two: {left}");
        // Nothing left beside it either — the write goes through a temporary
        // so a half-written note can never be delivered as a whole one, and a
        // temporary that outlived its rename would be a file `git status`
        // inside the sandbox starts reporting.
        let strays: Vec<_> = std::fs::read_dir(&s.gitdir)
            .unwrap()
            .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
            .filter(|n| n.contains("omh-note") && n != "omh-note")
            .collect();
        assert!(strays.is_empty(), "no scaffolding left behind: {strays:?}");
    }

    /// A note that cannot be written does not fail the sync, and does not
    /// leave a fragment behind claiming to be one.
    ///
    /// Both halves matter and the second is why the write goes through a
    /// rename. `fs::write` truncates first, so a disk that fills mid-sentence
    /// leaves a prefix on disk *and* returns an error — and the caller, told
    /// the note failed, warns that the agent will find nothing while the agent
    /// is handed half a sentence with the `git show HEAD` clause missing.
    #[test]
    fn a_note_that_cannot_be_written_leaves_nothing_claiming_to_be_one() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // A directory where the note goes: the rename cannot replace it, which
        // is a write failure omh can produce on demand.
        std::fs::create_dir(note_file(&s.gitdir)).unwrap();
        let why = s.leave_note(&note_for("main", 3, 0)).unwrap_err();
        assert!(
            format!("{why:#}").contains("omh-note"),
            "the failure names the file: {why:#}"
        );
        let strays: Vec<_> = std::fs::read_dir(&s.gitdir)
            .unwrap()
            .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
            .filter(|n| n.contains("omh-note") && n != "omh-note")
            .collect();
        assert!(
            strays.is_empty(),
            "and the half-written one is cleaned up rather than left to be read: {strays:?}"
        );
    }

    /// The hook photographs the tree and changes nothing the agent can see.
    ///
    /// The four properties its doc claims, against the shipped command:
    ///
    /// - HEAD does not move, the index is untouched, and `git status` says
    ///   afterwards exactly what it said before. This is the whole reason it
    ///   is a snapshot and not a `git commit` — an agent whose working tree
    ///   went clean at the end of every turn would find nothing to commit and
    ///   would rightly conclude omh had eaten its work.
    /// - a turn that changed nothing writes nothing, or an idle agent adds an
    ///   identical commit for as long as the session lives.
    /// - the snapshots chain, so the ref is a timeline rather than a single
    ///   photograph.
    #[test]
    fn a_turn_photographs_the_tree_and_leaves_the_agents_own_state_alone() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let head = |what: &str| git(&s.gitdir, &wt, &["rev-parse", what]).unwrap();
        let status = || git(&s.gitdir, &wt, &["status", "--porcelain"]).unwrap();

        assert_eq!(
            s.turns(&wt).unwrap(),
            None,
            "a session that has never finished a turn has no ref, which is not \
             the same answer as a count of none"
        );

        std::fs::write(wt.join("in-flight.rs"), "fn later() {}\n").unwrap();
        let before = (head("HEAD"), status());

        turn_snapshot(&s, &wt);
        assert_eq!(
            s.turns(&wt).unwrap(),
            Some(1),
            "a dirty turn is photographed"
        );
        assert_eq!(
            (head("HEAD"), status()),
            before,
            "and HEAD and the working tree are exactly as the agent left them"
        );

        turn_snapshot(&s, &wt);
        assert_eq!(
            s.turns(&wt).unwrap(),
            Some(1),
            "a turn that changed nothing writes nothing"
        );

        std::fs::write(wt.join("in-flight.rs"), "fn later() { todo!() }\n").unwrap();
        turn_snapshot(&s, &wt);
        assert_eq!(
            s.turns(&wt).unwrap(),
            Some(2),
            "and the next change is its own"
        );

        // A timeline, not one photograph: the newest snapshot reaches the
        // first, which is what makes restoring an earlier turn's tree
        // possible at all.
        assert_eq!(
            git(&s.gitdir, &wt, &["rev-list", "--count", TURN_REF])
                .unwrap()
                .trim(),
            "2",
            "the snapshots chain"
        );
        // A sandbox omh cannot read is not a sandbox with no snapshots. This
        // is the arm `.unwrap_or(0)` used to swallow on the way to an
        // irreversible delete.
        let broken = Shadow::new(&shadow_dir, "s99");
        std::fs::create_dir_all(&broken.gitdir).unwrap();
        assert!(
            broken.turns(&wt).is_err(),
            "a directory that is not a repository has no answer, not zero"
        );

        // The shipped invocation stages into an index of its own, made fresh.
        // The assertion this replaces looked for `i=/omh/shadow/index ` with a
        // trailing space — a string no rendering could ever produce, since the
        // path is always followed by `;`. It passed for every possible value
        // of the index path, including the agent's own.
        let shipped = turn_hook_for_the_sandbox();
        assert!(
            shipped.contains("i=$(mktemp)") && !shipped.contains("/omh/shadow/index"),
            "a fresh index, never the agent's staging area: {shipped}"
        );
        assert!(
            shipped.contains("rm -f \"$i\""),
            "and it is cleaned up, so nothing accumulates in the mount: {shipped}"
        );

        // …and the agent's own work is still uncommitted, which is the state
        // the snapshot exists to be a rollback from.
        assert!(
            status().contains("in-flight.rs"),
            "the agent still has its work to commit: {}",
            status()
        );
    }

    /// An agent cannot make its own snapshots look like unharvested work.
    ///
    /// `unkept`'s exclusion reaches `--all` and not `--reflog`, which is safe
    /// only because git writes no reflog for a ref outside `refs/heads`,
    /// `refs/remotes`, `refs/notes` and `HEAD`. `core.logAllRefUpdates =
    /// always` flips exactly that, and the config is the agent's to write — so
    /// the whole design rested on a default the sandbox could change.
    ///
    /// Now the key is swept on every launch. This asserts the sweep, not the
    /// git behaviour: with the value left in place, `unkept` finds the
    /// snapshots through the reflog and `rm` refuses over omh's own commits
    /// for the rest of the session.
    #[test]
    fn a_reflog_the_agent_turned_on_does_not_survive_the_next_launch() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        git(
            &s.gitdir,
            &wt,
            &["config", "core.logAllRefUpdates", "always"],
        )
        .unwrap();
        assert_eq!(
            git(
                &s.gitdir,
                &wt,
                &["config", "--get", "core.logallrefupdates"]
            )
            .unwrap()
            .trim(),
            "always",
            "the agent can set it"
        );

        s.ensure(&wt, &[]).unwrap();
        let after = Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args(["config", "--get", "core.logallrefupdates"])
            .output()
            .unwrap();
        assert!(
            !after.status.success(),
            "and the next launch takes it away, so the default the exclusion \
             relies on is the one in force: {after:?}"
        );
    }

    /// A hook that cannot do its job still ends the turn quietly.
    ///
    /// The claim `turn_hook_command` makes and the one nothing was checking.
    /// `every_hook_runs_quietly_when_its_tool_says_nothing` was credited with
    /// it and does not do it: `when` renders as `[ -d /omh/shadow ] || exit 0`,
    /// which is false on any host, so the body never executes there and the
    /// test passes identically against a body of `exit 17`.
    ///
    /// Three ways for it to be unable to work, all reachable in a sandbox: a
    /// gitdir that is not there, a worktree that is not there, and a HEAD that
    /// has never been born — `git checkout --orphan` is an ordinary move, and
    /// before the `read-tree --empty` fallback it silently ended snapshots for
    /// the rest of the session.
    #[test]
    fn a_hook_that_cannot_do_its_job_still_ends_the_turn_quietly() {
        let (_d, wt, shadow_dir) = fixture();
        let run = |gitdir: &str, worktree: &str| {
            let out = Command::new("sh")
                .arg("-c")
                .arg(turn_hook_command(gitdir, worktree))
                .env("GIT_CONFIG_GLOBAL", "/dev/null")
                .env("GIT_CONFIG_SYSTEM", "/dev/null")
                .output()
                .unwrap();
            assert!(
                out.status.success() && out.stdout.is_empty() && out.stderr.is_empty(),
                "a turn ends quietly whatever the hook found: {out:?}"
            );
        };

        run("/nowhere/gitdir", "/nowhere/worktree");
        run(shadow_dir.to_str().unwrap(), wt.to_str().unwrap());

        // An unborn HEAD, which is what the agent leaves behind after
        // `git checkout --orphan`. The snapshot must still be taken — the
        // whole point is a rollback target for a turn that went wrong, and
        // that is exactly the turn an orphan checkout produces.
        let s = Shadow::new(&shadow_dir, "s02");
        s.ensure(&wt, &[]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["checkout", "-q", "--orphan", "nothing-yet"],
        )
        .unwrap();
        std::fs::write(wt.join("in-flight.rs"), "fn later() {}\n").unwrap();
        turn_snapshot(&s, &wt);
        assert_eq!(
            s.turns(&wt).unwrap(),
            Some(1),
            "an unborn HEAD is still a tree worth photographing"
        );
    }

    /// A turn snapshot is omh's own, and none of the three guards that walk
    /// refs may mistake it for the agent's stranded work.
    ///
    /// Three queries see every ref in the sandbox, and a snapshot is by
    /// construction not an ancestor of HEAD — which is exactly the shape all
    /// three are hunting. Left alone they would each be permanently wrong, and
    /// wrong in the direction that blocks:
    ///
    /// - `preflight` refuses every `--keep`, naming omh's own commits as work
    ///   the agent stranded, and telling the user to go and delete them.
    /// - `checkpoints().unreachable` prints that as a warning on every `log`,
    ///   and `incomplete()` is true forever.
    /// - `unkept` feeds `at_stake`, so `omh sNN rm` refuses even for a session
    ///   whose work is entirely on the branch.
    ///
    /// The three do **not** get the same fix, which is why this asserts them
    /// apart. The first two ask *would a harvest drop commits?* — a snapshot
    /// is never replayed, so it is not one of those. The third asks *what
    /// would removing this destroy?* — a snapshot is a real answer to that,
    /// and it is counted, just not as the agent's own work.
    #[test]
    fn a_turn_snapshot_is_not_the_agents_stranded_work() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");

        // Clean before: nothing stranded, nothing unkept beyond the commit
        // the agent just made.
        s.preflight(&wt).expect("a plain session harvests");
        let (unkept_before, _) = s.unkept(&wt).unwrap();

        std::fs::write(wt.join("in-flight.rs"), "fn later() {}\n").unwrap();
        turn_snapshot(&s, &wt);

        s.preflight(&wt)
            .expect("a snapshot is not a commit the harvest would drop");
        assert_eq!(
            s.checkpoints(&wt).unwrap().unreachable,
            0,
            "and `log` does not warn about it every time"
        );

        // Counted, because it is the only copy of a tree the agent may have
        // since thrown away — that is the whole reason the hook exists.
        let (unkept_after, _) = s.unkept(&wt).unwrap();
        assert_eq!(
            unkept_after, unkept_before,
            "but it is not counted as the agent's own unharvested commits"
        );
        assert_eq!(
            s.turns(&wt).unwrap(),
            Some(1),
            "it is counted as what it is, and separately"
        );
    }

    /// The numbers are the interface — `diff N` and `--keep 1,3-4` take them —
    /// so they have to name the same commit tomorrow as today. Oldest first is
    /// what makes that true: a new checkpoint appends, and nothing renumbers.
    #[test]
    fn checkpoints_are_numbered_from_the_oldest_and_never_renumber() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");
        let before: Vec<usize> = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .map(|c| c.number)
            .collect();
        let first_subject = s.checkpoints(&wt).unwrap().commits[0].subject.clone();

        checkpoint(&s, &wt, "three.rs", "fn three() {}\n", "Add three");
        let after = s.checkpoints(&wt).unwrap().commits;

        assert_eq!(before, vec![1, 2], "two commits, numbered from the oldest");
        assert_eq!(
            after.iter().map(|c| c.number).collect::<Vec<_>>(),
            vec![1, 2, 3],
            "a third appends rather than renumbering"
        );
        assert_eq!(
            after[0].subject, first_subject,
            "number 1 is still the same commit it was"
        );
        assert_eq!(after[0].subject, "Add one");
        assert_eq!(after[2].subject, "Add three");
    }

    /// The seed is omh's commit, not the agent's, and it holds the whole
    /// starting tree: counted as a checkpoint it would be number 1 in every
    /// session, offering the user a review of their own files.
    #[test]
    fn the_seed_is_not_one_of_the_agents_checkpoints() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        assert!(
            s.checkpoints(&wt).unwrap().commits.is_empty(),
            "a sandbox that has committed nothing has nothing to show"
        );

        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        let one = s.checkpoints(&wt).unwrap().commits;
        assert_eq!(
            one.len(),
            1,
            "the agent's commit, and not the seed: {one:?}"
        );
    }

    /// Which work is already the branch's, from the replay point — the same
    /// record `--keep` replays from, so the line drawn here is the line the
    /// next harvest will act on rather than a second opinion about it.
    #[test]
    fn checkpoints_say_which_have_already_been_handed_over() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        let handed = checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");
        checkpoint(&s, &wt, "three.rs", "fn three() {}\n", "Add three");

        assert!(
            s.checkpoints(&wt)
                .unwrap()
                .commits
                .iter()
                .all(|c| !c.landed),
            "nothing has been handed over yet"
        );

        std::fs::write(&s.landed_record, format!("{handed}\n")).unwrap();
        let after = s.checkpoints(&wt).unwrap().commits;

        assert_eq!(
            after.iter().map(|c| c.landed).collect::<Vec<_>>(),
            vec![true, true, false],
            "the line falls after what the last harvest took: {after:?}"
        );
    }

    /// What a checkpoint touched, so the log answers *is this worth reading*
    /// without a second command.
    #[test]
    fn a_checkpoint_reports_what_it_touched() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        std::fs::write(wt.join("a.rs"), "one\ntwo\nthree\n").unwrap();
        std::fs::write(wt.join("b.rs"), "one\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["commit", "-q", "--no-verify", "-m", "Add two files"],
        )
        .unwrap();
        // …then change one of them, so added and removed are different numbers
        // and neither can stand in for the other.
        std::fs::write(wt.join("a.rs"), "one\nchanged\nthree\nfour\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["commit", "-q", "--no-verify", "-m", "Change a"],
        )
        .unwrap();

        let all = s.checkpoints(&wt).unwrap().commits;
        let touched = |c: &Checkpoint| {
            let t = c.touched.clone().expect("not a merge");
            (t.files, t.added, t.removed, t.uncounted)
        };
        assert_eq!(touched(&all[0]), (2, 4, 0, 0));
        assert_eq!(touched(&all[1]), (1, 2, 1, 0));
    }

    /// The agent can rewrite what the log says without touching a commit.
    ///
    /// `git replace` swaps one object for another at read time, and the gitdir
    /// is mounted read-write — the read-only `config` does not cover
    /// `refs/replace/`. Measured 2026-08-23 against git 2.55.0: with a
    /// replacement in place the log printed a forged subject and a forged
    /// (empty) file list beside the **real** commit id, on stdout, with nothing
    /// on stderr. The harvest fetches the real object, so `--keep` would land
    /// something the review never showed.
    ///
    /// This is the one command whose whole purpose is to be believed.
    #[test]
    fn a_replaced_object_cannot_change_what_the_log_says() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();
        let real = checkpoint(
            &s,
            &wt,
            "creds.rs",
            "const KEY: &str = \"\";\n",
            "real work",
        );

        // The agent forges an innocent-looking commit and points the real id at
        // it. Plain `git`, not this module's helper: this is the sandbox
        // acting, not omh.
        let forged = Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args([
                "commit-tree",
                &format!("{seed}^{{tree}}"),
                "-p",
                &seed,
                "-m",
                "tidy whitespace",
            ])
            .output()
            .unwrap();
        let forged = String::from_utf8_lossy(&forged.stdout).trim().to_string();
        Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args(["replace", "-f", &real, &forged])
            .output()
            .unwrap();

        let commits = s.checkpoints(&wt).unwrap().commits;
        assert_eq!(commits.len(), 1);
        assert_eq!(
            commits[0].subject, "real work",
            "the log reports the commit, not what the agent replaced it with"
        );
        assert_eq!(
            commits[0].touched.as_ref().map(|t| t.files),
            Some(1),
            "and what it really touched: {commits:?}"
        );
    }

    /// `info/grafts` rewrites ancestry from a file inside the mount.
    ///
    /// Two commits vanish from the list and git says so only in a deprecation
    /// hint on **stderr**, which `git` here discards on success. A review
    /// silently shortened to one line, over an exit code of 0, is how a user
    /// concludes there is little to review and runs `rm`.
    ///
    /// Measured 2026-08-23: neither `--no-replace-objects` nor
    /// `-c core.graftsFile=` stops this — the config key is simply not
    /// consulted. `GIT_GRAFT_FILE` pointed away from the gitdir is, which is
    /// why `GUEST_ENV` exists at all and why this guard is not in
    /// `NEUTRALISED` beside the others.
    #[test]
    fn a_graft_file_cannot_hide_commits_from_the_log() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();
        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");
        let head = checkpoint(&s, &wt, "three.rs", "fn three() {}\n", "Add three");

        std::fs::create_dir_all(s.gitdir.join("info")).unwrap();
        std::fs::write(s.gitdir.join("info/grafts"), format!("{head} {seed}\n")).unwrap();

        let commits = s.checkpoints(&wt).unwrap().commits;
        assert_eq!(
            commits
                .iter()
                .map(|c| c.subject.as_str())
                .collect::<Vec<_>>(),
            vec!["Add one", "Add two", "Add three"],
            "every checkpoint is still there: {commits:?}"
        );
    }

    /// A merge has no diff of its own, and *0 files* is a measurement.
    ///
    /// git prints the header for a merge and no numstat lines at all, so
    /// counting the absence renders a merge that brought in a whole branch
    /// exactly like an empty commit.
    #[test]
    fn a_merge_says_so_rather_than_reporting_that_it_touched_nothing() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();
        checkpoint(&s, &wt, "main.rs", "fn main() {}\n", "On the branch");
        git(&s.gitdir, &wt, &["checkout", "-q", "-b", "side", &seed]).unwrap();
        checkpoint(&s, &wt, "side.rs", "fn side() {}\n", "On the side");
        git(&s.gitdir, &wt, &["checkout", "-q", "-"]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["merge", "-q", "--no-ff", "side", "-m", "Merge the side"],
        )
        .unwrap();

        let commits = s.checkpoints(&wt).unwrap().commits;
        let merge = commits
            .iter()
            .find(|c| c.subject == "Merge the side")
            .unwrap_or_else(|| panic!("the merge is a checkpoint too: {commits:?}"));
        assert_eq!(merge.touched, None, "not measured, and not zero: {merge:?}");
        assert!(
            commits.iter().any(|c| c.touched.is_some()),
            "the ordinary commits are still measured"
        );
    }

    /// The numbers survive a merge, which is the case that broke them.
    ///
    /// Measured 2026-08-23: `--reverse` alone is commit-date order, so a side
    /// branch whose commits are older is spliced into the middle of the list
    /// and everything after it shifts down. The guard for the numbering
    /// invariant only appended to a linear history and never saw this.
    #[test]
    fn a_merge_does_not_renumber_the_checkpoints_already_listed() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();

        // Dated so the side branch is *older* than what follows it on the
        // branch — the arrangement date order gets wrong.
        let at = |when: &str, file: &str, subject: &str| {
            std::fs::write(wt.join(file), format!("// {subject}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            let out = Command::new("git")
                .envs(GUEST_ENV)
                .arg("--git-dir")
                .arg(&s.gitdir)
                .arg("--work-tree")
                .arg(&wt)
                .env("GIT_AUTHOR_DATE", when)
                .env("GIT_COMMITTER_DATE", when)
                .args(["commit", "-q", "--no-verify", "-m", subject])
                .output()
                .unwrap();
            assert!(out.status.success(), "{out:?}");
        };
        at("2026-08-20T11:00:00", "a.rs", "A");
        at("2026-08-20T13:00:00", "b.rs", "B");
        let before = s.checkpoints(&wt).unwrap().commits;

        git(&s.gitdir, &wt, &["checkout", "-q", "-b", "side", &seed]).unwrap();
        at("2026-08-20T12:00:00", "s.rs", "S, older than B");
        git(&s.gitdir, &wt, &["checkout", "-q", "-"]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["merge", "-q", "--no-ff", "side", "-m", "M"],
        )
        .unwrap();
        let after = s.checkpoints(&wt).unwrap().commits;

        for was in &before {
            let now = after
                .iter()
                .find(|c| c.id == was.id)
                .unwrap_or_else(|| panic!("{} left the list entirely", was.subject));
            assert_eq!(
                now.number, was.number,
                "{} was {} and is now {} — a selection typed before the merge would \
                 land a different commit",
                was.subject, was.number, now.number
            );
        }
    }

    /// A replay point the history no longer reaches is a question, not a list
    /// of new work.
    ///
    /// `rev-list seed..landed` *succeeds* in this state — measured: exit 0, the
    /// ids still resolve — so without asking about ancestry the set simply
    /// matches nothing, every checkpoint reads as new, and the log offers a
    /// `--keep` that `harvest` refuses for this exact reason.
    #[test]
    fn a_replay_point_the_history_lost_is_reported_rather_than_read_as_new_work() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();
        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        let handed = checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");
        std::fs::write(&s.landed_record, format!("{handed}\n")).unwrap();
        assert!(
            !s.checkpoints(&wt).unwrap().replay_point_lost,
            "nothing is wrong yet"
        );

        // The agent rewinds below the replay point — one of the four commands
        // this repository exists to give back.
        git(&s.gitdir, &wt, &["reset", "-q", "--hard", &seed]).unwrap();
        checkpoint(&s, &wt, "other.rs", "fn other() {}\n", "Different work");
        let read = s.checkpoints(&wt).unwrap();

        assert!(
            read.replay_point_lost,
            "omh cannot tell what the branch already has: {read:?}"
        );
        assert!(
            read.commits.iter().all(|c| !c.landed),
            "and marks nothing as handed over on a guess"
        );
    }

    /// Work on a branch the sandbox wandered off is invisible to this read, and
    /// `preflight` refuses to harvest over it.
    ///
    /// Reported rather than hidden, because the alternative is a user reading a
    /// clean review and then being refused by `--keep` citing commits they have
    /// never been shown.
    #[test]
    fn commits_this_read_cannot_reach_are_counted_and_said() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        assert_eq!(s.checkpoints(&wt).unwrap().unreachable, 0);

        git(&s.gitdir, &wt, &["checkout", "-q", "-b", "spike"]).unwrap();
        checkpoint(&s, &wt, "spike.rs", "fn spike() {}\n", "A spike");
        checkpoint(&s, &wt, "spike2.rs", "fn more() {}\n", "More of it");
        git(&s.gitdir, &wt, &["checkout", "-q", "-"]).unwrap();

        let read = s.checkpoints(&wt).unwrap();
        assert_eq!(
            read.unreachable, 2,
            "two commits are on no branch this read follows: {read:?}"
        );
        assert_eq!(read.commits.len(), 1, "and the list cannot show them");
    }

    /// Binary files count as files and never as zero lines.
    ///
    /// git answers `-` for a file it would not count, and a blank churn column
    /// beside *1 file* is how a 200MB blob reads as a mode-bit change.
    #[test]
    fn a_file_git_would_not_count_is_not_counted_as_nothing() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        std::fs::write(wt.join("blob.bin"), [0u8, 1, 2, 0, 255]).unwrap();
        std::fs::write(wt.join("text.rs"), "one\ntwo\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["commit", "-q", "--no-verify", "-m", "Both kinds"],
        )
        .unwrap();

        let touched = s.checkpoints(&wt).unwrap().commits[0]
            .touched
            .clone()
            .expect("not a merge");
        assert_eq!(
            (touched.files, touched.added, touched.uncounted),
            (2, 2, 1),
            "both files counted, the text lines counted, the blob's not invented: {touched:?}"
        );
    }

    /// The age comes from a clock, and two answers it must never give are a
    /// panic and a confident *just now*.
    ///
    /// `%ct` is the committer date, so this dates the commit rather than the
    /// authorship a rebase would have carried over.
    #[test]
    fn a_checkpoint_dated_in_the_future_reads_as_just_now_rather_than_failing() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        checkpoint(&s, &wt, "now.rs", "fn now() {}\n", "Made now");

        std::fs::write(wt.join("later.rs"), "fn later() {}\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        let out = Command::new("git")
            .envs(GUEST_ENV)
            .arg("--git-dir")
            .arg(&s.gitdir)
            .arg("--work-tree")
            .arg(&wt)
            .env("GIT_COMMITTER_DATE", "2099-01-01T00:00:00")
            .args(["commit", "-q", "--no-verify", "-m", "From the future"])
            .output()
            .unwrap();
        assert!(out.status.success(), "{out:?}");

        // …and one carrying an old *author* date, which is what `rebase`,
        // `cherry-pick` and `--amend` leave behind. The list is ordered by
        // commit date, so an age read from the author date would run
        // backwards down a list the reader takes as chronological — and this
        // is the only arrangement that tells the two dates apart. The
        // future-dated commit above cannot: setting the committer date alone
        // leaves the author date at *now*, so both readings answer zero.
        std::fs::write(wt.join("replayed.rs"), "fn replayed() {}\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        let out = Command::new("git")
            .envs(GUEST_ENV)
            .arg("--git-dir")
            .arg(&s.gitdir)
            .arg("--work-tree")
            .arg(&wt)
            .env("GIT_AUTHOR_DATE", "2020-01-01T00:00:00")
            .args([
                "commit",
                "-q",
                "--no-verify",
                "-m",
                "Written long ago, committed now",
            ])
            .output()
            .unwrap();
        assert!(out.status.success(), "{out:?}");

        let commits = s.checkpoints(&wt).unwrap().commits;
        assert_eq!(
            commits[0].age.map(|age| age < 300),
            Some(true),
            "a commit made now is dated now: {:?}",
            commits[0]
        );
        assert_eq!(
            commits[1].age,
            Some(0),
            "and one dated in the future reads as just now: {:?}",
            commits[1]
        );
        assert_eq!(
            commits[2].age.map(|age| age < 300),
            Some(true),
            "and one written years ago but committed now is dated by the commit: {:?}",
            commits[2]
        );
    }

    /// A number names one checkpoint, and the patch is that commit's.
    #[test]
    fn a_checkpoint_number_shows_that_checkpoints_own_patch() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        let second = checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");

        let patch = s.show(&wt, 2, crate::session::What::Patch).unwrap();
        assert!(
            patch.contains("+fn two() {}") && !patch.contains("+fn one() {}"),
            "checkpoint 2 is the second commit and nothing else: {patch}"
        );

        // Against git's own answer for that object, so the numbering and the
        // patch cannot drift apart while both look plausible.
        let theirs = Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args(["show", "-p", &second])
            .output()
            .unwrap();
        assert_eq!(
            patch,
            String::from_utf8_lossy(&theirs.stdout),
            "the same commit git would show for that id"
        );
    }

    /// A subject the agent chose cannot repaint a checkpoint review.
    ///
    /// `git show` prints the subject, and git quotes paths but not subjects —
    /// measured during the log work and reaching omh by a second route here.
    /// Asserted through the report, because that is where the rule lives:
    /// sanitised for a person, raw for a program.
    #[test]
    fn a_subject_the_agent_wrote_cannot_repaint_a_checkpoint_review() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        checkpoint(
            &s,
            &wt,
            "one.rs",
            "fn one() {}\n",
            "Fix \u{1b}[2K\rnothing at all",
        );

        let body = s.show(&wt, 1, crate::session::What::Summary).unwrap();
        let report = crate::report::Diff {
            label: "s01 checkpoint 1".into(),
            session: "s01".into(),
            checkpoint: Some(1),
            base: "its parent".into(),
            what: crate::session::What::Summary,
            body,
        };
        use crate::out::Report;
        let printed = report.human(&crate::out::Palette::plain());

        assert!(
            !printed.chars().any(|c| c.is_control() && c != '\n'),
            "no control character survives into omh's own output: {printed:?}"
        );
        assert!(
            printed.contains("nothing at all"),
            "the words still arrive: {printed}"
        );
        assert!(
            report.json()["summary"]
                .as_str()
                .is_some_and(|s| s.contains('\u{1b}')),
            "and a program still gets what git said: {}",
            report.json()
        );
    }

    /// A merge's summary and its patch describe the same change.
    ///
    /// Measured 2026-08-23 against git 2.55.0: `show --stat` on a merge reports
    /// the files it brought in, and `show -p` on the same merge prints
    /// **nothing** — git's default `--cc` collapses a clean merge to an empty
    /// diff. So `omh sNN diff 3` promised a change and `omh sNN diff 3 -p`, the
    /// very next command, showed none. `--first-parent` makes them agree, and
    /// changes neither answer on an ordinary commit.
    #[test]
    fn a_merges_summary_and_its_patch_do_not_contradict_each_other() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();
        checkpoint(&s, &wt, "main.rs", "fn main() {}\n", "On the branch");
        git(&s.gitdir, &wt, &["checkout", "-q", "-b", "side", &seed]).unwrap();
        checkpoint(&s, &wt, "side.rs", "fn side() {}\n", "On the side");
        git(&s.gitdir, &wt, &["checkout", "-q", "-"]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["merge", "-q", "--no-ff", "side", "-m", "Merge the side"],
        )
        .unwrap();
        let merge = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .find(|c| c.subject == "Merge the side")
            .map(|c| c.number)
            .expect("the merge is a checkpoint");

        let summary = s.show(&wt, merge, crate::session::What::Summary).unwrap();
        let patch = s.show(&wt, merge, crate::session::What::Patch).unwrap();

        assert!(
            summary.contains("side.rs"),
            "the summary names what the merge brought in: {summary}"
        );
        assert!(
            patch.contains("side.rs") && patch.contains("+fn side() {}"),
            "and the patch shows it, rather than being empty: {patch}"
        );
    }

    /// The user's pager is the last one named, so it is the one that runs.
    ///
    /// `NEUTRALISED` pins `core.pager` to `cat` so a host-side read never runs
    /// what the sandbox's config says — measured on a pty, a `core.pager` of
    /// `sh -c "echo …; cat"` executes on a plain `git show`. Appending the
    /// user's after it is what leaves paging working, and the *order* is the
    /// whole mechanism: put it first and `cat` wins and nothing ever pages.
    ///
    /// Asserted on the argument list rather than through a terminal, because
    /// the invariant is an ordering and a captured-output test cannot see a
    /// pager at all — git consults one only when stdout is a tty.
    #[test]
    fn the_pager_omh_names_last_is_the_users_own() {
        let ours = NEUTRALISED
            .iter()
            .position(|kv| kv.starts_with("core.pager="))
            .expect("the sandbox's pager is pinned");
        assert_eq!(
            NEUTRALISED[ours], "core.pager=cat",
            "pinned to something that cannot run anything"
        );

        // The command is built the same way `stream_show` builds it.
        let pinned: Vec<String> = NEUTRALISED
            .iter()
            .flat_map(|kv| ["-c".to_string(), kv.to_string()])
            .collect();
        let mine = format!("core.pager={}", user_pager(std::path::Path::new(".")));
        let full: Vec<String> = pinned
            .iter()
            .cloned()
            .chain(["-c".to_string(), mine.clone()])
            .collect();

        let at = |needle: &str| full.iter().position(|a| a == needle);
        assert!(
            at(&mine) > at("core.pager=cat"),
            "the user's pager comes after the sandbox's, or the sandbox's wins: {full:?}"
        );
        assert!(!mine.ends_with('='), "and it names something: {mine}");
    }

    /// A checkpoint read carries every guard an ordinary read carries.
    ///
    /// `stream_show` builds its own invocation rather than going through
    /// `git`, which is how a read ends up outside the list that makes reads
    /// safe. Asserted on the arguments so the two cannot drift apart quietly.
    #[test]
    fn a_checkpoint_read_is_guarded_like_every_other_read() {
        let args = show_args(crate::session::What::Patch, "abc123", "never");
        let args: Vec<&str> = args.iter().map(String::as_str).collect();
        let guarded = guarded(&args);

        assert_eq!(guarded.first().map(String::as_str), Some("show"));
        assert!(
            guarded.contains(&"--no-textconv".to_string())
                && guarded.contains(&"--no-ext-diff".to_string()),
            "the flags a read has to carry, after the verb: {guarded:?}"
        );
        assert!(
            guarded.contains(&"--first-parent".to_string()),
            "so a merge's patch is not empty: {guarded:?}"
        );
        assert!(
            guarded.contains(&"--color=never".to_string()),
            "and colour is omh's decision, not git's guess: {guarded:?}"
        );
    }

    /// A number outside the range is refused, and the refusal says what the
    /// numbers are.
    ///
    /// The list is the only place these numbers come from, so being told *no
    /// checkpoint 9* without being told there are three is being told to go
    /// and run the other command.
    #[test]
    fn a_checkpoint_number_the_session_does_not_have_is_refused() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let empty = s
            .show(&wt, 1, crate::session::What::Summary)
            .expect_err("nothing has been committed here");
        assert!(
            empty.to_string().contains("not committed anything"),
            "an empty sandbox says so rather than naming a range it does not have: {empty}"
        );

        checkpoint(&s, &wt, "one.rs", "fn one() {}\n", "Add one");
        checkpoint(&s, &wt, "two.rs", "fn two() {}\n", "Add two");
        let err = s
            .show(&wt, 9, crate::session::What::Summary)
            .expect_err("there is no checkpoint 9");
        assert!(
            err.to_string().contains('9') && err.to_string().contains("1 to 2"),
            "the refusal names both the number and the range: {err}"
        );
        // Zero is outside it too, and is what a reader who assumed the list was
        // zero-based would type first.
        assert!(
            s.show(&wt, 0, crate::session::What::Summary).is_err(),
            "the numbers start at 1"
        );
    }

    /// The options a listing declares, read the way git writes them.
    ///
    /// The shapes are taken from real `git <verb> -h` output rather than
    /// invented, because the first version of this was written against an
    /// invented sample: it matched only an option at the start of a line, and
    /// git puts the short alias first whenever there is one. Against the real
    /// cherry-pick listing it answered *no* to eight of the nine options that
    /// command has.
    #[test]
    fn an_option_listing_is_read_the_way_git_writes_it() {
        // Verbatim shapes from git 2.55.0.
        let help = "usage: git cherry-pick [--edit] [-n] [-m <parent-number>]\n\
            \x20   --quit                end revert or cherry-pick sequence\n\
            \x20   -n, --no-commit       don't automatically commit\n\
            \x20   -e, --[no-]edit       edit the commit message\n\
            \x20   --[no-]ff             allow fast-forward\n\
            \x20   --empty (stop|drop|keep)\n\
            \x20                         how to handle commits that become empty\n\
            \x20   -S, --[no-]gpg-sign[=<key-id>]\n\
            \x20                         GPG sign commit\n\
            \x20   --[no-]keep-redundant-commits\n\
            \x20                         deprecated: use --empty=keep instead\n\
            \x20   --verify              opposite of --no-rebase-merges\n";
        let found = options_in(help);
        let has = |o: &str| found.contains(o);

        assert!(has("--empty"), "the option omh actually asks about");
        // A long form behind a short one. The first version missed every one
        // of these, and its own test comment called `--no-commit` eternal.
        assert!(has("--no-commit") && has("-n"));
        assert!(has("--edit") && has("-e"), "and behind a negation as well");
        // A negatable option declares both spellings.
        assert!(has("--ff") && has("--no-ff"));
        assert!(has("--gpg-sign"), "…and one that also takes a value");
        assert!(has("--keep-redundant-commits"));

        assert!(
            !has("--empty=keep"),
            "a description that *mentions* an option does not declare one — this line \
             says `deprecated: use --empty=keep instead`"
        );
        assert!(!has("--no-such-option"), "and absence is absence");
        // The same rule on one line rather than two. `--verify  opposite of
        // --no-verify` is verbatim from `git rebase -h`: what follows the
        // description column is prose, and prose that names an option is not a
        // git that has it.
        assert!(has("--verify"), "the declaration is read");
        assert!(
            !has("--no-rebase-merges"),
            "and its description is not — that option is not declared here"
        );
        assert!(
            options_in("no options here at all\n").is_empty(),
            "text that is not a listing declares nothing, which is not the same as an \
             option being absent"
        );
    }

    /// The verb selects the listing. It is not decoration.
    ///
    /// Asking `cherry-pick` and reading `commit`'s answer is the mutation the
    /// first version of this test could not see: every plausible verb lists
    /// `-n`, so `-n` alone proves nothing about which one was asked. Both
    /// options here are ancient, so this does not go red as git grows.
    #[test]
    fn git_answers_for_the_verb_it_was_asked_about() {
        assert!(git_supports("merge", "--ff-only").unwrap());
        assert!(
            !git_supports("cherry-pick", "--ff-only").unwrap(),
            "cherry-pick has no --ff-only, and asking it must not answer for merge"
        );
        assert!(git_supports("cherry-pick", "--empty").unwrap());
        assert!(
            git_supports("cherry-pick", "--empty=").unwrap(),
            "the spelling every comment in this tree uses is the same question"
        );
        assert!(!git_supports("cherry-pick", "--no-such-option-ever").unwrap());
    }

    /// Not being able to ask is not an answer.
    ///
    /// A verb git does not know prints a suggestion rather than a listing, so
    /// nothing is declared — which must be an error, not *this option is
    /// absent*. Collapsed into `false`, a user with no git on PATH was told
    /// their git was too old to name checkpoints.
    #[test]
    fn a_git_that_cannot_answer_is_not_a_git_that_said_no() {
        let err = git_supports("not-a-git-verb-at-all", "--empty")
            .expect_err("nothing was listed, so nothing can be concluded");
        assert!(
            err.to_string().contains("listed no options"),
            "the refusal says what happened: {err}"
        );
    }

    /// `--keep 1,3-4` means those checkpoints, in that order.
    #[test]
    fn a_selection_names_checkpoints_in_the_order_it_lists_them() {
        assert_eq!(chosen("1,3-4", 4).unwrap(), vec![1, 3, 4]);
        assert_eq!(chosen("2", 4).unwrap(), vec![2], "one is a selection");
        assert_eq!(chosen("1-4", 4).unwrap(), vec![1, 2, 3, 4], "a whole range");
        // The user's order, not sorted. Reordering is half of what curating a
        // history is for, and a selection that silently sorted itself would
        // land a different history than the one on screen.
        assert_eq!(chosen("3,1", 4).unwrap(), vec![3, 1]);
        // Spaces are what a person types after a comma.
        assert_eq!(chosen(" 1, 3 - 4 ", 4).unwrap(), vec![1, 3, 4]);
    }

    /// Everything that is not a selection is refused, before anything moves.
    ///
    /// Each of these is a plausible thing to type, and each would otherwise
    /// resolve to *something* — an empty rebase, a commit picked twice, a
    /// number that means a different commit than the one on screen.
    #[test]
    fn a_selection_that_cannot_mean_what_it_says_is_refused() {
        let refused = |spec: &str, because: &str| {
            let err = chosen(spec, 4)
                .map(|got| format!("{got:?}"))
                .expect_err(&format!("`{spec}` is not a selection: {because}"));
            err.to_string()
        };

        assert!(
            refused("", "nothing was named").contains("no checkpoints"),
            "an empty selection is a question, not everything"
        );
        assert!(refused("0", "the numbers start at 1").contains('0'));
        assert!(
            refused("9", "there are four").contains("1 to 4"),
            "the refusal names the range the list actually has"
        );
        assert!(refused("2-9", "the range runs past the end").contains("1 to 4"));
        assert!(refused("two", "not a number").contains("two"));
        assert!(refused("1,,2", "an empty element").contains("1,,2"));
        assert!(
            refused("4-2", "backwards").contains("4-2"),
            "a descending range is ambiguous — reversing it is a guess"
        );
        assert!(
            refused("1,1", "twice").contains('1'),
            "a commit picked twice applies twice, which is not what the list showed"
        );
        assert!(refused("-", "no numbers at all").contains('-'));
    }

    /// A selection is checked against the session's own list, not against
    /// arithmetic.
    #[test]
    fn a_selection_is_bounded_by_what_the_session_has() {
        assert!(chosen("1", 1).is_ok());
        assert!(
            chosen("2", 1).is_err(),
            "one checkpoint means one valid number"
        );
        assert!(
            chosen("1", 0).is_err(),
            "and an empty sandbox has none at all"
        );
    }

    /// The isolation the sandbox is *for*, asserted as an invariant rather than
    /// a mount list: whatever else the shadow gains, the checkout's commits are
    /// never reachable from it. A shadow seeded by cloning, or by pointing at
    /// the real object store to save disk, would pass every other test here and
    /// hand the agent every branch you have.
    #[test]
    fn the_shadow_holds_no_commit_from_the_checkout() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let host_commit = head_of(&checkout);
        let reachable = Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args(["cat-file", "-e", &host_commit])
            .output()
            .unwrap();
        assert!(
            !reachable.status.success(),
            "the host's history must not be reachable from the sandbox's repo"
        );

        let count = git(&s.gitdir, &wt, &["rev-list", "--all", "--count"]).unwrap();
        assert_eq!(count.trim(), "1", "the seed, and nothing else");
        let remotes = git(&s.gitdir, &wt, &["remote"]).unwrap();
        assert!(remotes.trim().is_empty(), "nowhere to push: {remotes:?}");
    }

    /// The seed is the only fixed point a harvest can replay from, and the
    /// agent can write everywhere the container can reach. Recorded as a tag it
    /// went away with one `git tag -d`; recorded in the gitdir's config the
    /// agent rewrites it. So it lives on the host, outside the mount.
    #[test]
    fn the_seed_is_recorded_where_the_sandbox_cannot_reach_it() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let seed = std::fs::read_to_string(&s.seed_record).unwrap();
        assert!(!seed.is_empty(), "a seed has to be recorded");
        assert!(
            !s.seed_record.starts_with(&s.gitdir),
            "the record must not sit inside the directory the agent can write"
        );

        // the agent does its worst inside its own repo
        let _ = git(&s.gitdir, &wt, &["tag", "-d", "session-start"]);
        assert_eq!(
            std::fs::read_to_string(&s.seed_record).unwrap(),
            seed,
            "the seed survives the sandbox"
        );
    }

    /// `carry_in` puts files the repo does not track into the worktree — the
    /// user's `.env` among them. Left visible, the agent's first `git status`
    /// offers to add a secret, and `git add -A` takes it.
    #[test]
    fn a_carried_file_is_not_something_the_shadow_tracks() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[".env".to_string()]).unwrap();

        let tracked = git(&s.gitdir, &wt, &["ls-files"]).unwrap();
        assert!(
            !tracked.contains(".env"),
            "carried, not committed: {tracked}"
        );
        let status = git(&s.gitdir, &wt, &["status", "--porcelain"]).unwrap();
        assert!(
            status.trim().is_empty(),
            "a session opens on a clean tree or the agent starts by tidying: {status:?}"
        );
    }

    /// The one thing that stays walled. Everything else about this repo is
    /// meant to work, so the agent has no standing reason to think a push
    /// would — and `git push` reaching a real remote is the one mistake that
    /// leaves the machine.
    ///
    /// git's own hook rather than a shell pattern over the command line: git
    /// knows what a push is, and the pattern omh used to match `git` at all
    /// shipped broken once already by missing multi-line scripts.
    #[test]
    fn the_shadow_refuses_to_push() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let hook = s.gitdir.join("hooks/pre-push");
        assert!(hook.exists(), "a pre-push hook has to be installed");

        let out = Command::new("sh").arg(&hook).output().unwrap();
        assert!(!out.status.success(), "the hook must refuse");

        // Asserting the *message*, not the exit code. A non-zero exit is what a
        // shell syntax error gives you too — and that is exactly what this
        // shipped: `NO_PUSH` contains an apostrophe, the script wrapped it in
        // single quotes, and the hook refused only by failing to parse. The
        // agent got `unexpected EOF while looking for matching \`'\`` and no
        // mention of omh at all, while a test asserting failure stayed green.
        let said = String::from_utf8_lossy(&out.stderr);
        assert!(
            said.contains("omh s commit"),
            "a refusal has to say where work actually leaves: {said}"
        );
        assert!(
            !said.contains("syntax error") && !said.contains("unexpected"),
            "the hook must run, not merely fail to parse: {said}"
        );

        // git skips a hook it cannot execute, with a *hint* and exit 0 — the
        // push then succeeds. A mode this test does not check is a wall that is
        // not there.
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            let mode = std::fs::metadata(&hook).unwrap().permissions().mode();
            assert_eq!(mode & 0o111, 0o111, "git ignores a non-executable hook");
        }
    }

    /// Every other test here reaches the repository the way the *host* does,
    /// through the private `git()` helper, which passes `--work-tree`. The
    /// container never does that: it finds the repository through the `.git`
    /// pointer omh mounts onto `/work`, and takes the worktree from wherever
    /// that pointer sits.
    ///
    /// The difference is not academic. A `core.worktree` written on the host
    /// records a host path, outranks the pointer's own directory, and made
    /// every git command in the sandbox fail with `fatal: Invalid path` — while
    /// the whole suite stayed green, because `--work-tree` overrode the bad
    /// value on every call a test made. This resolves with no `--work-tree` at
    /// all, which is the only way that class of mistake is visible from here.
    #[test]
    fn the_pointer_file_alone_resolves_to_the_worktree_it_sits_in() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // the container's view: discovery through the pointer, nothing else
        std::fs::write(wt.join(".git"), format!("gitdir: {}\n", s.gitdir.display())).unwrap();
        let out = Command::new("git")
            .current_dir(&wt)
            .args(["rev-parse", "--show-toplevel"])
            .output()
            .unwrap();

        assert!(
            out.status.success(),
            "git must work through the pointer alone: {}",
            String::from_utf8_lossy(&out.stderr)
        );
        assert_eq!(
            String::from_utf8_lossy(&out.stdout).trim(),
            std::fs::canonicalize(&wt).unwrap().to_string_lossy(),
            "the worktree is where the pointer sits, not somewhere recorded on the host"
        );
    }

    /// `ensure` makes a repository out of seven subprocess calls, and a machine
    /// that dies between the first and the last leaves a directory that looks
    /// exactly like a finished one. Read as "seeded" because it exists, that
    /// shadow opens the session on a repository with no seed and no exclude
    /// list — the agent's first `git status` offers it the carried `.env`.
    ///
    /// So the directory only appears once it is complete, and a leftover from
    /// an attempt that did not get there is not mistaken for the real thing.
    #[test]
    fn a_half_built_shadow_is_never_mistaken_for_a_finished_one() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");

        // what a launch killed partway through leaves behind
        Command::new("git")
            .args(["init", "-q", "--bare"])
            .arg(&s.gitdir)
            .output()
            .unwrap();
        assert!(!s.seed_record.exists(), "it never got as far as the seed");

        s.ensure(&wt, &[".env".to_string()]).unwrap();

        assert!(s.seed_record.exists(), "the seed has to be recorded");
        assert_eq!(
            git(&s.gitdir, &wt, &["rev-list", "--all", "--count"])
                .unwrap()
                .trim(),
            "1",
            "a finished shadow has its seed commit"
        );
        let status = git(&s.gitdir, &wt, &["status", "--porcelain"]).unwrap();
        assert!(!status.contains(".env"), "and its exclude list: {status:?}");
    }

    /// A hook the *agent* plants in its own gitdir must not run when omh
    /// touches that gitdir from the host.
    ///
    /// Nothing host-side reads an existing shadow today, so this cannot fire
    /// yet — which is exactly why it is worth pinning now. The harvest
    /// `Session::remove` already promises is a host-side reader of commits the
    /// agent wrote, and it will be written by someone reading a doc comment.
    #[test]
    fn a_hook_the_sandbox_plants_does_not_run_on_the_host() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // what an agent with a writable gitdir can arrange
        let planted = shadow_dir.join("planted");
        let hooks = shadow_dir.join("evil-hooks");
        std::fs::create_dir_all(&hooks).unwrap();
        let hook = hooks.join("post-checkout");
        std::fs::write(&hook, format!("#!/bin/sh\ntouch {}\n", planted.display())).unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&hook, std::fs::Permissions::from_mode(0o755)).unwrap();
        }
        // written the way the agent would: into the repository's own config
        Command::new("git")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .args(["config", "core.hooksPath"])
            .arg(&hooks)
            .output()
            .unwrap();

        let _ = git(&s.gitdir, &wt, &["checkout", "-q", "--", "."]);

        assert!(
            !planted.exists(),
            "the sandbox's own config decided what ran on the host"
        );
    }

    /// "One string, two deliveries" is the reason `ARRANGEMENT` exists, and
    /// only one of the two was pinned. The rules side is asserted three times
    /// over; the seed commit's side was asserted nowhere, and replacing
    /// `seed_message()` with a bare "The session starts here." left the whole
    /// suite green.
    ///
    /// This is the delivery the module doc calls the one a rules section
    /// "cannot reach" — it arrives on `git log`, at the moment the agent is
    /// working out what this repository is — so losing it silently is losing
    /// the argument for the refactor.
    #[test]
    fn the_seed_commit_carries_the_arrangement_to_git_log() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let said = git(&s.gitdir, &wt, &["log", "-1", "--format=%B"]).unwrap();
        assert!(
            said.contains(ARRANGEMENT),
            "the agent reads this on `git log` and nowhere else: {said}"
        );
    }

    /// The whole point: the agent's commits reach the branch with the messages
    /// it wrote, and authored as the sandbox rather than as whoever the sandbox
    /// claimed to be.
    #[test]
    fn a_harvest_lands_the_agents_commits_under_the_sandboxs_name() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // the agent works, and claims to be someone it is not
        git(
            &s.gitdir,
            &wt,
            &["config", "user.name", "Nathanael Cherrier"],
        )
        .unwrap();
        git(
            &s.gitdir,
            &wt,
            &["config", "user.email", "nathanael@mindsers.it"],
        )
        .unwrap();
        std::fs::write(wt.join("f.txt"), "one\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "Fix the tap guard"]).unwrap();
        std::fs::write(wt.join("helper.rs"), "fn h() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-qm", "Extract helper"]).unwrap();
        std::fs::write(wt.join("tail.rs"), "fn t() {}").unwrap(); // never checkpointed

        let landed = s
            .harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
            .unwrap();

        let log = git_in(&checkout, &["log", "--format=%an|%s", "main..omh/s01"]).unwrap();
        let lines: Vec<&str> = log.lines().collect();
        assert_eq!(landed, lines.len(), "reported count is what landed: {log}");
        assert!(
            lines.iter().any(|l| l.ends_with("|Extract helper")),
            "the agent's own messages are the point: {log}"
        );
        assert!(
            lines.iter().all(|l| l.starts_with(AUTHOR_NAME)),
            "the sandbox says who it is on the way in, whatever it claimed: {log}"
        );
        assert!(
            git_in(&checkout, &["show", "omh/s01:tail.rs"]).is_ok(),
            "work the agent never checkpointed is still its work"
        );
        assert!(
            git_in(
                &checkout,
                &[
                    "rev-parse",
                    "--verify",
                    "-q",
                    "refs/omh/s01-scratch/harvest"
                ]
            )
            .is_err(),
            "the fetched ref goes once the branch has the work, or the \
             pre-curation objects stay reachable in the user's repository"
        );
    }

    /// A carried secret must not reach the branch, and a path check alone
    /// closes one of the ways it gets there. Measured before this existed: a
    /// path strip caught `git add -f .env` and let through a copy under another
    /// name and a value pasted into source.
    ///
    /// Refused rather than stripped. Removing the path leaves an empty commit
    /// with a misleading message and does nothing about the other shapes, and
    /// rewriting the agent's work to hide a secret is the user's call.
    #[test]
    fn a_harvest_refuses_a_commit_holding_something_you_carried_in() {
        for (name, plant) in [
            ("force-added", "add-f"),
            ("copied under another name", "copy"),
            ("pasted into source", "inline"),
            ("written into a commit message", "message"),
        ] {
            let (d, wt, shadow_dir) = fixture();
            let checkout = d.path().join("checkout");
            let s = Shadow::new(&shadow_dir, "s01");
            s.ensure(&wt, &[".env".to_string()]).unwrap();
            // the checkout is where omh reads the bytes it carried
            std::fs::write(checkout.join(".env"), "API_TOKEN=ghp_abc123def456\n").unwrap();
            std::fs::write(wt.join(".env"), "API_TOKEN=ghp_abc123def456\n").unwrap();

            match plant {
                "add-f" => {
                    git(&s.gitdir, &wt, &["add", "-f", ".env"]).unwrap();
                }
                "copy" => {
                    std::fs::copy(wt.join(".env"), wt.join("config.bak")).unwrap();
                    git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
                }
                "inline" => {
                    std::fs::write(wt.join("k.rs"), "const K = \"API_TOKEN=ghp_abc123def456\";")
                        .unwrap();
                    git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
                }
                // The door `-S` cannot see: a pickaxe reads diff content and
                // never the message. Measured — it finds nothing here.
                _ => {
                    std::fs::write(wt.join("note.rs"), "fn n() {}").unwrap();
                    git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
                }
            }
            let subject = if plant == "message" {
                "note: API_TOKEN=ghp_abc123def456"
            } else {
                "Save config"
            };
            git(&s.gitdir, &wt, &["commit", "-qm", subject]).unwrap();

            let err = s
                .harvest(&checkout, &wt, "omh/s01", &[".env".to_string()], Keep::All)
                .unwrap_err()
                .to_string();
            assert!(
                err.contains("carried"),
                "{name}: a harvest must refuse it, not carry it: {err}"
            );
            assert!(
                git_in(&checkout, &["log", "--oneline", "main..omh/s01"])
                    .unwrap()
                    .trim()
                    .is_empty(),
                "{name}: and the branch must be untouched"
            );
        }
    }

    /// A secret is matched as the bytes it is, not as a pattern.
    ///
    /// The needle carries a `*` deliberately, and the choice is the whole
    /// worth of the test. `--grep` reads a pattern in whatever language
    /// `grep.patternType` names, and `*` is a quantifier in all three of them,
    /// so this is red on any machine. A `+` would not be: it is literal under
    /// `basic`, which is the default, and only bites the people who set
    /// `extended` or `perl` — this test was written with one and passed for
    /// the author's dotfiles rather than for git.
    ///
    /// Planted in the **message only**, because that is the one door `-S`
    /// cannot see: the pickaxe is already a fixed string and would have caught
    /// it in a diff, and the test would then have proved nothing about the
    /// path it exists for.
    #[test]
    fn a_secret_that_looks_like_a_pattern_is_still_caught() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[".env".to_string()]).unwrap();

        // `*` and `/` are ordinary base64. As a pattern this matches `KEY=acd…`
        // and `KEY=abbcd…` — never the literal bytes the agent wrote.
        let secret = "KEY=ab*cd/ef12345==";
        std::fs::write(checkout.join(".env"), format!("{secret}\n")).unwrap();
        std::fs::write(wt.join(".env"), format!("{secret}\n")).unwrap();

        std::fs::write(wt.join("note.rs"), "fn n() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(
            &s.gitdir,
            &wt,
            &["commit", "-qm", &format!("ship with {secret}")],
        )
        .unwrap();

        let err = s
            .harvest(&checkout, &wt, "omh/s01", &[".env".to_string()], Keep::All)
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("carried"),
            "a secret that is its own regex still has to be refused: {err}"
        );
        assert!(
            git_in(&checkout, &["log", "--oneline", "main..omh/s01"])
                .unwrap()
                .trim()
                .is_empty(),
            "and the branch must be untouched"
        );
    }

    /// A carried line that is not valid regex must not break the harvest.
    ///
    /// The same defect from the other side, and this one takes the whole
    /// feature down rather than letting something through: an unbalanced `[` is
    /// a syntax error to `--grep`, so `git log` exits 128, the harvest fails,
    /// and `--keep` stays dead for that session until the file changes.
    ///
    /// Measured under every `grep.patternType` — `fatal: command line,
    /// 'SECRET=a[bc': brackets ([ ]) not balanced` under `basic` and
    /// `extended`, `missing terminating ] for character class` under `perl`.
    /// The wording moves with the setting; the exit code does not.
    #[test]
    fn a_carried_line_that_is_not_a_regex_does_not_break_the_harvest() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[".env".to_string()]).unwrap();

        let carried = "SECRET_TOKEN=a[bcdefgh";
        std::fs::write(checkout.join(".env"), format!("{carried}\n")).unwrap();
        std::fs::write(wt.join(".env"), format!("{carried}\n")).unwrap();

        std::fs::write(wt.join("work.rs"), "fn work() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-qm", "Ordinary work"]).unwrap();

        let landed = s
            .harvest(&checkout, &wt, "omh/s01", &[".env".to_string()], Keep::All)
            .expect("a carried line nobody committed is not a reason to refuse");
        assert_eq!(landed, 1, "the agent's commit still has to land");
    }

    /// Landing the same work twice is not landing it twice.
    ///
    /// The harvest replayed from the seed every time, and nothing recorded what
    /// had already been kept — so a second `--keep` offered commits that were
    /// on the branch already. Whether that duplicated them or died applying
    /// them depended on whether the patches still fitted; measured against git
    /// 2.55.0, an edit to a line a later commit also touched conflicts, and
    /// `--keep` reported nothing but `Could not apply`.
    ///
    /// Landing twice must be a no-op, and the branch must not move.
    #[test]
    fn harvesting_twice_keeps_nothing_the_second_time() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        std::fs::write(wt.join("f.txt"), "base\nadded\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "Add a line"]).unwrap();
        std::fs::write(wt.join("f.txt"), "base\nadded\nmore\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "Add another"]).unwrap();

        assert_eq!(
            s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
                .unwrap(),
            2
        );
        let tip = git_in(&checkout, &["rev-parse", "omh/s01"]).unwrap();

        assert_eq!(
            s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
                .unwrap(),
            0,
            "there is nothing new to keep, and saying so is the whole job"
        );
        assert_eq!(
            git_in(&checkout, &["rev-parse", "omh/s01"]).unwrap(),
            tip,
            "and a branch with nothing to add must not move"
        );
    }

    /// The second harvest takes what the first one did not.
    ///
    /// The loop the replay point exists for: work, keep, work again, keep
    /// again. Only the new commits land, in order, once each.
    ///
    /// **This is the shape of the loop, not the guard for it**, and that is
    /// measured rather than assumed: with the replay point neutralised it stays
    /// green. Replaying a round that is already on the branch gives git a patch
    /// whose id is upstream, and it drops it — so the incremental path repairs
    /// itself and says nothing.
    ///
    /// Editing the same line each round instead of appending was tried, on the
    /// theory that a stale context would make the replay conflict. It does not:
    /// the patch-id match happens first, and the test stays green that way too.
    /// The guard is `harvesting_twice_keeps_nothing_the_second_time`, where
    /// nothing is new and the replayed patch has nowhere to go.
    ///
    /// Recorded so the next person does not spend the same hour proving it.
    #[test]
    fn a_second_harvest_takes_only_what_is_new() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        std::fs::write(wt.join("f.txt"), "base\nfirst\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "The first round"]).unwrap();
        assert_eq!(
            s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
                .unwrap(),
            1
        );

        std::fs::write(wt.join("f.txt"), "base\nfirst\nsecond\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "The second round"]).unwrap();

        assert_eq!(
            s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
                .unwrap(),
            1,
            "only the round that has not landed yet"
        );
        let log = git_in(&checkout, &["log", "--format=%s", "main..omh/s01"]).unwrap();
        let subjects: Vec<&str> = log.lines().collect();
        assert_eq!(
            subjects,
            vec!["The second round", "The first round"],
            "each round lands once, in order: {log}"
        );
    }

    /// The refusal quotes the agent's subject line, so it may not carry escapes.
    ///
    /// `refuse_carried` names the commit it found — sha and subject, straight
    /// from `git log --oneline`. Measured: git quotes a *path* by default
    /// (`core.quotePath` renders an escape as a literal `\033`), and does not
    /// quote a **subject** at all, which arrives with its bytes intact.
    ///
    /// This is the message that says omh refused to publish a secret. A subject
    /// that can clear the line and print something else is a forged answer to
    /// the one question this whole guard exists to answer.
    #[test]
    fn a_refusal_cannot_be_repainted_by_the_subject_it_quotes() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[".env".to_string()]).unwrap();

        let secret = "API_TOKEN=ghp_abc123def456";
        std::fs::write(checkout.join(".env"), format!("{secret}\n")).unwrap();
        std::fs::write(wt.join(".env"), format!("{secret}\n")).unwrap();
        std::fs::write(wt.join("note.rs"), "fn n() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        // the secret is in the subject, which is the door `-S` cannot see — and
        // the subject also clears the line and writes its own answer
        git(
            &s.gitdir,
            &wt,
            &[
                "commit",
                "-qm",
                &format!(
                    "note: {secret}{}[2K\rcommitted to main, nothing carried",
                    '\u{1b}'
                ),
            ],
        )
        .unwrap();

        let err = s
            .harvest(&checkout, &wt, "omh/s01", &[".env".to_string()], Keep::All)
            .unwrap_err()
            .to_string();
        assert!(err.contains("carried"), "it still has to refuse: {err}");
        assert!(
            !err.contains('\u{1b}'),
            "and it may not hand the terminal to the subject it quotes: {err:?}"
        );
    }

    /// A converter the sandbox named does not run on the host.
    ///
    /// Both halves of a textconv driver are the agent's to write: the driver
    /// itself is a config key in a gitdir omh mounts read-write, and the
    /// `.gitattributes` naming it sits in `/work`. So a host-side read of the
    /// sandbox that produces a diff runs whatever the agent chose, as the user,
    /// outside the container.
    ///
    /// Measured against git 2.55.0, and not what the plan for this assumed:
    ///
    /// | read | textconv | external diff |
    /// |---|---|---|
    /// | `log -S` | **runs** | no |
    /// | `log -p`, `show` | **runs** | no |
    /// | `git diff` | **runs** | **runs** |
    /// | `log --grep` | no | no |
    ///
    /// So the live half is textconv, not `diff.external` — the log family will
    /// not run an external diff without `--ext-diff`, which nothing here
    /// passes. `--no-textconv` is what closes it, and it is a per-command flag
    /// rather than a config key, which is why the helpers add it by verb.
    #[test]
    fn a_converter_the_sandbox_named_does_not_run_on_the_host() {
        let (d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        let marker = d.path().join("THE-CONVERTER-RAN");
        let conv = d.path().join("conv.sh");
        std::fs::write(
            &conv,
            format!("#!/bin/sh\ntouch {}\ncat \"$1\"\n", marker.display()),
        )
        .unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&conv, std::fs::Permissions::from_mode(0o755)).unwrap();
        }

        // both halves are the agent's: the driver in its own config, the
        // attributes in its own worktree
        git(
            &s.gitdir,
            &wt,
            &["config", "diff.pwn.textconv", conv.to_str().unwrap()],
        )
        .unwrap();
        std::fs::write(wt.join(".gitattributes"), "* diff=pwn\n").unwrap();
        std::fs::write(wt.join("f.txt"), "SECRET=abc123def456\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-qm", "a checkpoint"]).unwrap();

        // omh reads the sandbox, on the host, the way a `log` or `show` would
        let _ = git(
            &s.gitdir,
            &wt,
            &["log", "--oneline", "-S", "SECRET=abc123def456", "HEAD"],
        );

        assert!(
            !marker.exists(),
            "a driver the sandbox named ran on the host, as the user"
        );
    }

    /// A key with two values is one key, and dropping it is one job.
    ///
    /// `config --list --name-only` prints a key once **per value**, so a
    /// multi-valued key arrives twice — and `--unset-all` removes every value
    /// on the first call, leaving the second to exit 5 with empty stderr. Read
    /// as a failure that aborts the launch, which is what it did: a shadow from
    /// before the config was mounted read-only, where an agent had ever run
    /// `git config --add` twice, could not be relaunched at all. The error
    /// named a key and said nothing else.
    ///
    /// The module's own rule, from the fetch a few hundred lines up: following
    /// omh's instructions must not brick omh. Neither may upgrading omh.
    #[test]
    fn a_key_with_two_values_does_not_brick_the_next_launch() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // what a shadow from before the read-only mount can hold
        git(&s.gitdir, &wt, &["config", "--add", "core.gitproxy", "one"]).unwrap();
        git(&s.gitdir, &wt, &["config", "--add", "core.gitproxy", "two"]).unwrap();
        assert_eq!(
            git(
                &s.gitdir,
                &wt,
                &["config", "--list", "--local", "--name-only"]
            )
            .unwrap()
            .lines()
            .filter(|k| k.trim() == "core.gitproxy")
            .count(),
            2,
            "the precondition is that git lists the key once per value"
        );

        s.ensure(&wt, &[])
            .expect("a key with two values is still just a key to drop");

        let config = std::fs::read_to_string(s.gitdir.join("config")).unwrap();
        assert!(!config.contains("gitproxy"), "and it is gone:\n{config}");
    }

    /// Refreshing keeps what git records about the repository itself.
    ///
    /// The allowlist cannot be a list of keys someone remembered: `git init`
    /// writes what it detected about *this* filesystem, and that differs by
    /// platform — `core.ignorecase` and `core.precomposeunicode` on macOS,
    /// neither on a case-sensitive Linux box, `extensions.objectformat` in a
    /// sha256 repository. Drop one and the repository git reads is not the one
    /// git made: paths that differ only in case become two files, accented
    /// filenames flip between NFC and NFD and read as modified.
    ///
    /// So the guard is measured against a fresh `git init` on the machine
    /// running it, rather than against a list in this file. A git that starts
    /// recording something new turns this red on the platform where it matters.
    #[test]
    fn refreshing_keeps_what_git_records_about_the_repository() {
        let (d, wt, shadow_dir) = fixture();
        let pristine = d.path().join("pristine.git");
        Command::new("git")
            .args(["init", "-q", "--bare", "--template="])
            .arg(&pristine)
            .output()
            .unwrap();
        let listed = Command::new("git")
            .arg("--git-dir")
            .arg(&pristine)
            .args(["config", "--list", "--local", "--name-only"])
            .output()
            .unwrap();
        let expected: Vec<String> = String::from_utf8_lossy(&listed.stdout)
            .lines()
            .map(str::trim)
            .filter(|k| !k.is_empty() && *k != "core.bare")
            .map(str::to_string)
            .collect();
        assert!(
            !expected.is_empty(),
            "a fresh `git init` records something, or this test is vacuous"
        );

        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        s.ensure(&wt, &[]).unwrap();

        let kept = git(
            &s.gitdir,
            &wt,
            &["config", "--list", "--local", "--name-only"],
        )
        .unwrap();
        for key in expected {
            assert!(
                kept.lines().any(|k| k.trim() == key),
                "{key} is something git recorded about this repository and omh \
                 dropped it. kept:\n{kept}"
            );
        }
    }

    /// The sandbox's config is omh's again at every launch.
    ///
    /// The gitdir is mounted read-write because the agent commits through it,
    /// so every key in it is the agent's to set — and several of them turn
    /// `git` into *run whatever this repository says*. `NEUTRALISED` answers
    /// that for the calls omh makes, but only for the ones that remember to,
    /// and only host-side: inside the container the agent reads its own config
    /// with nothing filtering it.
    ///
    /// So the file becomes omh's own view again on every launch, and what the
    /// agent set in between does not survive. Not because a relaunch is a
    /// boundary — it is not, the agent can set it again a second later — but
    /// because a key that persists is one omh will still be reading a week
    /// later, long after whatever wrote it.
    #[test]
    fn the_sandboxs_config_is_omhs_again_at_every_launch() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // the agent makes its repository into one that runs things
        for (key, value) in [
            ("diff.pwn.textconv", "/bin/sh"),
            ("core.sshCommand", "/bin/sh"),
            ("core.hooksPath", "/tmp/mine"),
        ] {
            git(&s.gitdir, &wt, &["config", key, value]).unwrap();
        }
        std::fs::write(wt.join("agent.rs"), "fn main() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-qm", "a checkpoint"]).unwrap();
        let checkpoint = git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap();

        s.ensure(&wt, &[]).unwrap();

        let config = std::fs::read_to_string(s.gitdir.join("config")).unwrap();
        for key in ["textconv", "sshCommand", "hooksPath"] {
            assert!(
                !config.contains(key),
                "{key} survived a relaunch:\n{config}"
            );
        }
        assert_eq!(
            git(&s.gitdir, &wt, &["config", "user.email"])
                .unwrap()
                .trim(),
            AUTHOR_EMAIL,
            "and the identity omh needs to commit at all is still there"
        );
        assert_eq!(
            git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap(),
            checkpoint,
            "and the agent's work is untouched"
        );
    }

    /// A record omh cannot read is not a session that never landed.
    ///
    /// The two failures this covers arrive by different routes and meet in the
    /// same place. **Unreadable** is a permissions or I/O fault on a record that
    /// exists. **Empty** is what an interrupted write leaves: `fs::write`
    /// truncates before it writes, so a process killed in that window leaves
    /// zero bytes where a commit id was — the very window this file documents a
    /// few dozen lines above, in the code that does the writing.
    ///
    /// Either one read as *never harvested* replays from the seed and skips the
    /// ancestry check with it, offering the branch everything it already has.
    /// That is the defect the record exists to close, so neither may spell the
    /// same as absent.
    #[test]
    fn a_record_omh_cannot_read_is_not_a_session_that_never_landed() {
        for (name, break_it) in [
            ("empty", 0usize),
            #[cfg(unix)]
            ("unreadable", 1usize),
        ] {
            let (d, wt, shadow_dir) = fixture();
            let checkout = d.path().join("checkout");
            let s = Shadow::new(&shadow_dir, "s01");
            s.ensure(&wt, &[]).unwrap();
            std::fs::write(wt.join("f.txt"), "base\nfirst\n").unwrap();
            git(&s.gitdir, &wt, &["commit", "-qam", "The first round"]).unwrap();
            s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
                .unwrap();
            let tip = git_in(&checkout, &["rev-parse", "omh/s01"]).unwrap();

            match break_it {
                0 => std::fs::write(&s.landed_record, "").unwrap(),
                _ => {
                    #[cfg(unix)]
                    {
                        use std::os::unix::fs::PermissionsExt;
                        std::fs::set_permissions(
                            &s.landed_record,
                            std::fs::Permissions::from_mode(0o000),
                        )
                        .unwrap();
                    }
                }
            }

            std::fs::write(wt.join("f.txt"), "base\nfirst\nsecond\n").unwrap();
            git(&s.gitdir, &wt, &["commit", "-qam", "The second round"]).unwrap();

            let outcome = s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All);
            assert!(
                outcome.is_err(),
                "{name}: a record omh cannot read must not read as a session that \
                 never landed"
            );
            assert_eq!(
                git_in(&checkout, &["rev-parse", "omh/s01"]).unwrap(),
                tip,
                "{name}: and the branch must not move on a refusal"
            );

            #[cfg(unix)]
            if break_it == 1 {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(
                    &s.landed_record,
                    std::fs::Permissions::from_mode(0o644),
                );
            }
        }
    }

    /// Removing a session takes its replay point with it.
    ///
    /// Ids come back around — `next_id` is the highest `sNN` plus one — so a
    /// record left behind is inherited by a session that has nothing to do with
    /// it, and says a branch has already been handed commits it has never seen.
    /// The seed goes for this reason and this goes with it.
    #[test]
    fn reaping_takes_the_replay_point_with_it() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        std::fs::write(wt.join("f.txt"), "base\nwork\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "Some work"]).unwrap();
        s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
            .unwrap();
        assert!(
            s.landed().unwrap().is_some(),
            "the precondition is that a harvest recorded one"
        );

        s.reap();

        assert!(
            Shadow::new(&shadow_dir, "s01").landed().unwrap().is_none(),
            "the next session to take this id must start from its own seed"
        );
    }

    /// A sandbox that rewound past what you kept is refused, not replayed.
    ///
    /// `git reset --hard` is one of the four commands this repository exists to
    /// give back, so an agent dropping a checkpoint below the point omh already
    /// harvested is ordinary. What is not ordinary is what a harvest could do
    /// about it: the record names a commit the history no longer reaches, and
    /// replaying from the seed instead would offer the branch work it already
    /// has. Neither is omh's to choose, so it stops and names the way out.
    #[test]
    fn a_sandbox_that_rewound_past_what_you_kept_is_refused() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let start = git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap();

        std::fs::write(wt.join("f.txt"), "base\nkept\n").unwrap();
        git(&s.gitdir, &wt, &["commit", "-qam", "Work that gets kept"]).unwrap();
        s.harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
            .unwrap();

        // the agent rewinds behind what omh already took
        git(&s.gitdir, &wt, &["reset", "-q", "--hard", start.trim()]).unwrap();
        std::fs::write(wt.join("g.txt"), "different\n").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-qm", "A different direction"]).unwrap();

        let err = s
            .harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
            .unwrap_err()
            .to_string();
        assert!(
            err.contains("omh s commit -m"),
            "a refusal has to say what to do instead: {err}"
        );
    }

    /// A carried file must never be **tracked** in the seed, and the reason is
    /// not the one you would guess.
    ///
    /// Tracking it looks like a straight improvement: a tracked file survives
    /// `git clean -fdx` without needing a mount, and without the mount the
    /// agent can edit it with `sed -i` and `mv`, which a mountpoint refuses
    /// with `Device or resource busy`. Measured, all of that is true.
    ///
    /// What is also true, and settles it: a tracked file is in the tree of
    /// **every commit that follows it**, so any fetch that brings one agent
    /// commit brings the file. The harvest fetches this repository into the
    /// *user's own*, so a carried secret in the seed is copied into the real
    /// repository by every harvest that gets past `preflight` — measured,
    /// readable there with `git cat-file -p`.
    ///
    /// Stated that way on purpose. "A fetch takes every reachable object" is
    /// true and invites three rebuttals that all fail: `--depth=1` fetches one
    /// commit and still carries the blob, because it is in that commit's tree;
    /// there is no transport that fetches a range; and the seed cannot be
    /// excluded because it is the rebase base `replant` needs. `--filter=blob:none`
    /// is worse than none of them — the shadow sets no `uploadpack.allowFilter`,
    /// so git warns, sends the blob anyway, and leaves the user's repository a
    /// promisor pointing at a directory `omh s rm` deletes.
    ///
    /// On the success path it is unreachable afterwards, because the scratch
    /// ref is deleted, and "gc will get it eventually" is not a thing to say
    /// about somebody's credentials. On a *failure* path it is worse than that:
    /// the ref is kept deliberately — that is what makes a failed replant
    /// recoverable — so the secret would sit reachable from a live ref in the
    /// user's repository until someone noticed it. Every refusal after the
    /// fetch lands there: a carried file in a commit, a branch that moved, a
    /// replant that conflicted.
    ///
    /// So the mount stays and `sed -i` stays broken, and this guards the trade
    /// against being quietly reversed by someone fixing the visible half.
    #[test]
    fn a_carried_file_is_never_in_the_seed_the_harvest_fetches() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        std::fs::write(wt.join(".env"), "API_TOKEN=ghp_abc123def456\n").unwrap();
        std::fs::write(wt.join("cert.pem"), "BEGIN-ghp_abc123def456-END\n").unwrap();
        // Two, because a partially-effective exclusion passes a test that
        // carries one.
        s.ensure(&wt, &[".env".to_string(), "cert.pem".to_string()])
            .unwrap();

        let tracked = git(&s.gitdir, &wt, &["ls-tree", "-r", "--name-only", "HEAD"]).unwrap();
        // `ls-files`, not only `ls-tree`: staging the file without committing it
        // is the *other* way to make `git clean` spare it, and it leaves the
        // tree clean. `harvest` then runs `add -A .` and commits the index
        // before it fetches, so the index-only variant leaks too — through a
        // commit omh makes itself.
        let staged = git(&s.gitdir, &wt, &["ls-files"]).unwrap();
        for probe in [&tracked, &staged] {
            assert!(
                !probe.contains(".env") && !probe.contains("cert.pem"),
                "the seed must neither track nor stage a carried file: {probe}"
            );
        }

        // and the half that matters — what a harvest would carry across
        git_in(
            &checkout,
            &[
                "-c",
                "protocol.file.allow=always",
                "fetch",
                "-q",
                &s.gitdir.to_string_lossy(),
                "+HEAD:refs/omh/probe",
            ],
        )
        .unwrap();
        let objects = git_in(&checkout, &["rev-list", "--objects", "refs/omh/probe"]).unwrap();
        // Non-vacuity. An enumeration that returns nothing passes every
        // assertion below without looking at anything, and one typo in the
        // refspec is all that takes.
        assert!(
            !objects.trim().is_empty(),
            "the probe enumerated no objects, so it proved nothing"
        );
        let mut read_a_blob = false;
        for line in objects.lines() {
            let oid = line.split_whitespace().next().unwrap_or_default();
            // `unwrap`, not `unwrap_or_default`: an unreadable object would
            // otherwise pass this assertion as an empty string, which is the
            // vacuous pass this test exists to not be.
            let body = git_in(&checkout, &["cat-file", "-p", oid]).unwrap();
            read_a_blob |= body.contains("base");
            assert!(
                !body.contains("ghp_abc123def456"),
                "a fetch put the carried secret in the user's repository: {line}"
            );
        }
        assert!(
            read_a_blob,
            "the enumeration never read file content, so a leak in one would \
             not have been seen"
        );
    }

    /// A selection lands exactly those commits, in the order it named them.
    ///
    /// The order is the half that is easy to get wrong and impossible to see:
    /// a rebase that sorted the todo would land the same set and a different
    /// history, and every assertion about *which* commits arrived would still
    /// pass.
    #[test]
    fn a_selection_lands_those_commits_in_that_order() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        // Separate files, so reordering is a clean replay rather than a
        // conflict — what is under test is the selection, not merge.
        for m in ["one", "two", "three", "four"] {
            std::fs::write(wt.join(format!("{m}.rs")), format!("fn {m}() {{}}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qm", m]).unwrap();
        }
        let ids: Vec<String> = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .map(|c| c.id.clone())
            .collect();

        // `--keep 3,1` — the third checkpoint, then the first.
        let landed = s
            .harvest(
                &checkout,
                &wt,
                "omh/s01",
                &[],
                Keep::These(vec![ids[2].clone(), ids[0].clone()]),
            )
            .unwrap();

        let log = git_in(&checkout, &["log", "--format=%s", "main..omh/s01"]).unwrap();
        let on_branch: Vec<&str> = log.lines().collect();
        assert_eq!(
            on_branch,
            vec!["one", "three"],
            "newest first from `git log`, so `three` was applied first: {log}"
        );
        assert_eq!(landed, 2, "and the count is what arrived: {log}");
    }

    /// What a selection leaves out stays out.
    ///
    /// The commits omh did not name are still in the fetched range, and a
    /// rebase that ignored the todo would replay all four while every
    /// assertion about the two that *are* there kept passing.
    #[test]
    fn a_selection_leaves_the_rest_where_they_were() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        for m in ["one", "two", "three", "four"] {
            std::fs::write(wt.join(format!("{m}.rs")), format!("fn {m}() {{}}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qm", m]).unwrap();
        }
        let ids: Vec<String> = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .map(|c| c.id.clone())
            .collect();

        s.harvest(
            &checkout,
            &wt,
            "omh/s01",
            &[],
            Keep::These(vec![ids[0].clone(), ids[1].clone()]),
        )
        .unwrap();

        let log = git_in(&checkout, &["log", "--format=%s", "main..omh/s01"]).unwrap();
        assert_eq!(log.lines().count(), 2, "two of the four: {log}");
        assert!(
            !log.contains("three") && !log.contains("four"),
            "the ones not named are not on the branch: {log}"
        );
        // The assertion that used to stand here — that the sandbox still holds
        // `three` and `four` — was a tautology: nothing in `harvest` touches
        // the sandbox's own history, so no mutation could redden it. Worse,
        // its comment claimed it was what made a second `--keep` able to take
        // them, which was the one thing that was **not** true. The real guard
        // is the next test.
    }

    /// A selection takes exactly what it names, and does not sweep the
    /// uncommitted tail into a commit nobody asked for.
    ///
    /// `harvest` commits whatever the agent left behind before it does
    /// anything else, so `--keep` never drops the tail of a session. For a
    /// selection that sweep is a trap: the numbers were resolved *before* it
    /// ran, so the commit it makes is one the user could not have named. It
    /// would be created, left unapplied, and — before the replay point learned
    /// to stop at what was skipped — recorded as handed over. Work invented by
    /// the command that then abandoned it.
    #[test]
    fn a_selection_does_not_sweep_up_work_the_user_could_not_have_named() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        for m in ["one", "two"] {
            std::fs::write(wt.join(format!("{m}.rs")), format!("fn {m}() {{}}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qm", m]).unwrap();
        }
        let ids: Vec<String> = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .map(|c| c.id.clone())
            .collect();
        // …and then the agent keeps working, without committing.
        std::fs::write(wt.join("in-flight.rs"), "fn later() {}\n").unwrap();

        s.harvest(
            &checkout,
            &wt,
            "omh/s01",
            &[],
            Keep::These(vec![ids[0].clone()]),
        )
        .unwrap();

        let log = git(&s.gitdir, &wt, &["log", "--format=%s"]).unwrap();
        assert!(
            !log.contains("Work in progress"),
            "a selection made a commit the user never named: {log}"
        );
        let read = s.checkpoints(&wt).unwrap();
        assert_eq!(
            read.commits.len(),
            2,
            "still two checkpoints, not three: {read:?}"
        );
        assert!(
            read.uncommitted > 0,
            "and the tail is still where the next `--keep` can see it: {read:?}"
        );
    }

    /// A second `--keep` brings the rest, which a partial handover must not
    /// make impossible.
    ///
    /// This is the guard for the defect the record write used to carry:
    /// `harvest` recorded the fetched HEAD whatever was taken, so after
    /// `--keep 1,3` checkpoints 2 and 4 read as already handed over. `log`
    /// drew no divider, a second `--keep` said *nothing new to keep*, naming
    /// one refused it as already on the branch, and `omh sNN rm` then deleted
    /// the only copy — with every screen the user could check agreeing the
    /// work was safe.
    ///
    /// Two harvests, because one cannot see it. Every assertion about the
    /// first is satisfied by the broken version.
    #[test]
    fn a_second_keep_brings_what_the_first_selection_left() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        for m in ["one", "two", "three", "four"] {
            std::fs::write(wt.join(format!("{m}.rs")), format!("fn {m}() {{}}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qm", m]).unwrap();
        }
        let ids: Vec<String> = s
            .checkpoints(&wt)
            .unwrap()
            .commits
            .iter()
            .map(|c| c.id.clone())
            .collect();

        // `--keep 1,3` — skipping 2, which is what makes the replay point
        // unable to advance past it.
        s.harvest(
            &checkout,
            &wt,
            "omh/s01",
            &[],
            Keep::These(vec![ids[0].clone(), ids[2].clone()]),
        )
        .unwrap();
        let read = s.checkpoints(&wt).unwrap();
        assert!(
            read.commits.iter().filter(|c| c.landed).count() <= 1,
            "only what was taken and everything before it may read as handed over: {:?}",
            read.commits
        );

        // …and then the rest.
        let landed = s
            .harvest(&checkout, &wt, "omh/s01", &[], Keep::All)
            .unwrap();
        let log = git_in(&checkout, &["log", "--format=%s", "main..omh/s01"]).unwrap();
        for m in ["one", "two", "three", "four"] {
            assert!(
                log.contains(m),
                "`{m}` never reached the branch across two harvests: {log}"
            );
        }
        assert!(
            landed >= 2,
            "the second harvest brought the ones the first left: {log}"
        );
    }

    /// Curation is `--edit`'s headline behaviour, and this is the only test
    /// that executes it. It cannot be reached from `tests/cli.rs` by
    /// construction — `--edit` refuses without a terminal and no test process
    /// has one — so if this goes, the `-i` path has no coverage at all.
    ///
    /// It once read "every other test passes `curate: false` while `--keep`
    /// only ever passes `true`", which was true of the `bool` that `Keep`
    /// replaced in #56 and is worth keeping only as the reason the test
    /// exists: deleting the `-i` left the suite green.
    ///
    /// A sequence editor that *edits* the todo, not one that accepts it. An
    /// editor that exits without touching the list is behaviourally identical
    /// to `-q`, so it proves the branch was taken and nothing about what it
    /// does — this drops a line, which is the whole point of opening the list.
    ///
    /// It also pins the count. Reported from the commits *fetched*, "kept 3"
    /// printed over a branch that got 1.
    #[test]
    fn curating_drops_what_the_user_drops_and_says_so() {
        let (d, wt, shadow_dir) = fixture();
        let checkout = d.path().join("checkout");
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        // Separate files, so dropping the middle commit is a clean drop
        // rather than a conflict — the curation is what is under test here.
        for m in ["one", "two", "three"] {
            std::fs::write(wt.join(format!("{m}.rs")), format!("fn {m}() {{}}\n")).unwrap();
            git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qm", m]).unwrap();
        }

        // the user opens the list and deletes the middle pick
        let editor = d.path().join("drop-one.sh");
        std::fs::write(&editor, "#!/bin/sh\nsed -i.bak '2d' \"$1\"\n").unwrap();
        #[cfg(unix)]
        {
            use std::os::unix::fs::PermissionsExt;
            std::fs::set_permissions(&editor, std::fs::Permissions::from_mode(0o755)).unwrap();
        }
        git_in(
            &checkout,
            &["config", "sequence.editor", &editor.to_string_lossy()],
        )
        .unwrap();

        let landed = s
            .harvest(&checkout, &wt, "omh/s01", &[], Keep::Edit)
            .unwrap();

        let log = git_in(&checkout, &["log", "--format=%s", "main..omh/s01"]).unwrap();
        let on_branch = log.lines().count();
        assert_eq!(on_branch, 2, "one of the three was dropped: {log}");
        assert_eq!(
            landed, on_branch,
            "the number reported is what landed, not what was offered: {log}"
        );
    }

    /// Two of the three states that made a harvest report success while leaving
    /// commits behind — measured against the replant that ran without them.
    /// The third, an interrupted rebase, has its own test below.
    #[test]
    fn a_harvest_refuses_a_history_it_cannot_see_all_of() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let cp = |m: &str| {
            std::fs::write(wt.join("f.txt"), format!("{m}\n")).unwrap();
            git(&s.gitdir, &wt, &["commit", "-qam", m]).unwrap();
        };
        cp("first");
        cp("second");
        s.preflight(&wt).expect("a clean history harvests");

        // the agent looks at an old checkpoint and does not come back
        git(&s.gitdir, &wt, &["checkout", "-q", "HEAD~1"]).unwrap();
        let err = s.preflight(&wt).unwrap_err().to_string();
        assert!(err.contains("detached"), "{err}");
        assert!(err.contains(&s.branch), "say how to put it back: {err}");

        // a branch it made and wandered off
        git(&s.gitdir, &wt, &["checkout", "-q", &s.branch]).unwrap();
        git(&s.gitdir, &wt, &["branch", "aside", "HEAD"]).unwrap();
        git(&s.gitdir, &wt, &["reset", "-q", "--hard", "HEAD~1"]).unwrap();
        let err = s.preflight(&wt).unwrap_err().to_string();
        assert!(
            err.contains("no branch it is on can reach"),
            "stranded commits are the ones no other check sees: {err}"
        );
    }

    /// An interrupted rebase leaves HEAD on something that is neither the work
    /// nor a decision anyone made, and the marker is right there in the gitdir.
    #[test]
    fn a_harvest_refuses_a_repository_left_mid_rebase() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        std::fs::create_dir_all(s.gitdir.join("rebase-merge")).unwrap();

        let err = s.preflight(&wt).unwrap_err().to_string();
        assert!(
            err.contains("rebase-merge"),
            "name what is in progress: {err}"
        );
    }

    /// The seed is the fixed point a replant measures from, and it is recorded
    /// on the host precisely so the sandbox cannot move it.
    #[test]
    fn the_seed_survives_the_sandbox_deleting_everything_it_can_reach() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();
        let seed = s.seed().unwrap();

        let _ = git(&s.gitdir, &wt, &["tag", "-d", "session-start"]);
        let _ = std::fs::remove_dir_all(s.gitdir.join("refs/tags"));

        assert_eq!(s.seed().unwrap(), seed);
        assert!(!seed.is_empty());
    }

    /// Checkpointing is the feature, and a checkpoint is a commit, and git
    /// refuses to commit without an identity. The container carries no global
    /// git config, so unless the repository brings its own the agent's first
    /// `git commit` dies on `Author identity unknown` — the whole point of the
    /// module, unavailable, on a machine that has never been configured.
    ///
    /// Asserted as *the repository carries an identity*, not as "a commit
    /// works here", because a commit working here proves nothing. git invents
    /// an identity from the OS user and hostname when it can, so on a developer
    /// machine the commit succeeds with no config at all — deleting the two
    /// lines this guards left it green, while CI, whose runner has an empty
    /// gecos field, failed on `empty ident name`. A test that cannot go red on
    /// the machine you are writing it on is decoration.
    ///
    /// The commit is still made, because config that is set and not honoured
    /// would be its own bug.
    #[test]
    fn the_agent_can_commit_without_any_identity_of_its_own() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        // a file the seed already tracks, so `-a` stages it
        std::fs::write(wt.join("f.txt"), "base\nthe agent's work\n").unwrap();
        let out = Command::new("git")
            .current_dir(&wt)
            .env("GIT_CONFIG_GLOBAL", "/dev/null")
            .env("GIT_CONFIG_SYSTEM", "/dev/null")
            .arg("--git-dir")
            .arg(&s.gitdir)
            .arg("--work-tree")
            .arg(&wt)
            .args(["commit", "-q", "-am", "a checkpoint"])
            .output()
            .unwrap();

        assert!(
            out.status.success(),
            "a sandbox with no git identity must still be able to check point: {}{}",
            String::from_utf8_lossy(&out.stdout),
            String::from_utf8_lossy(&out.stderr)
        );

        // The assertion that can actually fail here.
        for (key, want) in [("user.name", AUTHOR_NAME), ("user.email", AUTHOR_EMAIL)] {
            let got = git(&s.gitdir, &wt, &["config", "--local", key]).unwrap_or_default();
            assert_eq!(
                got.trim(),
                want,
                "the repository has to bring its own {key}; nothing in a \
                 container supplies one"
            );
        }
    }

    /// The exclude list follows the mounts, and the mounts change under it.
    ///
    /// omh derives what the sandbox's repository must not track from the mounts
    /// it is about to make, and wrote that list once — when the repository was
    /// created. Switch a capability on afterwards and the mount it adds inside
    /// `/work` is a file the existing sandbox neither tracks nor excludes, so
    /// the agent's own `git add -A` commits omh's rendered document — MCP
    /// environment and all — into a history `omh s commit --keep` replays onto
    /// the branch.
    ///
    /// Asserted as the property that matters rather than as a line in a file:
    /// the document cannot be staged. A test that greps `info/exclude` passes
    /// for a list that git never reads.
    #[test]
    fn a_capability_added_later_is_still_kept_out_of_the_sandboxs_history() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[".env".to_string()]).unwrap();

        std::fs::write(wt.join("agent.rs"), "fn main() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A"]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-q", "-m", "a checkpoint"]).unwrap();
        let checkpoint = git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap();

        // the next launch mounts one more document inside /work
        let grown = [".env".to_string(), ".mcp.json".to_string()];
        s.ensure(&wt, &grown).unwrap();

        // what the mount would put there, credentials and all
        std::fs::write(wt.join(".mcp.json"), "{\"env\":{\"TOKEN\":\"sk-live-42\"}}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A", "."]).unwrap();
        let staged = git(&s.gitdir, &wt, &["diff", "--cached", "--name-only"]).unwrap();

        assert!(
            !staged.contains(".mcp.json"),
            "a document omh mounted is not the agent's work to commit: staged {staged:?}"
        );
        assert_eq!(
            git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap(),
            checkpoint,
            "and refreshing the list must not disturb what the agent already did"
        );
    }

    /// Relaunching into a running session is ordinary — `omh claude` twice, an
    /// editor attaching alongside a terminal. Re-seeding there would throw away
    /// every checkpoint the agent had made, which is the one thing this whole
    /// repository exists to keep.
    #[test]
    fn seeding_twice_keeps_the_work_the_agent_already_did() {
        let (_d, wt, shadow_dir) = fixture();
        let s = Shadow::new(&shadow_dir, "s01");
        s.ensure(&wt, &[]).unwrap();

        std::fs::write(wt.join("agent.rs"), "fn main() {}").unwrap();
        git(&s.gitdir, &wt, &["add", "-A"]).unwrap();
        git(&s.gitdir, &wt, &["commit", "-q", "-m", "a checkpoint"]).unwrap();
        let checkpoint = git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap();

        s.ensure(&wt, &[]).unwrap();

        assert_eq!(
            git(&s.gitdir, &wt, &["rev-parse", "HEAD"]).unwrap(),
            checkpoint,
            "a relaunch must not discard the agent's checkpoints"
        );
    }
}