csift 0.10.2

ripgrep for Claude Code session transcripts: fast regex list/search over ~/.claude/projects/**/*.jsonl
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
5645
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
5678
5679
5680
5681
5682
5683
5684
5685
5686
5687
5688
5689
5690
5691
5692
5693
5694
5695
5696
5697
5698
5699
5700
5701
5702
5703
5704
5705
5706
5707
5708
5709
5710
5711
5712
5713
5714
5715
5716
5717
5718
5719
5720
5721
5722
5723
5724
5725
5726
5727
5728
5729
5730
5731
5732
5733
5734
5735
5736
5737
5738
5739
5740
5741
5742
5743
5744
5745
5746
5747
5748
5749
5750
5751
5752
5753
5754
5755
5756
5757
5758
5759
5760
5761
5762
5763
5764
5765
5766
5767
5768
5769
5770
5771
5772
5773
5774
5775
5776
5777
5778
5779
5780
5781
5782
5783
5784
5785
5786
5787
5788
5789
5790
5791
5792
5793
5794
5795
5796
5797
5798
5799
5800
5801
5802
5803
5804
5805
5806
5807
5808
5809
5810
5811
5812
5813
5814
5815
5816
5817
5818
5819
5820
5821
5822
5823
5824
5825
5826
5827
5828
5829
5830
5831
5832
5833
5834
5835
5836
5837
5838
5839
5840
5841
5842
5843
5844
5845
5846
5847
5848
5849
5850
5851
5852
5853
5854
5855
5856
5857
5858
5859
5860
5861
5862
5863
5864
5865
5866
5867
5868
5869
5870
5871
5872
5873
5874
5875
5876
5877
5878
5879
5880
5881
5882
5883
5884
5885
5886
5887
5888
5889
5890
5891
5892
5893
5894
5895
5896
5897
5898
5899
5900
5901
5902
5903
5904
5905
5906
5907
5908
5909
5910
5911
5912
5913
5914
5915
5916
5917
5918
5919
5920
5921
5922
5923
5924
5925
5926
5927
5928
5929
5930
5931
5932
5933
5934
5935
5936
5937
5938
5939
5940
5941
5942
5943
5944
5945
5946
5947
5948
5949
5950
5951
5952
5953
5954
5955
5956
5957
5958
5959
5960
5961
5962
5963
5964
5965
5966
5967
5968
5969
5970
5971
5972
5973
5974
5975
5976
5977
5978
5979
5980
5981
5982
5983
5984
5985
5986
5987
5988
5989
5990
5991
5992
5993
5994
5995
5996
5997
5998
5999
6000
6001
6002
6003
6004
6005
6006
6007
6008
6009
6010
6011
6012
6013
6014
6015
6016
6017
6018
6019
6020
6021
6022
6023
6024
6025
6026
6027
6028
6029
6030
6031
6032
6033
6034
6035
6036
6037
6038
6039
6040
6041
6042
6043
6044
6045
6046
6047
6048
6049
6050
6051
6052
6053
6054
6055
6056
6057
6058
6059
6060
6061
6062
6063
6064
6065
6066
6067
6068
6069
6070
6071
6072
6073
6074
6075
6076
6077
6078
6079
6080
6081
6082
6083
6084
6085
6086
6087
6088
6089
6090
6091
6092
6093
6094
6095
6096
6097
6098
6099
6100
6101
6102
6103
6104
6105
6106
6107
6108
6109
6110
6111
6112
6113
6114
6115
6116
6117
6118
6119
6120
6121
6122
6123
6124
6125
6126
6127
6128
6129
6130
6131
6132
6133
6134
6135
6136
6137
6138
6139
6140
6141
6142
6143
6144
6145
6146
6147
6148
6149
6150
6151
6152
6153
6154
6155
6156
6157
6158
6159
6160
6161
6162
6163
6164
6165
6166
6167
6168
6169
6170
6171
6172
6173
6174
6175
6176
6177
6178
6179
6180
6181
6182
6183
6184
6185
6186
6187
6188
6189
6190
6191
6192
6193
6194
6195
6196
6197
6198
6199
6200
6201
6202
6203
6204
6205
6206
6207
6208
6209
6210
6211
6212
6213
6214
6215
6216
6217
6218
6219
6220
6221
6222
6223
6224
6225
6226
6227
6228
6229
6230
6231
6232
6233
6234
6235
6236
6237
6238
6239
6240
6241
6242
6243
6244
6245
6246
6247
6248
6249
6250
6251
6252
6253
6254
6255
6256
6257
6258
6259
6260
6261
6262
6263
6264
6265
6266
6267
6268
6269
6270
6271
6272
6273
6274
6275
6276
6277
6278
6279
6280
6281
6282
6283
6284
6285
6286
6287
6288
6289
6290
6291
6292
6293
6294
6295
6296
6297
6298
6299
6300
6301
6302
6303
6304
6305
6306
6307
6308
6309
6310
6311
6312
6313
6314
6315
6316
6317
6318
6319
6320
6321
6322
6323
6324
6325
6326
6327
6328
6329
6330
6331
6332
6333
6334
6335
6336
6337
6338
6339
6340
6341
6342
6343
6344
6345
6346
6347
6348
6349
6350
6351
6352
6353
6354
6355
6356
6357
6358
6359
6360
6361
6362
6363
6364
6365
6366
6367
6368
6369
6370
6371
6372
6373
6374
6375
6376
6377
6378
6379
6380
6381
6382
6383
6384
6385
6386
6387
6388
6389
6390
6391
6392
6393
6394
6395
6396
6397
6398
6399
6400
6401
6402
6403
6404
6405
6406
6407
6408
6409
6410
6411
6412
6413
6414
6415
6416
6417
6418
6419
6420
6421
6422
6423
6424
6425
6426
6427
6428
6429
6430
6431
6432
6433
6434
6435
6436
6437
6438
6439
6440
6441
6442
6443
6444
6445
6446
6447
6448
6449
6450
6451
6452
6453
6454
6455
6456
6457
6458
6459
6460
6461
6462
6463
6464
6465
6466
6467
6468
6469
6470
6471
6472
6473
6474
6475
6476
6477
6478
6479
6480
6481
6482
6483
6484
6485
6486
6487
6488
6489
6490
6491
6492
6493
6494
6495
6496
6497
6498
6499
6500
6501
6502
6503
6504
6505
6506
6507
6508
6509
6510
6511
6512
6513
6514
6515
6516
6517
6518
6519
6520
6521
6522
6523
6524
6525
6526
6527
6528
6529
6530
6531
6532
6533
6534
6535
6536
6537
6538
6539
6540
6541
6542
6543
6544
6545
6546
6547
6548
6549
6550
6551
6552
6553
6554
6555
6556
6557
6558
6559
6560
6561
6562
6563
6564
6565
6566
6567
6568
6569
6570
6571
6572
6573
6574
6575
6576
6577
6578
6579
6580
6581
6582
6583
6584
6585
6586
6587
6588
6589
6590
6591
6592
6593
6594
6595
6596
6597
6598
6599
6600
6601
6602
6603
6604
6605
6606
6607
6608
6609
6610
6611
6612
6613
6614
6615
6616
6617
6618
6619
6620
6621
6622
6623
6624
6625
6626
6627
6628
6629
6630
6631
6632
6633
6634
6635
6636
6637
6638
6639
6640
6641
6642
6643
6644
6645
6646
6647
6648
6649
6650
6651
6652
6653
6654
6655
6656
6657
6658
6659
6660
6661
6662
6663
6664
6665
6666
6667
6668
6669
6670
6671
6672
6673
6674
6675
6676
6677
6678
6679
6680
6681
6682
6683
6684
6685
6686
6687
6688
6689
6690
6691
6692
6693
6694
6695
6696
6697
6698
6699
6700
6701
6702
6703
6704
6705
6706
6707
6708
6709
6710
6711
6712
6713
6714
6715
6716
6717
6718
6719
6720
6721
6722
6723
6724
6725
6726
6727
6728
6729
6730
6731
6732
6733
6734
6735
6736
6737
6738
6739
6740
6741
6742
6743
6744
6745
6746
6747
6748
6749
6750
6751
6752
6753
6754
6755
6756
6757
6758
6759
6760
6761
6762
6763
6764
6765
6766
6767
6768
6769
6770
6771
6772
6773
6774
6775
6776
6777
6778
6779
6780
6781
6782
6783
6784
6785
6786
6787
6788
6789
6790
6791
6792
6793
6794
6795
6796
6797
6798
6799
6800
6801
6802
6803
6804
6805
6806
6807
6808
6809
6810
6811
6812
6813
6814
6815
6816
6817
6818
6819
6820
6821
6822
6823
6824
6825
6826
6827
6828
6829
6830
6831
6832
6833
6834
6835
6836
6837
6838
6839
6840
6841
6842
6843
6844
6845
6846
6847
6848
6849
6850
6851
6852
6853
6854
6855
6856
6857
6858
6859
6860
6861
6862
6863
6864
6865
6866
6867
6868
6869
6870
6871
6872
6873
6874
6875
6876
6877
6878
6879
6880
6881
6882
6883
6884
6885
6886
6887
6888
6889
6890
6891
6892
6893
6894
6895
6896
6897
6898
6899
6900
6901
6902
6903
6904
6905
6906
6907
6908
6909
6910
6911
6912
6913
6914
6915
6916
6917
6918
6919
6920
6921
6922
6923
6924
6925
6926
6927
6928
6929
6930
6931
6932
6933
6934
6935
6936
6937
6938
6939
6940
6941
6942
6943
6944
6945
6946
6947
6948
6949
6950
6951
6952
6953
6954
6955
6956
6957
6958
6959
6960
6961
6962
6963
6964
6965
6966
6967
6968
6969
6970
6971
6972
6973
6974
6975
6976
6977
6978
6979
6980
6981
6982
6983
6984
6985
6986
6987
6988
6989
6990
6991
6992
6993
6994
6995
6996
6997
6998
6999
7000
7001
7002
7003
7004
7005
7006
7007
7008
7009
7010
7011
7012
7013
7014
7015
7016
7017
7018
7019
7020
7021
7022
7023
7024
7025
7026
7027
7028
7029
7030
7031
7032
7033
7034
7035
7036
7037
7038
7039
7040
7041
7042
7043
7044
7045
7046
7047
7048
7049
7050
7051
7052
7053
7054
7055
7056
7057
7058
7059
7060
7061
7062
7063
7064
7065
7066
7067
7068
7069
7070
7071
7072
7073
7074
7075
7076
7077
7078
7079
7080
7081
7082
7083
7084
7085
7086
7087
7088
7089
7090
7091
7092
7093
7094
7095
7096
7097
7098
7099
7100
7101
7102
7103
7104
7105
7106
7107
7108
7109
7110
7111
7112
7113
7114
7115
7116
7117
7118
7119
7120
7121
7122
7123
7124
7125
7126
7127
7128
7129
7130
7131
7132
7133
7134
7135
7136
7137
7138
7139
7140
7141
7142
7143
7144
7145
7146
7147
7148
7149
7150
7151
7152
7153
7154
7155
7156
7157
7158
7159
7160
7161
7162
7163
7164
7165
7166
7167
7168
7169
7170
7171
7172
7173
7174
7175
7176
7177
7178
7179
7180
7181
7182
7183
7184
7185
7186
7187
7188
7189
7190
7191
7192
7193
7194
7195
7196
7197
7198
7199
7200
7201
7202
7203
7204
7205
7206
7207
7208
7209
7210
7211
7212
7213
7214
7215
7216
7217
7218
7219
7220
7221
7222
7223
7224
7225
7226
7227
7228
7229
7230
7231
7232
7233
7234
7235
7236
7237
7238
7239
7240
7241
7242
7243
7244
7245
7246
7247
7248
7249
7250
7251
7252
7253
7254
7255
7256
7257
7258
7259
7260
7261
7262
7263
7264
7265
7266
7267
7268
7269
7270
7271
7272
7273
7274
7275
7276
7277
7278
7279
7280
7281
7282
7283
7284
7285
7286
7287
7288
7289
7290
7291
7292
7293
7294
7295
7296
7297
7298
7299
7300
7301
7302
7303
7304
7305
7306
7307
7308
7309
7310
7311
7312
7313
7314
7315
7316
7317
7318
7319
7320
7321
7322
7323
7324
7325
7326
7327
7328
7329
7330
7331
7332
7333
7334
7335
7336
7337
7338
7339
7340
7341
7342
7343
7344
7345
7346
7347
7348
7349
7350
7351
7352
7353
7354
7355
7356
7357
7358
7359
7360
7361
7362
7363
7364
7365
7366
7367
7368
7369
7370
7371
7372
7373
7374
7375
7376
7377
7378
7379
7380
7381
7382
7383
7384
7385
7386
7387
7388
7389
7390
7391
7392
7393
7394
7395
7396
7397
7398
7399
7400
7401
7402
7403
7404
7405
7406
7407
7408
7409
7410
7411
7412
7413
7414
7415
7416
7417
7418
7419
7420
7421
7422
7423
7424
7425
7426
7427
7428
7429
7430
7431
7432
7433
7434
7435
7436
7437
7438
7439
7440
7441
7442
7443
7444
7445
7446
7447
7448
7449
7450
7451
7452
7453
7454
7455
7456
7457
7458
7459
7460
7461
7462
7463
7464
7465
7466
7467
7468
7469
7470
7471
7472
7473
7474
7475
7476
7477
7478
7479
7480
7481
7482
7483
7484
7485
7486
7487
7488
7489
7490
7491
7492
7493
7494
7495
7496
7497
7498
7499
7500
7501
7502
7503
7504
7505
7506
7507
7508
7509
7510
7511
7512
7513
7514
7515
7516
7517
7518
7519
7520
7521
7522
7523
7524
7525
7526
7527
7528
7529
7530
7531
7532
7533
7534
7535
7536
7537
7538
7539
7540
7541
7542
7543
7544
7545
7546
7547
7548
7549
7550
7551
7552
7553
7554
7555
7556
7557
7558
7559
7560
7561
7562
7563
7564
7565
7566
7567
7568
7569
7570
7571
7572
7573
7574
7575
7576
7577
7578
7579
7580
7581
7582
7583
7584
7585
7586
7587
7588
7589
7590
7591
7592
7593
7594
7595
7596
7597
7598
7599
7600
7601
7602
7603
7604
7605
7606
7607
7608
7609
7610
7611
7612
7613
7614
7615
7616
7617
7618
7619
7620
7621
7622
7623
7624
7625
7626
7627
7628
7629
7630
7631
7632
7633
7634
7635
7636
7637
7638
7639
7640
7641
7642
7643
7644
7645
7646
7647
7648
7649
7650
7651
7652
7653
7654
7655
7656
7657
7658
7659
7660
7661
7662
7663
7664
7665
7666
7667
7668
7669
7670
7671
7672
7673
7674
7675
7676
7677
7678
7679
7680
7681
7682
7683
7684
7685
7686
7687
7688
7689
7690
7691
7692
7693
7694
7695
7696
7697
7698
7699
7700
7701
7702
7703
7704
7705
7706
7707
7708
7709
7710
7711
7712
7713
7714
7715
7716
7717
7718
7719
7720
7721
7722
7723
7724
7725
7726
7727
7728
7729
7730
7731
7732
7733
7734
7735
7736
7737
7738
7739
7740
7741
7742
7743
7744
7745
7746
7747
7748
7749
7750
7751
7752
7753
7754
7755
7756
7757
7758
7759
7760
7761
7762
7763
7764
7765
7766
7767
7768
7769
7770
7771
7772
7773
7774
7775
7776
7777
7778
7779
7780
7781
7782
7783
7784
7785
7786
7787
7788
7789
7790
7791
7792
7793
7794
7795
7796
7797
7798
7799
7800
7801
7802
7803
7804
7805
7806
7807
7808
7809
7810
7811
7812
7813
7814
7815
7816
7817
7818
7819
7820
7821
7822
7823
7824
7825
7826
7827
7828
7829
7830
7831
7832
7833
7834
7835
7836
7837
7838
7839
7840
7841
7842
7843
7844
7845
7846
7847
7848
7849
7850
7851
7852
7853
7854
7855
7856
7857
7858
7859
7860
7861
7862
7863
7864
7865
7866
7867
7868
7869
7870
7871
7872
7873
7874
7875
7876
7877
7878
7879
7880
7881
7882
7883
7884
7885
7886
7887
7888
7889
7890
7891
7892
7893
7894
7895
7896
7897
7898
7899
7900
7901
7902
7903
7904
7905
7906
7907
7908
7909
7910
7911
7912
7913
7914
7915
7916
7917
7918
7919
7920
7921
7922
7923
7924
7925
7926
7927
7928
7929
7930
7931
7932
7933
7934
7935
7936
7937
7938
7939
7940
7941
7942
7943
7944
7945
7946
7947
7948
7949
7950
7951
7952
7953
7954
7955
7956
7957
7958
7959
7960
7961
7962
7963
7964
7965
7966
7967
7968
7969
7970
7971
7972
7973
7974
7975
7976
7977
7978
7979
7980
7981
7982
7983
7984
7985
7986
7987
7988
7989
7990
7991
7992
7993
7994
7995
7996
7997
7998
7999
8000
8001
8002
8003
8004
8005
8006
8007
8008
8009
8010
8011
8012
8013
8014
8015
8016
8017
8018
8019
8020
8021
8022
8023
8024
8025
8026
8027
8028
8029
8030
8031
8032
8033
8034
8035
8036
8037
8038
8039
8040
8041
8042
8043
8044
8045
8046
8047
8048
8049
8050
8051
8052
8053
8054
8055
8056
8057
8058
8059
8060
8061
8062
8063
8064
8065
8066
8067
8068
8069
8070
8071
8072
8073
8074
8075
8076
8077
8078
8079
8080
8081
8082
8083
8084
8085
8086
8087
8088
8089
8090
8091
8092
8093
8094
8095
8096
8097
8098
8099
8100
8101
8102
8103
8104
8105
8106
8107
8108
8109
8110
8111
8112
8113
8114
8115
8116
8117
8118
8119
8120
8121
8122
8123
8124
8125
8126
8127
8128
8129
8130
8131
8132
8133
8134
8135
8136
8137
8138
8139
8140
8141
8142
8143
8144
8145
8146
8147
8148
8149
8150
8151
8152
8153
8154
8155
8156
8157
8158
8159
8160
8161
8162
8163
8164
8165
8166
8167
8168
8169
8170
8171
8172
8173
8174
8175
8176
8177
8178
8179
8180
8181
8182
8183
8184
8185
8186
8187
8188
8189
8190
8191
8192
8193
8194
8195
8196
8197
8198
8199
8200
8201
8202
8203
8204
8205
8206
8207
8208
8209
8210
8211
8212
8213
8214
8215
8216
8217
8218
8219
8220
8221
8222
8223
8224
8225
8226
8227
8228
8229
8230
8231
8232
8233
8234
8235
8236
8237
8238
8239
8240
8241
8242
8243
8244
8245
8246
8247
8248
8249
8250
8251
8252
8253
8254
8255
8256
8257
8258
8259
8260
8261
8262
8263
8264
8265
8266
8267
8268
8269
8270
8271
8272
8273
8274
8275
8276
8277
8278
8279
8280
8281
8282
8283
8284
8285
8286
8287
8288
8289
8290
8291
8292
8293
8294
8295
8296
8297
8298
8299
8300
8301
8302
8303
8304
8305
8306
8307
8308
8309
8310
8311
8312
8313
8314
8315
8316
8317
8318
8319
8320
8321
8322
8323
8324
8325
8326
8327
8328
8329
8330
8331
8332
8333
8334
8335
8336
8337
8338
8339
8340
8341
8342
8343
8344
8345
8346
8347
8348
8349
8350
8351
8352
8353
8354
8355
8356
8357
8358
8359
8360
8361
8362
8363
8364
8365
8366
8367
8368
8369
8370
8371
8372
8373
8374
8375
8376
8377
8378
8379
8380
8381
8382
8383
8384
8385
8386
8387
8388
8389
8390
8391
8392
8393
8394
8395
8396
8397
8398
8399
8400
8401
8402
8403
8404
8405
8406
8407
8408
8409
8410
8411
8412
8413
8414
8415
8416
8417
8418
8419
8420
8421
8422
8423
8424
8425
8426
8427
8428
8429
8430
8431
8432
8433
8434
8435
8436
8437
8438
8439
8440
8441
8442
8443
8444
8445
8446
8447
8448
8449
8450
8451
8452
8453
8454
8455
8456
8457
8458
8459
8460
8461
8462
8463
8464
8465
8466
8467
8468
8469
8470
8471
8472
8473
8474
8475
8476
8477
8478
8479
8480
8481
8482
8483
8484
8485
8486
8487
8488
8489
8490
8491
8492
8493
8494
8495
8496
8497
8498
8499
8500
8501
8502
8503
8504
8505
8506
8507
8508
8509
8510
8511
8512
8513
8514
8515
8516
8517
8518
8519
8520
8521
8522
8523
8524
8525
8526
8527
8528
8529
8530
8531
8532
8533
8534
8535
8536
8537
8538
8539
8540
8541
8542
8543
8544
8545
8546
8547
8548
8549
8550
8551
8552
8553
8554
8555
8556
8557
8558
8559
8560
8561
8562
8563
8564
8565
8566
8567
8568
8569
8570
8571
8572
8573
8574
8575
8576
8577
8578
8579
8580
8581
8582
8583
8584
8585
8586
8587
8588
8589
8590
8591
8592
8593
8594
8595
8596
8597
8598
8599
8600
8601
8602
8603
8604
8605
8606
8607
8608
8609
8610
8611
8612
8613
8614
8615
8616
8617
8618
8619
8620
8621
8622
8623
8624
8625
8626
8627
8628
8629
8630
8631
8632
8633
8634
8635
8636
8637
8638
8639
8640
8641
8642
8643
8644
8645
8646
8647
8648
8649
8650
8651
8652
8653
8654
8655
8656
8657
8658
8659
8660
8661
8662
8663
8664
8665
8666
8667
8668
8669
8670
8671
8672
8673
8674
8675
8676
8677
8678
8679
8680
8681
8682
8683
8684
8685
8686
8687
8688
8689
8690
8691
8692
8693
8694
8695
8696
8697
8698
8699
8700
8701
8702
8703
8704
8705
8706
8707
8708
8709
8710
8711
8712
8713
8714
8715
8716
8717
8718
8719
8720
8721
8722
8723
8724
8725
8726
8727
8728
8729
8730
8731
8732
8733
8734
8735
8736
8737
8738
8739
8740
8741
8742
8743
8744
8745
8746
8747
8748
8749
8750
8751
8752
8753
8754
8755
8756
8757
8758
8759
8760
8761
8762
8763
8764
8765
8766
8767
8768
8769
8770
8771
8772
8773
8774
8775
8776
8777
8778
8779
8780
8781
8782
8783
8784
8785
8786
8787
8788
8789
8790
8791
8792
8793
8794
8795
8796
8797
8798
8799
8800
8801
8802
8803
8804
8805
8806
8807
8808
8809
8810
8811
8812
8813
8814
8815
8816
8817
8818
8819
8820
8821
8822
8823
8824
8825
8826
8827
8828
8829
8830
8831
8832
8833
8834
8835
8836
8837
8838
8839
8840
8841
8842
8843
8844
8845
8846
8847
8848
8849
8850
8851
8852
8853
8854
8855
8856
8857
8858
8859
8860
8861
8862
8863
8864
8865
8866
8867
8868
8869
8870
8871
8872
8873
8874
8875
8876
8877
8878
8879
8880
8881
8882
8883
8884
8885
8886
8887
8888
8889
8890
8891
8892
8893
8894
8895
8896
8897
8898
8899
8900
8901
8902
8903
8904
8905
8906
8907
8908
8909
8910
8911
8912
8913
8914
8915
8916
8917
8918
8919
8920
8921
8922
8923
8924
8925
8926
8927
8928
8929
8930
8931
8932
8933
8934
8935
8936
8937
8938
8939
8940
8941
8942
8943
8944
8945
8946
8947
8948
8949
8950
8951
8952
8953
8954
8955
8956
8957
8958
8959
8960
8961
8962
8963
8964
8965
8966
8967
8968
8969
8970
8971
8972
8973
8974
8975
8976
8977
8978
8979
8980
8981
8982
8983
8984
8985
8986
8987
8988
8989
8990
8991
8992
8993
8994
8995
8996
8997
8998
8999
9000
9001
9002
9003
9004
9005
9006
9007
9008
9009
9010
9011
9012
9013
9014
9015
9016
9017
9018
9019
9020
9021
9022
9023
9024
9025
9026
9027
9028
9029
9030
9031
9032
9033
9034
9035
9036
9037
9038
9039
9040
9041
9042
9043
9044
9045
9046
9047
9048
9049
9050
9051
9052
9053
9054
9055
9056
9057
9058
9059
9060
9061
9062
9063
9064
9065
9066
9067
9068
9069
9070
9071
9072
9073
9074
9075
9076
9077
9078
9079
9080
9081
9082
9083
9084
9085
9086
9087
9088
9089
9090
9091
9092
9093
9094
9095
9096
9097
9098
9099
9100
9101
9102
9103
9104
9105
9106
9107
9108
9109
9110
9111
9112
9113
9114
9115
9116
9117
9118
9119
9120
9121
9122
9123
9124
9125
9126
9127
9128
9129
9130
9131
9132
9133
9134
9135
9136
9137
9138
9139
9140
9141
9142
9143
9144
9145
9146
9147
9148
9149
9150
9151
9152
9153
9154
9155
9156
9157
9158
9159
9160
9161
9162
9163
9164
9165
9166
9167
9168
9169
9170
9171
9172
9173
9174
9175
9176
9177
9178
9179
9180
9181
9182
9183
9184
9185
9186
9187
9188
9189
9190
9191
9192
9193
9194
9195
9196
9197
9198
9199
9200
9201
9202
9203
9204
9205
9206
9207
9208
9209
9210
9211
9212
9213
9214
9215
9216
9217
9218
9219
9220
9221
9222
9223
9224
9225
9226
9227
9228
9229
9230
9231
9232
9233
9234
9235
9236
9237
9238
9239
9240
9241
9242
9243
9244
9245
9246
9247
9248
9249
9250
9251
9252
9253
9254
9255
9256
9257
9258
9259
9260
9261
9262
9263
9264
9265
9266
9267
9268
9269
9270
9271
9272
9273
9274
9275
9276
9277
9278
9279
9280
9281
9282
9283
9284
9285
9286
9287
9288
9289
9290
9291
9292
9293
9294
9295
9296
9297
9298
9299
9300
9301
9302
9303
9304
9305
9306
9307
9308
9309
9310
9311
9312
9313
9314
9315
9316
9317
9318
9319
9320
9321
9322
9323
9324
9325
9326
9327
9328
9329
9330
9331
9332
9333
9334
9335
9336
9337
9338
9339
9340
9341
9342
9343
9344
9345
9346
9347
9348
9349
9350
9351
9352
9353
9354
9355
9356
9357
9358
9359
9360
9361
9362
9363
9364
9365
9366
9367
9368
9369
9370
9371
9372
9373
9374
9375
9376
9377
9378
9379
9380
9381
9382
9383
9384
9385
9386
9387
9388
9389
9390
9391
9392
9393
9394
9395
9396
9397
9398
9399
9400
9401
9402
9403
9404
9405
9406
9407
9408
9409
9410
9411
9412
9413
9414
9415
9416
9417
9418
9419
9420
9421
9422
9423
9424
9425
9426
9427
9428
9429
9430
9431
9432
9433
9434
9435
9436
9437
9438
9439
9440
9441
9442
9443
9444
9445
9446
9447
9448
9449
9450
9451
9452
9453
9454
9455
9456
9457
9458
9459
9460
9461
9462
9463
9464
9465
9466
9467
9468
9469
9470
9471
9472
9473
9474
9475
9476
9477
9478
9479
9480
9481
9482
9483
9484
9485
9486
9487
9488
9489
9490
9491
9492
9493
9494
9495
9496
9497
9498
9499
9500
9501
9502
9503
9504
9505
9506
9507
9508
9509
9510
9511
9512
9513
9514
9515
9516
9517
9518
9519
9520
9521
9522
9523
9524
9525
9526
9527
9528
9529
9530
9531
9532
9533
9534
9535
9536
9537
9538
9539
9540
9541
9542
9543
9544
9545
9546
9547
9548
9549
9550
9551
9552
9553
9554
9555
9556
9557
9558
9559
9560
9561
9562
9563
9564
9565
9566
9567
9568
9569
9570
9571
9572
9573
9574
9575
9576
9577
9578
9579
9580
9581
9582
9583
9584
9585
9586
9587
9588
9589
9590
9591
9592
9593
9594
9595
9596
9597
9598
9599
9600
9601
9602
9603
9604
9605
9606
9607
9608
9609
9610
9611
9612
9613
9614
9615
9616
9617
9618
9619
9620
9621
9622
9623
9624
9625
9626
9627
9628
9629
9630
9631
9632
9633
9634
9635
9636
9637
9638
9639
9640
9641
9642
9643
9644
9645
9646
9647
9648
9649
9650
9651
9652
9653
9654
9655
9656
9657
9658
9659
9660
9661
9662
9663
9664
9665
9666
9667
9668
9669
9670
9671
9672
9673
9674
9675
9676
9677
9678
9679
9680
9681
9682
9683
9684
9685
9686
9687
9688
9689
9690
9691
9692
9693
9694
9695
9696
9697
9698
9699
9700
9701
9702
9703
9704
9705
9706
9707
9708
9709
9710
9711
9712
9713
9714
9715
9716
9717
9718
9719
9720
9721
9722
9723
9724
9725
9726
9727
9728
9729
9730
9731
9732
9733
9734
9735
9736
9737
9738
9739
9740
9741
9742
9743
9744
9745
9746
9747
9748
9749
9750
9751
9752
9753
9754
9755
9756
9757
9758
9759
9760
9761
9762
9763
9764
9765
9766
9767
9768
9769
9770
9771
9772
9773
9774
9775
9776
9777
9778
9779
9780
9781
9782
9783
9784
9785
9786
9787
9788
9789
9790
9791
9792
9793
9794
9795
9796
9797
9798
9799
9800
9801
9802
9803
9804
9805
9806
9807
9808
9809
9810
9811
9812
9813
9814
9815
9816
9817
9818
9819
9820
9821
9822
9823
9824
9825
9826
9827
9828
9829
9830
9831
9832
9833
9834
9835
9836
9837
9838
9839
9840
9841
9842
9843
9844
9845
9846
9847
9848
9849
9850
9851
9852
9853
9854
9855
9856
9857
9858
9859
9860
9861
9862
9863
9864
9865
9866
9867
9868
9869
9870
9871
9872
9873
9874
9875
9876
9877
9878
9879
9880
9881
9882
9883
9884
9885
9886
9887
9888
9889
9890
9891
9892
9893
9894
9895
9896
9897
9898
9899
9900
9901
9902
9903
9904
9905
9906
9907
9908
9909
9910
9911
9912
9913
9914
9915
9916
9917
9918
9919
9920
9921
9922
9923
9924
9925
9926
9927
9928
9929
9930
9931
9932
9933
9934
9935
9936
9937
9938
9939
9940
9941
9942
9943
9944
9945
9946
9947
9948
9949
9950
9951
9952
9953
9954
9955
9956
9957
9958
9959
9960
9961
9962
9963
9964
9965
9966
9967
9968
9969
9970
9971
9972
9973
9974
9975
9976
9977
9978
9979
9980
9981
9982
9983
9984
9985
9986
9987
9988
9989
9990
9991
9992
9993
9994
9995
9996
9997
9998
9999
10000
10001
10002
10003
10004
10005
10006
10007
10008
10009
10010
10011
10012
10013
10014
10015
10016
10017
10018
10019
10020
10021
10022
10023
10024
10025
10026
10027
10028
10029
10030
10031
10032
10033
10034
10035
10036
10037
10038
10039
10040
10041
10042
10043
10044
10045
10046
10047
10048
10049
10050
10051
10052
10053
10054
10055
10056
10057
10058
10059
10060
10061
10062
10063
10064
10065
10066
10067
10068
10069
10070
10071
10072
10073
10074
10075
10076
10077
10078
10079
10080
10081
10082
10083
10084
10085
10086
10087
10088
10089
10090
10091
10092
10093
10094
10095
10096
10097
10098
10099
10100
10101
10102
10103
10104
10105
10106
10107
10108
10109
10110
10111
10112
10113
10114
10115
10116
10117
10118
10119
10120
10121
10122
10123
10124
10125
10126
10127
10128
10129
10130
10131
10132
10133
10134
10135
10136
10137
10138
10139
10140
10141
10142
10143
10144
10145
10146
10147
10148
10149
10150
10151
10152
10153
10154
10155
10156
10157
10158
10159
10160
10161
10162
10163
10164
10165
10166
10167
10168
10169
10170
10171
10172
10173
10174
10175
10176
10177
10178
10179
10180
10181
10182
10183
10184
10185
10186
10187
10188
10189
10190
10191
10192
10193
10194
10195
10196
10197
10198
10199
10200
10201
10202
10203
10204
10205
10206
10207
10208
10209
10210
10211
10212
10213
10214
10215
10216
10217
10218
10219
10220
10221
10222
10223
10224
10225
10226
10227
10228
10229
10230
10231
10232
10233
10234
10235
10236
10237
10238
10239
10240
10241
10242
10243
10244
10245
10246
10247
10248
10249
10250
10251
10252
10253
10254
10255
10256
10257
10258
10259
10260
10261
10262
10263
10264
10265
10266
10267
10268
10269
10270
10271
10272
10273
10274
10275
10276
10277
10278
10279
10280
10281
10282
10283
10284
10285
10286
10287
10288
10289
10290
10291
10292
10293
10294
10295
10296
10297
10298
10299
10300
10301
10302
10303
10304
10305
10306
10307
10308
10309
10310
10311
10312
10313
10314
10315
10316
10317
10318
10319
10320
10321
10322
10323
10324
10325
10326
10327
10328
10329
10330
10331
10332
10333
10334
10335
10336
10337
10338
10339
10340
10341
10342
10343
10344
10345
10346
10347
10348
10349
10350
10351
10352
10353
10354
10355
10356
10357
10358
10359
10360
10361
10362
10363
10364
10365
10366
10367
10368
10369
10370
10371
10372
10373
10374
10375
10376
10377
10378
10379
10380
10381
10382
10383
10384
10385
10386
10387
10388
10389
10390
10391
10392
10393
10394
10395
10396
10397
10398
10399
10400
10401
10402
10403
10404
10405
10406
10407
10408
10409
10410
10411
10412
10413
10414
10415
10416
10417
10418
10419
10420
10421
10422
10423
10424
10425
10426
10427
10428
10429
10430
10431
10432
10433
10434
10435
10436
10437
10438
10439
10440
10441
10442
10443
10444
10445
10446
10447
10448
10449
10450
10451
10452
10453
10454
10455
10456
10457
10458
10459
10460
10461
10462
10463
10464
10465
10466
10467
10468
10469
10470
10471
10472
10473
10474
10475
10476
10477
10478
10479
10480
10481
10482
10483
10484
10485
10486
10487
10488
10489
10490
10491
10492
10493
10494
10495
10496
10497
10498
10499
10500
10501
10502
10503
10504
10505
10506
10507
10508
10509
10510
10511
10512
10513
10514
10515
10516
10517
10518
10519
10520
10521
10522
10523
10524
10525
10526
10527
10528
10529
10530
10531
10532
10533
10534
10535
10536
10537
10538
10539
10540
10541
10542
10543
10544
10545
10546
10547
10548
10549
10550
10551
10552
10553
10554
10555
10556
10557
10558
10559
10560
10561
10562
10563
10564
10565
10566
10567
10568
10569
10570
10571
10572
10573
10574
10575
10576
10577
10578
10579
10580
10581
10582
10583
10584
10585
10586
10587
10588
10589
10590
10591
10592
10593
10594
10595
10596
10597
10598
10599
10600
10601
10602
10603
10604
10605
10606
10607
10608
10609
10610
10611
10612
10613
10614
10615
10616
10617
10618
10619
10620
10621
10622
10623
10624
10625
10626
10627
10628
10629
10630
10631
10632
10633
10634
10635
10636
10637
10638
10639
10640
10641
10642
10643
10644
10645
10646
10647
10648
10649
10650
10651
10652
10653
10654
10655
10656
10657
10658
10659
10660
10661
10662
10663
10664
10665
10666
10667
10668
10669
10670
10671
10672
10673
10674
10675
10676
10677
10678
10679
10680
10681
10682
10683
10684
10685
10686
10687
10688
10689
10690
10691
10692
10693
10694
10695
10696
10697
10698
10699
10700
10701
10702
10703
10704
10705
10706
10707
10708
10709
10710
10711
10712
10713
10714
10715
10716
10717
10718
10719
10720
10721
10722
10723
10724
10725
10726
10727
10728
10729
10730
10731
10732
10733
10734
10735
10736
10737
10738
10739
10740
10741
10742
10743
10744
10745
10746
10747
10748
10749
10750
10751
10752
10753
10754
10755
10756
10757
10758
10759
10760
10761
10762
10763
10764
10765
10766
10767
10768
10769
10770
10771
10772
10773
10774
10775
10776
10777
10778
10779
10780
10781
10782
10783
10784
10785
10786
10787
10788
10789
10790
10791
10792
10793
10794
10795
10796
10797
10798
10799
10800
10801
10802
10803
10804
10805
10806
10807
10808
10809
10810
10811
10812
10813
10814
10815
10816
10817
10818
10819
10820
10821
10822
10823
10824
10825
10826
10827
10828
10829
10830
10831
10832
10833
10834
10835
10836
10837
10838
10839
10840
10841
10842
10843
10844
10845
10846
10847
10848
10849
10850
10851
10852
10853
10854
10855
10856
10857
10858
10859
10860
10861
10862
10863
10864
10865
10866
10867
10868
10869
10870
10871
10872
10873
10874
10875
10876
10877
10878
10879
10880
10881
10882
10883
10884
10885
10886
10887
10888
10889
10890
10891
10892
10893
10894
10895
10896
10897
10898
10899
10900
10901
10902
10903
10904
10905
10906
10907
10908
10909
10910
10911
10912
10913
10914
10915
10916
10917
10918
10919
10920
10921
10922
10923
10924
10925
10926
10927
10928
10929
10930
10931
10932
10933
10934
10935
10936
10937
10938
10939
10940
10941
10942
10943
10944
10945
10946
10947
10948
10949
10950
10951
10952
10953
10954
10955
10956
10957
10958
10959
10960
10961
10962
10963
10964
10965
10966
10967
10968
10969
10970
10971
10972
10973
10974
10975
10976
10977
10978
10979
10980
10981
10982
10983
10984
10985
10986
10987
10988
10989
10990
10991
10992
10993
10994
10995
10996
10997
10998
10999
11000
11001
11002
11003
11004
11005
11006
11007
11008
11009
11010
11011
11012
11013
11014
11015
11016
11017
11018
11019
11020
11021
11022
11023
11024
11025
11026
11027
11028
11029
11030
11031
11032
11033
11034
11035
11036
11037
11038
11039
11040
11041
11042
11043
11044
11045
11046
11047
11048
11049
11050
11051
11052
11053
11054
11055
11056
11057
11058
11059
11060
11061
11062
11063
11064
11065
11066
11067
11068
11069
11070
11071
11072
11073
11074
11075
11076
11077
11078
11079
11080
11081
11082
11083
11084
11085
11086
11087
11088
11089
11090
11091
11092
11093
11094
11095
11096
11097
11098
11099
11100
11101
11102
11103
11104
11105
11106
11107
11108
11109
11110
11111
11112
11113
11114
11115
11116
11117
11118
11119
11120
11121
11122
11123
11124
11125
11126
11127
11128
11129
11130
11131
11132
11133
11134
11135
11136
11137
11138
11139
11140
11141
11142
11143
11144
11145
11146
11147
11148
11149
11150
11151
11152
11153
11154
11155
11156
11157
11158
11159
11160
11161
11162
11163
11164
11165
11166
11167
11168
11169
11170
11171
11172
11173
11174
11175
11176
11177
11178
11179
11180
11181
11182
11183
11184
11185
11186
11187
11188
11189
11190
11191
11192
11193
11194
11195
11196
11197
11198
11199
11200
11201
11202
11203
11204
11205
11206
11207
11208
11209
11210
11211
11212
11213
11214
11215
11216
11217
11218
11219
11220
11221
11222
11223
11224
11225
11226
11227
11228
11229
11230
11231
11232
11233
11234
11235
11236
11237
11238
11239
11240
11241
11242
11243
11244
11245
11246
11247
11248
11249
11250
11251
11252
11253
11254
11255
11256
11257
11258
11259
11260
11261
11262
11263
11264
11265
11266
11267
11268
11269
11270
11271
11272
11273
11274
11275
11276
11277
11278
11279
11280
11281
11282
11283
11284
11285
11286
11287
11288
11289
11290
11291
11292
11293
11294
11295
11296
11297
11298
11299
11300
11301
11302
11303
11304
11305
11306
11307
11308
11309
11310
11311
11312
11313
11314
11315
11316
11317
11318
11319
11320
11321
11322
11323
11324
11325
11326
11327
11328
11329
11330
11331
11332
11333
11334
11335
11336
11337
11338
11339
11340
11341
11342
11343
11344
11345
11346
11347
11348
11349
11350
11351
11352
11353
11354
11355
11356
11357
11358
11359
11360
11361
11362
11363
11364
11365
11366
11367
11368
11369
11370
11371
11372
11373
11374
11375
11376
11377
11378
11379
11380
11381
11382
11383
11384
11385
11386
11387
11388
11389
11390
11391
11392
11393
11394
11395
11396
11397
11398
11399
11400
11401
11402
11403
11404
11405
11406
11407
11408
11409
11410
11411
11412
11413
11414
11415
11416
11417
11418
11419
11420
11421
11422
11423
11424
11425
11426
11427
11428
11429
11430
11431
11432
11433
11434
11435
11436
11437
11438
11439
11440
11441
11442
11443
11444
11445
11446
11447
11448
11449
11450
11451
11452
11453
11454
11455
11456
11457
11458
11459
11460
11461
11462
11463
11464
11465
11466
11467
11468
11469
11470
11471
11472
11473
11474
11475
11476
11477
11478
11479
11480
11481
11482
11483
11484
11485
11486
11487
11488
11489
11490
11491
11492
11493
11494
11495
11496
11497
11498
11499
11500
11501
11502
11503
11504
11505
11506
11507
11508
11509
11510
11511
11512
11513
11514
11515
11516
11517
11518
11519
11520
11521
11522
11523
11524
11525
11526
11527
11528
11529
11530
11531
11532
11533
11534
11535
11536
11537
11538
11539
11540
11541
11542
11543
11544
11545
11546
11547
11548
11549
11550
11551
11552
11553
11554
11555
11556
11557
11558
11559
11560
11561
11562
11563
11564
11565
11566
11567
11568
11569
11570
11571
11572
11573
11574
11575
11576
11577
11578
11579
11580
11581
11582
11583
11584
11585
11586
11587
11588
11589
11590
11591
11592
11593
11594
11595
11596
11597
11598
11599
11600
11601
11602
11603
11604
11605
11606
11607
11608
11609
11610
11611
11612
11613
11614
11615
11616
11617
11618
11619
11620
11621
11622
11623
11624
11625
11626
11627
11628
11629
11630
11631
11632
11633
11634
11635
11636
11637
11638
11639
11640
11641
11642
11643
11644
11645
11646
11647
11648
11649
11650
11651
11652
11653
11654
11655
11656
11657
11658
11659
11660
11661
11662
11663
11664
11665
11666
11667
11668
11669
11670
11671
11672
11673
11674
11675
11676
11677
11678
11679
11680
11681
11682
11683
11684
11685
11686
11687
11688
11689
11690
11691
11692
11693
11694
11695
11696
11697
11698
11699
11700
11701
11702
11703
11704
11705
11706
11707
11708
11709
11710
11711
11712
11713
11714
11715
11716
11717
11718
11719
11720
11721
11722
11723
11724
11725
11726
11727
11728
11729
11730
11731
11732
11733
11734
11735
11736
11737
11738
11739
11740
11741
11742
11743
11744
11745
11746
11747
11748
11749
11750
11751
11752
11753
11754
11755
11756
11757
11758
11759
11760
11761
11762
11763
11764
11765
11766
11767
11768
11769
11770
11771
11772
11773
11774
11775
11776
11777
11778
11779
11780
11781
11782
11783
11784
11785
11786
11787
11788
11789
11790
11791
11792
11793
11794
11795
11796
11797
11798
11799
11800
11801
11802
11803
11804
11805
11806
11807
11808
11809
11810
11811
11812
11813
11814
11815
11816
11817
11818
11819
11820
11821
11822
11823
11824
11825
11826
11827
11828
11829
11830
11831
11832
11833
11834
11835
11836
11837
11838
11839
11840
11841
11842
11843
11844
11845
11846
11847
11848
11849
11850
11851
11852
11853
11854
11855
11856
11857
11858
11859
11860
11861
11862
11863
11864
11865
11866
11867
11868
11869
11870
11871
11872
11873
11874
11875
11876
11877
11878
11879
11880
11881
11882
11883
11884
11885
11886
11887
11888
11889
11890
11891
11892
11893
11894
11895
11896
11897
11898
11899
11900
11901
11902
11903
11904
11905
11906
11907
11908
11909
11910
11911
11912
11913
11914
11915
11916
11917
11918
11919
11920
11921
11922
11923
11924
11925
11926
11927
11928
11929
11930
11931
11932
11933
11934
11935
11936
11937
11938
11939
11940
11941
11942
11943
11944
11945
11946
11947
11948
11949
11950
11951
11952
11953
11954
11955
11956
11957
11958
11959
11960
11961
11962
11963
11964
11965
11966
11967
11968
11969
11970
11971
11972
11973
11974
11975
11976
11977
11978
11979
11980
11981
11982
11983
11984
11985
11986
11987
11988
11989
11990
11991
11992
11993
11994
11995
11996
11997
11998
11999
12000
12001
12002
12003
12004
12005
12006
12007
12008
12009
12010
12011
12012
12013
12014
12015
12016
12017
12018
12019
12020
12021
12022
12023
12024
12025
12026
12027
12028
12029
12030
12031
12032
12033
12034
12035
12036
12037
12038
12039
12040
12041
12042
12043
12044
12045
12046
12047
12048
12049
12050
12051
12052
12053
12054
12055
12056
12057
12058
12059
12060
12061
12062
12063
12064
12065
12066
12067
12068
12069
12070
12071
12072
12073
12074
12075
12076
12077
12078
12079
12080
12081
12082
12083
12084
12085
12086
12087
12088
12089
12090
12091
12092
12093
12094
12095
12096
12097
12098
12099
12100
12101
12102
12103
12104
12105
12106
12107
12108
12109
12110
12111
12112
12113
12114
12115
12116
12117
12118
12119
12120
12121
12122
12123
12124
12125
12126
12127
12128
12129
12130
12131
12132
12133
12134
12135
12136
12137
12138
12139
12140
12141
12142
12143
12144
12145
12146
12147
12148
12149
12150
12151
12152
12153
12154
12155
12156
12157
12158
12159
12160
12161
12162
12163
12164
12165
12166
12167
12168
12169
12170
12171
12172
12173
12174
12175
12176
12177
12178
12179
12180
12181
12182
12183
12184
12185
12186
12187
12188
12189
12190
12191
12192
12193
12194
12195
12196
12197
12198
12199
12200
12201
12202
12203
12204
12205
12206
12207
12208
12209
12210
12211
12212
12213
12214
12215
12216
12217
12218
12219
12220
12221
12222
12223
12224
12225
12226
12227
12228
12229
12230
12231
12232
12233
12234
12235
12236
12237
12238
12239
12240
12241
12242
12243
12244
12245
12246
12247
12248
12249
12250
12251
12252
12253
12254
12255
12256
12257
12258
12259
12260
12261
12262
12263
12264
12265
12266
12267
12268
12269
12270
12271
12272
12273
12274
12275
12276
12277
12278
12279
12280
12281
12282
12283
12284
12285
12286
12287
12288
12289
12290
12291
12292
12293
12294
12295
12296
12297
12298
12299
12300
12301
12302
12303
12304
12305
12306
12307
12308
12309
12310
12311
12312
12313
12314
12315
12316
12317
12318
12319
12320
12321
12322
12323
12324
12325
12326
12327
12328
12329
12330
12331
12332
12333
12334
12335
12336
12337
12338
12339
12340
12341
12342
12343
12344
12345
12346
12347
12348
12349
12350
12351
12352
12353
12354
12355
12356
12357
12358
12359
12360
12361
12362
12363
12364
12365
12366
12367
12368
12369
12370
12371
12372
12373
12374
12375
12376
12377
12378
12379
12380
12381
12382
12383
12384
12385
12386
12387
12388
12389
12390
12391
12392
12393
12394
12395
12396
12397
12398
12399
12400
12401
12402
12403
12404
12405
12406
12407
12408
12409
12410
12411
12412
12413
12414
12415
12416
12417
12418
12419
12420
12421
12422
12423
12424
12425
12426
12427
12428
12429
12430
12431
12432
12433
12434
12435
12436
12437
12438
12439
12440
12441
12442
12443
12444
12445
12446
12447
12448
12449
12450
12451
12452
12453
12454
12455
12456
12457
12458
12459
12460
12461
12462
12463
12464
12465
12466
12467
12468
12469
12470
12471
12472
12473
12474
12475
12476
12477
12478
12479
12480
12481
12482
12483
12484
12485
12486
12487
12488
12489
12490
12491
12492
12493
12494
12495
12496
12497
12498
12499
12500
12501
12502
12503
12504
12505
12506
12507
12508
12509
12510
12511
12512
12513
12514
12515
12516
12517
12518
12519
12520
12521
12522
12523
12524
12525
12526
12527
12528
12529
12530
12531
12532
12533
12534
12535
12536
12537
12538
12539
12540
12541
12542
12543
12544
12545
12546
12547
12548
12549
12550
12551
12552
12553
12554
12555
12556
12557
12558
12559
12560
12561
12562
12563
12564
12565
12566
12567
12568
12569
12570
12571
12572
12573
12574
12575
12576
12577
12578
12579
12580
12581
12582
12583
12584
12585
12586
12587
12588
12589
12590
12591
12592
12593
12594
12595
12596
12597
12598
12599
12600
12601
12602
12603
12604
12605
12606
12607
12608
12609
12610
12611
12612
12613
12614
12615
12616
12617
12618
12619
12620
12621
12622
12623
12624
12625
12626
12627
12628
12629
12630
12631
12632
12633
12634
12635
12636
12637
12638
12639
12640
12641
12642
12643
12644
12645
12646
12647
12648
12649
12650
12651
12652
12653
12654
12655
12656
12657
12658
12659
12660
12661
12662
12663
12664
12665
12666
12667
12668
12669
12670
12671
12672
12673
12674
12675
12676
12677
12678
12679
12680
12681
12682
12683
12684
12685
12686
12687
12688
12689
12690
12691
12692
12693
12694
12695
12696
12697
12698
12699
12700
12701
12702
12703
12704
12705
12706
12707
12708
12709
12710
12711
12712
12713
12714
12715
12716
12717
12718
12719
12720
12721
12722
12723
12724
12725
12726
12727
12728
12729
12730
12731
12732
12733
12734
12735
12736
12737
12738
12739
12740
12741
12742
12743
12744
12745
12746
12747
12748
12749
12750
12751
12752
12753
12754
12755
12756
12757
12758
12759
12760
12761
12762
12763
12764
12765
12766
12767
12768
12769
12770
12771
12772
12773
12774
12775
12776
12777
12778
12779
12780
12781
12782
12783
12784
12785
12786
12787
12788
12789
12790
12791
12792
12793
12794
12795
12796
12797
12798
12799
12800
12801
12802
12803
12804
12805
12806
12807
12808
12809
12810
12811
12812
12813
12814
12815
12816
12817
12818
12819
12820
12821
12822
12823
12824
12825
12826
12827
12828
12829
12830
12831
12832
12833
12834
12835
12836
12837
12838
12839
12840
12841
12842
12843
12844
12845
12846
12847
12848
12849
12850
12851
12852
12853
12854
12855
12856
12857
12858
12859
12860
12861
12862
12863
12864
12865
12866
12867
12868
12869
12870
12871
12872
12873
12874
12875
12876
12877
12878
12879
12880
12881
12882
12883
12884
12885
12886
12887
12888
12889
12890
12891
12892
12893
12894
12895
12896
12897
12898
12899
12900
12901
12902
12903
12904
12905
12906
12907
12908
12909
12910
12911
12912
12913
12914
12915
12916
12917
12918
12919
12920
12921
12922
12923
12924
12925
12926
12927
12928
12929
12930
12931
12932
12933
12934
12935
12936
12937
12938
12939
12940
12941
12942
12943
12944
12945
12946
12947
12948
12949
12950
12951
12952
12953
12954
12955
12956
12957
12958
12959
12960
12961
12962
12963
12964
12965
12966
12967
12968
12969
12970
12971
12972
12973
12974
12975
12976
12977
12978
12979
12980
12981
12982
12983
12984
12985
12986
12987
12988
12989
12990
12991
12992
12993
12994
12995
12996
12997
12998
12999
13000
13001
13002
13003
13004
13005
13006
13007
13008
13009
13010
13011
13012
13013
13014
13015
13016
13017
13018
13019
13020
13021
13022
13023
13024
13025
13026
13027
13028
13029
13030
13031
13032
13033
13034
13035
13036
13037
13038
13039
13040
13041
13042
13043
13044
13045
13046
13047
13048
13049
13050
13051
13052
13053
13054
13055
13056
13057
13058
13059
13060
13061
13062
13063
13064
13065
13066
13067
13068
13069
13070
13071
13072
13073
13074
13075
13076
13077
13078
13079
13080
13081
13082
13083
13084
13085
13086
13087
13088
13089
13090
13091
13092
13093
13094
13095
13096
13097
13098
13099
13100
13101
13102
13103
13104
13105
13106
13107
13108
13109
13110
13111
13112
13113
13114
13115
13116
13117
13118
13119
13120
13121
13122
13123
13124
13125
13126
13127
13128
13129
13130
13131
13132
13133
13134
13135
13136
13137
13138
13139
13140
13141
13142
13143
13144
13145
13146
13147
13148
13149
13150
13151
13152
13153
13154
13155
13156
13157
13158
13159
13160
13161
13162
13163
13164
13165
13166
13167
13168
13169
13170
13171
13172
13173
13174
13175
13176
13177
13178
13179
13180
13181
13182
13183
13184
13185
13186
13187
13188
13189
13190
13191
13192
13193
13194
13195
13196
13197
13198
13199
13200
13201
13202
13203
13204
13205
13206
13207
13208
13209
13210
13211
13212
13213
13214
13215
13216
13217
13218
13219
13220
13221
13222
13223
13224
13225
13226
13227
13228
13229
13230
13231
13232
13233
13234
13235
13236
13237
13238
13239
13240
13241
13242
13243
13244
13245
13246
13247
13248
13249
13250
13251
13252
13253
13254
13255
13256
13257
13258
13259
13260
13261
13262
13263
13264
13265
13266
13267
13268
13269
13270
13271
13272
13273
13274
13275
13276
13277
13278
13279
13280
13281
13282
13283
13284
13285
13286
13287
13288
13289
13290
13291
13292
13293
13294
13295
13296
13297
13298
13299
13300
13301
13302
13303
13304
13305
13306
13307
13308
13309
13310
13311
13312
13313
13314
13315
13316
13317
13318
13319
13320
13321
13322
13323
13324
13325
13326
13327
13328
13329
13330
13331
13332
13333
13334
13335
13336
13337
13338
13339
13340
13341
13342
13343
13344
13345
13346
13347
13348
13349
13350
13351
13352
13353
13354
13355
13356
13357
13358
13359
13360
13361
13362
13363
13364
13365
13366
13367
13368
13369
13370
13371
13372
13373
13374
13375
13376
13377
13378
13379
13380
13381
13382
13383
13384
13385
13386
13387
13388
13389
13390
13391
13392
13393
13394
13395
13396
13397
13398
13399
13400
13401
13402
13403
13404
13405
13406
13407
13408
13409
13410
13411
13412
13413
13414
13415
13416
13417
13418
13419
13420
13421
13422
13423
13424
13425
13426
13427
13428
13429
13430
13431
13432
13433
13434
13435
13436
13437
13438
13439
13440
13441
13442
13443
13444
13445
13446
13447
13448
13449
13450
13451
13452
13453
13454
13455
13456
13457
13458
13459
13460
13461
13462
13463
13464
13465
13466
13467
13468
13469
13470
13471
13472
13473
13474
13475
13476
13477
13478
13479
13480
13481
13482
13483
13484
13485
13486
13487
13488
13489
13490
13491
13492
13493
13494
13495
13496
13497
13498
13499
13500
13501
13502
13503
13504
13505
13506
13507
13508
13509
13510
13511
13512
13513
13514
13515
13516
13517
13518
13519
13520
13521
13522
13523
13524
13525
13526
13527
13528
13529
13530
13531
13532
13533
13534
13535
13536
13537
13538
13539
13540
13541
13542
13543
13544
13545
13546
13547
13548
13549
13550
13551
13552
13553
13554
13555
13556
13557
13558
13559
13560
13561
13562
13563
13564
13565
13566
13567
13568
13569
13570
13571
13572
13573
13574
13575
13576
13577
13578
13579
13580
13581
13582
13583
13584
13585
13586
13587
13588
13589
13590
13591
13592
13593
13594
13595
13596
13597
13598
13599
13600
13601
13602
13603
13604
13605
13606
13607
13608
13609
13610
13611
13612
13613
13614
13615
13616
13617
13618
13619
13620
13621
13622
13623
13624
13625
13626
13627
13628
13629
13630
13631
13632
13633
13634
13635
13636
13637
13638
13639
13640
13641
13642
13643
13644
13645
13646
13647
13648
13649
13650
13651
13652
13653
13654
13655
13656
13657
13658
13659
13660
13661
13662
13663
13664
13665
13666
13667
13668
13669
13670
13671
13672
13673
13674
13675
13676
13677
13678
13679
13680
13681
13682
13683
13684
13685
13686
13687
13688
13689
13690
13691
13692
13693
13694
13695
13696
13697
13698
13699
13700
13701
13702
13703
13704
13705
13706
13707
13708
13709
13710
13711
13712
13713
13714
13715
13716
13717
13718
13719
13720
13721
13722
13723
13724
13725
13726
13727
13728
13729
13730
13731
13732
13733
13734
13735
13736
13737
13738
13739
13740
13741
13742
13743
13744
13745
13746
13747
13748
13749
13750
13751
13752
13753
13754
13755
13756
13757
13758
13759
13760
13761
13762
13763
13764
13765
13766
13767
13768
13769
13770
13771
13772
13773
13774
13775
13776
13777
13778
13779
13780
13781
13782
13783
13784
13785
13786
13787
13788
13789
13790
13791
13792
13793
13794
13795
13796
13797
13798
13799
13800
13801
13802
13803
13804
13805
13806
13807
13808
13809
13810
13811
13812
13813
13814
13815
13816
13817
13818
13819
13820
13821
13822
13823
13824
13825
13826
13827
13828
13829
13830
13831
13832
13833
13834
13835
13836
13837
13838
13839
13840
13841
13842
13843
13844
13845
13846
13847
13848
13849
13850
13851
13852
13853
13854
13855
13856
13857
13858
13859
13860
13861
13862
13863
13864
13865
13866
13867
13868
13869
13870
13871
13872
13873
13874
13875
13876
13877
13878
13879
13880
13881
13882
13883
13884
13885
13886
13887
13888
13889
13890
13891
13892
13893
13894
13895
13896
13897
13898
13899
13900
13901
13902
13903
13904
13905
13906
13907
13908
13909
13910
13911
13912
13913
13914
13915
13916
13917
13918
13919
13920
13921
13922
13923
13924
13925
13926
13927
13928
13929
13930
13931
13932
13933
13934
13935
13936
13937
13938
13939
13940
13941
13942
13943
13944
13945
13946
13947
13948
13949
13950
13951
13952
13953
13954
13955
13956
13957
13958
13959
13960
13961
13962
13963
13964
13965
13966
13967
13968
13969
13970
13971
13972
13973
13974
13975
13976
13977
13978
13979
13980
13981
13982
13983
13984
13985
13986
13987
13988
13989
13990
13991
13992
13993
13994
13995
13996
13997
13998
13999
14000
14001
14002
14003
14004
14005
14006
14007
14008
14009
14010
14011
14012
14013
14014
14015
14016
14017
14018
14019
14020
14021
14022
14023
14024
14025
14026
14027
14028
14029
14030
14031
14032
14033
14034
14035
14036
14037
14038
14039
14040
14041
14042
14043
14044
14045
14046
14047
14048
14049
14050
14051
14052
14053
14054
14055
14056
14057
14058
14059
14060
14061
14062
14063
14064
14065
14066
14067
14068
14069
14070
14071
14072
14073
14074
14075
14076
14077
14078
14079
14080
14081
14082
14083
14084
14085
14086
14087
14088
14089
14090
14091
14092
14093
14094
14095
14096
14097
14098
14099
14100
14101
14102
14103
14104
14105
14106
14107
14108
14109
14110
14111
14112
14113
14114
14115
14116
14117
14118
14119
14120
14121
14122
14123
14124
14125
14126
14127
14128
14129
14130
14131
14132
14133
14134
14135
14136
14137
14138
14139
14140
14141
14142
14143
14144
14145
14146
14147
14148
14149
14150
14151
14152
14153
14154
14155
14156
14157
14158
14159
14160
14161
14162
14163
14164
14165
14166
14167
14168
14169
14170
14171
14172
14173
14174
14175
14176
14177
14178
14179
14180
14181
14182
14183
14184
14185
14186
14187
14188
14189
14190
14191
14192
14193
14194
14195
14196
14197
14198
14199
14200
14201
14202
14203
14204
14205
14206
14207
14208
14209
14210
14211
14212
14213
14214
14215
14216
14217
14218
14219
14220
14221
14222
14223
14224
14225
14226
14227
14228
14229
14230
14231
14232
14233
14234
14235
14236
14237
14238
14239
14240
14241
14242
14243
14244
14245
14246
14247
14248
14249
14250
14251
14252
14253
14254
14255
14256
14257
14258
14259
14260
14261
14262
14263
14264
14265
14266
14267
14268
14269
14270
14271
14272
14273
14274
14275
14276
14277
14278
14279
14280
14281
14282
14283
14284
14285
14286
14287
14288
14289
14290
14291
14292
14293
14294
14295
14296
14297
14298
14299
14300
14301
14302
14303
14304
14305
14306
14307
14308
14309
14310
14311
14312
14313
14314
14315
14316
14317
14318
14319
14320
14321
14322
14323
14324
14325
14326
14327
14328
14329
14330
14331
14332
14333
14334
14335
14336
14337
14338
14339
14340
14341
14342
14343
14344
14345
14346
14347
14348
14349
14350
14351
14352
14353
14354
14355
14356
14357
14358
14359
14360
14361
14362
14363
14364
14365
14366
14367
14368
14369
14370
14371
14372
14373
14374
14375
14376
14377
14378
14379
14380
14381
14382
14383
14384
14385
14386
14387
14388
14389
14390
14391
14392
14393
14394
14395
14396
14397
14398
14399
14400
14401
14402
14403
14404
14405
14406
14407
14408
14409
14410
14411
14412
14413
14414
14415
14416
14417
14418
14419
14420
14421
14422
14423
14424
14425
14426
14427
14428
14429
14430
14431
14432
14433
14434
14435
14436
14437
14438
14439
14440
14441
14442
14443
14444
14445
14446
14447
14448
14449
14450
14451
14452
14453
14454
14455
14456
14457
14458
14459
14460
14461
14462
14463
14464
14465
14466
14467
14468
14469
14470
14471
14472
14473
14474
14475
14476
14477
14478
14479
14480
14481
14482
14483
14484
14485
14486
14487
14488
14489
14490
14491
14492
14493
14494
14495
14496
14497
14498
14499
14500
14501
14502
14503
14504
14505
14506
14507
14508
14509
14510
14511
14512
14513
14514
14515
14516
14517
14518
14519
14520
14521
14522
14523
14524
14525
14526
14527
14528
14529
14530
14531
14532
14533
14534
14535
14536
14537
14538
14539
14540
14541
14542
14543
14544
14545
14546
14547
14548
14549
14550
14551
14552
14553
14554
14555
14556
14557
14558
14559
14560
14561
14562
14563
14564
14565
14566
14567
14568
14569
14570
14571
14572
14573
14574
14575
14576
14577
14578
14579
14580
14581
14582
14583
14584
14585
14586
14587
14588
14589
14590
14591
14592
14593
14594
14595
14596
14597
14598
14599
14600
14601
14602
14603
14604
14605
14606
14607
14608
14609
14610
14611
14612
14613
14614
14615
14616
14617
14618
14619
14620
14621
14622
14623
14624
14625
14626
14627
14628
14629
14630
14631
14632
14633
14634
14635
14636
14637
14638
14639
14640
14641
14642
14643
14644
14645
14646
14647
14648
14649
14650
14651
14652
14653
14654
14655
14656
14657
14658
14659
14660
14661
14662
14663
14664
14665
14666
14667
14668
14669
14670
14671
14672
14673
14674
14675
14676
14677
14678
14679
14680
14681
14682
14683
14684
14685
14686
14687
14688
14689
14690
14691
14692
14693
14694
14695
14696
14697
14698
14699
14700
14701
14702
14703
14704
14705
14706
14707
14708
14709
14710
14711
14712
14713
14714
14715
14716
14717
14718
14719
14720
14721
14722
14723
14724
14725
14726
14727
14728
14729
14730
14731
14732
14733
14734
14735
14736
14737
14738
14739
14740
14741
14742
14743
14744
14745
14746
14747
14748
14749
14750
14751
14752
14753
14754
14755
14756
14757
14758
14759
14760
14761
14762
14763
14764
14765
14766
14767
14768
14769
14770
14771
14772
14773
14774
14775
14776
14777
14778
14779
14780
14781
14782
14783
14784
14785
14786
14787
14788
14789
14790
14791
14792
14793
14794
14795
14796
14797
14798
14799
14800
14801
14802
14803
14804
14805
14806
14807
14808
14809
14810
14811
14812
14813
14814
14815
14816
14817
14818
14819
14820
14821
14822
14823
14824
14825
14826
14827
14828
14829
14830
14831
14832
14833
14834
14835
14836
14837
14838
14839
14840
14841
14842
14843
14844
14845
14846
14847
14848
14849
14850
14851
14852
14853
14854
14855
14856
14857
14858
14859
14860
14861
14862
14863
14864
14865
14866
14867
14868
14869
14870
14871
14872
14873
14874
14875
14876
14877
14878
14879
14880
14881
14882
14883
14884
14885
14886
14887
14888
14889
14890
14891
14892
14893
14894
14895
14896
14897
14898
14899
14900
14901
14902
14903
14904
14905
14906
14907
14908
14909
14910
14911
14912
14913
14914
14915
14916
14917
14918
14919
14920
14921
14922
14923
14924
14925
14926
14927
14928
14929
14930
14931
14932
14933
14934
14935
14936
14937
14938
14939
14940
14941
14942
14943
14944
14945
14946
14947
14948
14949
14950
14951
14952
14953
14954
14955
14956
14957
14958
14959
14960
14961
14962
14963
14964
14965
14966
14967
14968
14969
14970
14971
14972
14973
14974
14975
14976
14977
14978
14979
14980
14981
14982
14983
14984
14985
14986
14987
14988
14989
14990
14991
14992
14993
14994
14995
14996
14997
14998
14999
15000
15001
15002
15003
15004
15005
15006
15007
15008
15009
15010
15011
15012
15013
15014
15015
15016
15017
15018
15019
15020
15021
15022
15023
15024
15025
15026
15027
15028
15029
15030
15031
15032
15033
15034
15035
15036
15037
15038
15039
15040
15041
15042
15043
15044
15045
15046
15047
15048
15049
15050
15051
15052
15053
15054
15055
15056
15057
15058
15059
15060
15061
15062
15063
15064
15065
15066
15067
15068
15069
15070
15071
15072
15073
15074
15075
15076
15077
15078
15079
15080
15081
15082
15083
15084
15085
15086
15087
15088
15089
15090
15091
15092
15093
15094
15095
15096
15097
15098
15099
15100
15101
15102
15103
15104
15105
15106
15107
15108
15109
15110
15111
15112
15113
15114
15115
15116
15117
15118
15119
15120
15121
15122
15123
15124
15125
15126
15127
15128
15129
15130
15131
15132
15133
15134
15135
15136
15137
15138
15139
15140
15141
15142
15143
15144
15145
15146
15147
15148
15149
15150
15151
15152
15153
15154
15155
15156
15157
15158
15159
15160
15161
15162
15163
15164
15165
15166
15167
15168
15169
15170
15171
15172
15173
15174
15175
15176
15177
15178
15179
15180
15181
15182
15183
15184
15185
15186
15187
15188
15189
15190
15191
15192
15193
15194
15195
15196
15197
15198
15199
15200
15201
15202
15203
15204
15205
15206
15207
15208
15209
15210
15211
15212
15213
15214
15215
15216
15217
15218
15219
15220
15221
15222
15223
15224
15225
15226
15227
15228
15229
15230
15231
15232
15233
15234
15235
15236
15237
15238
15239
15240
15241
15242
15243
15244
15245
15246
15247
15248
15249
15250
15251
15252
15253
15254
15255
15256
15257
15258
15259
15260
15261
15262
15263
15264
15265
15266
15267
15268
15269
15270
15271
15272
15273
15274
15275
15276
15277
15278
15279
15280
15281
15282
15283
15284
15285
15286
15287
15288
15289
15290
15291
15292
15293
15294
15295
15296
15297
15298
15299
15300
15301
15302
15303
15304
15305
15306
15307
15308
15309
15310
15311
15312
15313
15314
15315
15316
15317
15318
15319
15320
15321
15322
15323
15324
15325
15326
15327
15328
15329
15330
15331
15332
15333
15334
15335
15336
15337
15338
15339
15340
15341
15342
15343
15344
15345
15346
15347
15348
15349
15350
15351
15352
15353
15354
15355
15356
15357
15358
15359
15360
15361
15362
15363
15364
15365
15366
15367
15368
15369
15370
15371
15372
15373
15374
15375
15376
15377
15378
15379
15380
15381
15382
15383
15384
15385
15386
15387
15388
15389
15390
15391
15392
15393
15394
15395
15396
15397
15398
15399
15400
15401
15402
15403
15404
15405
15406
15407
15408
15409
15410
15411
15412
15413
15414
15415
15416
15417
15418
15419
15420
15421
15422
15423
15424
15425
15426
15427
15428
15429
15430
15431
15432
15433
15434
15435
15436
15437
15438
15439
15440
15441
15442
15443
15444
15445
15446
15447
15448
15449
15450
15451
15452
15453
15454
15455
15456
15457
15458
15459
15460
15461
15462
15463
15464
15465
15466
15467
15468
15469
15470
15471
15472
15473
15474
15475
15476
15477
15478
15479
15480
15481
15482
15483
15484
15485
15486
15487
15488
15489
15490
15491
15492
15493
15494
15495
15496
15497
15498
15499
15500
15501
15502
15503
15504
15505
15506
15507
15508
15509
15510
15511
15512
15513
15514
15515
15516
15517
15518
15519
15520
15521
15522
15523
15524
15525
15526
15527
15528
15529
15530
15531
15532
15533
15534
15535
15536
15537
15538
15539
15540
15541
15542
15543
15544
15545
15546
15547
15548
15549
15550
15551
15552
15553
15554
15555
15556
15557
15558
15559
15560
15561
15562
15563
15564
15565
15566
15567
15568
15569
15570
15571
15572
15573
15574
15575
15576
15577
15578
15579
15580
15581
15582
15583
15584
15585
15586
15587
15588
15589
15590
15591
15592
15593
15594
15595
15596
15597
15598
15599
15600
15601
15602
15603
15604
15605
15606
15607
15608
15609
15610
15611
15612
15613
15614
15615
15616
15617
15618
15619
15620
15621
15622
15623
15624
15625
15626
15627
15628
15629
15630
15631
15632
15633
15634
15635
15636
15637
15638
15639
15640
15641
15642
15643
15644
15645
15646
15647
15648
15649
15650
15651
15652
15653
15654
15655
15656
15657
15658
15659
15660
15661
15662
15663
15664
15665
15666
15667
15668
15669
15670
15671
15672
15673
15674
15675
15676
15677
15678
15679
15680
15681
15682
15683
15684
15685
15686
15687
15688
15689
15690
15691
15692
15693
15694
15695
15696
15697
15698
15699
15700
15701
15702
15703
15704
15705
15706
15707
15708
15709
15710
15711
15712
15713
15714
15715
15716
15717
15718
15719
15720
15721
15722
15723
15724
15725
15726
15727
15728
15729
15730
15731
15732
15733
15734
15735
15736
15737
15738
15739
15740
15741
15742
15743
15744
15745
15746
15747
15748
15749
15750
15751
15752
15753
15754
15755
15756
15757
15758
15759
15760
15761
15762
15763
15764
15765
15766
15767
15768
15769
15770
15771
15772
15773
15774
15775
15776
15777
15778
15779
15780
15781
15782
15783
15784
15785
15786
15787
15788
15789
15790
15791
15792
15793
15794
15795
15796
15797
15798
15799
15800
15801
15802
15803
15804
15805
15806
15807
15808
15809
15810
15811
15812
15813
15814
15815
15816
15817
15818
15819
15820
15821
15822
15823
15824
15825
15826
15827
15828
15829
15830
15831
15832
15833
15834
15835
15836
15837
15838
15839
15840
15841
15842
15843
15844
15845
15846
15847
15848
15849
15850
15851
15852
15853
15854
15855
15856
15857
15858
15859
15860
15861
15862
15863
15864
15865
15866
15867
15868
15869
15870
15871
15872
15873
15874
15875
15876
15877
15878
15879
15880
15881
15882
15883
15884
15885
15886
15887
15888
15889
15890
15891
15892
15893
15894
15895
15896
15897
15898
15899
15900
15901
15902
15903
15904
15905
15906
15907
15908
15909
15910
15911
15912
15913
15914
15915
15916
15917
15918
15919
15920
15921
15922
15923
15924
15925
15926
15927
15928
15929
15930
15931
15932
15933
15934
15935
15936
15937
15938
15939
15940
15941
15942
15943
15944
15945
15946
15947
15948
15949
15950
15951
15952
15953
15954
15955
15956
15957
15958
15959
15960
15961
15962
15963
15964
15965
15966
15967
15968
15969
15970
15971
15972
15973
15974
15975
15976
15977
15978
15979
15980
15981
15982
15983
15984
15985
15986
15987
15988
15989
15990
15991
15992
15993
15994
15995
15996
15997
15998
15999
16000
16001
16002
16003
16004
16005
16006
16007
16008
16009
16010
16011
16012
16013
16014
16015
16016
16017
16018
16019
16020
16021
16022
16023
16024
16025
16026
16027
16028
16029
16030
16031
16032
16033
16034
16035
16036
16037
16038
16039
16040
16041
16042
16043
16044
16045
16046
16047
16048
16049
16050
16051
16052
16053
16054
16055
16056
16057
16058
16059
16060
16061
16062
16063
16064
16065
16066
16067
16068
16069
16070
16071
16072
16073
16074
16075
16076
16077
16078
16079
16080
16081
16082
16083
16084
16085
16086
16087
16088
16089
16090
16091
16092
16093
16094
16095
16096
16097
16098
16099
16100
16101
16102
16103
16104
16105
16106
16107
16108
16109
16110
16111
16112
16113
16114
16115
16116
16117
16118
16119
16120
16121
16122
16123
16124
16125
16126
16127
16128
16129
16130
16131
16132
16133
16134
16135
16136
16137
16138
16139
16140
16141
16142
16143
16144
16145
16146
16147
16148
16149
16150
16151
16152
16153
16154
16155
16156
16157
16158
16159
16160
16161
16162
16163
16164
16165
16166
16167
16168
16169
16170
16171
16172
16173
16174
16175
16176
16177
16178
16179
16180
16181
16182
16183
16184
16185
16186
16187
16188
16189
16190
16191
16192
16193
16194
16195
16196
16197
16198
16199
16200
16201
16202
16203
16204
16205
16206
16207
16208
16209
16210
16211
16212
16213
16214
16215
16216
16217
16218
16219
16220
16221
16222
16223
16224
16225
16226
16227
16228
16229
16230
16231
16232
16233
16234
16235
16236
16237
16238
16239
16240
16241
16242
16243
16244
16245
16246
16247
16248
16249
16250
16251
16252
16253
16254
16255
16256
16257
16258
16259
16260
16261
16262
16263
16264
16265
16266
16267
16268
16269
16270
16271
16272
16273
16274
16275
16276
16277
16278
16279
16280
16281
16282
16283
16284
16285
16286
16287
16288
16289
16290
16291
16292
16293
16294
16295
16296
16297
16298
16299
16300
16301
16302
16303
16304
16305
16306
16307
16308
16309
16310
16311
16312
16313
16314
16315
16316
16317
16318
16319
16320
16321
16322
16323
16324
16325
16326
16327
16328
16329
16330
16331
16332
16333
16334
16335
16336
16337
16338
16339
16340
16341
16342
16343
16344
16345
16346
16347
16348
16349
16350
16351
16352
16353
16354
16355
16356
16357
16358
16359
16360
16361
16362
16363
16364
16365
16366
16367
16368
16369
16370
16371
16372
16373
16374
16375
16376
16377
16378
16379
16380
16381
16382
16383
16384
16385
16386
16387
16388
16389
16390
16391
16392
16393
16394
16395
16396
16397
16398
16399
16400
16401
16402
16403
16404
16405
16406
16407
16408
16409
16410
16411
16412
16413
16414
16415
16416
16417
16418
16419
16420
16421
16422
16423
16424
16425
16426
16427
16428
16429
16430
16431
16432
16433
16434
16435
16436
16437
16438
16439
16440
16441
16442
16443
16444
16445
16446
16447
16448
16449
16450
16451
16452
16453
16454
16455
16456
16457
16458
16459
16460
16461
16462
16463
16464
16465
16466
16467
16468
16469
16470
16471
16472
16473
16474
16475
16476
16477
16478
16479
16480
16481
16482
16483
16484
16485
16486
16487
16488
16489
16490
16491
16492
16493
16494
16495
16496
16497
16498
16499
16500
16501
16502
16503
16504
16505
16506
16507
16508
16509
16510
16511
16512
16513
16514
16515
16516
16517
16518
16519
16520
16521
16522
16523
16524
16525
16526
16527
16528
16529
16530
16531
16532
16533
16534
16535
16536
16537
16538
16539
16540
16541
16542
16543
16544
16545
16546
16547
16548
16549
16550
16551
16552
16553
16554
16555
16556
16557
16558
16559
16560
16561
16562
16563
16564
16565
16566
16567
16568
16569
16570
16571
16572
16573
16574
16575
16576
16577
16578
16579
16580
16581
16582
16583
16584
16585
16586
16587
16588
16589
16590
16591
16592
16593
16594
16595
16596
16597
16598
16599
16600
16601
16602
16603
16604
16605
16606
16607
16608
16609
16610
16611
16612
16613
16614
16615
16616
16617
16618
16619
16620
16621
16622
16623
16624
16625
16626
16627
16628
16629
16630
16631
16632
16633
16634
16635
16636
16637
16638
16639
16640
16641
16642
16643
16644
16645
16646
16647
16648
16649
16650
16651
16652
16653
16654
16655
16656
16657
16658
16659
16660
16661
16662
16663
16664
16665
16666
16667
16668
16669
16670
16671
16672
16673
16674
16675
16676
16677
16678
16679
16680
16681
16682
16683
16684
16685
16686
16687
16688
16689
16690
16691
16692
16693
16694
16695
16696
16697
16698
16699
16700
16701
16702
16703
16704
16705
16706
16707
16708
16709
16710
16711
16712
16713
16714
16715
16716
16717
16718
16719
16720
16721
16722
16723
16724
16725
16726
16727
16728
16729
16730
16731
16732
16733
16734
16735
16736
16737
16738
16739
16740
16741
16742
16743
16744
16745
16746
16747
16748
16749
16750
16751
16752
16753
16754
16755
16756
16757
16758
16759
16760
16761
16762
16763
16764
16765
16766
16767
16768
16769
16770
16771
16772
16773
16774
16775
16776
16777
16778
16779
16780
16781
16782
16783
16784
16785
16786
16787
16788
16789
16790
16791
16792
16793
16794
16795
16796
16797
16798
16799
16800
16801
16802
16803
16804
16805
16806
16807
16808
16809
16810
16811
16812
16813
16814
16815
16816
16817
16818
16819
16820
16821
16822
16823
16824
16825
16826
16827
16828
16829
16830
16831
16832
16833
16834
16835
16836
16837
16838
16839
16840
16841
16842
16843
16844
16845
16846
16847
16848
16849
16850
16851
16852
16853
16854
16855
16856
16857
16858
16859
16860
16861
16862
16863
16864
16865
16866
16867
16868
16869
16870
16871
16872
16873
16874
16875
16876
16877
16878
16879
16880
16881
16882
16883
16884
16885
16886
16887
16888
16889
16890
16891
16892
16893
16894
16895
16896
16897
16898
16899
16900
16901
16902
16903
16904
16905
16906
16907
16908
16909
16910
16911
16912
16913
16914
16915
16916
16917
16918
16919
16920
16921
16922
16923
16924
16925
16926
16927
16928
16929
16930
16931
16932
16933
16934
16935
16936
16937
16938
16939
16940
16941
16942
16943
16944
16945
16946
16947
16948
16949
16950
16951
16952
16953
16954
16955
16956
16957
16958
16959
16960
16961
16962
16963
16964
16965
16966
16967
16968
16969
16970
16971
16972
16973
16974
16975
16976
16977
16978
16979
16980
16981
16982
16983
16984
16985
16986
16987
16988
16989
16990
16991
16992
16993
16994
16995
16996
16997
16998
16999
17000
17001
17002
17003
17004
17005
17006
17007
17008
17009
17010
17011
17012
17013
17014
17015
17016
17017
17018
17019
17020
17021
17022
17023
17024
17025
17026
17027
17028
17029
17030
17031
17032
17033
17034
17035
17036
17037
17038
17039
17040
17041
17042
17043
17044
17045
17046
17047
17048
17049
17050
17051
17052
17053
17054
17055
17056
17057
17058
17059
17060
17061
17062
17063
17064
17065
17066
17067
17068
17069
17070
17071
17072
17073
17074
17075
17076
17077
17078
17079
17080
17081
17082
17083
17084
17085
17086
17087
17088
17089
17090
17091
17092
17093
17094
17095
17096
17097
17098
17099
17100
17101
17102
17103
17104
17105
17106
17107
17108
17109
17110
17111
17112
17113
17114
17115
17116
17117
17118
17119
17120
17121
17122
17123
17124
17125
17126
17127
17128
17129
17130
17131
17132
17133
17134
17135
17136
17137
17138
17139
17140
17141
17142
17143
17144
17145
17146
17147
17148
17149
17150
17151
17152
17153
17154
17155
17156
17157
17158
17159
17160
17161
17162
17163
17164
17165
17166
17167
17168
17169
17170
17171
17172
17173
17174
17175
17176
17177
17178
17179
17180
17181
17182
17183
17184
17185
17186
17187
17188
17189
17190
17191
17192
17193
17194
17195
17196
17197
17198
17199
17200
17201
17202
17203
17204
17205
17206
17207
17208
17209
17210
17211
17212
17213
17214
17215
17216
17217
17218
17219
17220
17221
17222
17223
17224
17225
17226
17227
17228
17229
17230
17231
17232
17233
17234
17235
17236
17237
17238
17239
17240
17241
17242
17243
17244
17245
17246
17247
17248
17249
17250
17251
17252
17253
17254
17255
17256
17257
17258
17259
17260
17261
17262
17263
17264
17265
17266
17267
17268
17269
17270
17271
17272
17273
17274
17275
17276
17277
17278
17279
17280
17281
17282
17283
17284
17285
17286
17287
17288
17289
17290
17291
17292
17293
17294
17295
17296
17297
17298
17299
17300
17301
17302
17303
17304
17305
17306
17307
17308
17309
17310
17311
17312
17313
17314
17315
17316
17317
17318
17319
17320
17321
17322
17323
17324
17325
17326
17327
17328
17329
17330
17331
17332
17333
17334
17335
17336
17337
17338
17339
17340
17341
17342
17343
17344
17345
17346
17347
17348
17349
17350
17351
17352
17353
17354
17355
17356
17357
17358
17359
17360
17361
17362
17363
17364
17365
17366
17367
17368
17369
17370
17371
17372
17373
17374
17375
17376
17377
17378
17379
17380
17381
17382
17383
17384
17385
17386
17387
17388
17389
17390
17391
17392
17393
17394
17395
17396
17397
17398
17399
17400
17401
17402
17403
17404
17405
17406
17407
17408
17409
17410
17411
17412
17413
17414
17415
17416
17417
17418
17419
17420
17421
17422
17423
17424
17425
17426
17427
17428
17429
17430
17431
17432
17433
17434
17435
17436
17437
17438
17439
17440
17441
17442
17443
17444
17445
17446
17447
17448
17449
17450
17451
17452
17453
17454
17455
17456
17457
17458
17459
17460
17461
17462
17463
17464
17465
17466
17467
17468
17469
17470
17471
17472
17473
17474
17475
17476
17477
17478
17479
17480
17481
17482
17483
17484
17485
17486
17487
17488
17489
17490
17491
17492
17493
17494
17495
17496
17497
17498
17499
17500
17501
17502
17503
17504
17505
17506
17507
17508
17509
17510
17511
17512
17513
17514
17515
17516
17517
17518
17519
17520
17521
17522
17523
17524
17525
17526
17527
17528
17529
17530
17531
17532
17533
17534
17535
17536
17537
17538
17539
17540
17541
17542
17543
17544
17545
17546
17547
17548
17549
17550
17551
17552
17553
17554
17555
17556
17557
17558
17559
17560
17561
17562
17563
17564
17565
17566
17567
17568
17569
17570
17571
17572
17573
17574
17575
17576
17577
17578
17579
17580
17581
17582
17583
17584
17585
17586
17587
17588
17589
17590
17591
17592
17593
17594
17595
17596
17597
17598
17599
17600
17601
17602
17603
17604
17605
17606
17607
17608
17609
17610
17611
17612
17613
17614
17615
17616
17617
17618
17619
17620
17621
17622
17623
17624
17625
17626
17627
17628
17629
17630
17631
17632
17633
17634
17635
17636
17637
17638
17639
17640
17641
17642
17643
17644
17645
17646
17647
17648
17649
17650
17651
17652
17653
17654
17655
17656
17657
17658
17659
17660
17661
17662
17663
17664
17665
17666
17667
17668
17669
17670
17671
17672
17673
17674
17675
17676
17677
17678
17679
17680
17681
17682
17683
17684
17685
17686
17687
17688
17689
17690
17691
17692
17693
17694
17695
17696
17697
17698
17699
17700
17701
17702
17703
17704
17705
17706
17707
17708
17709
17710
17711
17712
17713
17714
17715
17716
17717
17718
17719
17720
17721
17722
17723
17724
17725
17726
17727
17728
17729
17730
17731
17732
17733
17734
17735
17736
17737
17738
17739
17740
17741
17742
17743
17744
17745
17746
17747
17748
17749
17750
17751
17752
17753
17754
17755
17756
17757
17758
17759
17760
17761
17762
17763
17764
17765
17766
17767
17768
17769
17770
17771
17772
17773
17774
17775
17776
17777
17778
17779
17780
17781
17782
17783
17784
17785
17786
17787
17788
17789
17790
17791
17792
17793
17794
17795
17796
17797
17798
17799
17800
17801
17802
17803
17804
17805
17806
17807
17808
17809
17810
17811
17812
17813
17814
17815
17816
17817
17818
17819
17820
17821
17822
17823
17824
17825
17826
17827
17828
17829
17830
17831
17832
17833
17834
17835
17836
17837
17838
17839
17840
17841
17842
17843
17844
17845
17846
17847
17848
17849
17850
17851
17852
17853
17854
17855
17856
17857
17858
17859
17860
17861
17862
17863
17864
17865
17866
17867
17868
17869
17870
17871
17872
17873
17874
17875
17876
17877
17878
17879
17880
17881
17882
17883
17884
17885
17886
17887
17888
17889
17890
17891
17892
17893
17894
17895
17896
17897
17898
17899
17900
17901
17902
17903
17904
17905
17906
17907
17908
17909
17910
17911
17912
17913
17914
17915
17916
17917
17918
17919
17920
17921
17922
17923
17924
17925
17926
17927
17928
17929
17930
17931
17932
17933
17934
17935
17936
17937
17938
17939
17940
17941
17942
17943
17944
17945
17946
17947
17948
17949
17950
17951
17952
17953
17954
17955
17956
17957
17958
17959
17960
17961
17962
17963
17964
17965
17966
17967
17968
17969
17970
17971
17972
17973
17974
17975
17976
17977
17978
17979
17980
17981
17982
17983
17984
17985
17986
17987
17988
17989
17990
17991
17992
17993
17994
17995
17996
17997
17998
17999
18000
18001
18002
18003
18004
18005
18006
18007
18008
18009
18010
18011
18012
18013
18014
18015
18016
18017
18018
18019
18020
18021
18022
18023
18024
18025
18026
18027
18028
18029
18030
18031
18032
18033
18034
18035
18036
18037
18038
18039
18040
18041
18042
18043
18044
18045
18046
18047
18048
18049
18050
18051
18052
18053
18054
18055
18056
18057
18058
18059
18060
18061
18062
18063
18064
18065
18066
18067
18068
18069
18070
18071
18072
18073
18074
18075
18076
18077
18078
18079
18080
18081
18082
18083
18084
18085
18086
18087
18088
18089
18090
18091
18092
18093
18094
18095
18096
18097
18098
18099
18100
18101
18102
18103
18104
18105
18106
18107
18108
18109
18110
18111
18112
18113
18114
18115
18116
18117
18118
18119
18120
18121
18122
18123
18124
18125
18126
18127
18128
18129
18130
18131
18132
18133
18134
18135
18136
18137
18138
18139
18140
18141
18142
18143
18144
18145
18146
18147
18148
18149
18150
18151
18152
18153
18154
18155
18156
18157
18158
18159
18160
18161
18162
18163
18164
18165
18166
18167
18168
18169
18170
18171
18172
18173
18174
18175
18176
18177
18178
18179
18180
18181
18182
18183
18184
18185
18186
18187
18188
18189
18190
18191
18192
18193
18194
18195
18196
18197
18198
18199
18200
18201
18202
18203
18204
18205
18206
18207
18208
18209
18210
18211
18212
18213
18214
18215
18216
18217
18218
18219
18220
18221
18222
18223
18224
18225
18226
18227
18228
18229
18230
18231
18232
18233
18234
18235
18236
18237
18238
18239
18240
18241
18242
18243
18244
18245
18246
18247
18248
18249
18250
18251
18252
18253
18254
18255
18256
18257
18258
18259
18260
18261
18262
18263
18264
18265
18266
18267
18268
18269
18270
18271
18272
18273
18274
18275
18276
18277
18278
18279
18280
18281
18282
18283
18284
18285
18286
18287
18288
18289
18290
18291
18292
18293
18294
18295
18296
18297
18298
18299
18300
18301
18302
18303
18304
18305
18306
18307
18308
18309
18310
18311
18312
18313
18314
18315
18316
18317
18318
18319
18320
18321
18322
18323
18324
18325
18326
18327
18328
18329
18330
18331
18332
18333
18334
18335
18336
18337
18338
18339
18340
18341
18342
18343
18344
18345
18346
18347
18348
18349
18350
18351
18352
18353
18354
18355
18356
18357
18358
18359
18360
18361
18362
18363
18364
18365
18366
18367
18368
18369
18370
18371
18372
18373
18374
18375
18376
18377
18378
18379
18380
18381
18382
18383
18384
18385
18386
18387
18388
18389
18390
18391
18392
18393
18394
18395
18396
18397
18398
18399
18400
18401
18402
18403
18404
18405
18406
18407
18408
18409
18410
18411
18412
18413
18414
18415
18416
18417
18418
18419
18420
18421
18422
18423
18424
18425
18426
18427
18428
18429
18430
18431
18432
18433
18434
18435
18436
18437
18438
18439
18440
18441
18442
18443
18444
18445
18446
18447
18448
18449
18450
18451
18452
18453
18454
18455
18456
18457
18458
18459
18460
18461
18462
18463
18464
18465
18466
18467
18468
18469
18470
18471
18472
18473
18474
18475
18476
18477
18478
18479
18480
18481
18482
18483
18484
18485
18486
18487
18488
18489
18490
18491
18492
18493
18494
18495
18496
18497
18498
18499
18500
18501
18502
18503
18504
18505
18506
18507
18508
18509
18510
18511
18512
18513
18514
18515
18516
18517
18518
18519
18520
18521
18522
18523
18524
18525
18526
18527
18528
18529
18530
18531
18532
18533
18534
18535
18536
18537
18538
18539
18540
18541
18542
18543
18544
18545
18546
18547
18548
18549
18550
18551
18552
18553
18554
18555
18556
18557
18558
18559
18560
18561
18562
18563
18564
18565
18566
18567
18568
18569
18570
18571
18572
18573
18574
18575
18576
18577
18578
18579
18580
18581
18582
18583
18584
18585
18586
18587
18588
18589
18590
18591
18592
18593
18594
18595
18596
18597
18598
18599
18600
18601
18602
18603
18604
18605
18606
18607
18608
18609
18610
18611
18612
18613
18614
18615
18616
18617
18618
18619
18620
18621
18622
18623
18624
18625
18626
18627
18628
18629
18630
18631
18632
18633
18634
18635
18636
18637
18638
18639
18640
18641
18642
18643
18644
18645
18646
18647
18648
18649
18650
18651
18652
18653
18654
18655
18656
18657
18658
18659
18660
18661
18662
18663
18664
18665
18666
18667
18668
18669
18670
18671
18672
18673
18674
18675
18676
18677
18678
18679
18680
18681
18682
18683
18684
18685
18686
18687
18688
18689
18690
18691
18692
18693
18694
18695
18696
18697
18698
18699
18700
18701
18702
18703
18704
18705
18706
18707
18708
18709
18710
18711
18712
18713
18714
18715
18716
18717
18718
18719
18720
18721
18722
18723
18724
18725
18726
18727
18728
18729
18730
18731
18732
18733
18734
18735
18736
18737
18738
18739
18740
18741
18742
18743
18744
18745
18746
18747
18748
18749
18750
18751
18752
18753
18754
18755
18756
18757
18758
18759
18760
18761
18762
18763
18764
18765
18766
18767
18768
18769
18770
18771
18772
18773
18774
18775
18776
18777
18778
18779
18780
18781
18782
18783
18784
18785
18786
18787
18788
18789
18790
18791
18792
18793
18794
18795
18796
18797
18798
18799
18800
18801
18802
18803
18804
18805
18806
18807
18808
18809
18810
18811
18812
18813
18814
18815
18816
18817
18818
18819
18820
18821
18822
18823
18824
18825
18826
18827
18828
18829
18830
18831
18832
18833
18834
18835
18836
18837
18838
18839
18840
18841
18842
18843
18844
18845
18846
18847
18848
18849
18850
18851
18852
18853
18854
18855
18856
18857
18858
18859
18860
18861
18862
18863
18864
18865
18866
18867
18868
18869
18870
18871
18872
18873
18874
18875
18876
18877
18878
18879
18880
18881
18882
18883
18884
18885
18886
18887
18888
18889
18890
18891
18892
18893
18894
18895
18896
18897
18898
18899
18900
18901
18902
18903
18904
18905
18906
18907
18908
18909
18910
18911
18912
18913
18914
18915
18916
18917
18918
18919
18920
18921
18922
18923
18924
18925
18926
18927
18928
18929
18930
18931
18932
18933
18934
18935
18936
18937
18938
18939
18940
18941
18942
18943
18944
18945
18946
18947
18948
18949
18950
18951
18952
18953
18954
18955
18956
18957
18958
18959
18960
18961
18962
18963
18964
18965
18966
18967
18968
18969
18970
18971
18972
18973
18974
18975
18976
18977
18978
18979
18980
18981
18982
18983
18984
18985
18986
18987
18988
18989
18990
18991
18992
18993
18994
18995
18996
18997
18998
18999
19000
19001
19002
19003
19004
19005
19006
19007
19008
19009
19010
19011
19012
19013
19014
19015
19016
19017
19018
19019
{
  "schema": 1,
  "about": "Every Claude Code behavior csift depends on, one claim per entry. Each check names the instrument that ran, what it observed and the counting rule; the README badge version is admitted only when every claim carries a check at that version. Never bulk-edit the checks.",
  "verified_claude_code": "2.1.258",
  "claims": [
    {
      "id": "BG-001",
      "area": "background-tasks",
      "behavior": "A backgrounded shell launch is an ordinary `type:\"assistant\"` record carrying a `Bash` (on Windows `PowerShell`) `tool_use` whose input is `{command, description?, timeout?, run_in_background:true, dangerouslyDisableSandbox?}`. Across 2942 corpus launches `description` was present on 99.4% and `timeout` on 25.0%. The flag is NOT exclusive to the two shell tools (the `Agent` tool takes it too, 257 launches) and it is NOT the only way into the background: a FOREGROUND shell command that exceeds its timeout is auto-moved to the background, minting a `backgroundTaskId` with no `run_in_background` on the launching tool_use (80 corpus cases, CC 2.1.210 through 2.1.258).",
      "depends": "csift's launch ingester keys on exactly that shape (tool name `Bash` or `PowerShell` plus `input.run_in_background == true`); a renamed input flag would make every background task invisible and collapse the `idle-background-open` verdict.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "49-59",
          "snippet": "            Block::ToolUse {\n                id: Some(id),\n                name: Some(name),\n                input: Some(input),\n                ..\n            } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n                && input\n                    .get(\"run_in_background\")\n                    .and_then(serde_json::Value::as_bool)\n                    == Some(true))\n                || name == \"Monitor\" =>"
        },
        {
          "path": "src/live/background.rs",
          "lines": "4-7",
          "snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//!   tool_result arrives within milliseconds (\"Command running in background with ID:\n//!   <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//!   at once - it is invisible to the unreturned-call logic by construction;"
        }
      ],
      "instrument": "`rg -l '\"run_in_background\":\\s*true' ~/.claude/projects --glob '*.jsonl'`, then parse each matching line's `tool_use` blocks. Counting rule: one launch = one `tool_use` block with `name` in {Bash, PowerShell} and `input.run_in_background === true`, keyed by (file, tool_use id) and deduped on that key - a raw line count over-reads because the field is echoed back inside tool_result text.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt ; rg -o '.{0,200}run_in_background:[^,]{0,80}' cc258.strings.txt ; rg -o '.{0,300}The PowerShell command to execute.{0,500}' cc258.strings.txt ; csift search 'run_in_background' --raw --max-count 0 | python3 (parse every tool_use block, dedupe by tool_use id) ; csift search 'was moved to the background \\(ID' --raw --max-count 0",
          "observed": "Binary, the shell tool handler destructure: `let{command:we,description:Ie,timeout:Pe,run_in_background:De}=e`. Binary, the PowerShell tool input schema: `tt({command:i().refine(eU,Js).describe(\"The PowerShell command to execute\"),timeout:$M(A().optional()).describe(`Optional timeout in milliseconds (max ${We()})`),description:i().optional().describe(\"Clear, concise description of what this command does in active voice.\"),run_in_background:aA(M().optional()).describe(\"Set to true to run this command in the background.\"),dangerouslyDisableSandbox:aA(M().optional()).describe(\"Set this to true to dangerously override sandbox mode and run commands without sandboxing.\")})`. Corpus: 2942 distinct Bash launches, 0 PowerShell launches; description present on 2925 (99.4%), timeout on 735 (25.0%); input key sets 2192 {command,description,run_in_background} / 731 +timeout / 13 {command,run_in_background} / 4 {command,run_in_background,timeout} / 2 +dangerouslyDisableSandbox. A further 257 tool_use blocks carry run_in_background:true under the tool name Agent. Separately, 80 tool_result records read `... was moved to the background (ID: ...)` and 0 of their 80 launching tool_use blocks carried run_in_background at all (58 of the 80 still carry toolUseResult.backgroundTaskId); their records span versions 2.1.210 through 2.1.258, 7 of them at 2.1.258.",
          "rule": "One launch = one tool_use block whose name is Bash or PowerShell and whose input.run_in_background === true, deduped by tool_use id over the whole corpus (a raw line count over-reads because the flag is echoed inside result text). Percentages are over that deduped launch set. The auto-background count is one per tool_result block whose text contains `moved to the background (ID:`, deduped by tool_use id.",
          "note": "The tool-name-plus-flag shape csift keys on is intact in 2.1.258 and the code site is verbatim at src/live/background_scan.rs:49-59. Two corrections. (1) A fifth input key `dangerouslyDisableSandbox` exists in both shell schemas and appears on disk. (2) The clause 'there is no other on-disk marker for it' is wrong: an auto-backgrounded foreground command is a real background task with a real 9-char id that csift's ingester cannot see, because the ingester requires input.run_in_background == true. The PowerShell half of the claim is confirmed only from the binary schema; this machine has 0 PowerShell tool_use blocks on disk, so a Windows session would be needed to observe the record shape."
        }
      ]
    },
    {
      "id": "BG-002",
      "area": "background-tasks",
      "behavior": "The paired `tool_result` for a backgrounded shell opens `Command running in background with ID: <id>. Output is being written to: <abs path>.` and then appends, conditionally, ONE of two notify sentences (`You will be notified when it completes.` on 97.2% of corpus results, or `If it exits while you are still working you will be notified, but it is terminated when you give your final response...` on 1.0%) or neither (1.8%), then usually `To check interim output, use Read on that file path.`, then optionally a `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.` line (34.0%). The structured `toolUseResult` is `{stdout, stderr, interrupted, isImage, noOutputExpected, backgroundTaskId}` on 51.9% of results, that set plus `backgroundCwdHint` on 33.0%, absent entirely on 13.7% (the subagent lane), and may also carry `assistantAutoBackgrounded`, `backgroundEndsWithFinalResponse` or `dangerouslyDisableSandbox`.",
      "depends": "csift lifts the task id after the literal `with ID: ` and the output path after `written to: `, falling back to `toolUseResult.backgroundTaskId` / `taskId`; a reworded result string breaks the text path and leaves the id null on the lanes where the structured field is absent.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "104-121",
          "snippet": "                    let text = content\n                        .as_ref()\n                        .map(crate::model::tool_result_content_text)\n                        .unwrap_or_default();\n                    if task.id.is_none() {\n                        task.id = after_marker(&text, \"with ID: \")\n                            .or_else(|| after_marker(&text, \"(task \"))\n                            .or_else(|| {\n                                let v = rec.tool_use_result_value()?;\n                                v.get(\"backgroundTaskId\")\n                                    .or_else(|| v.get(\"taskId\"))?\n                                    .as_str()\n                                    .map(str::to_string)\n                            });\n                    }\n                    if task.output_file.is_none() {\n                        task.output_file = after_marker(&text, \"written to: \");\n                    }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "165-179",
          "snippet": "pub(crate) fn after_marker(text: &str, marker: &str) -> Option<String> {\n    let start = text.find(marker)? + marker.len();\n    let rest = &text[start..];\n    let end = rest\n        .char_indices()\n        .find(|&(i, c)| {\n            c.is_whitespace()\n                || c == ','\n                || c == ')'\n                || (c == '.' && rest[i + 1..].starts_with([' ', '\\n']))\n        })\n        .map_or(rest.len(), |(i, _)| i);\n    let tok = rest[..end].trim_end_matches('.');\n    (!tok.is_empty()).then(|| tok.to_string())\n}"
        }
      ],
      "instrument": "Launch any command with `run_in_background` and read the result record: `csift search 'Command running in background' @<session> --raw | head -1`. Counting rule: one verbatim result string per launch; the unit test `helpers_parse_the_result_text_and_locate_the_main_transcript` in src/live/tests/background_scan.rs pins both markers.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -o '.{0,150}Command running in background with ID: \\$\\{e\\}.{0,400}' cc258.strings.txt ; rg -o 'To check interim output.{0,200}' cc258.strings.txt ; csift search 'Command running in background with ID' --raw --max-count 0 | python3 (bucket the tool_result text and the toolUseResult key set, dedupe by tool_use id)",
          "observed": "Binary: the result text is assembled from three parts, `[v,C,I].filter(Boolean).join(\" \")`, where v = `Command running in background with ID: ${e}. Output is being written to: ${n}.` (or, on the auto-background path, `...id not complete within its ${...}s timeout and was moved to the background (ID: ${e}). Output is being written to: ${n}.`), C is one of `You will be notified when it completes.` / `If it exits while you are still working you will be notified, but it is terminated when you give your final response and no notification can follow that - so do not end your turn to wait for it; if you need its result, wait for it before giving your final response.` / omitted, and I = `To check interim output, use ${_} on that file path.` or omitted. Corpus, 3157 deduped launch results: 3070 carry `You will be notified when it completes.`, 31 carry the `If it exits while you are still working` variant, 56 carry neither; 3095 carry `To check interim output, use Read on that file path.`; 1072 (34.0%) carry a trailing line `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.`. toolUseResult key sets: 1638 (51.9%) exactly {stdout,stderr,interrupted,isImage,noOutputExpected,backgroundTaskId}; 1041 (33.0%) that set plus `backgroundCwdHint`; 434 (13.7%) NO toolUseResult field at all; 21 plus `assistantAutoBackgrounded`; 15 without `backgroundTaskId`; 5 plus `backgroundEndsWithFinalResponse`; 2 plus `dangerouslyDisableSandbox`. A live launch in this session returned the `If it exits while you are still working` variant.",
          "rule": "One result per tool_result block whose text contains `Command running in background with ID`, deduped by tool_use id; text variants bucketed by which notify sentence the text contains; key sets read off the carrying record's top-level toolUseResult.",
          "note": "Both marker literals csift lifts on - `with ID: ` and `written to: ` - are intact, so the text extraction path still works and src/live/background_scan.rs:104-121 and :165-179 are verbatim. What is wrong is the word 'exactly': the text now has three notify branches and an optional cwd-hint tail, and the toolUseResult key set has grown by four optional fields and is absent outright on one result in seven. The auto-background opener (`moved to the background (ID: ...)`) carries neither `with ID: ` nor `(task `, so on that path csift's id can only come from the structured fallback."
        }
      ]
    },
    {
      "id": "BG-003",
      "area": "background-tasks",
      "behavior": "A backgrounded shell's `tool_result` normally arrives sub-second (corpus median 0.42s, 90.4% under 1s) but not always: 9.6% of 2941 pairings took over 1s, 48 took over 5s and the slowest took 197.9s, and 1 launch of 2942 was never paired at all. During that window the launch IS an unreturned call. Once paired - which is the steady state for the whole life of the background command - the tail state machine reports `no pending call`, so a running background shell is invisible to unreturned-call liveness logic.",
      "depends": "This is why csift added the whole-file background scan and the seventh verdict `idle-background-open`; before it, a session idling at `end_turn` with a running background build reported `idle-eot` and satisfied `wait --until stop`.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "4-7",
          "snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//!   tool_result arrives within milliseconds (\"Command running in background with ID:\n//!   <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//!   at once - it is invisible to the unreturned-call logic by construction;"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "358-367",
          "snippet": "    } else if eot_shape && background.open_counted() > 0 {\n        notes.push(\n            \"the turn ended, but background task(s) have not returned - by design (a dev \\\n             server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n             the next session start. `--background-since` / `--ignore-background` narrow \\\n             what counts; kill a dead one with the tool or the shell\"\n                .to_string(),\n        );\n        Verdict::IdleBackgroundOpen"
        }
      ],
      "instrument": "Launch `sleep 600` with `run_in_background`, then run `csift status @<session>`: the tail row reads `no pending call; last stop_reason end_turn` while the verdict is `idle-background-open` with a shell row. Counting rule: one launch, one status read; the e2e test `an_open_background_shell_is_the_seventh_verdict_with_its_row` in tests/cli/live/background.rs pins it against a fixture.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'run_in_background' --raw --max-count 0 and csift search 'Command running in background with ID' --raw --max-count 0, joined in python on tool_use id, delta = result record timestamp minus launch record timestamp ; then a live check: a `sleep 45` launched with run_in_background:true followed immediately by `csift status @trap:<marker>`",
          "observed": "2942 launches, 2941 paired (99.97%), 1 never paired. Deltas: min 0.049s, p50 0.416s, p90 0.965s, p99 7.397s, max 197.855s; 90.4% under 1s, 95.2% under 2s, 48 over 5s. No multi-tool_use-block confound (0 of the 2942 launches shared its assistant record with another tool_use block). Live `csift status` while the background sleep was open printed `tail  unreturned Bash call  (0s ago)` for the FOREGROUND csift call itself, and the background sleep appeared only in the separate section as `background  3 open; 154 completed, 1 failed, 15 killed, 0 stopped` with a `bg  shell  <9-char b-id>  launched ... output 0 B` row.",
          "rule": "One delta per (launch tool_use id, its tool_result) pair, both timestamps read off the carrying records; unpaired = a launch tool_use id that no tool_result block in the corpus names.",
          "note": "The load-bearing half - a running background shell never shows up as a pending call, so it needs its own scan and its own verdict - is confirmed live: the status tail row named a different, foreground call while the open sleep was counted only under the background section. 'Within milliseconds' is the median, not the rule; the launch is genuinely unreturned for a stretch on roughly one launch in ten. src/live/background.rs:4-7 and src/live/verdict.rs:358-367 are verbatim. The live verdict was `running` rather than `idle-background-open` because the session was mid-turn, which is the documented precondition (idle-background-open needs a clean end_turn)."
        }
      ]
    },
    {
      "id": "BG-004",
      "area": "background-tasks",
      "behavior": "An async agent launch is a `tool_result` whose `toolUseResult` is `{isAsync:true, status:\"async_launched\", agentId, description, resolvedModel?, prompt, outputFile, canReadOutputFile}` (resolvedModel is optional - absent on 36 of 298 corpus acks - and a `modelsUsed` array appears when more than one model was used) and whose text opens `Async agent launched successfully.`, now followed on 66% of corpus acks by a parenthetical warning not to quote the result. The `agentId` is a 17-character `a`-led id and the completion reuses the `<task-notification>` machinery with `<task-id>` equal to it.",
      "depends": "csift's launch ingester detects the launch by the sentinel `toolUseResult.status == \"async_launched\"` (falling back to the text prefix) and lifts `agentId`, `description` and `outputFile` from the structured echo; a renamed sentinel makes async agents invisible to the background section.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "124-133",
          "snippet": "                // An async agent launch: the sentinel status on the structured echo.\n                let probe = rec.tur_probe();\n                let launched = probe\n                    .as_ref()\n                    .and_then(|p| p.status.as_ref())\n                    .and_then(serde_json::Value::as_str)\n                    == Some(\"async_launched\");\n                if !launched {\n                    continue;\n                }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "142-156",
          "snippet": "                tasks.entry(tuid.clone()).or_insert(BgTask {\n                    kind: BgKind::Agent,\n                    id: s(\"agentId\"),\n                    tool_use_id: tuid.clone(),\n                    description: s(\"description\"),\n                    command: None,\n                    launched_utc: rec.timestamp.clone(),\n                    lane: lane.to_string(),\n                    output_file: s(\"outputFile\"),\n                    state: BgState::Open,\n                    returned_utc: None,\n                    output_bytes: None,\n                    output_age_secs: None,\n                    ignored_by: None,\n                });"
        },
        {
          "path": "src/live/background.rs",
          "lines": "8-9",
          "snippet": "//! - an async AGENT: a tool_result whose `toolUseResult` is `{isAsync:true,\n//!   status:\"async_launched\", agentId, description, outputFile}`;"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "180-185",
          "snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
        },
        {
          "path": "src/subagent/spawn.rs",
          "lines": "258",
          "snippet": "pub(crate) const ASYNC_LAUNCH_SENTINEL: &str = \"Async agent launched\";"
        }
      ],
      "instrument": "`rg -l '\"status\":\"async_launched\"' ~/.claude/projects --glob '*.jsonl'`, then parse a matching line's `toolUseResult` (each hit must also carry `agentId` and `outputFile`). Counting rule: one launch per (file, tool_use id) whose `toolUseResult.status == \"async_launched\"`; the unit test `async_agent_launches_and_the_stopped_notice` in src/live/tests/background_scan.rs pins the sentinel.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; src/model/markers.rs ASYNC_LAUNCH_ACK_PREFIX doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -o '.{0,180}async_launched.{0,180}' cc258.strings.txt ; csift search 'Async agent launched successfully' --raw --max-count 0 | python3 (bucket toolUseResult key sets, agentId length and first character, first line of the ack text, dedupe by tool_use id)",
          "observed": "Binary output schema: `n=c({status:x(\"async_launched\"),isAsync:x(!0).optional(),agentId:i().describe(\"The ID of the async agent\"),description:i().describe(\"The description of the task\"),resolvedModel:i().optional().describe(\"Model i...`; binary runtime object: `{data:{isAsync:!0,status:\"async_launched\",agentId:js.agentId,description:r,resolvedModel:Rn,prompt:e,outputFile:pl(js.agentId),canReadOutputFile:si}}` and a second site `{data:{isAsync:!0,status:\"async_launched\",agentId:xs,description:r,resolvedModel:ui,...sr.length>1&&{modelsUsed:[...sr]},prompt:e,outputFile:pl(xs),canReadOutputFile:$g}}`. Corpus: 298 deduped acks; agentId length 17 on 298 of 298 and first character `a` on 298 of 298; toolUseResult key sets 262 = {agentId,canReadOutputFile,description,isAsync,outputFile,prompt,resolvedModel,status} and 36 = the same minus `resolvedModel`. First line of the ack text: 196 read `Async agent launched successfully. (This tool result is internal metadata - never quote or paste any part of it, including the agentId below, into a user-facing reply.)` and 102 read `Async agent launched successfully.`. In the classified notification corpus 183 sections carry a 17-character `a`-led `<task-id>`.",
          "rule": "One ack per tool_result block whose text starts with `Async agent launched successfully`, deduped by tool_use id; key sets and agentId shape read off the carrying record's toolUseResult; the 17-char task-id count is one per `<task-notification>` section in records csift classifies harness.notification.*.",
          "note": "The sentinel csift keys on - toolUseResult.status == 'async_launched' - and the text-prefix fallback are both intact, and all five code sites are verbatim. Two corrections: `resolvedModel` is optional in the schema and missing on 12% of corpus acks, and a ninth key `modelsUsed` exists in the binary for the multi-model case; and the ack's first line is no longer just the one sentence."
        }
      ]
    },
    {
      "id": "BG-005",
      "area": "background-tasks",
      "behavior": "The async launch ack is NOT the child's report: the child's report arrives LATER as a `<task-notification>` carrying a `<result>` body, joined back to its launching spawn through the pulse's embedded `<tool-use-id>`; a bare launch-ack pulse with no `<result>` carries no communication direction at all.",
      "depends": "csift labels the ack `agent.tool.result` only (never a child return) and emits an extra `agent.communication.inbox` view (child to self) for a result-bearing pulse, excerpting the `<result>` body; conflating the two attributes the child's work to the launch instant, and without the id join the report is attributed to nobody and degrades to `?`.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "70-81",
          "snippet": "    /// True when this carrier is an ASYNC-LAUNCH ACK, not a child return (smoke-found bug): an\n    /// ASYNC/background `Agent` spawn's tool_result is the immediate launch confirmation\n    /// (`\"Async agent launched successfully.\\nagentId: …\"`), shaped on disk as\n    /// `toolUseResult.{isAsync:true, status:\"async_launched\"}`. It shares the spawn\n    /// `tool_use_id`, so the [`SpawnLookup`] WOULD resolve it - but it is the LAUNCH ack, not\n    /// the work product. The async child's real report arrives LATER via the\n    /// `<task-notification>` `<result>` pulse (G1 → `agent.communication.inbox`), never via this\n    /// tool_result. So a launch ack is `agent.tool.result` ONLY (unlike a SYNC one-shot Task\n    /// return, which IS the child's reply → `…inbox`). Robust dual detection: the structured\n    /// `toolUseResult` shape first, then the content prefix ([`ASYNC_LAUNCH_ACK_PREFIX`]) for a\n    /// record lacking the structured field.\n    pub(crate) fn is_async_launch_ack(&self) -> bool {"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "461-464",
          "snippet": "                if section.contains(NOTIFICATION_RESULT_TAG) {\n                    let child = extract_xml_tag(section, \"tool-use-id\")\n                        .and_then(|id| ctx.spawn.and_then(|sp| sp.child_for_spawn_tool_use_id(&id)))\n                        .unwrap_or_else(|| \"?\".to_string());"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "180-185",
          "snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
        }
      ],
      "instrument": "`rg -o '<task-notification>.*?</task-notification>' <transcript> | rg -c '<result>'` against the total notification count. Counting rule: one observation per notification section, bucketed by whether it carries a `<result>` tag.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/live/background.rs module doc; src/model/classify.rs comment; src/model/markers.rs ASYNC_LAUNCH_ACK_PREFIX doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' -t harness.notification --raw --max-count 0 (2698 records) and csift search 'Async agent launched successfully' --raw --max-count 0, joined in python: every `<task-notification>` section indexed by its `<tool-use-id>`, then matched against each ack's tool_use id, bucketed by `<result>` presence and by timestamp order",
          "observed": "298 async launch acks; 155 of them have at least one notification section joined by `<tool-use-id>`; 155 of 155 of those sections carry a `<result>` tag; 155 of 155 have the notification timestamp strictly LATER than the ack's, gap min 78.3s, median 530.5s, max 5101.2s. Across all 1849 distinct tool-use-id-bearing notification sections only 322 carry `<result>` - shell and monitor completions do not.",
          "rule": "One join per (ack tool_use id, notification section naming that id); `<result>` presence by literal tag search inside the section; ordering by the two carrying records' timestamps. The 143 acks with no notification are agents that never reported before the session ended, or whose pulse landed on a line csift does not classify.",
          "note": "Both halves confirmed with no correction needed: the report is never the ack (100% of joined notifications are strictly later, by 78s to 85 minutes), the join key is the launching tool_use id, and result-bearing sections are a minority (322 of 1849) so a non-result pulse genuinely has nothing to attribute. src/model/classify.rs:70-81 and :461-464 and src/model/markers.rs:167-172 are verbatim."
        }
      ]
    },
    {
      "id": "BG-006",
      "area": "background-tasks",
      "behavior": "The `Monitor` tool's input schema requires `description` and takes optional `timeout_ms` (integer, minimum 1000, default 300000, maximum 3600000, ignored when persistent) and `persistent` (boolean, default false), plus EXACTLY ONE of `command` (each stdout line is an event) or `ws:{url, protocols?}` (ws:// or wss:// only); remote sessions cap the timeout at 1800000 and force `persistent:false`.",
      "depends": "csift models a Monitor arm as a background task of kind `monitor` and names it by `input.command` or, for a websocket monitor, by `input.ws.url`; a monitor with neither field would render nameless.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "61-74",
          "snippet": "                let get = |k: &str| {\n                    input\n                        .get(k)\n                        .and_then(serde_json::Value::as_str)\n                        .map(str::to_string)\n                };\n                // A websocket monitor has no command: its url is the thing to name.\n                let command = get(\"command\").or_else(|| {\n                    input\n                        .get(\"ws\")\n                        .and_then(|w| w.get(\"url\"))\n                        .and_then(serde_json::Value::as_str)\n                        .map(str::to_string)\n                });"
        }
      ],
      "instrument": "Read the Monitor tool's live input schema from the harness tool listing, then census the corpus: `rg -c '\"name\":\"Monitor\"' ~/.claude/projects --glob '*.jsonl'` and enumerate the key sets of the matching `tool_use` inputs. Counting rule: one key-set observation per `tool_use` block whose `name` is `Monitor`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -o 'var H=3600000,B=1800000,j=300000;.{0,2200}' cc258.strings.txt ; csift search 'Monitor' -t agent.tool.use --raw --max-count 0 | python3 (census the input key sets of every tool_use block named Monitor)",
          "observed": "Binary, verbatim: `var H=3600000,B=1800000,j=300000;` then `function ie(){return{description:i().describe(\"Short human-readable description of what you are monitoring (shown in notifications).\"),timeout_ms:A().min(1000).optional().default(j).describe(`Kill the monitor after this deadline. Default ${j}ms, max ${H}ms. Ignored when persistent is true.`),persistent:M().optional().default(!1).describe(\"Run for the lifetime of the session (no timeout). Use for session-length watches like PR monitoring or log tails. Stop with TaskStop.\")}}`, the ws sub-schema `se=()=>c({url:i().refine(...).refine((e)=>{...return(o.protocol===\"ws:\"||o.protocol===\"wss:\")&&!o.username&&!o.password...},\"url must be a valid ASCII ws:// or wss:// URL with no userinfo or whitespace\"),protocols:R(i().regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/,\"protocol must be an RFC 6455 token\")).refine(...).optional()})`, the assembly `ue=m(()=>tt({...ie(),command:re().optional().describe(oe),ws:se().optional()}).refine((e)=>le(e.command,e.ws),\"exactly one of command or ws\").refine(ce,ae))` with `ae={message:\"timeout_ms must be <= ${H}\",path:[\"timeout_ms\"]}` and `ce(e){return e.persistent||e.timeout_ms<=H}`, and the remote clamp `function yjn(e){if(!a.CLAUDE_CODE_REMOTE)return{timeout_ms:e.timeout_ms,persistent:e.persistent};return{timeout_ms:e.persistent?B:Math.min(e.timeout_ms,B),persistent:!1}}`. Corpus: 434 tool_use blocks named Monitor, input key sets 405 {command,description,persistent,timeout_ms}, 25 {command,description,persistent}, and 4 blocks whose keys match no schema field; 0 `ws` monitors.",
          "rule": "Schema read verbatim off the 2.1.258 binary; one key-set observation per tool_use block whose name is Monitor, deduped by tool_use id.",
          "note": "Every element of the claim reads back verbatim from 2.1.258: description required, timeout_ms integer min 1000 default 300000 max 3600000 and ignored when persistent, persistent boolean default false, exactly one of command or ws, ws restricted to ws:// or wss:// with an optional RFC 6455 protocols array, and the remote clamp to 1800000 with persistent forced false. The websocket form is unexercised on this machine (0 of 434 arms), so the ws branch is confirmed from the schema only, not from a record."
        }
      ]
    },
    {
      "id": "BG-007",
      "area": "background-tasks",
      "behavior": "A schema-valid `Monitor` arm is always immediately paired on disk (430 of 430 corpus arms whose input matches the schema had a `Monitor started` result; 4 further Monitor tool_use blocks with inputs matching no schema field got no result at all). The result text has two forms, `Monitor started (task <id>, timeout <N>ms). ...` and `Monitor started (task <id>, persistent - runs until TaskStop or session end). ...`, each continuing `You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.`. `toolUseResult` is exactly `{taskId, timeoutMs, persistent}` (absent on the subagent lane, 3 of 430), with `timeoutMs` 0 when `persistent` is true.",
      "depends": "csift extracts the monitor id after the literal `(task ` and falls back to `toolUseResult.taskId`, so an armed monitor never appears as an unreturned call and is visible only through the background section.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "10-17",
          "snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//!   websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//!   started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//!   with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//!   tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//!   close it; a termination notice (`<status>completed</status>`, summary opening\n//!   `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//!   (measured: 30% of armed monitors produced no notification at all)."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "100-111",
          "snippet": "                if let Some(task) = tasks.get_mut(tuid) {\n                    // The shell result: the task id + output path live in the text (and\n                    // the id also in `toolUseResult.backgroundTaskId`); a Monitor arm reads\n                    // `Monitor started (task <id>, …)` with `toolUseResult.taskId`.\n                    let text = content\n                        .as_ref()\n                        .map(crate::model::tool_result_content_text)\n                        .unwrap_or_default();\n                    if task.id.is_none() {\n                        task.id = after_marker(&text, \"with ID: \")\n                            .or_else(|| after_marker(&text, \"(task \"))\n                            .or_else(|| {"
        }
      ],
      "instrument": "`rg -o 'Monitor started \\(task [a-z0-9]{9}' ~/.claude/projects --glob '*.jsonl' | wc -l` against `rg -c '\"name\":\"Monitor\"'`, then join each arm's `tool_use` id to a `tool_result` in the same file. Counting rule: one arm per `tool_use` block; an arm is unreturned when no `tool_result` block in the same file carries its id.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -o '.{0,250}3600000.{0,250}' cc258.strings.txt ; csift search 'Monitor' -t agent.tool.use --raw --max-count 0 and csift search 'Monitor started' --raw --max-count 0, joined in python on tool_use id",
          "observed": "Binary, verbatim: `return{data:{taskId:r,timeoutMs:T?0:g,persistent:T}}`. Corpus: 434 Monitor tool_use blocks, 430 paired with a `Monitor started` result, 4 unreturned - and all 4 of those have inputs that match no schema field (key sets `()`, `('agents',)`, `('max_results','query')`, and one using `timeout` instead of `timeout_ms`); among the 430 schema-shaped arms the pairing is 430 of 430. Result text has exactly two forms: 274 `Monitor started (task <id>, persistent - runs until TaskStop or session end). You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.` and 156 `Monitor started (task <id>, timeout <N>ms). You will be notified on each event. Keep working - do not poll or sleep. Events may arrive while you are waiting for the user - an event is not their reply.`. toolUseResult is exactly {persistent, taskId, timeoutMs} on 427 and absent on 3 (subagent lane). persistent true implied timeoutMs 0 on 274 of 274; the largest non-persistent timeoutMs observed was 3600000.",
          "rule": "One arm per tool_use block named Monitor deduped by tool_use id; an arm is paired when some tool_result block in the corpus names its id AND that result's text starts with `Monitor started`; unreturned means no tool_result at all names the id.",
          "note": "Three corrections. The 'none unreturned' absolute is refuted at 4 of 434, though every one of the four is a malformed call rather than a real arm, so the operational claim - a well-formed monitor never shows up as a pending call - stands. The quoted result text is missing its final sentence about events not being the user's reply, and the persistent branch has its own wording with no `timeout <N>ms` at all. The `timeoutMs` 0-when-persistent rule reads back verbatim from the binary. src/live/background.rs:10-17 and src/live/background_scan.rs:100-111 are verbatim."
        }
      ]
    },
    {
      "id": "BG-008",
      "area": "background-tasks",
      "behavior": "A command Monitor is a `local_bash` task in the harness, so its task id is drawn from the SAME `b`-prefixed namespace as a backgrounded shell's; only the tool NAME distinguishes a monitor from a shell.",
      "depends": "csift classifies a background row's kind from the tool name, never from the id shape; an id-prefix classifier would mislabel every backgrounded shell as a monitor and merge the two into one id space.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "10-17",
          "snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//!   websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//!   started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//!   with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//!   tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//!   close it; a termination notice (`<status>completed</status>`, summary opening\n//!   `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//!   (measured: 30% of armed monitors produced no notification at all)."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "75-80",
          "snippet": "                tasks.entry(id.clone()).or_insert(BgTask {\n                    kind: if name == \"Monitor\" {\n                        BgKind::Monitor\n                    } else {\n                        BgKind::Shell\n                    },"
        }
      ],
      "instrument": "`rg -o 'Monitor started \\(task [a-z0-9]+' ~/.claude/projects --glob '*.jsonl' | sort -u` against `rg -o '\"backgroundTaskId\":\"[a-z0-9]+\"' | sort -u`: both sets start with `b`. Counting rule: distinct ids on each side after dedupe.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; CHANGELOG 0.10.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -o '.{0,120}local_bash.{0,320}' cc258.strings.txt ; csift search 'Monitor started' --raw --max-count 0 and csift search 'Command running in background with ID' --raw --max-count 0 | python3 (collect toolUseResult.taskId and toolUseResult.backgroundTaskId, dedupe, compare length, first character and overlap)",
          "observed": "Binary: `var pi={local_bash:\"b\",local_agent:\"a\",remote_agent:\"r\",in_process_teammate:\"t\",local_workflow:\"w\",monitor_mcp:\"m\",monitor_ws:\"s\",mcp_task:\"k\",dream:\"d\",auto_mode_scan:\"e\"}` - a command monitor has no dedicated prefix, while the two MCP/websocket monitor kinds do (`m`, `s`). Corpus: 427 distinct monitor taskIds, all 9 characters and all first-character `b`; 2707 distinct shell backgroundTaskIds, all 9 characters and all first-character `b`; the two sets overlap in 0 ids.",
          "rule": "Distinct ids after dedupe on each side; a shared namespace is evidenced by identical length and prefix with disjoint draws, since a per-kind prefix would separate them.",
          "note": "Confirmed from both ends. The binary's prefix map has entries for monitor_mcp (`m`) and monitor_ws (`s`) but none for a command monitor, and on disk every one of 427 command-monitor task ids is `b`-led and 9 characters - the same shape and the same namespace as the 2707 backgrounded-shell ids, with zero collisions. So an id-prefix classifier really cannot tell a command monitor from a backgrounded shell, and csift's use of the tool name is the only sound discriminator. A websocket monitor would fall in the `s` namespace instead, but none exist on this machine."
        }
      ]
    },
    {
      "id": "BG-009",
      "area": "background-tasks",
      "behavior": "A harness background-task id is exactly 9 characters - one kind-prefix character plus 8 drawn from `0123456789abcdefghijklmnopqrstuvwxyz` - with the prefix map `local_bash:\"b\"`, `local_agent:\"a\"`, `remote_agent:\"r\"`, `in_process_teammate:\"t\"`, `local_workflow:\"w\"`, `monitor_mcp:\"m\"`, `monitor_ws:\"s\"`, `mcp_task:\"k\"`, `dream:\"d\"`, `auto_mode_scan:\"e\"`, fallback `\"x\"`.",
      "depends": "csift never parses the prefix to decide a task's kind - it uses the tool name - precisely because a command Monitor is a `local_bash` task and shares the `b` namespace with backgrounded shells.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "75-80",
          "snippet": "                tasks.entry(id.clone()).or_insert(BgTask {\n                    kind: if name == \"Monitor\" {\n                        BgKind::Monitor\n                    } else {\n                        BgKind::Shell\n                    },"
        },
        {
          "path": "src/live/background.rs",
          "lines": "10-17",
          "snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//!   websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//!   started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//!   with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//!   tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//!   close it; a termination notice (`<status>completed</status>`, summary opening\n//!   `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//!   (measured: 30% of armed monitors produced no notification at all)."
        }
      ],
      "instrument": "`rg -o '<task-id>[a-z0-9]{9}</task-id>' ~/.claude/projects --glob '*.jsonl' | sed 's/.*>\\(.\\).*/\\1/' | sort | uniq -c` - only prefix characters from the map appear. Counting rule: one observation per extracted 9-character id; ids of other lengths (the 17-character `a`-led agent id) are a different namespace and must be excluded.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -o '.{0,120}local_bash.{0,320}' cc258.strings.txt ; csift search '' -t harness.notification --raw --max-count 0 | python3 (extract every <task-id>, bucket by length and first character) ; plus one live background launch in this session",
          "observed": "Binary, verbatim: `var pi={local_bash:\"b\",local_agent:\"a\",remote_agent:\"r\",in_process_teammate:\"t\",local_workflow:\"w\",monitor_mcp:\"m\",monitor_ws:\"s\",mcp_task:\"k\",dream:\"d\",auto_mode_scan:\"e\"},yt=\"0123456789abcdefghijklmnopqrstuvwxyz\";function Yh(h){let x=pi[h]??\"x\",I=ci(8),O=x;for(let W=0;W<8;W++)O+=yt[I[W]%yt.length];re...` - one prefix character from the map (fallback `\"x\"`) plus 8 characters drawn from that 36-character alphabet, so exactly 9. Corpus: of 2692 classified notification sections, 2510 carry a 9-character `<task-id>` (2369 prefix `b`, 141 prefix `w`) and 183 carry a 17-character `a`-led id (the async agentId, a different namespace). A live background launch in this session minted a 9-character `b`-led id.",
          "rule": "One observation per `<task-id>` extracted from a `<task-notification>` section in a record csift classifies harness.notification.*, bucketed by length then first character; 17-character `a`-led ids are excluded as agentIds, per the claim's own rule.",
          "note": "The prefix map, the alphabet, the `x` fallback and the 1+8 = 9 length all read back verbatim from 2.1.258 - this is the strongest-evidenced claim in the set. Only two of the ten kinds are exercised on disk here (`b` local_bash 2369, `w` local_workflow 141); the other eight prefixes are confirmed from the map alone. Note that `local_agent`'s `a` prefix yields a 9-character TASK id, which is a different thing from the 17-character `a`-led agentId, and no 9-character `a`-led id appears in this corpus."
        }
      ]
    },
    {
      "id": "BG-010",
      "area": "background-tasks",
      "behavior": "A background task's completion is a `<task-notification>` whose inner tags are `<task-id>`, `<tool-use-id>`, `<output-file>`, `<status>` and `<summary>`, optionally plus `<usage>`, `<result>`, `<note>`, `<diagnostics>`, `<failures>` or `<recovery>`; a Monitor EVENT pulse is a different, three-tag shape `{event, summary, task-id}` with no `<tool-use-id>` and no `<status>` (838 of 2692 corpus sections). The `<tool-use-id>` equals the LAUNCHING tool_use id exactly - 1665 of 1849 corpus sections resolved to a launch still on disk, and none disagreed.",
      "depends": "csift keys its task map by the launching tool_use id and resolves carriers by `<tool-use-id>` first and any `<task-id>` second, latest carrier wins; without the tool_use id join a task launched from a subagent lane cannot be matched to its completion at all.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "19-21",
          "snippet": "//! Completion is a `<task-notification>` whose `<tool-use-id>` equals the launching\n//! tool_use id (an exact join; the 9-char `backgroundTaskId` is a second key, absent on\n//! 43% of subagent-lane launches), with `<status>` completed | failed | killed | stopped."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "205-216",
          "snippet": "    for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n        let task_ids = all_xml_tags(section, \"task-id\");\n        let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n        carriers.push(Carrier {\n            task_ids,\n            tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n            status: extract_xml_tag(section, \"status\"),\n            event: extract_xml_tag(section, \"event\"),\n            ts: rec.timestamp.clone(),\n            orphan_summary: orphan,\n        });\n    }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "279-291",
          "snippet": "/// Join carriers to launches: `<tool-use-id>` first (exact), any `<task-id>` second.\n/// The latest carrier wins (an agent notifies again after a resume).\npub(crate) fn resolve_carriers(\n    tasks: &mut BTreeMap<String, BgTask>,\n    carriers: &[Carrier],\n    notes: &mut Vec<String>,\n) {\n    let mut by_id: BTreeMap<String, String> = BTreeMap::new();\n    for (tuid, t) in tasks.iter() {\n        if let Some(id) = &t.id {\n            by_id.insert(id.clone(), tuid.clone());\n        }\n    }"
        }
      ],
      "instrument": "For each background launch, search the main transcript for its launching tool_use id and read the notification section that names it: `csift search '<tool_use_id>' @<session>`; compare the resolved count with `csift status @<session> --format json | jq '.background'`. Counting rule: one completion per launching tool_use id, latest carrier wins.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "A live background launch in this session, then reading the `<task-notification>` it produced and comparing its `<tool-use-id>` with the launching tool_use id read back off disk via csift search '<the launch description>' @trap:<marker> --raw ; then corpus-wide: csift search '' -t harness.notification --raw --max-count 0 | python3 (tag frequency per section, and join every section's <tool-use-id> against the launch ids collected for background shells, async acks and monitor arms)",
          "observed": "Live: the notification's `<tool-use-id>` was byte-identical to the launching Bash tool_use id, and its inner tags were exactly `<task-id> <tool-use-id> <output-file> <status> <summary>`. Corpus: 2692 notification sections in 2698 records; tag frequencies task-id 2692, summary 2692, status 1854, tool-use-id 1849, output-file 1849, event 838, usage 323, result 322, note 126, diagnostics 65, failures 26, recovery 3 - the claimed five-tag set is exactly 1523 sections (56.6%), and 838 sections are event pulses carrying only {event, summary, task-id}. Join: 1849 distinct `<tool-use-id>`s, of which 1665 matched a launch found on disk (1400 background shells, 155 async acks, 110 monitor arms); the 184 unmatched carry task-ids that are 141 workflow (`w`), 27 async agent (17-char `a`) and 16 shell (`b`) whose launching records were not in scope. No `<tool-use-id>` was observed to disagree with the launch it named.",
          "rule": "One section per `<task-notification>` block in a record csift classifies harness.notification.*, deduped by record uuid; a tag counts once per section; a join succeeds when the section's `<tool-use-id>` string equals a collected launch tool_use id exactly.",
          "note": "The join key is confirmed the strongest way available - a launch made in this session and matched against the notification it produced minutes later - and at corpus scale nothing contradicts it. The tag list needed widening: six further tags appear (usage and result on about 12% of sections each), and the claim's five-tag shape is only 56.6% of sections because Monitor event pulses are a distinct three-tag form. The '2502 of 2502' figure is not reproducible against this corpus and has been replaced by a stated denominator; the 184 unresolved ids are workflow tasks and launches whose transcripts are out of scope, not join failures. src/live/background.rs:19-21 and src/live/background_scan.rs:215-226 and :249-261 are verbatim."
        }
      ]
    },
    {
      "id": "BG-011",
      "area": "background-tasks",
      "behavior": "The terminal `<status>` values form a small OPEN set: `completed`, `failed` (its summary ends `failed with exit code N`), `killed` (summary `Background command \"X\" was stopped`) and `stopped`.",
      "depends": "csift maps those four literals onto its background state enum with `completed` as the fallback, so a NEW status value would silently be booked as completed rather than surfaced.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "44-74",
          "snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n    /// Launched, no completion carrier names it yet.\n    Open,\n    Completed,\n    Failed,\n    Killed,\n    /// Claude Code's own orphan reconciliation at the next session start, or an\n    /// explicit `stopped` status.\n    Stopped,\n    /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n    TimedOut,\n}"
        },
        {
          "path": "src/live/background.rs",
          "lines": "89-96",
          "snippet": "    pub(crate) fn from_status(status: Option<&str>) -> Self {\n        match status {\n            Some(\"failed\") => BgState::Failed,\n            Some(\"killed\") => BgState::Killed,\n            Some(\"stopped\") => BgState::Stopped,\n            _ => BgState::Completed,\n        }\n    }"
        }
      ],
      "instrument": "The ledger's `rg -o '<status>[a-z]*</status>' ~/.claude/projects | sort | uniq -c` over-reads. Run over a single project directory it returns a FIFTH value, <status>running</status> x8, and every one of those is a csift unit-test fixture quoted inside a tool_result or an assistant message in a dev transcript. Parse the three carrier fields (a type:\"user\" string content, a queue-operation content, a queued_command attachment prompt) instead of grepping raw bytes.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'function ka\\('",
          "observed": "Delivered type:\"user\" notification sections corpus-wide: <status>completed</status> 1808, failed 83, killed 80, stopped 5 - no fifth value (all three carriers together: completed 8693, failed 412, killed 195, stopped 10). Summary shapes per status: 79 of 83 failed end \"failed with exit code N\"; 78 of 80 killed end '\" was stopped'; 1366 completed end \"completed (exit code N)\", 132 are 'Agent \"X\" finished', 111 open with \"Monitor\". Binary: function ka({taskId:e,toolUseId:n,taskType:r,outputFile:o,status:d,summary:f,body:_,trailing:v}){let C=[[Use,e],[jdr,n],[qBe,r],[Gdr,o],[zx,d],[j0,f]]  - the emitter takes status as a free-form string and writes the tag only when it is truthy, so the set is genuinely open rather than a typed enum.",
          "rule": "One observation per <task-notification> section inside a delivered type:\"user\" string record, so the queue-operation and attachment echoes of the same delivery are not counted three times. A section with no <task-id> is prose that merely quotes the marker and is dropped.",
          "note": "The behavior text needed no change: four values, and open by construction. The corpus cannot distinguish an open set from a closed one, but the binary can, and it shows a free-string status - which is what makes csift's `_ => BgState::Completed` fallback in from_status a silent-mislabel risk rather than a safe default. Both code snippets (src/live/background.rs:62-74 and 89-96) are verbatim in the current file."
        }
      ]
    },
    {
      "id": "BG-012",
      "area": "background-tasks",
      "behavior": "toolUseResult.backgroundTaskId is a 9-character SECOND key, never the primary join key: it was present on 2533 of the 2533 main-lane launches that have a result record (one further main-lane launch never returned a result), but on only 350 of 624 subagent-lane launches (274 carry none, 43.9%).",
      "depends": "csift joins on `<tool-use-id>` first and any `<task-id>` second; making `backgroundTaskId` primary would strand 43% of subagent-lane launches as permanently open.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "19-21",
          "snippet": "//! Completion is a `<task-notification>` whose `<tool-use-id>` equals the launching\n//! tool_use id (an exact join; the 9-char `backgroundTaskId` is a second key, absent on\n//! 43% of subagent-lane launches), with `<status>` completed | failed | killed | stopped."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "108-117",
          "snippet": "                    if task.id.is_none() {\n                        task.id = after_marker(&text, \"with ID: \")\n                            .or_else(|| after_marker(&text, \"(task \"))\n                            .or_else(|| {\n                                let v = rec.tool_use_result_value()?;\n                                v.get(\"backgroundTaskId\")\n                                    .or_else(|| v.get(\"taskId\"))?\n                                    .as_str()\n                                    .map(str::to_string)\n                            });"
        }
      ],
      "instrument": "For every background launch, parse `toolUseResult` on the paired result and test for a `backgroundTaskId` key, bucketing by whether the file path contains a `subagents` component. Counting rule: one presence/absence observation per launch keyed (file, tool_use id).",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify2.py - a walk of ~/.claude/projects/**/*.jsonl that json-parses only lines carrying one of the byte needles run_in_background | 'Command running in background' | async_launched | task-notification | 'Monitor started' | '\"Monitor\"' | queued_command | backgroundTaskId, ingests launches exactly as src/live/background_scan.rs ingest_launches does, and reads notifications ONLY from the three carrier fields ingest_carriers reads; python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section",
          "observed": "Main lane: 2534 backgrounded-shell launches; 2533 of them have a paired tool_result on disk and 2533 of those 2533 carry toolUseResult.backgroundTaskId (the single exception never returned a result at all, so it carries no key rather than lacking one). Subagent lane: 624 launches, all with a result on disk - 350 carry backgroundTaskId, 274 do not (43.9%). Task-id length census over delivered notifications: 9 characters x2626, 17 x192, 42 x2.",
          "rule": "One presence/absence observation per launch keyed (file, tool_use id). A launch is subagent-lane iff its file path has a `subagents` component. Presence = the literal key backgroundTaskId in the paired tool_result record's toolUseResult object; the 9-character family is the shell/monitor id namespace, 17 characters is the async-agent id.",
          "note": "Ratio unchanged, denominators grew with the corpus. The asymmetry is real and reproduces: every main-lane shell result that exists carries the key, while nearly half of subagent-lane ones do not, so a backgroundTaskId-primary join would strand them. Both code snippets are verbatim (src/live/background.rs:19-21 module doc, src/live/background_scan.rs:108-117)."
        }
      ]
    },
    {
      "id": "BG-013",
      "area": "background-tasks",
      "behavior": "...1015 of 2520 returned main-lane shells (40.3%) never produce a user record. The queue pair is `operation:\"enqueue\"` (no reason) followed by `operation:\"remove\"` carrying a reason: `absorbed_mid_turn` when the notification lands in a turn already in flight, `delivered_to_agent` when it is handed to an idle subagent lane (a further 2145 removes carry no reason).",
      "depends": "csift's carrier ingester reads all three carrier shapes; restricting it to `type:\"user\"` records loses 40% of shell completions, leaving them permanently `open` and flipping the `idle-background-open` verdict.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "22-27",
          "snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "225-236",
          "snippet": "pub(crate) fn carrier_text(rec: &Record) -> Option<String> {\n    if rec.is_type(\"queue-operation\") {\n        rec.content_str().map(str::to_string)\n    } else if rec.attachment_type().as_deref() == Some(\"queued_command\") {\n        rec.attachment_value()\n            .and_then(|v| v.get(\"prompt\")?.as_str().map(str::to_string))\n    } else if let Some(Content::Text(s)) = rec.message.as_ref().and_then(|m| m.content.as_ref()) {\n        Some(s.clone())\n    } else {\n        None\n    }\n}"
        }
      ],
      "instrument": "Take a known background task id from a launch result and `rg -n '<that id>' <the main transcript>`, then classify each matching line by its top-level `type`. Counting rule: one carrier observation per matching line, bucketed into `queue-operation` / `attachment` / `user`; a launch with no `user` row but a `queue-operation` row is a mid-turn delivery.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus a live probe: arm a non-persistent Monitor from inside a subagent lane at CC 2.1.258, then `rg -c '<task-id>' <the subagent transcript>` against `rg -c '<task-id>' <the parent main transcript>` and json-decode each matching line",
          "observed": "Notification sections by carrier field corpus-wide: queue-operation 8088, user 2964, attachment 2132. Every one of the 2132 attachment carriers has attachment.commandMode == \"task-notification\". queue-operation fields: enqueue with no reason 5803, remove with no reason 2145, remove/absorbed_mid_turn 129, remove/delivered_to_agent 12. Of 2520 main-lane shell launches that were joined to a completion carrier, 1015 (40.3%) have no type:\"user\" carrier at all. Live probe: one Monitor arm produced two notifications, and for both the only on-disk carriers were, in the parent main transcript, ('queue-operation','enqueue',reason None) and ('queue-operation','remove','absorbed_mid_turn') - no user record anywhere.",
          "rule": "One carrier observation per (record, <task-notification> section), bucketed by which of the three fields supplied the text. A launch counts as \"no user record\" when every carrier joined to it (by <tool-use-id> first, any <task-id> second) is a queue-operation or attachment.",
          "note": "The three-carrier law reproduces exactly, live and in the corpus, and the attachment's commandMode is 100% consistent. The new detail is the `reason` field on the remove line: it names WHY no user record was written, and `delivered_to_agent` is the mechanism behind the BG-014 exception. Both code snippets are verbatim."
        }
      ]
    },
    {
      "id": "BG-014",
      "area": "background-tasks",
      "behavior": "Background-task launches appear in EVERY lane and completions land in the MAIN transcript: 613 of 624 subagent-lane shell launches were completed there. Notifications are NOT strictly absent from subagent transcripts - when the harness delivers one to an IDLE agent it writes it into that agent's own transcript as an isMeta:true user record (12 measured, in 5 child files, CC 2.1.159 through 2.1.258), and every such delivery is also on the parent's queue-operation enqueue + remove/delivered_to_agent line - so a main-only carrier read still sees all of them.",
      "depends": "csift reads launches from every lane but carriers only from the main transcript, resolving the main file from a subagent target; scoping the notification index per file instead of per session root inflated never-returned from 24 to 631, a 26x false-positive rate.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "22-27",
          "snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
        },
        {
          "path": "src/live/background.rs",
          "lines": "250-264",
          "snippet": "pub(crate) fn main_transcript_for(path: &Path) -> PathBuf {\n    if !crate::subagent::is_subagent_path(path) {\n        return path.to_path_buf();\n    }\n    let mut dir = path.parent();\n    while let Some(d) = dir {\n        if d.file_name().and_then(|n| n.to_str()) == Some(\"subagents\") {\n            if let Some(session_dir) = d.parent() {\n                return session_dir.with_extension(\"jsonl\");\n            }\n        }\n        dir = d.parent();\n    }\n    path.to_path_buf()\n}"
        },
        {
          "path": "src/live/background.rs",
          "lines": "303-306",
          "snippet": "            ingest_launches(&rec, &lane, &mut tasks);\n            if is_main {\n                ingest_carriers(&rec, &mut carriers, &mut notes);\n            }"
        }
      ],
      "instrument": "Take a background id launched inside a `subagents/agent-*.jsonl` and `rg -c '<that id>'` in the child file against the parent transcript: the completion appears only in the parent. Counting rule: one launch per (file, tool_use id), `rg -c` counting matching lines; the e2e test `subagent_launches_complete_in_the_parent_main_transcript` in src/live/tests/background_scan.rs pins it.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type",
          "observed": "624 subagent-lane shell launches; 613 were closed by a carrier in the PARENT main transcript (98.2%). But 14 <task-notification> sections carrying a <task-id> do sit inside 5 subagent transcripts (record `version` 2.1.252 x11, 2.1.258 x2, 2.1.159 x1). 12 of the 14 are genuine deliveries - isMeta:true with a non-empty <summary> (6 read 'Agent \"...\" finished', 6 read 'Background command \"...\" completed') - and each of those 12 pairs with a main-lane queue-operation enqueue plus a remove whose reason is `delivered_to_agent` for the same task id. The remaining 2 have no <summary> and isMeta null: prose quoting the marker.",
          "rule": "One section per (file, <task-notification> block) with at least one <task-id>; lane from the presence of a `subagents` path component; a section counts as a genuine delivery only when the record is isMeta:true and the block has a non-empty <summary>. Pairing = the same task id appearing in a main-lane queue-operation line of the same session root.",
          "note": "The operational law csift depends on survives intact: reading carriers from the main file only loses nothing, because the child-lane copy is always shadowed by the parent's queue line. What fails is the absolute wording 'zero notifications exist in any subagent transcript' - it is refuted by 12 genuine records, the oldest at CC 2.1.159, so this is a sampling miss in the original two-specimen measurement, not a version change. All three code snippets are verbatim (src/live/background.rs:22-27, 250-264, 303-306)."
        }
      ]
    },
    {
      "id": "BG-015",
      "area": "background-tasks",
      "behavior": "A Monitor armed by a SUBAGENT is delivered into that subagent's conversation (the emitter passes the owning agent id), but its <task-notification> is persisted ONLY in the parent MAIN transcript, as a queue-operation enqueue + remove/absorbed_mid_turn pair. Measured at CC 2.1.258 by arming one, and across 39 subagent-lane arms in the corpus: zero monitor notifications in any child transcript.",
      "depends": "csift joins every completion through the three carriers `background_scan::carrier_text` reads - a user string record (the idle delivery), a `queue-operation` enqueue line and a `queued_command` attachment (the mid-turn delivery) - and since v0.10.2 `wait --until notification` and the wait activity census read the same three carriers through `delivered_pulse_labels`; a queue `remove`/`dequeue` repeats the enqueue's pulse and delivers nothing. Before v0.10.2 the condition matched only `automation_label`, which requires a `message{}` string, so a pulse absorbed mid-turn (roughly every other completion) could never fire it.",
      "code": [
        {
          "path": "src/live/conditions.rs",
          "lines": "16-18",
          "snippet": "    /// A task-notification lands in the MAIN transcript (they never land in children),\n    /// optionally payload-matched.\n    Notification(Option<regex::Regex>),"
        },
        {
          "path": "src/live/conditions.rs",
          "lines": "111-115",
          "snippet": "    match cond {\n        Cond::Notification(re) => {\n            if !is_main {\n                return false;\n            }"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "180-184",
          "snippet": "    pub fn automation_label(&self) -> Option<String> {\n        let content = self.message.as_ref()?.content.as_ref()?;\n        let Content::Text(s) = content else {\n            return None;\n        };"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "242-257",
          "snippet": "pub(crate) fn delivered_pulse_labels(rec: &Record) -> Vec<String> {\n    if rec.is_type(\"queue-operation\") && rec.operation.as_deref() != Some(\"enqueue\") {\n        return Vec::new();\n    }\n    let Some(text) = carrier_text(rec) else {\n        return Vec::new();\n    };\n    text.split(TASK_NOTIFICATION_PREFIX)\n        .skip(1)\n        .map(|section| {\n            crate::model::automation_label_for_section(&format!(\n                \"{TASK_NOTIFICATION_PREFIX}{section}\"\n            ))\n        })\n        .collect()\n}"
        },
        {
          "path": "src/live/conditions.rs",
          "lines": "112-126",
          "snippet": "        Cond::Notification(re) => {\n            if !is_main {\n                return false;\n            }\n            // The idle delivery is a user record; a pulse absorbed mid-turn lands ONLY\n            // on a queue-operation enqueue line and a queued_command attachment\n            // (v0.10.2: the three carriers `background_scan` joins, not the user\n            // record alone - the agents-stopped notice stays a user-record label).\n            let mut labels = crate::live::delivered_pulse_labels(rec);\n            if labels.is_empty() {\n                let Some(label) = rec.automation_label() else {\n                    return false;\n                };\n                labels.push(label);\n            }"
        }
      ],
      "instrument": "Arm a Monitor from inside a subagent and let it fire, then `rg -c '<task-notification>' <that subagents/agent-*.jsonl>` - a nonzero count refutes the main-only law. Counting rule: one carrier per matching line inside a subagent transcript; for background-shell completions the same count is zero corpus-wide.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "Live probe at CC 2.1.258: Monitor(command='sh -c \\'sleep 3; echo LEDGERPROBE_EVENT_ONE; sleep 120\\'', persistent=false, timeout_ms=20000) armed from inside a running subagent lane, then `rg -c '<task-id>' ~/.claude/projects/<project>/<session>/subagents/workflows/<wf>/agent-<id>.jsonl` against `rg -c '<task-id>' ~/.claude/projects/<project>/<session>.jsonl`, json-decoding every matching line; plus python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Monitor timed out'",
          "observed": "The arm returned 'Monitor started (task <9-char id>, timeout 20000ms)'. Both the event pulse (<event>LEDGERPROBE_EVENT_ONE</event>) and the timeout notice (<event>[Monitor timed out \\u2014 re-arm if needed.]</event>, em dash in the real bytes) were DELIVERED into the subagent's own conversation, but on disk both were written ONLY to the parent main transcript, twice each as ('queue-operation','enqueue') and ('queue-operation','remove','absorbed_mid_turn'). The subagent's own transcript holds zero notification records for that task id: its 2 raw byte matches are the probe's own Bash tool_use and its tool_result echoing the id. Corpus agrees: of 39 subagent-lane Monitor arms, 37 notified into the main transcript, 2 never notified, and 0 notified into a child lane. The binary does pass an owning agent id on the emit - JM(f,\"[Monitor timed out \\u2014 re-arm if needed.]\",x,{isHousekeeping:!0,agentId:O}) - but that is a delivery address, not a transcript destination.",
          "rule": "One notification per matching line; a line counts as a carrier only when the <task-notification> text comes from a type:\"user\" string content, a queue-operation content, or a queued_command attachment prompt - a tool_use or tool_result that merely contains the id is not a carrier. Landing lane = the presence of a `subagents` component in the path of the file holding the carrier.",
          "note": "Refuted by the decisive instrument the claim itself named. The conversational delivery is real - the pulse and the timeout both arrived in the subagent's own turn - which is presumably what the claim generalized from; the on-disk consequence it asserted does not follow. No version change was established: 2.1.258 was measured live and the 39 corpus arms span 2.1.191 to 2.1.252, all behaving the same way. Both code snippets (src/live/conditions.rs:16-18 and 111-115) are verbatim."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "drifted",
          "instrument": "python line census over the 67 main transcripts of this corpus: every line containing `<task-notification>` bucketed by line type (user record with role user / queue-operation enqueue / other queue operation / attachment)",
          "observed": "3218 pulse-bearing user records, 5893 queue enqueue lines, 2315 other queue operations, 4480 attachment carriers: the enqueue count exceeds the user-record count by 2675, the pulses absorbed mid-turn",
          "rule": "one line = one carrier; a pulse with an enqueue line and no user record was absorbed mid-turn",
          "note": "csift 0.10.2: Cond::Notification and the wait activity census read the enqueue line and the queued_command attachment through live::delivered_pulse_labels (a remove/dequeue counts nothing); pinned by the e2e p12_notification_fires_on_a_queue_enqueue_line_not_on_its_remove and the activity unit test"
        }
      ]
    },
    {
      "id": "BG-016",
      "area": "background-tasks",
      "behavior": "...an EVENT pulse carrying (task-id, summary, event) and NO <status> (841 sections measured, all with <event>), and a TERMINATION notice carrying (task-id, tool-use-id, output-file, status, summary) whose <status> was completed in 111 of 111 cases.",
      "depends": "csift keeps a monitor OPEN through event pulses (a carrier with no `<status>` and no timeout event leaves the state untouched) and closes it only on the termination notice or a timeout event; treating a pulse as a completion closes a persistent monitor on its first event. The rendered label likewise falls back to the `<event>` payload instead of fabricating `completed`.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "309-322",
          "snippet": "                let state = if c.orphan_summary {\n                    orphaned += 1;\n                    Some(BgState::Stopped)\n                } else if c.status.is_some() {\n                    Some(BgState::from_status(c.status.as_deref()))\n                } else if c\n                    .event\n                    .as_deref()\n                    .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n                {\n                    Some(BgState::TimedOut)\n                } else {\n                    None // a Monitor event pulse: the monitor is still armed\n                };"
        },
        {
          "path": "src/model/automation.rs",
          "lines": "93-97",
          "snippet": "    /// The `<event>` payload, if present - where a Monitor / ScheduleWakeup pulse carries its\n    /// real outcome (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`). Often\n    /// the only outcome signal on a Monitor pulse (which usually has no `<status>`), so the\n    /// label falls back to it instead of fabricating `completed`.\n    pub event: Option<String>,"
        }
      ],
      "instrument": "On a monitor-rich transcript, extract every `<task-notification>` block in `type:\"user\"` records whose `<summary>` first word is `Monitor` and bucket by presence of `<status>` against `<event>`. Counting rule: one observation per notification BLOCK (a record may hold several), counting only delivered `type:\"user\"` records so the queue and attachment echoes are not triple-counted; the unit test `a_monitor_is_open_through_event_pulses_until_it_ends_or_times_out` in src/live/tests/background_scan.rs pins the state machine.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "src/model/automation.rs comment; src/live/background.rs module doc; CHANGELOG 0.10.0; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify6.py / bgverify7.py - the same walk restricted to type:\"user\" string records, censusing <status>, <summary> shape and <task-id> length per notification section; plus the live Monitor probe described under BG-015",
          "observed": "Delivered type:\"user\" notification sections whose <summary> starts with 'Monitor': 841 event pulses carrying <event> and NO <status>, plus 111 termination notices carrying <status>, and <status> was completed in 111 of 111. Zero sections had neither a status nor an event. The live probe reproduced both shapes from one arm: an event pulse (<summary>Monitor event: \"...\"</summary><event>LEDGERPROBE_EVENT_ONE</event>, no status) and then the timeout pulse, also with no status.",
          "rule": "One observation per <task-notification> section in a delivered type:\"user\" record whose <summary> begins with the word Monitor, so the queue and attachment echoes are not triple-counted; bucketed by presence of <status> against presence of <event>.",
          "note": "The 111-of-111 figure reproduces exactly; the pulse count grew 828 -> 841 with the corpus. The two shapes are cleanly separated - no section carries both, none carries neither - so csift's rule (a carrier with no status and no timeout event leaves the state untouched) has no ambiguous input in this corpus. Both code snippets are verbatim (src/live/background_scan.rs:279-292, src/model/automation.rs:93-97)."
        }
      ]
    },
    {
      "id": "BG-017",
      "area": "background-tasks",
      "behavior": "A Monitor timeout is delivered as an event pulse whose <event> payload is the literal [Monitor timed out \\u2014 re-arm if needed.] - with an EM DASH (U+2014), not a hyphen. It is emitted from exactly two sites in the binary and never from a Monitor tool_result; 51 of the 54 corpus sections carrying the phrase have it inside <event>, the other 3 being one async-agent completion notification that quotes it. It fires only for a non-persistent monitor - the timeout timer is not scheduled when persistent is set.",
      "depends": "csift closes a monitor as `timed-out` on a case-insensitive `timed out` substring of the `<event>` payload rather than on the exact bracketed literal, so a reworded template still closes the task.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "44-74",
          "snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n    /// Launched, no completion carrier names it yet.\n    Open,\n    Completed,\n    Failed,\n    Killed,\n    /// Claude Code's own orphan reconciliation at the next session start, or an\n    /// explicit `stopped` status.\n    Stopped,\n    /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n    TimedOut,\n}"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "309-322",
          "snippet": "                let state = if c.orphan_summary {\n                    orphaned += 1;\n                    Some(BgState::Stopped)\n                } else if c.status.is_some() {\n                    Some(BgState::from_status(c.status.as_deref()))\n                } else if c\n                    .event\n                    .as_deref()\n                    .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n                {\n                    Some(BgState::TimedOut)\n                } else {\n                    None // a Monitor event pulse: the monitor is still armed\n                };"
        }
      ],
      "instrument": "`rg -o 'Monitor timed out.{0,30}' ~/.claude/projects --glob '*.jsonl'` and classify each occurrence by its enclosing structure (inside `<event>` against quoted in some other tool's result text). Counting rule: one occurrence per structural position; the raw line count over-reads about elevenfold because csift's own source and docs quote the literal.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Monitor timed out'; python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; plus the live Monitor probe described under BG-015",
          "observed": "The real bytes use an EM DASH: the live probe delivered <event>[Monitor timed out \\u2014 re-arm if needed.]</event>, and the binary has exactly two emit sites, both JM(<sink>,\"[Monitor timed out \\u2014 re-arm if needed.]\",<taskId>,{isHousekeeping:!0,agentId:<id>}) - one on the shell-monitor path, one on the websocket path. Corpus: 54 notification sections contain the phrase, 51 inside an <event> tag and 3 outside it; the 3 are one async-agent completion notification (the same task id, its main-lane queue pair plus one child-lane copy) whose body quotes the phrase - none is a Monitor tool_result. Non-persistence is structural in the binary: `let D=C?void 0:setTimeout((E,f,x,O,I)=>{...},T,W,g,v.taskId,y,p)` where C is the persistent flag, so no timer is scheduled at all for a persistent monitor; 274 of 432 corpus arms are persistent.",
          "rule": "One occurrence per <task-notification> section containing the phrase, classified by whether the section's <event> value itself contains it. The claim's raw `rg -o` over the corpus over-reads by roughly elevenfold because csift's own source, docs and dev transcripts quote the literal.",
          "note": "csift closes a monitor as timed-out on a case-insensitive `timed out` substring of the <event> payload. That is looser than it looks in this corpus: 6 Monitor event pulses carry payloads ending 'ssh=Connection to 127.0.0.1 port 2224 timed out', i.e. the MONITORED command's own output, which the substring rule would book as a monitor timeout. Matching the bracketed literal (or requiring the phrase at the payload start) would remove that false positive. Both code snippets are verbatim."
        }
      ]
    },
    {
      "id": "BG-018",
      "area": "background-tasks",
      "behavior": "Almost every armed monitor does notify: only 7 of 430 distinct armed task ids (1.6%) produced no notification at all. The 30% figure is a counting-rule artifact - counting only notifications delivered as a type:\"user\" record gives 129 of 430 (30.0%), because the other monitors' pulses were all absorbed mid-turn onto the queue-operation and attachment carriers (the BG-013 path).",
      "depends": "csift reports an armed monitor as `open` with the explicit honesty note rather than inferring it is still live; absence of an event is never evidence of absence, and a persistent monitor must never be reported as failed.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "10-17",
          "snippet": "//! - a MONITOR: the `Monitor` tool_use (a command whose stdout lines are events, or a\n//!   websocket), armed on disk as an immediately-paired pair whose result reads `Monitor\n//!   started (task <id>, …)` with `toolUseResult.taskId`. It shares the `b…` id namespace\n//!   with backgrounded shells (both are `local_bash` tasks in the harness), so only the\n//!   tool name tells them apart. Event pulses (`Monitor event: …`, no `<status>`) never\n//!   close it; a termination notice (`<status>completed</status>`, summary opening\n//!   `Monitor`) or a timeout event does; a PERSISTENT monitor never returns by design\n//!   (measured: 30% of armed monitors produced no notification at all)."
        }
      ],
      "instrument": "Build the set of armed monitor task ids (from `Monitor started (task <id>` result texts) and the set of fired ids (from `<task-id>` inside Monitor-summary notifications) and take the difference. Counting rule: distinct ids on both sides, deduped on the id string because one session copied into two project directories duplicates ids.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6.13; src/live/background.rs module doc; CHANGELOG 0.10.0; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type",
          "observed": "432 Monitor arms resolving to 430 distinct task ids (every id recovered from the arm's own result text 'Monitor started (task <id>'). Counting fired ids across ALL THREE carriers, only 7 of 430 armed monitors (1.6%) produced no notification at all - 3 non-persistent and 4 persistent. Restricting the fired set to notifications delivered as a type:\"user\" record raises that to 129 of 430 (30.0%), which reproduces the ledger's 132 of 426. By lane: 388 of 393 main-lane arms and 37 of 39 subagent-lane arms notified.",
          "rule": "Distinct task-id strings on both sides. Armed = the id in the arm's tool_result text. Fired = the id appears in any <task-notification> <task-id> tag within the same session root; the two variants differ only in whether queue-operation and attachment carriers count as a firing.",
          "note": "This is the sharpest correction in the batch: the ledger's headline number measured 'never delivered as a user record', not 'never fired'. The honesty conclusion csift draws is unchanged and still right - an armed monitor is reported open, never inferred live - but the src/live/background.rs module doc's parenthetical '(measured: 30% of armed monitors produced no notification at all)' should read 1.6%, or say 'no user-record notification'. The code snippet at src/live/background.rs:10-17 is verbatim, including that parenthetical."
        }
      ]
    },
    {
      "id": "BG-019",
      "area": "background-tasks",
      "behavior": "...and 12 of 476 joined launches notified two or four times (11 twice, 1 four times); 169 of the 476 were closed with no user-record delivery at all - their notifications rode the queue/attachment carriers.",
      "depends": "csift's carrier resolution takes the LATEST carrier per task, so a resumed agent's re-notification overwrites the earlier state rather than double-closing it.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "279-281",
          "snippet": "/// Join carriers to launches: `<tool-use-id>` first (exact), any `<task-id>` second.\n/// The latest carrier wins (an agent notifies again after a resume).\npub(crate) fn resolve_carriers("
        }
      ],
      "instrument": "`rg -c '<note>' ~/.claude/projects --glob '*.jsonl'` and, for one agent id, count the distinct line numbers of `<task-notification>` records naming it. Counting rule: one notification per distinct matching line in the main transcript; more than one line for one id is a re-notification.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify2.py - a walk of ~/.claude/projects/**/*.jsonl that json-parses only lines carrying one of the byte needles run_in_background | 'Command running in background' | async_launched | task-notification | 'Monitor started' | '\"Monitor\"' | queued_command | backgroundTaskId, ingests launches exactly as src/live/background_scan.rs ingest_launches does, and reads notifications ONLY from the three carrier fields ingest_carriers reads; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'A task-notification fires each time this agent'",
          "observed": "The <note> text is verbatim in the binary, closing tag included: 'A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>', and it appears on 135 delivered user-record sections in the corpus. Of 482 async-agent launches, 476 joined to at least one carrier; counting distinct user-record delivery instants per launch gives 0 -> 169 launches, 1 -> 295, 2 -> 11, 4 -> 1. So 12 launches notified two or four times.",
          "rule": "One launch per (file, tool_use id) whose toolUseResult.status is async_launched; one delivery per distinct timestamp among that launch's type:\"user\" carriers, so the queue-operation and attachment echoes of one delivery are not counted as repeats.",
          "note": "The count of repeat-notifiers reproduces exactly at 12; only the denominator changes (158 -> 476 joined launches). The non-terminality is confirmed at the source: the harness ships the caveat in the notification itself, so csift's latest-carrier-wins resolution is reading a documented contract, not guessing. The code snippet at src/live/background_scan.rs:249-251 is verbatim."
        }
      ]
    },
    {
      "id": "BG-020",
      "area": "background-tasks",
      "behavior": "...plus a sentinel <task-id>__orphan_summary__:<kind></task-id> where kind is one of agent | shell | workflow (only :shell observed here), <status>stopped</status>, and a summary reading 'N background <kind> task(s) from the previous session have no completion record ... They may have been stopped (via the UI, Monitor timeout, or agent teardown \\u2014 these leave no transcript marker)' - an em dash, not a hyphen. The harness also uses a SECOND sentinel form, __orphan_summary_live__:<id>, to exclude a still-live task from the summary.",
      "depends": "csift reads ALL `<task-id>` tags (not just the first), marks every named task `stopped` and discloses the count; that harness sentence is the honesty bound csift adopts - NOT RETURNED IS NOT PROOF OF STILL RUNNING - so absence of a completion is never reported as proof of liveness.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "29-35",
          "snippet": "//! At the next session start Claude Code reconciles orphans itself: one notification\n//! carrying several `<task-id>` tags plus `__orphan_summary__:shell`, status `stopped`,\n//! whose summary says the tasks \"may have been stopped (via the UI, Monitor timeout, or\n//! agent teardown - these leave no transcript marker)\". That sentence is the honesty\n//! bound: NOT RETURNED IS NOT PROOF OF STILL RUNNING. The `<output-file>` from the\n//! launch is a real file (an agent's is a symlink to its transcript); its size and\n//! mtime are an independent \"still producing output\" signal, one `stat` per open task."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "205-216",
          "snippet": "    for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n        let task_ids = all_xml_tags(section, \"task-id\");\n        let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n        carriers.push(Carrier {\n            task_ids,\n            tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n            status: extract_xml_tag(section, \"status\"),\n            event: extract_xml_tag(section, \"event\"),\n            ts: rec.timestamp.clone(),\n            orphan_summary: orphan,\n        });\n    }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "260-277",
          "snippet": "pub(crate) fn all_xml_tags(s: &str, tag: &str) -> Vec<String> {\n    let open = format!(\"<{tag}>\");\n    let close = format!(\"</{tag}>\");\n    let mut out = Vec::new();\n    let mut at = 0usize;\n    while let Some(i) = s[at..].find(&open) {\n        let start = at + i + open.len();\n        let Some(j) = s[start..].find(&close) else {\n            break;\n        };\n        let inner = s[start..start + j].trim();\n        if !inner.is_empty() {\n            out.push(inner.to_string());\n        }\n        at = start + j + close.len();\n    }\n    out\n}"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "330-336",
          "snippet": "    if orphaned > 0 {\n        notes.push(format!(\n            \"{orphaned} task(s) were reconciled as stopped by Claude Code at a later session \\\n             start (its orphan summary: no completion record; a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker)\"\n        ));\n    }"
        }
      ],
      "instrument": "`rg -l '__orphan_summary__:' ~/.claude/projects --glob '*.jsonl'`, then parse a matching line and count its `<task-id>` tags; `csift status @<that session> --format json | jq '.background.notes'` shows the reconciliation note. Counting rule: one reconciliation record per matching line; a record with N real ids plus one `__orphan_summary__:` sentinel reconciles N tasks.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 bgverify3.py / bgverify4.py / bgverify5.py - the same walk, additionally keying every notification by (session root, task-id) so a task's carriers can be listed per lane and per carrier type; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'orphan_summary|no completion record'",
          "observed": "5 sections in the corpus carry an __orphan_summary__ sentinel. They are 2 genuine reconciliation events, each written twice - once as a queue-operation line and once as a type:\"user\" record about 90 ms later - naming 3 and 2 real task ids respectively, plus the sentinel __orphan_summary__:shell and <status>stopped</status>; the fifth section is prose quoting the marker. The summary text matches the claim except for an EM DASH: 'They may have been stopped (via the UI, Monitor timeout, or agent teardown \\u2014 these leave no transcript marker)'. The binary carries the template - '${r.length} background ${o} task(s) from the previous session have no completion record. ... They have been marked ${e}. ... Task ids in this notification beginning with \"${Oe}\" are internal scan markers, not tasks.' - and the sentinel family: Oe=\"__orphan_summary\",tt=`${Oe}__:`,nt=`${Oe}_live__:` with if(d===\"agent\"||d===\"shell\"||d===\"workflow\")o.summarizedKinds.add(d).",
          "rule": "One reconciliation record per matching line; a record with N real ids plus one __orphan_summary__: sentinel reconciles N tasks; a line whose section has no <task-id> is prose quoting the marker and is dropped. Distinct events counted by (timestamp, summary).",
          "note": "The mechanism and the honesty sentence both hold, verified at the binary and in the corpus. Two gaps worth carrying: csift's `t.starts_with(\"__orphan_summary__\")` does not match the __orphan_summary_live__: form (0 occurrences in this corpus, so untested in practice), and the sentinel kind can be agent or workflow, not only shell - the note csift renders says 'shell' nowhere, so it is safe either way. All three code snippets are verbatim (src/live/background_scan.rs:215-226, 230-247, 300-306)."
        }
      ]
    },
    {
      "id": "BG-021",
      "area": "background-tasks",
      "behavior": "Orphan reconciliation aggregates by KIND with different thresholds - shells aggregate above 1 orphan (`i.length>1`), agents and workflows only above 20 (`length>Fe`, Fe=20). The aggregate marks agents `failed` and shells/workflows `stopped`. A truncated aggregate says `First 20 task ids:` and an untruncated one says `Task ids:`. The two sentinels are `__orphan_summary__:` (kind suffix) and `__orphan_summary_live__:` (one per live exclusion); NEITHER has a genuine on-disk specimen in this corpus - all 90 matched records for the two literals are dev-session prose. A single orphan below the threshold arrives as an ordinary one-id notification with <status>stopped</status> (3 on-disk specimens, all shells).",
      "depends": "csift's orphan detection keys on the `__orphan_summary__` prefix of a `<task-id>`, so a below-threshold singular orphan is handled by the normal carrier path with no special case.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "205-216",
          "snippet": "    for section in text.split(TASK_NOTIFICATION_PREFIX).skip(1) {\n        let task_ids = all_xml_tags(section, \"task-id\");\n        let orphan = task_ids.iter().any(|t| t.starts_with(\"__orphan_summary__\"));\n        carriers.push(Carrier {\n            task_ids,\n            tool_use_id: extract_xml_tag(section, \"tool-use-id\"),\n            status: extract_xml_tag(section, \"status\"),\n            event: extract_xml_tag(section, \"event\"),\n            ts: rec.timestamp.clone(),\n            orphan_summary: orphan,\n        });\n    }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "309-322",
          "snippet": "                let state = if c.orphan_summary {\n                    orphaned += 1;\n                    Some(BgState::Stopped)\n                } else if c.status.is_some() {\n                    Some(BgState::from_status(c.status.as_deref()))\n                } else if c\n                    .event\n                    .as_deref()\n                    .is_some_and(|e| e.to_ascii_lowercase().contains(\"timed out\"))\n                {\n                    Some(BgState::TimedOut)\n                } else {\n                    None // a Monitor event pulse: the monitor is still armed\n                };"
        }
      ],
      "instrument": "Read the aggregation templates out of the installed Claude Code binary's own strings (`strings <binary> | grep __orphan_summary`), and corpus-side count the `<task-id>` tags on each orphan notification. Counting rule: one template per distinct literal; the aggregation branch is a single threshold comparison per kind.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > cc258a.strings; rg -o '.{300}__orphan_summary.{800}' cc258a.strings; rg -a -o -b 'Oe=\"__orphan_summary\"' ~/.local/share/claude/versions/2.1.258  (byte offset 178948531) then dd bs=1 skip=178943000 count=60000 | tr -d '\\0' > orphan_region.txt; rg -o 'function st\\(.{0,1400}' orphan_region.txt; csift search '__orphan_summary__:' --count-by label; csift search '__orphan_summary_live__:' --count-by label; csift search 'No completion record was found' -t harness.notification --raw",
          "observed": "binary: `Fe=20,In=172800000,Oe=\"__orphan_summary\",tt=`${Oe}__:`,nt=`${Oe}_live__:``. Shell branch: `resume: ${i.length} background shell command(s) orphaned by previous process exit`),i.length>1){...st(\"stopped\",\"shell\",\"shell command\",i,...)`. Agent branch: `c.length>Fe){...st(\"failed\",\"agent\",\"agent\",c,...)`. Workflow branch: `i.length>Fe){...st(\"stopped\",\"workflow\",\"workflow\",i,...)`. Aggregate template: `I=c.length===r.length?`Task ids: ${c.join(\", \")}.`:`First ${Fe} task ids: ${c.join(\", \")}.`` plus the trailing sentence `Task ids in this notification beginning with \"${Oe}\" are internal scan markers, not tasks.`. Corpus: 3 records classified harness.notification.task carry `No completion record was found for this background shell command from the previous session`, each with exactly 1 <task-id> tag and <status>stopped</status>. `__orphan_summary__:` matched 70 records across 8 label keys, 0 of them under any harness.notification.* leaf; `__orphan_summary_live__:` matched 20 records across 6 label keys, 0 under any harness.notification.* leaf.",
          "rule": "Binary side: one template per distinct string literal; one threshold comparison per orphan kind (three comparison sites total). Corpus side: one observation per record csift 0.10.0 emits under `-t harness.notification` over all 7611 sessions in scope; a sentinel counts as having an on-disk specimen only if a matched record carries a harness.notification.* label (a match under agent.* / user.* is prose quoting the sentinel, not a harness-written notice).",
          "note": "Both code snippets are verbatim in the current tree at the stated lines (src/live/background_scan.rs:215-226 and 279-292). The only correction is the sentinel-specimen claim: the ledger implies `__orphan_summary__:` has an on-disk specimen and only the `_live__` twin lacks one; measured, neither does, so csift's aggregate-orphan path (`orphan_summary: true` -> BgState::Stopped) has never been exercised by real corpus data on this machine, only by fixtures."
        }
      ]
    },
    {
      "id": "BG-022",
      "area": "background-tasks",
      "behavior": "Agent orphans branch three ways - handed to auto-resume, `stopped`, or `failed`. Auto-resume requires FOUR conditions together, not two: an auto-resume callback is present, the agent was launched by the Agent tool or by a forked skill, its meta is fetchable, and its transcript mtime is younger than 172800000 ms (48 hours). Otherwise the split is `stopped` when the transcript mtime is known OR the transcript is fetchable (or the agent was redispatched), and `failed` when neither holds - the harness's own wording for `failed` is that its in-process state was lost. That 48-hour constant is the only aging rule in the whole orphan family (one comparison site; the module carries no other 4+-digit literal).",
      "depends": "csift does not implement resume prediction, but the rule bounds what an OPEN async agent can mean: a lane younger than 48 hours may be auto-resumed rather than orphaned, so `open` is genuinely undecidable there.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "44-74",
          "snippet": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BgState {\n    /// Launched, no completion carrier names it yet.\n    Open,\n    Completed,\n    Failed,\n    Killed,\n    /// Claude Code's own orphan reconciliation at the next session start, or an\n    /// explicit `stopped` status.\n    Stopped,\n    /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n    TimedOut,\n}"
        }
      ],
      "instrument": "Read the constant from the installed Claude Code binary's orphan-reconciliation strings (`strings <binary> | grep 172800000`), and corpus-side look for a second launch in the same agent lane after a gap. Counting rule: one constant, one comparison site.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -a -o -b 'Oe=\"__orphan_summary\"' ~/.local/share/claude/versions/2.1.258; dd if=<binary> bs=1 skip=178943000 count=60000 | tr -d '\\0' > orphan_region.txt; python slice of orphan_region.txt around 'background agent(s) orphaned'; python regex census of 4+-digit literals over the module span (from `var An=new RegExp` to `function $n({calls:e`)",
          "observed": "`Fe=20,In=172800000`. Auto-resume gate: `if(r!==void 0&&(A.launchedByAgentTool===!0||A.launchedByForkedSkill===!0)&&v&&p!==null&&_-p<In){I.push({agentId:A.agentId,description:A.description,outputFile:A.outputFile,isWebFetchLaunch:A.isWebFetchLaunch});continue}` followed by `let U=p!==null||O,q=A.redispatched||U?\"stopped\":\"failed\"`. Handoff log: `resume: handing ${I.length} disk-resumable orphaned agent(s) to the bg auto-resume path`. The stopped summary reads `either way its transcript is saved, so its progress is not lost`; the failed summary reads `Its in-process state was lost.` Module span = 13725 bytes and contains exactly one numeric literal of 4 or more digits: 172800000 (count of the string in the whole 60000-byte region: 2, both the declaration and no second comparison).",
          "rule": "One constant, one comparison site. An 'aging rule' counts as a comparison of a now-timestamp against a stored millisecond timestamp; the census scanned every 4+-digit numeric literal in the orphan module and found only 172800000 (Fe=20 is a count threshold, not an age).",
          "note": "The ledger's paraphrase 'stopped when the transcript survives' is close but the actual predicate is mtime-known OR fetchable, and a redispatched agent short-circuits to `stopped` regardless. The csift snippet (src/live/background.rs:62-74, the BgState enum) is verbatim in the current tree."
        }
      ]
    },
    {
      "id": "BG-023",
      "area": "background-tasks",
      "behavior": "Every orphan notice is enqueued with `shouldQuery:false`, whose own schema prose reads `appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query`; the notice nonetheless lands as a `type:\"user\"` record whose parentUuid chain is followed by a real assistant turn 52-86 seconds later (measured 51.8 / 60.8 / 85.8 s on the 3 corpus specimens).",
      "depends": "csift keeps the orphan notice as a turn opener (`harness.notification.*`) rather than dropping it, because a genuine generation follows it through the attachment chain; classifying it as non-delivered would lose a real turn boundary.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "325-329",
          "snippet": "        // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n        if is_agents_stopped_notice(s) {\n            push_unique(out, Class::NotificationSubagent);\n            return;\n        }"
        }
      ],
      "instrument": "Locate an orphan notice line and walk forward: the next record is an `attachment` at delta about 0 whose `parentUuid` equals the notice `uuid`, and the first assistant record follows tens of seconds later. Counting rule: one walk per specimen; timestamp deltas in seconds between the notice and the first following `type:\"assistant\"` record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -a -o 'shouldQuery:M\\(\\).optional\\(\\).describe\\(\"[^\"]{0,300}' ~/.local/share/claude/versions/2.1.258 ; rg -a -o 'appended to the transcript[^\"`]{0,200}' ~/.local/share/claude/versions/2.1.258 ; python walk over the 3 corpus specimens found by `csift search 'No completion record was found for this background shell' -t harness.notification --format json` (session id + line), reading the owning .jsonl directly",
          "observed": "Schema prose: `shouldQuery:M().optional().describe(\"When false, the message is appended to the transcript without triggering an assistant turn. It will be merged into the next user message that does query.` Both orphan emit sites pass `shouldQuery:!1` (the singular `Ra({value:ka({...status:\"stopped\"...}),agentId:Ze(),mode:\"task-notification\",skipAttachments:!0,priority:\"next\",shouldQuery:!1})` and the aggregate `st(...)`). Corpus, 3/3 specimens: the record is `type:\"user\"`; the very next line is `type:\"attachment\"` at delta 0.0 s whose parentUuid equals the notice uuid; the first following `type:\"assistant\"` record lands at +51.849 s, +85.804 s and +60.753 s; walking that assistant record's parentUuid chain reaches the notice in 3/3.",
          "rule": "One walk per specimen. Delta = seconds between the notice's `timestamp` and the `timestamp` of the first following record with `type:\"assistant\"` in the same file. Chain check = follow `parentUuid` upward through a uuid index of the following 40 lines until the notice uuid is reached or the chain runs out (cap 12 hops).",
          "note": "Only the seconds range needed correction; the structural half (next line an attachment at delta 0 parented to the notice, then a real assistant turn whose parentUuid chain reaches the notice) reproduced 3/3. In 2 of the 3 specimens a genuine user record intervenes between the notice and the assistant turn, so the delta measures 'time until the next generation', not 'time the notice took to trigger one' - the notice by construction triggers none. The csift snippet (src/model/classify.rs:325-329) is verbatim in the current tree."
        }
      ]
    },
    {
      "id": "BG-024",
      "area": "background-tasks",
      "behavior": "The stopped/orphan notice family shares MOST of one record shape: `type:\"user\"`, `userType:\"external\"`, `origin:{\"kind\":\"task-notification\"}` and a `message.content` that is a STRING (never an array) hold 2700/2700. The rest are near-universal, not universal: `promptSource:\"system\"` on 2576/2700 (absent on 124), `isSidechain:false` on 2698/2700, and the `isMeta` key IS present (true) on 2/2700 - so 'no isMeta key at all' is false. The measured XML/plain split was 2689 XML-wrapped against 11 plain-text out of 2700, not 2683/17.",
      "depends": "csift classifies these by CONTENT shape (the `<task-notification>` prefix, or the agents-stopped templates) rather than by `origin` / `promptSource`, so a plain-text notice with no recognizable template still classifies as the human; those two fields are the authoritative discriminator csift does not yet read.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "325-329",
          "snippet": "        // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n        if is_agents_stopped_notice(s) {\n            push_unique(out, Class::NotificationSubagent);\n            return;\n        }"
        }
      ],
      "instrument": "`rg -l '\"origin\":\\{\"kind\":\"task-notification\"\\}' ~/.claude/projects --glob '*.jsonl'`, then for each matching record test whether the content starts with `<task-notification>`. Counting rule: one observation per RECORD whose parsed `origin.kind == \"task-notification\"`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t harness.notification --raw --max-count 0 > notif_raw.jsonl (2700 lines, whole corpus) + python field census; cross-check scoped to ONE project directory: rg -a -c --no-filename '\"origin\":\\{\"kind\":\"task-notification\"' ~/.claude/projects/<one-project-dir> --glob '*.jsonl' | paste -sd+ | bc  versus  csift search '' -t harness.notification --raw --max-count 0 @<that-project-dir>",
          "observed": "n=2700. origin == {\"kind\":\"task-notification\"}: 2700/2700. message.content is a STRING: 2700/2700 (0 arrays). type:\"user\" + userType:\"external\": 2700/2700. isSidechain:false 2698, isSidechain:true 2. promptSource:\"system\" 2576, key absent 124. `isMeta` key PRESENT on 2 records (both true, both isSidechain:true). Content shape: 2689 begin with `<task-notification>`, 11 do not (9 agents-stopped notices, 2 wrapped in a `[SYSTEM NOTIFICATION - NOT USER INPUT]` hook preamble that still contains a <task-notification> block). Scoped cross-check: rg counts 184 origin-keyed lines, csift emits 183 - the one difference is an isMeta:true / isSidechain:true type:\"user\" record in a subagent lane whose content is the hook preamble followed by a <task-notification> block; csift classifies it to an EMPTY label set (`csift search '' @<that-lane> --count-by label` returns 453 records across 6 labels, none harness or user, and `csift show @<that-lane> --line <N>` answers `no such record(s)`).",
          "rule": "Primary census: one observation per RECORD csift 0.10.0 emits under `-t harness.notification --raw` over all 7611 sessions in scope; each field counted once per record. Cross-check census: one observation per raw line in one project directory containing the compact byte pair `\"origin\":{\"kind\":\"task-notification\"`.",
          "note": "The ledger's 'depends' clause is materially understated and is now a measured csift gap: keying on content shape rather than on `origin`/`promptSource` does not merely mislabel an unrecognized notice as the human - at least one origin-keyed notice (isMeta:true, in a subagent lane, hook preamble before the <task-notification> block) classifies to an EMPTY label set and becomes invisible to `search` under every selector AND unaddressable by `show --line`. Two structurally identical records elsewhere in the corpus DO surface under -t harness.notification, so the discriminator was not isolated here; what is decided is that the two censuses disagree by exactly one record in the one directory measured. Note also that the primary census is csift-keyed, so the true origin-keyed population is >= 2700."
        }
      ]
    },
    {
      "id": "BG-025",
      "area": "background-tasks",
      "behavior": "When background agents are stopped from the UI, Claude Code writes a PLAIN-TEXT `type:\"user\"` record with no XML at all, in two templates: `Background agent \"<desc>\" was stopped by the user.` for one agent and `N background agents were stopped by the user: \"a\", \"b\".` for several - naming a count and truncated prompt prefixes but never an agent id, so the record carries no join key to the tasks it names.",
      "depends": "csift classifies both forms `harness.notification.subagent` through a synthetic-marker predicate (the singular form has no leading digit, so a digit-led predicate alone leaks it back to `user.message` as a human turn), never lets it open a turn, renders `[subagent stopped] ...`, and - because the notice names no id - emits an un-joinable note rather than marking any specific task stopped.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "54-75",
          "snippet": "/// The harness's \"N background agent(s) were stopped by the user: ...\" notice (v0.10.0):\n/// a plain-string `type:\"user\"` record Claude Code writes when async agents are killed\n/// from the UI. It names a count and truncated prompt prefixes, never an id, and it\n/// triggers no generation - the model sees it only alongside the next real prompt. Not\n/// the operator, never a turn opener; classifies `harness.notification.subagent`.\n#[must_use]\npub fn is_agents_stopped_notice(content: &str) -> bool {\n    let s = content.trim_start();\n    // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n    // the user.` (no count).\n    if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n        return true;\n    }\n    let digits = s.bytes().take_while(u8::is_ascii_digit).count();\n    if digits == 0 {\n        return false;\n    }\n    let rest = &s[digits..];\n    (rest.starts_with(\" background agent was stopped by the user\")\n        || rest.starts_with(\" background agents were stopped by the user\"))\n        && rest.contains(AGENTS_STOPPED_MARKER)\n}"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "325-329",
          "snippet": "        // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n        if is_agents_stopped_notice(s) {\n            push_unique(out, Class::NotificationSubagent);\n            return;\n        }"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "188-204",
          "snippet": "    if crate::model::is_agents_stopped_notice(&text) {\n        // The notice rides both a queue enqueue line and the user record: one note per\n        // (count, second). It names no id, so csift cannot say WHICH agents it stopped.\n        let head = text.trim_start();\n        let n = head.bytes().take_while(u8::is_ascii_digit).count();\n        let count = if n == 0 { \"1\" } else { &head[..n] };\n        let ts = rec.timestamp.as_deref().unwrap_or(\"?\");\n        let note = format!(\n            \"{count} background agent(s) were stopped by the user at {} - the notice names \\\n             no id, so csift cannot mark which agents it stopped\",\n            ts.get(..19).unwrap_or(ts)\n        );\n        if !notes.contains(&note) {\n            notes.push(note);\n        }\n        return;\n    }"
        }
      ],
      "instrument": "`rg -c 'background agents were stopped by the user:' ~/.claude/projects --glob '*.jsonl'` and `rg -c 'Background agent \"' ...`; then `csift show @<session> --line <N> --format json | jq .label` on a hit expects `harness.notification.subagent`. Counting rule: one notice per matching line, both templates counted; the e2e test `the_agents_stopped_notice_is_harness_not_human` in tests/cli/live/background.rs pins the classification.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md v0.10.0 ledger; CHANGELOG 0.10.0; src/model/markers.rs comment; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -a -o '.{260}mode:\"task-notification\".{80}' ~/.local/share/claude/versions/2.1.258 (the gesture-handler site); python template census over notif_raw.jsonl (the whole-corpus `csift search '' -t harness.notification --raw` dump); csift search 'stopped by the user' --count-by label; csift show @<session> --line <N> --format json for one plural and one singular specimen",
          "observed": "Binary, verbatim: `let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\",skipAttachments:!0})`. Corpus: 9 plain-text specimens (8 plural, 1 singular); 9/9 contain no `a<16hex>` agent-id token and no `toolu_` token; 9/9 truncate the named prompts with an ellipsis inside the quotes. `--count-by label` over 'stopped by the user': 10 records under harness.notification.subagent. `csift show --line` on a plural specimen: label harness.notification.subagent, labels ['harness.notification.subagent']; on the singular specimen: the same.",
          "rule": "One notice per matching record, both templates counted, over all 7611 sessions in scope. 'Names no id' = no substring matching the agent-id shape `a[0-9a-f]{16,17}` and no `toolu_` prefix anywhere in the content.",
          "note": "Both templates are byte-verbatim in the 2.1.258 binary and both classify harness.notification.subagent on real records. The two named e2e tests exist: tests/cli/live/background.rs:357 the_agents_stopped_notice_is_harness_not_human and tests/cli/live/background.rs:404 rows_name_a_subagent_lane_and_a_live_output_file. All three csift snippets (src/model/markers.rs:49-70, src/model/classify.rs:325-329, src/live/background_scan.rs:198-214) are verbatim in the current tree. The `--count-by label` figure is 10 rather than 9 because one XML `<task-notification>` whose summary contains the phrase also carries that leaf."
        }
      ]
    },
    {
      "id": "BG-026",
      "area": "background-tasks",
      "behavior": "The interrupt gesture kills only tasks its predicate admits: a `local_agent` that is running (or completed with work still pending) and an `in_process_teammate` that is running. Backgrounded shells and Monitors appear in no kill predicate and no kill helper on that path. The ESCAPE binding passes `suppressBackgroundAgentKill:true` and so never kills agents; the general Ctrl-C interrupt binding does NOT pass it (it is passed on Ctrl-C only in the viewing-an-agent screen branch). The double-press kill-agents chord kills within a 3000 ms window (`j4e=3000`), which is also the confirm toast's timeout.",
      "depends": "csift's `idle-background-open` verdict treats an open shell or monitor as still open after an interrupt; assuming the interrupt closed everything produces a false `idle-eot`, and the resume path starts with an empty in-memory task list so survivors surface only as orphans.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "11-14",
          "snippet": "    /// The turn ended (a clean end_turn) but N background task(s) the lens counts have\n    /// not returned - neither running nor stopped: by design (a dev server, a watcher)\n    /// or not, csift cannot tell. Never satisfies `--until stop`.\n    IdleBackgroundOpen,"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "358-367",
          "snippet": "    } else if eot_shape && background.open_counted() > 0 {\n        notes.push(\n            \"the turn ended, but background task(s) have not returned - by design (a dev \\\n             server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n             the next session start. `--background-since` / `--ignore-background` narrow \\\n             what counts; kill a dead one with the tool or the shell\"\n                .to_string(),\n        );\n        Verdict::IdleBackgroundOpen"
        },
        {
          "path": "src/cli/live_args.rs",
          "lines": "55-59",
          "snippet": "        PERSISTENT monitor never returns by design - name it with --ignore-background. NOT RETURNED IS NOT PROOF OF \\\n        RUNNING: Claude Code's own orphan summary says a UI stop, a Monitor timeout or \\\n        agent teardown leaves no transcript marker, and it reconciles only at the next \\\n        session start. A long session commonly carries several to dozens of dangling \\\n        or days-old tasks.\\n\\n\\"
        }
      ],
      "instrument": "Behavioural, on a live interactive session: launch a `run_in_background` sleep and an async agent, press the interrupt key, then check with `ps` that the shell is alive and that `csift status @<session> --format json | jq '.background'` still lists it open while the agent shows stopped. Only a live interactive session can produce this. Counting rule: one launch of each kind, one interrupt, three observations.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "AGENTS.md section 4; SPEC.md section 6 v0.10.0 ledger; SKILL.md status BACKGROUND TASKS paragraph; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -a -o -b 'suppressBackgroundAgentKill' ~/.local/share/claude/versions/2.1.258 (offsets 70769408, 180829503, 180831093, 180831371); dd if=<binary> bs=1 skip=180827000 count=8000 | tr -d '\\0' > kill_region.txt; rg -a -o 'function fr\\(.{0,200}' ~/.local/share/claude/versions/2.1.258 ; rg -a -o 'function fzn\\(.{0,240}' ~/.local/share/claude/versions/2.1.258",
          "observed": "Kill predicate: `function she(T){return fr(T)&&(T.status===\"running\"||Dm(T))||T.type===\"in_process_teammate\"&&T.status===\"running\"}` with `function fr(e){return typeof e===\"object\"&&e!==null&&\"type\" in e&&e.type===\"local_agent\"}` and `function Dm(e){return e.status===\"completed\"&&Hk(e).size>0}`. The kill-all routine filters `Object.entries(po).filter(([,jn])=>she(jn))`; its teardown helper only touches local_agent (`function fzn(e,n,r=\"user\"){for(...)if(fr(d)&&Dm(d))WO(o,n,r);for(...)if(d.type===\"local_agent\"&&d.status===\"running\")WO(o,n,r)}`) and in_process_teammate. Escape: `je(\"chat:cancel\",()=>{...pn({gesture:\"escape\",suppressBackgroundAgentKill:!0})},{context:\"Chat\",...})`. Chord window: `var ihe=null,j4e=3000;` used as `if(Sr-oo.current<=j4e){...}` and as the confirm toast `timeoutMs:j4e` with text `Press ${...} again to stop background agents`. No killShell / KillShell / killBackground / shellRegistry / bgShells token occurs anywhere in the 8000-byte gesture-handler region.",
          "rule": "One predicate, one filter site, one window constant. 'Kills type X' = X appears in the predicate the kill filter uses; 'does not kill X' = X appears in no kill predicate and no kill helper reachable from the interrupt handler within the extracted region.",
          "note": "The mechanism half is decided by the binary. The live half - that a backgrounded shell process is still alive after the interrupt - was NOT reproduced here: no interrupt was pressed. What would decide it: in a live interactive session launch a run_in_background sleep and an async agent, press Ctrl-C, then check `ps` for the shell pid and `csift status @<session> --format json` for the two rows. One residual caveat: the interrupt path does call the wake-registry reset (clearing pending rescans and bumping a scan generation) in one branch, so whether an ARMED Monitor survives an interrupt is not settled by the kill predicate alone."
        }
      ]
    },
    {
      "id": "BG-027",
      "area": "background-tasks",
      "behavior": "On resume the harness rebuilds its background-task picture by SCANNING THE TRANSCRIPT, not from any persisted registry - there is no on-disk background-task store (~/.claude/tasks is the TODO store: 177 entries, 0 *.output files, per-session <n>.json todo objects). Session crons are re-registered and disk-resumable agents younger than 48 hours are handed to the auto-resume path; shells and workflows are only notified once and written a TERMINAL status (`stopped`), which the harness emits as a system task_notification event rather than as a jsonl record (0 such records in the project directory measured). So the orphan verdict leaves no transcript marker of its own, and a still-running shell from a previous process is carried forward marked stopped rather than running.",
      "depends": "csift's whole-file transcript scan is the only instrument that survives a resume, and it is why the seventh verdict's note says Claude Code reconciles only at the next session start.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "358-367",
          "snippet": "    } else if eot_shape && background.open_counted() > 0 {\n        notes.push(\n            \"the turn ended, but background task(s) have not returned - by design (a dev \\\n             server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n             the next session start. `--background-since` / `--ignore-background` narrow \\\n             what counts; kill a dead one with the tool or the shell\"\n                .to_string(),\n        );\n        Verdict::IdleBackgroundOpen"
        }
      ],
      "instrument": "Launch a long `run_in_background` command, exit the session, resume it, then compare the terminal's task list (empty) with `csift status @<session> --format json | jq '.background.open'` (still lists the row) and `ps` (the process may still be alive). Counting rule: one launch, three observations.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SKILL.md status BACKGROUND TASKS paragraph; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python slice of orphan_region.txt around 'let f=Mn(c);$n(f)' and around 'function Fn(' / 'function On(' / 'function Mn('; rg -a -o 'function hs\\(.{0,500}' ~/.local/share/claude/versions/2.1.258 ; ls ~/.claude/tasks | wc -l; find ~/.claude/tasks -name '*.output' | wc -l; python dump of one ~/.claude/tasks/<session>/<n>.json; rg -a -o '\"subtype\":\"[a-z_]+\"' ~/.claude/projects/<one-project-dir> --glob '*.jsonl' | sort | uniq -c",
          "observed": "Resume driver: `try{let f=Mn(c);$n(f),await En(f,n,o,r,i),Fn(f,n),On(f,n),bre(Mv(n.all()),i)}catch(f){h(f)}` where `Mn(e)` rebuilds the launch inventory by iterating the TRANSCRIPT records (`for(let A of e)if(A.type===\"assistant\"){...tool_use...}else if(A.type===\"user\"){...toolUseResult...}`). `$n({calls:e,results:n,deletedCronIds:o})` re-registers crons via `Z5({id,cron,prompt,createdAt,recur...})` under a `recurringMaxAgeMs` guard. `En` hands disk-resumable agents to the auto-resume path (`resume: handing ${I.length} disk-resumable orphaned agent(s) to the bg auto-resume path`). `Fn` (shells) and `On` (workflows) only notify and mark terminal via `hs(c.taskId,\"stopped\",{toolUseId:c.toolUseId,summary:it})`, where `function hs(e,n,r){...\\_u({type:\"system\",subtype:\"task_notification\",task_id:e,tool_use_id:...,status:n,output_file:...,summary:...})}`. Disk: ~/.claude/tasks holds 177 entries and 0 files named *.output; its contents are the TODO store (per-session directories of <n>.json with keys activeForm, blockedBy, blocks, description, id, status, subject, plus .lock and .highwatermark). In one project directory the subtype census over *.jsonl returns only stop_hook_summary, turn_duration, away_summary and compact_boundary - zero `\"subtype\":\"task_notification\"` lines (all 71 occurrences of the string task_notification in that directory are prose inside message content).",
          "rule": "One read per function in the extracted orphan module; the disk side is one census of ~/.claude/tasks (entry count, *.output count, key set of one JSON file) and one scoped subtype census over one project directory (one observation per matching raw line).",
          "note": "'Reported once as an orphan and then forgotten' is imprecise in one direction: the orphan is not dropped, it is written a terminal status in the in-process registry, so a task list shows it as stopped (wrongly, if the OS process is still alive) until eviction. The live half of the claim - the terminal task list being empty after a resume while ps still shows the process - was NOT reproduced here; what would decide it: launch a long run_in_background command, exit and resume the session, then compare the terminal's task list against `csift status @<session> --format json | jq '.background.open'` and `ps`. The csift snippet (src/live/verdict.rs:358-367) is verbatim in the current tree."
        }
      ]
    },
    {
      "id": "BG-028",
      "area": "background-tasks",
      "behavior": "The `<output-file>` a launch names is a real file on disk - for an async agent it is a SYMLINK to that agent's own transcript, for a shell a regular growing `.output` file - so its size and last-write time are an independent still-producing-output signal.",
      "depends": "csift does one `stat` per open task and prints the bytes and the age; it is the only liveness evidence available for a task the harness never writes about again, and the agent symlink is why that file must not be read as an ordinary log.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "29-35",
          "snippet": "//! At the next session start Claude Code reconciles orphans itself: one notification\n//! carrying several `<task-id>` tags plus `__orphan_summary__:shell`, status `stopped`,\n//! whose summary says the tasks \"may have been stopped (via the UI, Monitor timeout, or\n//! agent teardown - these leave no transcript marker)\". That sentence is the honesty\n//! bound: NOT RETURNED IS NOT PROOF OF STILL RUNNING. The `<output-file>` from the\n//! launch is a real file (an agent's is a symlink to its transcript); its size and\n//! mtime are an independent \"still producing output\" signal, one `stat` per open task."
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "340-353",
          "snippet": "pub(crate) fn stat_output(t: &mut BgTask) {\n    let Some(p) = t.output_file.as_deref() else {\n        return;\n    };\n    let Ok(meta) = std::fs::metadata(p) else {\n        return;\n    };\n    t.output_bytes = Some(meta.len());\n    if let Ok(modified) = meta.modified() {\n        if let Ok(age) = std::time::SystemTime::now().duration_since(modified) {\n            t.output_age_secs = Some(i64::try_from(age.as_secs()).unwrap_or(i64::MAX));\n        }\n    }\n}"
        }
      ],
      "instrument": "`csift status @<session> --format json | jq '.background.tasks[] | {id, output_file, output_bytes, output_age_secs}'`, then `ls -l` the named path: an agent entry is a symlink into a `subagents/agent-*.jsonl`, a shell entry a regular `.output` file. Counting rule: one stat per open task.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 1; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python extraction of every <output-file> value from notif_raw.jsonl (the whole-corpus `csift search '' -t harness.notification --raw --max-count 0` dump) followed by os.path.islink / os.path.isfile / os.readlink / os.path.getsize on each; csift status @<session> --format json | (tasks[] | {kind,state,output_bytes,output_age_secs}) on two sessions with open shell tasks",
          "observed": "1835 distinct <output-file> paths. On disk now: 92 symlinks, 46 regular files, 1697 gone (OS temp cleanup). 92/92 symlinks resolve, and 92/92 name exactly `agent-<task-id>.jsonl` - the launching agent's own transcript; their sizes read through the link range 88066 to 6572829 bytes. 46/46 regular files carry shell-shaped (36) or workflow-shaped (10) task ids, sizes 10 to 263769 bytes; zero agent-shaped ids are regular files and zero shell-shaped ids are symlinks. csift status on two sessions reports open shell rows with output_bytes=10/10/0 and output_age_secs=100979/100983/8381.",
          "rule": "One stat per distinct <output-file> path harvested from the notification records. 'Symlink to that agent's own transcript' = os.readlink basename equals 'agent-' + the task id + '.jsonl'. Kind inferred from the task-id shape: a<16-17 hex> = agent, b<8-10 alnum> = shell, w-led = workflow.",
          "note": "Holds, with two additions the claim does not make. (1) A workflow launch also gets a regular .output file, so 'agent = symlink, everything else = regular file' is the sharper rule. (2) The signal is only available while the OS temp tree survives: 1697 of 1835 named paths are already gone, so a missing output file is not evidence about the task. Separately, csift's own output_file was null on every agent-kind open row in the two sessions probed (it takes the path from the launch result text, which names one for shells), so in practice csift stats shell output files and not agent symlinks. Both snippets (src/live/background.rs:29-35, src/live/background_scan.rs:310-323) are verbatim in the current tree."
        }
      ]
    },
    {
      "id": "BG-029",
      "area": "background-tasks",
      "behavior": "A background task's `<output-file>` lives under the per-session scratch directory at `<scratch>/<encoded-project>/<session-uuid>/tasks/<task-id>.output`, not under `<claude-home>/tasks/`; that directory holds a shell's regular files and an agent's transcript symlinks side by side.",
      "depends": "csift takes the path verbatim from the launch result text (after `written to: `) and stats it, so it never has to reconstruct the scratch location; a relocation of the store is transparent as long as the result text keeps naming the path.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "165-179",
          "snippet": "pub(crate) fn after_marker(text: &str, marker: &str) -> Option<String> {\n    let start = text.find(marker)? + marker.len();\n    let rest = &text[start..];\n    let end = rest\n        .char_indices()\n        .find(|&(i, c)| {\n            c.is_whitespace()\n                || c == ','\n                || c == ')'\n                || (c == '.' && rest[i + 1..].starts_with([' ', '\\n']))\n        })\n        .map_or(rest.len(), |(i, _)| i);\n    let tok = rest[..end].trim_end_matches('.');\n    (!tok.is_empty()).then(|| tok.to_string())\n}"
        }
      ],
      "instrument": "Launch a background command, read the path out of its result text, and `ls -la` that directory: it holds regular `.output` files and symlinks and is not under `~/.claude/tasks/`. Counting rule: one path per launch; the e2e test `rows_name_a_subagent_lane_and_a_live_output_file` in tests/cli/live/background.rs pins the stat path.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python census over the 1835 <output-file> values extracted from notif_raw.jsonl: segment count, leading-dash test on the project segment, uuid-shape test on the session segment, parent-directory basename, extension; plus ls ~/.claude/tasks | wc -l and find ~/.claude/tasks -name '*.output' | wc -l and a python dump of one ~/.claude/tasks/<session>/<n>.json",
          "observed": "1835/1835 paths have exactly 7 path segments of the shape <os-temp-root>/claude-<uid>/<encoded-project>/<session-uuid>/tasks/<task-id>.output. Project segment starts with '-' (the encoded-cwd form): 1835/1835. Session segment matches the 8-4-4-4-12 uuid shape: 1835/1835. Parent directory basename == 'tasks': 1835/1835. Extension == '.output': 1835/1835. ~/.claude/tasks holds 177 entries and 0 files named *.output; a sample file there is a TODO object with keys activeForm, blockedBy, blocks, description, id, status, subject. Of the 8 tasks directories that still exist, 3 hold both a symlink and a regular file.",
          "rule": "One path per distinct <output-file> value; the shape test is a per-segment predicate applied to every path. 'Side by side' = a directory whose surviving entries include at least one symlink and at least one regular file.",
          "note": "Holds exactly as stated. The scratch root under the per-session directory is the OS temp tree, not ~/.claude, and ~/.claude/tasks is a different store entirely (the TODO list). csift takes the path verbatim from the launch result text via after_marker, so it never reconstructs this layout - src/live/background_scan.rs:165-179 is verbatim in the current tree, and the named e2e test exists at tests/cli/live/background.rs:404."
        }
      ]
    },
    {
      "id": "BG-030",
      "area": "background-tasks",
      "behavior": "A never-returned background launch is usually far from EOF but not always: over 88 never-returned launches in 63 top-level sessions the distance to EOF ranges 0.0 MB / 4 lines to 502.8 MB / 145674 lines (median 48.5 MB / 23854 lines), and 74 of 88 (84%) sit beyond the 512 KB tail window while 14 sit inside it. Shells alone top out at 375.5 MB and 109866 lines. Among launches that did return, 4.7% (84 of 1805 main-lane launches with a locatable completion) were more than 512 KB from their completion. 88 of 3849 corpus launches never returned at all (58 monitors, 24 shells, 6 agents), and 84 of the 88 were launched more than a day before their session's last record.",
      "depends": "csift's background report is a whole-file scan behind a byte prefilter (measured +0.2-0.4 s worst case) rather than a tail read, because the 512 KB tail window covers none of the never-returned population; a tail-only implementation reports zero open tasks on any long session.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "21",
          "snippet": "/// The five raw-byte needles (R13 law: bare value substrings, serialization-safe)."
        }
      ],
      "instrument": "For each never-returned launch compute (file length - byte offset of the launch line) and (total lines - launch line number). Counting rule: one distance per launch; a launch is never-returned when no `<task-notification>` carrier anywhere under the same session root names its tool_use id or its backgroundTaskId.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; src/live/background.rs module doc; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift list --format json --max-count 0 --no-subagents  (63 distinct top-level sessions); for each: csift status @<id> --format json > statusjson/<id>.json; python aggregation of background.{open,completed,failed,killed,stopped,timed_out} and background.tasks[]; then for each open task a python locate of its tool_use_id in the launching file (main transcript first, then that session's subagent lanes) recording byte offset, line number, file size and total line count; and a separate single-pass python scan of the 66 main transcripts joining launches to <task-notification> carriers by tool-use-id",
          "observed": "63 top-level sessions, 3849 launches (sum of the six state counters), states {completed 3542, failed 110, killed 82, open 88, timed_out 19, stopped 8}. All 88 open (never-returned) launches located. Distance to EOF, all kinds: min 0.0 MB / 4 lines, median 48.5 MB / 23854 lines, max 502.8 MB / 145674 lines; 74 of 88 sit beyond the 512 KB tail window, 14 sit inside it. Shells alone (n=24): min 0.0 MB / 4 lines, median 31.5 MB / 4336 lines, max 375.5 MB / 109866 lines; 13 of 24 beyond 512 KB. Agents (n=6): 6 of 6 beyond 512 KB. Monitors (n=58): 55 of 58 beyond 512 KB. Age: 84 of 88 open launches were launched more than a day before their session's last timestamped record (shells 23 of 24). Returned launches, main lanes only: 84 of 1805 = 4.7% sat more than 512 KB from their completion.",
          "rule": "A launch is never-returned when csift 0.10.0 status reports its state as `open` (no completion carrier names its tool_use id or its background task id). Total launches = the sum of the six state counters over all 63 top-level sessions. Distance to EOF = (file size in bytes - byte offset of the launch line) and (total lines - launch line number) in the file the launch was issued from. Age = seconds between the task's launched_utc and the newest timestamp in the session's main transcript. The returned-launch distance is a separate cruder pass: launches and carriers taken from the 66 main transcripts only, joined by tool-use-id, distance = bytes from the launch line to the nearest following carrier.",
          "note": "The ledger's two upper numbers reproduce exactly on the shell subgroup (375 MB and 109866 lines), which is how the original was almost certainly scoped; its lower bounds (56 MB, 4545 lines) do not survive a full census, and 'sits far from EOF' has 14 counterexamples inside the tail window. Its '24 of 3133 launches never returned' is a shells-only count: 24 shells still, but out of 3849 launches now, with 88 never-returned across all kinds. The load-bearing conclusion is untouched - a 512 KB tail read would miss 84% of the never-returned population - and the tail constant snippet (src/live/tail.rs:14-16) is verbatim in the current tree."
        }
      ]
    },
    {
      "id": "BG-031",
      "area": "background-tasks",
      "behavior": "The harness writes NOTHING about a still-running background SHELL at end of turn: the 2.1.258 turn_duration record factory emits only durationMs, budgetTokens, budgetLimit, budgetNudges, messageCount, pendingBackgroundAgentCount and pendingWorkflowCount, and neither pending count can ever include a shell - the counter buckets a live task into pendingAgents only when it is a non-main-session local_agent with isBackgrounded set, and into pendingWorkflows only when its type is local_workflow, while a background shell is type local_bash and a Monitor is monitor_mcp/monitor_ws. Across 4637 turn_duration records on this corpus the 9 observed key sets contain no shell field of any kind. Such records ARE written while background work is open: on a replay of 4638 observations, 4375 had at least one background shell open, 1920 at least one async agent, 2232 at least one async workflow.",
      "depends": "csift never reads an end-of-turn telemetry record as proof the session is done and scans the whole main transcript for launches and completions instead; this hole is exactly what the `idle-background-open` verdict fills.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "354-367",
          "snippet": "    } else if running_shape {\n        Verdict::Running\n    } else if children.live_count > 0 {\n        Verdict::WaitingChildren\n    } else if eot_shape && background.open_counted() > 0 {\n        notes.push(\n            \"the turn ended, but background task(s) have not returned - by design (a dev \\\n             server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n             the next session start. `--background-since` / `--ignore-background` narrow \\\n             what counts; kill a dead one with the tool or the shell\"\n                .to_string(),\n        );\n        Verdict::IdleBackgroundOpen"
        },
        {
          "path": "src/live/background.rs",
          "lines": "292-306",
          "snippet": "        let mut pos = 0usize;\n        while pos < bytes.len() {\n            let end = memchr::memchr(b'\\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);\n            let line = &bytes[pos..end];\n            pos = end + 1;\n            if !line_is_bg_candidate(line) {\n                continue;\n            }\n            let Ok(Some(rec)) = crate::parse::parse_line(line) else {\n                continue;\n            };\n            ingest_launches(&rec, &lane, &mut tasks);\n            if is_main {\n                ingest_carriers(&rec, &mut carriers, &mut notes);\n            }"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "309-314",
          "snippet": "    let eot_shape = main_tail.unreturned_use.is_none()\n        && (main_tail\n            .last_stop_reason\n            .as_deref()\n            .is_some_and(|s| s == \"end_turn\")\n            || registry_shell);"
        }
      ],
      "instrument": "Replay a transcript maintaining the set of `run_in_background` launches with no `<task-notification>` naming them yet; at each `turn_duration` line record that set's size and the record's key set. Counting rule: one observation per `turn_duration` record, \"open at that instant\" meaning launched earlier in the file with no completion carrier seen; live-side, `csift search \"\" @<session> -t harness.meta.turn-duration --max-count -1 --no-truncate` shows no shell field while `csift status @<session>` reports `idle-background-open` with a shell row.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 6 v0.10.0 ledger; CHANGELOG 0.10.0; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}pendingBackgroundAgentCount.{0,160}'  (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function gje\\({tasks.{0,600}'  (3) a python pass over every *.jsonl under ~/.claude/projects (7791 files) that (a) key-set-censuses every record with type==\"system\" and subtype==\"turn_duration\" and (b) walks each transcript in line order maintaining the set of background launches with no completion carrier yet, sampling that set at each turn_duration record",
          "observed": "Binary record factory: function B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,pendingBackgroundAgentCount:o,pendingWorkflowCount:d,timestamp:new Date().toISOString(),uuid:oT(),isMeta:!1}} - no shell field of any kind. Binary counter: function gje({tasks:l,queuedCommands:p=[]}){...y=(M)=>{if(GE(M)&&M.isBackgrounded)g.add(M.id);else if(M.type===\"local_workflow\")T.add(M.id)};for(let M of Object.values(l))if(M.status===\"running\"||Fs(M.status)&&!M.notified)y(M);...return{pendingAgents:g.size,pendingWorkflows:T.size}} with function GE(e){return fr(e)&&e.agentType!==\"main-session\"} - a background shell is task type local_bash and falls in neither bucket. Corpus: 4637 turn_duration records in 54 files, 9 distinct key sets, key union = cwd, durationMs, entrypoint, gitBranch, isMeta, isSidechain, messageCount, parentUuid, pendingBackgroundAgentCount, pendingWorkflowCount, sessionId, sessionKind, slug, subtype, timestamp, type, userType, uuid, version (19 keys, none shell-related). Replay over 4638 observations: 4375 (94.3%) had >=1 open background shell, 1920 (41.4%) had >=1 open async agent, 2232 (48.1%) had >=1 open async workflow.",
          "rule": "One observation per turn_duration record. 'Open at that instant' = a launch seen earlier in the SAME transcript with no later <task-notification> in that transcript naming its <tool-use-id> or its <task-id>. Shell launch = a Bash/PowerShell tool_use whose input.run_in_background is true; async agent = an Agent/Task tool_use with run_in_background true, or a toolUseResult with status==\"async_launched\" carrying an agentId; async workflow = a toolUseResult with status==\"async_launched\" carrying a taskId. Key-set census counts one key set per record.",
          "note": "The behaviour holds and is now sourced, not merely sampled: the binary's own record factory and pending-count function prove the shell exclusion for every turn_duration record, not just the 100 the claim sampled. The claim's population (628) is not reproducible here - a whole-corpus scan finds 4637 such records in 54 files and no single project directory yields 628 - and the two ratios are far higher than claimed (94% vs 16% for an open shell, 41% vs 8% for an open async agent), so treat 100/628 and 51/628 as artefacts of an unstated scope, not as a rate. One forward-looking gap: 2.1.258 also emits budgetTokens, budgetLimit and budgetNudges on this record (undefined values are dropped by JSON.stringify, so none appear in this corpus), and the terminal renderer additionally reads briefHiddenCount; csift's Record models none of the four. The corpus is live - a second pass 15 minutes later saw 4638 records."
        }
      ]
    },
    {
      "id": "BG-032",
      "area": "background-tasks",
      "behavior": "pendingBackgroundAgentCount on a turn_duration record is never emitted as the literal 0 - the emitter writes `k>0?k:void 0` so a zero count becomes undefined and is dropped from the JSON, and absence of the key therefore means zero. The field does NOT track a transcript-derived set of launched-but-unreturned agents: it is a snapshot of the harness's live in-memory task map (a task counts when its status is running, or when it is terminal but not yet notified), so a transcript replay disagrees with it in both directions - on this corpus the field matched the replayed open-agent set in only 25 of the 136 records that carry it, reading LOWER than the replay 102 times and HIGHER 9 times.",
      "depends": "any consumer reading the field must treat an absent key as 0 rather than as unknown; the residual under-count comes from agents that notified and were resumed (a notification is not terminal) and from agents spawned in a child lane.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "177-184",
          "snippet": "    /// `turn_duration`: background agents still running at turn end (the REPL's\n    /// \"Waiting for N agents\" line). Optional; measured on ~3% of records.\n    #[serde(default, rename = \"pendingBackgroundAgentCount\")]\n    pub pending_background_agent_count: Option<serde_json::Value>,\n\n    /// `turn_duration`: workflows still running at turn end. Optional (~5%).\n    #[serde(default, rename = \"pendingWorkflowCount\")]\n    pub pending_workflow_count: Option<serde_json::Value>,"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "197-209",
          "snippet": "            for (key, v) in [\n                (\"messageCount\", rec.message_count.as_ref()),\n                (\n                    \"pendingBackgroundAgentCount\",\n                    rec.pending_background_agent_count.as_ref(),\n                ),\n                (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n            ] {\n                if let Some(n) = Record::u64_field(v) {\n                    fields.push(format!(\"{key}={n}\"));\n                }\n            }\n            (!fields.is_empty()).then(|| format!(\"[turn duration: {}]\", fields.join(\" \")))"
        }
      ],
      "instrument": "`rg -c '\"pendingBackgroundAgentCount\":\\s*0' ~/.claude/projects --glob '*.jsonl'` returns no matches while `rg -c '\"pendingBackgroundAgentCount\"'` returns many. Counting rule: one matching line = one record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}pendingAgents.{0,500}'  (2) a python census of every type==\"system\" subtype==\"turn_duration\" record under ~/.claude/projects, tabulating the presence and value of pendingBackgroundAgentCount  (3) the same replay as BG-031, comparing the field against the replayed open async-agent set",
          "observed": "Binary emitter: if(k>0||x>0)return{durationMs:g,pendingBackgroundAgentCount:k>0?k:void 0,pendingWorkflowCount:x>0?x:void 0,...}; return{durationMs:...,pendingBackgroundAgentCount:void 0,pendingWorkflowCount:void 0,...} - the zero case is literally undefined, which JSON.stringify omits. Corpus: 136 of 4637 turn_duration records (2.93%) carry the key; observed values 1:59, 2:32, 3:17, 4:9, 5:5, 6:2, 7:4, 8:2, 9:2, 10:1, 11:1, 12:1, 13:1 - minimum 1, zero occurrences of the value 0. Agreement with the replay: on the 136 key-bearing records, field==replay 25, field<replay 102, field>replay 9; counting an absent key as 0 over all 4638 observations, match 2741, field<replay 1888, field>replay 9.",
          "rule": "One record = one observation. 'Never 0' is decided on parsed values, not on a byte pattern, so it is immune to JSON spacing. Agreement compares the field (absent read as 0) against the count of open async agents by the BG-031 rule at that record.",
          "note": "First half of the claim is now proven at the source, not merely sampled: the ternary `k>0?k:void 0` is why no literal 0 exists, so a consumer reading an absent key as 'unknown' rather than 0 is wrong at every version that ships this emitter. The second half does not survive its own counting rule: 608 of 628 does not reproduce (25 of 136 on key-bearing records, 2741 of 4638 counting absence as zero), and mismatches are not one-directional - 9 observations have the field ABOVE the replay. The mechanism explains both: the field counts a live in-process map that dies with the session process, while a transcript replay accumulates launches that no notification ever closed (a long session carries dozens of dangling arms), so the replay drifts upward across resumes; conversely a task that already notified but is still running stays counted by the harness while the replay has closed it, which is the 9-case direction the original claim described."
        }
      ]
    },
    {
      "id": "BG-033",
      "area": "background-tasks",
      "behavior": "ScheduleWakeup mints NO task id at all. At 2.1.258 it takes {delaySeconds, prompt, reason, noop} - noop is a fourth REQUIRED field ('`noop` is required when `stop` is not true'), true meaning the tick changed nothing so consecutive ticks collapse in the terminal - or the {stop:true} form; its toolUseResult is {clampedDelaySeconds, scheduledFor, wasClamped}, plus {cancelledWakeups, stopped} on the stop form. delaySeconds is clamped to [60, 3600]. A scheduled wakeup can never be joined to a later notification by id.",
      "depends": "csift's background scanner does not model `ScheduleWakeup`; its cadence pulses reach the taxonomy only as `harness.schedule.wakeup` / `harness.notification.monitor` records, never as background tasks.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "21-39",
          "snippet": "/// The five raw-byte needles (R13 law: bare value substrings, serialization-safe).\npub(crate) fn line_is_bg_candidate(line: &[u8]) -> bool {\n    static FINDERS: std::sync::LazyLock<Vec<memmem::Finder<'static>>> =\n        std::sync::LazyLock::new(|| {\n            [\n                &b\"run_in_background\"[..],\n                b\"Command running in background\",\n                b\"async_launched\",\n                b\"task-notification\",\n                b\"stopped by the user\",\n                b\"\\\"Monitor\\\"\",\n                b\"Monitor started\",\n            ]\n            .into_iter()\n            .map(memmem::Finder::new)\n            .collect()\n        });\n    FINDERS.iter().any(|f| f.find(line).is_some())\n}"
        }
      ],
      "instrument": "`rg -c '\"name\":\"ScheduleWakeup\"' ~/.claude/projects --glob '*.jsonl'`, then enumerate the paired results' `toolUseResult` key sets - no id-shaped field appears. Counting rule: one key-set observation per result record, one row per `ScheduleWakeup` tool_use block.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}clampedDelaySeconds.{0,300}'  (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}noop:.{0,400}'  (3) a python pass over the 4 transcripts under ~/.claude/projects that contain \"name\":\"ScheduleWakeup\", tabulating every ScheduleWakeup tool_use input key set and the toolUseResult key set of every record carrying the paired tool_result block",
          "observed": "Binary output schema: c({scheduledFor:A().describe(\"Epoch ms timestamp when the next wakeup will fire\"),clampedDelaySeconds:A().describe(\"Actual delay used after clamping to runtime bounds\"),wasClamped:M().describe(\"True if the requested delaySeconds was outside [60, 3600]\"),stopped:M().optional(),cancelledWakeups:A().optional()}) - no id-shaped field. Binary call path: if(d===!0)return{data:{scheduledFor:0,clampedDelaySeconds:0,wasClamped:!1,stopped:!0,cancelledWakeups:yYn()}} plus the guards '`delaySeconds` and `reason` are required when `stop` is not true.', '`prompt` is required when `stop` is not true.', '`noop` is required when `stop` is not true.'. Corpus: 562 ScheduleWakeup tool_use blocks - 561 with input keys {delaySeconds, prompt, reason} and 1 with {stop}; 560 paired results - 559 with toolUseResult keys {clampedDelaySeconds, scheduledFor, wasClamped} and 1 with {cancelledWakeups, clampedDelaySeconds, scheduledFor, stopped, wasClamped}. No key in any of the 562 inputs or 560 results is an id.",
          "rule": "One row per ScheduleWakeup tool_use block (keyed by block id); one key-set observation per record whose message.content carries a tool_result whose tool_use_id is one of those ids.",
          "note": "The load-bearing half - no id, so no id join - is confirmed twice: by the binary's declared output schema and by 560 real paired results, none of which carries an id-shaped key. The input list needed a fourth field: 2.1.258 requires `noop` unless stop is true, and none of the 562 recorded blocks carries it because they predate that field. Also newly pinned from the binary: delaySeconds is clamped to [60, 3600] and wasClamped reports the clamp."
        }
      ]
    },
    {
      "id": "BG-034",
      "area": "background-tasks",
      "behavior": "Claude Code's task tools persist ONE JSON file per task under a per-session directory in <claude-home>/tasks/, and TWO directory-name forms exist on real disks: the full session uuid (16 here) and the newer session-<first 8 uuid chars> form (161 here). Each file carries {id, subject, description, status, blocks, blockedBy} with STRING ids and list-valued blocks/blockedBy; activeForm is OPTIONAL (absent on 86 of 402 files) and two further optional keys are observed in the wild, owner (36 files) and metadata (26 files). The status set is OPEN - pending, in_progress and completed observed.",
      "depends": "csift reads BOTH directory forms and merges them, renders anything that is not `completed` as an open row with its verbatim status (in_progress first, with blockers) and folds completed ones to a count; probing only one form silently reports no tasks for a session using the other, and a numeric-id assumption or a closed status enum would drop rows.",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "1-10",
          "snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
        },
        {
          "path": "src/live/tasks.rs",
          "lines": "37-41",
          "snippet": "    let tasks_root = home.join(\"tasks\");\n    let mut dirs = vec![tasks_root.join(owner_id)];\n    if let Some(prefix) = owner_id.get(..8) {\n        dirs.push(tasks_root.join(format!(\"session-{prefix}\")));\n    }"
        }
      ],
      "instrument": "`ls ~/.claude/tasks | sed 's/^session-.*/session-form/' | sort | uniq -c` shows both forms; `jq -r .status ~/.claude/tasks/*/*.json | sort | uniq -c` gives the status census; `csift status @<session> --format json | jq '{tasks, tasks_completed}'` prints the rows (null = no dir, [] = a dir with nothing). Counting rule: one row per non-completed task file across both directory forms.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "AGENTS.md section 1; SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) ls ~/.claude/tasks | sed 's/^session-.*/session-form/' | sed -E 's/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/uuid-form/' | sort | uniq -c  (2) a python census of every ~/.claude/tasks/*/*.json: key set, status value, python type of id, blocks and blockedBy  (3) csift status @<8-char session prefix> --format json for a session that owns BOTH a uuid-form and a session-form directory, compared with the on-disk file counts",
          "observed": "Directory forms: 161 session-form, 16 uuid-form (177 directories). 402 task files, 5 distinct key sets: 258 {activeForm, blockedBy, blocks, description, id, status, subject}; 82 the same minus activeForm; 32 with an extra owner; 26 with an extra metadata; 4 minus activeForm plus owner. id is a str in 402 of 402; blocks and blockedBy are lists in 402 of 402. Status census: completed 272, pending 99, in_progress 31. Merge check on one session: 7 files in its uuid-form directory + 21 in its session-form directory = 28; csift status reported 10 open rows + tasks_completed 18 = 28, and the disk census for those 28 was completed 18, in_progress 1, pending 9. An empty session-form directory reported tasks [] with tasks_completed 0.",
          "rule": "One row per *.json file under a tasks directory; a directory counts once per name form; open row = any file whose status is not 'completed', across both directory forms for the owning session.",
          "note": "Both directory forms are real and csift merges them: the one session on this disk that owns both forms has 7 + 21 files and csift status reports exactly 10 open + 18 completed. Two corrections to the shape: activeForm is not universal (86 of 402 files lack it) and the file can carry owner or metadata, so a fixed 7-key expectation is wrong even though a strict reader would survive it. The string-id and open-status halves hold exactly (402 of 402 string ids; three status values observed)."
        }
      ]
    },
    {
      "id": "BG-035",
      "area": "background-tasks",
      "behavior": "Neither live store carries background-task state: the session registry rows contain no `monitor` or `taskId` field, and `<claude-home>/tasks/` holds only the task-list files, never a 9-character `b`-led background task id.",
      "depends": "csift reconstructs background tasks from the transcript alone and reads `<claude-home>/tasks/` only as the task-list section; a future registry that did carry live task state would let the whole-file scan be replaced by a cheap read.",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "1-10",
          "snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
        },
        {
          "path": "src/live/background.rs",
          "lines": "37-39",
          "snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
        }
      ],
      "instrument": "`grep -ril 'monitor\\|taskId' ~/.claude/sessions/` and `grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/` - both empty. Counting rule: one hit = one file; zero files on either side is the expected observation.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "grep -ril 'monitor\\|taskId' ~/.claude/sessions/ ; grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/ ; plus a python key-set census of every parseable file in ~/.claude/sessions and, as a control, rg -o '<task-id>[a-z0-9]{9}</task-id>' over ~/.claude/projects",
          "observed": "0 files matched in ~/.claude/sessions (14 entries) and 0 files matched under ~/.claude/tasks (402 task files). The registry key sets are {bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version} and small variants of it, plus token sidecars {peerToken, pidDomain, procStart} - no monitor key, no taskId key. Control: b-led 9-character task ids are abundant in the transcripts (14198 of 14715 nine-character <task-id> values start with b), so the probe would have fired had such an id been persisted.",
          "rule": "One hit = one file; zero files on either side is the expected observation. The control counts one occurrence per <task-id> match, bucketed by first character.",
          "note": "Both stores are silent about background tasks, and the control rules out a vacuous probe: the id shape the grep looks for is the dominant one in the transcripts (14198 b-led of 14715 nine-character task ids) yet appears in none of the 402 task files, and none of the 14 registry entries carries a monitor or taskId key. The consequence for csift stands - the transcript scan is the only background instrument, and <claude-home>/tasks/ is only the task-list section."
        }
      ]
    },
    {
      "id": "BG-036",
      "area": "background-tasks",
      "behavior": "Only a BACKGROUND session (`CLAUDE_CODE_SESSION_KIND == \"bg\"`) writes a live in-flight registry, at `~/.claude/jobs/<short-id>/state.json` carrying `inFlight:{tasks, queued, kinds[], drainableMonitors, wake?}`; an interactive session returns early and writes nothing there.",
      "depends": "csift's live layer does not read that file, so for an INTERACTIVE session the transcript scan is the only background instrument; it is also the only place a live-armed signal exists if csift ever adds one.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "37-39",
          "snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
        }
      ],
      "instrument": "`cat ~/.claude/jobs/*/state.json` - an interactive-only machine shows `{\"inFlight\":{\"tasks\":0,\"queued\":0,\"kinds\":[],\"drainableMonitors\":0}}` or no directory at all; the `kinds` array carries `monitor` / `monitor_ws` only for a background session. Counting rule: one file per background job directory.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}CLAUDE_CODE_SESSION_KIND.{0,200}'  (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}inFlight:.{0,260}'  (3) find ~/.claude/jobs -type f and cat of the one state.json found",
          "observed": "Binary writer gate: async function bre(e,n){let r=a.CLAUDE_JOB_DIR;if(!r||a.CLAUDE_CODE_SESSION_KIND!==\"bg\")return; ... await As(r,{...o,inFlight:{tasks:e.count,queued:d.queued,kinds:[...e.kinds],...e.drainableMonitors!==void 0&&e.drainableMonitors>0&&{drainableMonitors:e.drainableMonitors},...d.wake!==void 0&&{wake:d.wake}},updatedAt:...}). Persisted schema: inFlight:c({tasks:A(),queued:A(),kinds:R(i()),drainableMonitors:A().int().nonnegative().optional(),wake:c({at:A().optional(),reason:i().optional(),fires:A().int().nonnegative(),keepalive:x(!0).optional()}).optional().catch(void 0)}).optional(). Containment check: let r=Ce(Se(),\"jobs\")+ve; if(!o.startsWith(r))return!1 - the job directory must sit under <claude-home>/jobs/. Disk: exactly one job directory, named by an 8-character short id, holding state.json and timeline.jsonl; its state.json carries \"inFlight\":{\"tasks\":0,\"queued\":0,\"kinds\":[],\"drainableMonitors\":0} and \"template\":\"bg\". No other session on this machine (14 registry entries, 15 project directories) has a jobs directory.",
          "rule": "One file per background job directory; the gate is decided by reading the writer's early return, not by absence alone.",
          "note": "The gate is proven at the source rather than inferred from an empty directory: the writer returns immediately unless CLAUDE_JOB_DIR is set AND CLAUDE_CODE_SESSION_KIND == \"bg\", and the job directory is separately validated to sit under <claude-home>/jobs/. The claimed inFlight shape matches the persisted schema exactly, with two refinements worth knowing: drainableMonitors is itself optional (2.1.258 writes it only when greater than zero, so the observed zero on disk was written by an earlier build), and wake is an object {at?, reason?, fires, keepalive?}. The kinds array is populated from a task census that can push monitor, monitor_ws and session_cron; the one file on this machine has an empty kinds array, so the monitor entries themselves are not observed here."
        }
      ]
    },
    {
      "id": "BG-037",
      "area": "background-tasks",
      "behavior": "A BACKGROUND SESSION lane is self-identifying on disk: records written by a session whose kind is bg carry \"sessionKind\":\"bg\", and the marking is inherited by the subagent transcripts that session spawns. It does NOT mark asynchrony: on this corpus all 6225 occurrences belong to one background-job session (951 on its own top-level transcript, 5274 across its 20 subagent transcripts), while the 150 asynchronous launches made from interactive sessions produced child transcripts carrying no sessionKind at all. The value set is not a singleton either - the binary accepts bg, daemon and daemon-worker; bg is simply the only kind this machine has recorded.",
      "depends": "csift's child-lane classifier does not read it - an async agent is treated as an ordinary child lane by tail shape alone - so `sessionKind` is an available but unused discriminator if async-versus-sync ever needs distinguishing.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "46-56",
          "snippet": "pub(crate) fn children_report(\n    main_jsonl: &Path,\n    returned: &std::collections::HashSet<String>,\n) -> Result<ChildrenReport> {\n    let mut report = ChildrenReport::default();\n    let subs = crate::subagent::subagent_transcript_files(main_jsonl).unwrap_or_default();\n    for sub in &subs {\n        let sid = crate::subagent::session_id_from_path(sub);\n        let shape = tail_shape(sub)?;\n        // The RECORD-tail instant is the semantic fact (mtime can lag or lead it).\n        let tail_age = shape.last_ts_utc.as_deref().and_then(|t| age_secs(Some(t)));"
        }
      ],
      "instrument": "`rg -o '\"sessionKind\":\"[^\"]*\"' ~/.claude/projects --glob '*.jsonl' | sort | uniq -c` expects one distinct value. Counting rule: one occurrence per matching record; the files carrying it are all `subagents/agent-*.jsonl`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) rg -o '\"sessionKind\":\"[^\"]*\"' ~/.claude/projects --glob '*.jsonl' -N --no-filename | sort | uniq -c  (2) rg -l '\"sessionKind\":\"bg\"' over the same tree, grouped by owning session and by file shape  (3) rg -l 'async_launched' over the same tree  (4) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,150}sessionKind.{0,150}'",
          "observed": "6225 occurrences, all \"bg\" - one distinct value. But the carriers are 21 files that ALL belong to ONE background-job session: its own top-level transcript (951 occurrences) and its 20 built-in subagent transcripts (5274 occurrences). Zero occurrences under any subagents/workflows/** path, and zero in any transcript of an interactive session, even though 150 async_launched launches are spread across many interactive sessions in the same corpus. Binary: function Ij(){let e=a.CLAUDE_CODE_SESSION_KIND;if(e===\"bg\"||e===\"daemon\"||e===\"daemon-worker\")return e;return} and the resume-list reader U=B===\"bg\"||B===\"daemon\"||B===\"daemon-worker\"?B:void 0 - three admissible values, not one.",
          "rule": "One occurrence per matching record; files grouped by the session directory that owns them and by whether the path contains a subagents/ component and a workflows/ component.",
          "note": "The value census survives (6225 of 6225 are bg) but the attribution does not: the field tracks the SESSION's kind, not whether a lane was launched asynchronously. The decisive counter-observation is that async launches exist in many interactive sessions here (150 async_launched results) and none of their child transcripts carries the field, while every child of the single bg-job session does. Anyone using sessionKind as an async-versus-sync discriminator would misclassify every async lane started from an interactive session, and a reader treating bg as the only value will break on a daemon or daemon-worker session."
        }
      ]
    },
    {
      "id": "BG-038",
      "area": "background-tasks",
      "behavior": "A background task can be designed never to return (a dev server, a watcher, a persistent monitor), so no completion record will ever arrive for it and `not returned` can never be read as `still running`.",
      "depends": "csift's `wait` REQUIRES `--timeout` and exits 124 on expiry with an at-exit report - the single documented exception to its zero-or-non-zero exit contract - and the `idle-background-open` verdict is neither running nor stopped, so it never satisfies `--until stop`, which admits only idle-eot and stale-dead (a process proven dead still stops the wait with a task open).",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "36-42",
          "snippet": "    // The timeout is REQUIRED (v0.10.0): a background task may never return by design\n    // (a dev server, a watcher), so an unbounded wait on `stop` is a bug, not a wait.\n    let Some(timeout_secs) = args.timeout else {\n        bail!(\n            \"wait needs --timeout <SECS>: a background task can be designed never to return \\\n             (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, \\\n             branch on exit 124, and read the at-exit report; narrow what counts with \\"
        },
        {
          "path": "src/live/wait.rs",
          "lines": "206-218",
          "snippet": "/// The timeout elapsed: render `fired:\"timeout\"` + exit 124 (never through the error\n/// path - a timeout is a NORMAL monitor outcome, just a distinguishable one).\nfn finish_timeout(\n    args: &WaitArgs,\n    main: &Path,\n    lens: &BackgroundLens,\n    activity: &Activity,\n    waited: std::time::Duration,\n) -> Result<()> {\n    let assessment = assess_path(main, args.want_subagents(), lens)?;\n    emit_wait(args, \"timeout\", main, &assessment, activity, waited)?;\n    let _ = std::io::stdout().flush();\n    std::process::exit(i32::from(TIMEOUT_EXIT));"
        },
        {
          "path": "src/live/conditions.rs",
          "lines": "198-206",
          "snippet": "/// Match a fresh assessment against the verdict-class conditions.\npub(crate) fn verdict_matches(cond: &Cond, verdict: Verdict) -> bool {\n    match cond {\n        Cond::Stop => matches!(verdict, Verdict::IdleEot | Verdict::StaleDead),\n        Cond::Hitl => verdict == Verdict::WaitingHitl,\n        Cond::VerdictIs(v) => verdict == *v,\n        _ => false,\n    }\n}"
        }
      ],
      "instrument": "`csift wait @<session> --until stop` without `--timeout` expects a pointed refusal naming the reason; then `csift wait @<session> --until stop --timeout 3; echo $?` expects 124. Counting rule: one invocation per branch, exit code observed.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "AGENTS.md section 4; SPEC.md section 6 v0.10.0 ledger; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift wait @<8-char session prefix> --until stop; echo $?   then   csift wait @<same prefix> --until stop --timeout 3; echo $?   plus csift status @<same prefix> --format json for the open-task census",
          "observed": "Without --timeout: 'csift: error: wait needs --timeout <SECS>: a background task can be designed never to return (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, branch on exit 124, and read the at-exit report; narrow what counts with --background-since now / --ignore-background <RE>', exit 1. With --timeout 3: exit 124, at-exit report 'fired timeout / verdict waiting-children / waited 4s', and a background line '12 open; 1079 completed, 10 failed, 3 killed, 1 stopped'. The 12 open arms are 61 to 91 days old and are re-arm loops, watch loops and monitors - launches for which no completion record will ever arrive. Code: Cond::Stop => matches!(verdict, Verdict::IdleEot | Verdict::StaleDead) in src/live/conditions.rs:190, so IdleBackgroundOpen never satisfies --until stop.",
          "rule": "One invocation per branch, exit code observed; open background arms counted once per launch with no completion carrier, ages taken from the launch instant.",
          "note": "Both branches ran: the refusal names the reason and exits 1, and the bounded wait exits 124 with the at-exit report. The premise is corroborated on real data rather than assumed - the session used carries 12 background arms open for 61 to 91 days against 1079 completed, which is what 'designed never to return' looks like on disk, and matches Claude Code's own position that a UI stop, a Monitor timeout or agent teardown leaves no transcript marker. The 'never satisfies --until stop' half is settled by the condition table, which admits only idle-eot and stale-dead."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "refined",
          "instrument": "python scan of src/ for string literals carrying interior runs of eight or more spaces, and the installed csift 0.10.1 run as `wait @<id> --until stop` without --timeout",
          "observed": "two literals (the wait.rs timeout guard message and the recover replay.rs bash-append boundary detail) carried 14- and 34-space runs where line-continuation backslashes had been lost; 0.10.1 printed `never to return              (a dev server`; the guard, exit 1 and exit 124 behaved as claimed",
          "rule": "a run of spaces inside a literal that is no padding format is a lost continuation",
          "note": "both literals rewritten with `\\` continuations in 0.10.2; semantics unchanged"
        }
      ]
    },
    {
      "id": "BG-039",
      "area": "background-tasks",
      "behavior": "A `<task-notification>` whose `<summary>` opens `Background command \"...\"` is ALWAYS a background-command pulse whatever its quoted name says; the `Monitor` / `Scheduled` / `cron` classification comes from the summary PREFIX only.",
      "depends": "csift's summary classifier routes on the leading phrase alone; the retired quoted-name heuristic (a `monitor` / `re-arm` / `liveness` word inside the quoted name) produced 40 spurious `monitor` records against zero genuine Monitor pulses on one project.",
      "code": [
        {
          "path": "src/model/automation.rs",
          "lines": "33-44",
          "snippet": "    /// Classify from the `<summary>`. Case-insensitive on the known leading prefixes; anything\n    /// else (or a missing summary) is [`AutomationKind::Task`]. The `monitor`/`scheduled`/`cron`\n    /// LEADING prefixes route a Monitor-tool pulse or termination notice\n    /// (`Monitor event: …` / `Monitor \"…\" …`) to [`AutomationKind::Monitor`]. A `Background\n    /// command \"…\"` pulse is ALWAYS `background-command`, whatever its quoted name says\n    /// (v0.10.0: the former quoted-name heuristic - `monitor`/`re-arm`/`liveness` in the name\n    /// routed to `Monitor` - predates the real Monitor tool and double-booked; measured on one\n    /// project it produced 40 `monitor` records against zero genuine Monitor pulses). This\n    /// does NOT cover `ScheduleWakeup` wakeup-tick prompts (isMeta records that never reach\n    /// this classifier).\n    #[must_use]\n    pub fn from_summary(summary: Option<&str>) -> Self {"
        }
      ],
      "instrument": "`csift search 'task-notification' <project dir> --count-by label` and compare the `harness.notification.monitor` count with the number of `Monitor started (task ` arms in the same scope. Counting rule: one record per matched record on each side.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/model/automation.rs comment; CHANGELOG 0.10.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "In one project directory under ~/.claude/projects: csift search '' <project dir> -t harness.notification --count-by label ; csift search 'Background command \"[^\"]*(?i)(monitor|re-arm|liveness)' <project dir> -t harness.notification --count-by label ; csift search '' <project dir> -t harness.notification.monitor --no-truncate ; rg -o 'Monitor started \\(task [a-z0-9]+' over the same directory",
          "observed": "Scope label census: 651 harness.notification.background-command, 96 workflow, 67 subagent, 1 monitor, 1 task (816 records). The quoted-name probe: 586 records whose summary is Background command \"<name containing monitor, re-arm or liveness>\" ALL classify harness.notification.background-command and ZERO classify .monitor. The single .monitor record in scope renders as '[monitor <task-id> <status>] Monitor event: \"<name>\"' - a summary-prefix match - and its arm is a genuine Monitor tool result, 'Monitor started (task <id>, persistent - runs until TaskStop or session end)' with toolUseResult {taskId, timeoutMs, persistent}, in the same session. 6 distinct Monitor arms exist in the scope (7 occurrences of the arm string).",
          "rule": "One record per matched record on each side (--count-by label counts records, not regex occurrences); Monitor arms counted as distinct task ids in the 'Monitor started (task <id>' text.",
          "note": "The retirement is confirmed by a counting rule a stranger can rerun: 586 notification records whose quoted background-command name contains monitor, re-arm or liveness - exactly the words the retired heuristic keyed on - now all land under harness.notification.background-command, and none under .monitor. The only .monitor record in the scope earns it by summary PREFIX (Monitor event:) and is backed by a real Monitor tool arm, so the prefix-only rule is doing the classifying. Note this scope is not the zero-genuine-Monitor project the code comment cites: 6 distinct Monitor arms exist here, of which only one produced a notification record in scope."
        }
      ]
    },
    {
      "id": "BASH-001",
      "area": "bash-shell",
      "behavior": "Claude Code keeps NO long-lived shell: every Bash tool call builds its own command string and spawns a fresh shell whose starting directory is a TRACKED cwd, and that tracked value is stamped on every jsonl record as the top-level `cwd` field - so `cwd` is not a per-session constant (measured: 19 of 7586 transcripts carry more than one value, the largest carrying 8; 18 records corpus-wide carry `/`). The field is present on subagent transcripts too (measured 454,020/454,020 user+assistant records across 7,502 subagent files) and is unchanged across compaction boundaries (231/231).",
      "depends": "csift's bash operand resolution needs NO cross-command state: each record carries its own spawn cwd, so `files`/`recover` join a relative operand against the carrying record's own `cwd` at the zero-inference `cwd-joined` class, in a subagent lane exactly as in the main thread. A shared-shell model would attribute operands to the wrong directory, and an absent `cwd` would degrade every such row to `unresolved`.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "4-8",
          "snippet": "//! ## How Claude Code manages the Bash tool's cwd (verified against CC 2.1.237)\n//!\n//! Claude Code does NOT keep a long-lived shell. Every Bash tool call spawns a fresh\n//! shell whose starting directory is a TRACKED cwd, and that tracked value is stamped\n//! on every jsonl record as the top-level `cwd` field. Concretely:"
        },
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "31-32",
          "snippet": "//! - `CwdJoined`: a relative operand before any `cd`; joined to the record's own `cwd`\n//!   field. That value is data Claude Code wrote, so the join involves no inference."
        }
      ],
      "instrument": "python/jq over ~/.claude/projects: for every line whose `type` is user or assistant collect the top-level `cwd` and count distinct values per transcript; separately count such records lacking the field under `*/subagents/**/*.jsonl` (expect zero). Counting rule: one `cwd` value per record, deduplicated per file.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; CHANGELOG 0.8.0; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import os,json,collections;R=os.path.expanduser('~/.claude/projects');per=collections.Counter();ua=0;miss=0;sub=0;submiss=0;slash=0;\\nfor dp,dn,fns in os.walk(R):\\n for fn in fns:\\n  if not fn.endswith('.jsonl') or fn=='journal.jsonl': continue\\n  p=os.path.join(dp,fn);isub='subagents' in p.split(os.sep);seen=set()\\n  for raw in open(p,'rb'):\\n   try: r=json.loads(raw)\\n   except Exception: continue\\n   if r.get('type') not in ('user','assistant'): continue\\n   c=r.get('cwd')\\n   ...\\\" (full script: walk every *.jsonl under ~/.claude/projects, json-parse every line, and for each type user|assistant record read the top-level cwd; per file count distinct cwd values; count records missing cwd, split top-level vs subagent lane; count records whose cwd is exactly '/'; at every system/compact_boundary compare the cwd of the last user|assistant record before it with the first after it)",
          "observed": "7586 transcripts. Top-level lane: 201151 user|assistant records, 15 with no cwd. Subagent lane: 454020 user|assistant records, 0 with no cwd. Distinct-cwd-per-file histogram: 7563 files with 1 value, 11 with 2, 6 with 3, 1 with 4, 1 with 8 (max 8, not 7); 10 top-level and 9 subagent files carry more than one value. 18 records carry cwd \"/\". 232 compact_boundary records; the cwd before and after the boundary is identical in 231/231 comparable boundaries, 0 differ. Binary corroboration: one command string is built per call and spawned with its own cwd - strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'buildExecCommand' shows 'let{commandString:ht,cwdFilePath:Lt}=await Ue.buildExecCommand(e,{id:ot,...})' with a per-call id.",
          "rule": "One cwd value per type user|assistant record, deduplicated per file. A file counts as multi-cwd when its distinct set exceeds one. Boundary comparison: one comparison per compact_boundary that has a timestamped user|assistant record on each side.",
          "note": "Structure holds; the illustrative numbers were stale. Two shifts worth recording: (a) multi-cwd transcripts are rare (19 of 7586) but the maximum is now 8 distinct values in one file, not 7; (b) the `/` cwd is now 18 records corpus-wide rather than 51 in one session, so the earlier session has aged out. Subagent coverage strengthened by two orders of magnitude and is still exactly 100%. All cited code lines (src/bash_mutations/cwd.rs 4-8 and 31-32) exist verbatim in the current file."
        }
      ]
    },
    {
      "id": "BASH-002",
      "area": "bash-shell",
      "behavior": "Claude Code recovers a Bash call's ending directory by appending `pwd -P >| <tmp>/claude-<id>-cwd` as the last element of an `&&`-joined command chain and reading that file back; because the read-back is `&&`-chained, a command that exits non-zero never advances the tracked cwd (the file is never written, the read throws, the tracker is left alone). The read-back result is consumed only under `if(result && !pinned && !result.backgroundTaskId)`, so a backgrounded command is excluded by an explicit `backgroundTaskId` gate and a PINNED lane (any subagent lane) is excluded outright. Under a sandbox the file is named `cwd-<id>` in the sandbox temp dir instead.",
      "depends": "csift can trust the record `cwd` as the shell's true spawn directory without replaying exit status; the lexical `cd` inference is applied only INSIDE one command and is labelled `cd-tracked`, so a `cd` that failed at runtime never silently relocates later operand resolution.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "10-12",
          "snippet": "//! - The runner appends `&& pwd -P >| <tmp>/claude-<id>-cwd` to the command and reads\n//!   the file back afterward. Because the read-back is `&&`-chained, a command that\n//!   exits non-zero, and any backgrounded command, never advances the tracked cwd."
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary for 2.1.258, grepping for `pwd -P` and the `claude-`...`-cwd` tempfile template: expect one read-back site, `&&`-chained. Live check: run `cd /tmp && false` in one Bash call, then `csift show @main --turn -1 --raw | jq -r .cwd` on the next record - the value must be unchanged. Counting rule: one tracked value per completed call.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'pwd -P'  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{60}claude-.{0,40}-cwd.{60}'  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'if\\(Do&&!C&&!Do\\.backgroundTaskId\\).{0,300}'  ;  then live, in two separate Bash calls: (1) `cd /private/tmp && false` (2) `pwd -P`",
          "observed": "Exactly one read-back site: `ke.push(`pwd -P >| ${Ko([W])}`)` immediately followed by `let De=ke.join(\" && \")`, so the read-back is the last element of an &&-joined chain; an optional `CLAUDE_CODE_SHELL_PREFIX` wraps the finished chain. The target file is `W=U?u$e(j,`cwd-${v.id}`):u$e(B,`claude-${v.id}-cwd`)` - `claude-<id>-cwd` in the normal temp dir, `cwd-<id>` under a sandbox temp dir. The read-back is consumed under a triple gate: `if(Do&&!C&&!Do.backgroundTaskId)` then `readFileBytes(Dr)` ... `du(wo,d,Ot)`; on any throw it records `tengu_shell_set_cwd {success:false}` and leaves the tracked cwd alone. LIVE: call 1 `cd /private/tmp && false` exited 1; call 2 `pwd -P` printed the unchanged project directory.",
          "rule": "String presence and adjacency in the 2.1.258 binary (one occurrence per site); then one tracked value per completed call, read by running `pwd -P` as the whole of the next call.",
          "note": "Both stated suppressors verified. The correction adds a THIRD suppressor the claim did not know about: the same read-back is skipped when the lane is pinned, which is the case for every subagent lane (see BASH-003). Also worth recording for csift: the tempfile name has a second form under sandboxing. src/bash_mutations/cwd.rs 10-12 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-003",
      "area": "bash-shell",
      "behavior": "In a TOP-LEVEL session a `cd` into a project SUBDIRECTORY persists silently into later Bash calls - the tracked cwd, and so the record's `cwd` field, follows it with no notice of any kind (measured 45/49 such commands, still true at 2.1.251/2.1.252/2.1.257). In a SUBAGENT lane the cwd is PINNED: the post-command read-back is skipped, so the same `cd` does not persist (measured 1/2265) and the record `cwd` stays the session directory for the whole lane.",
      "depends": "csift never assumes the record `cwd` equals the project root: `list` deliberately reports a session's FIRST-seen `cwd` (last-seen could be a transient subdirectory) and `files` never treats a drifted cwd as a different project.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "13-14",
          "snippet": "//! - A `cd` into a SUBDIRECTORY of the project persists silently into later tool\n//!   calls; the tracked cwd (and so the record `cwd` field) follows it. A reset back"
        }
      ],
      "instrument": "`csift search 'cd ' <target> -t agent.tool.use --format json | jq -r .cwd` and compare consecutive tool_use records across a subdirectory `cd`: the value follows it, with no reset notice on the intervening result. Counting rule: one comparison per adjacent (use, next-use) pair in one transcript.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; per transcript take the FIRST cwd seen on a user|assistant record; for every Bash tool_use whose command matches ^\\\\s*cd\\\\s+(<absolute path under that first cwd>) remember the target; count it a PERSIST when a LATER record in the same transcript carries that target as its own top-level cwd; bucket by lane and by record version\\\"  ;  plus live, in two separate Bash calls: (1) `cd <project>/src/bash_mutations && pwd -P` (2) `pwd -P`  ;  plus csift show @trap:<marker> --line 1.. --raw --max-count 0 | (count distinct cwd)",
          "observed": "Top-level lane: 49 `cd <absolute subdirectory>` commands, 45 followed by a later record carrying that subdirectory as its cwd (91.8%), including at versions 2.1.257 (2), 2.1.252 (1), 2.1.251 (4). Subagent lane: 2265 such commands, 1 persisted (0.04%). LIVE in this subagent lane: call 1 `cd .../src/bash_mutations && pwd -P` printed the subdirectory; call 2 `pwd -P` printed the project root again, with no notice of any kind. csift raw read of this lane: 199 user|assistant records, exactly 1 distinct cwd. Binary: the cwd read-back and the reset check are both wrapped in a pin test - `Ne=!Ie||Pe?.pinCwd===!0` with `Ie=!n.agentId`, and the reset call site is `if(W=n.session.project.cwd,!Ne){...}`.",
          "rule": "One comparison per `cd <absolute subdirectory>` Bash tool_use: persist = some later record in the same transcript carries that exact path as its top-level cwd. Bucketed by lane (a path component named subagents) and by the record's version field.",
          "note": "The claim as written is lane-blind and is false for subagent lanes on current Claude Code. This matters for csift beyond documentation: `cd-tracked` operand resolution inside one subagent command is unaffected (it is in-command lexical tracking), but any reasoning that a subagent's later record cwd reflects an earlier `cd` is wrong. It also means the reset notice of BASH-004 can never appear in a subagent transcript, which the corpus confirms (0 of 5254). src/bash_mutations/cwd.rs 13-14 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-004",
      "area": "bash-shell",
      "behavior": "Claude Code resets the shell cwd when the shell ENDED outside the original cwd unioned with the `/add-dir` directories (membership is an every-path-inside-a-working-directory test), and the reset prints `Shell cwd was reset to <path>` into the tool result. The reset check runs only in UN-PINNED lanes: in a subagent lane it is skipped entirely, so the notice never appears there (0 of 5254 corpus notices sit in a subagent transcript).",
      "depends": "The notice is the only visible trace that a reset happened; csift treats it as a positive marker only and never parses it to learn the directory - it reads the record's own `cwd` field.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "13-18",
          "snippet": "//! - A `cd` into a SUBDIRECTORY of the project persists silently into later tool\n//!   calls; the tracked cwd (and so the record `cwd` field) follows it. A reset back\n//!   to the original directory happens only when the shell ends OUTSIDE the original\n//!   cwd and the `/add-dir` set, and it prints \"Shell cwd was reset to <path>\" into\n//!   the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//!   reset happens on every call and silently."
        }
      ],
      "instrument": "`csift search 'Shell cwd was reset to' --count-by session` over ~/.claude/projects. Counting rule: one record per reset notice.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'Shell cwd was reset to' --count-by version   ;   csift search 'Shell cwd was reset to' --count-by session   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}Shell cwd was reset to.{0,300}'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function e_\\(e,n,r,o=fb\\(n\\)\\).{0,160}'",
          "observed": "csift: 5356 matched records across 61 session keys; by version 46 at 2.1.258, 22 at 2.1.257, 15 at 2.1.252, 37 at 2.1.251. Binary, verbatim: `var gBt=(e)=>`${e.trim()}\\nShell cwd was reset to ${ne()}``, `ymt=/(?:^|\\n)(Shell cwd was reset to .+)$/`, and `function hBt(e,n){let r=e.project.cwd,o=ye(),d=Rpr();if(d||r!==o&&!e_(r,n)){try{du(o,e)}catch{return!0}if(!d)return s(\"tengu_bash_tool_reset_to_original_dir\",{}),!0}return!1}`. The membership test is `function e_(e,n,r,o=fb(n)){let d=r??Ir(e),f=Array.from(o).flatMap((_)=>QGt(_));return d.every((_)=>f.some((w)=>Dp(_,w,{caseFold:!1,uncShapeParity:!0})))}` - every path must sit inside one of the working directories. The call site is gated on the pin flag: `if(W=n.session.project.cwd,!Ne){let Rr=I();if(hBt(n.session,Rr.toolPermissionContext))U=gBt(\"\")}`.",
          "rule": "One record per line carrying the notice (csift census, one record per matched record); binary evidence is string presence plus the surrounding branch, one occurrence per site.",
          "note": "Still live: 46 notice records at 2.1.258 out of 5356 corpus-wide. Two additions: (a) the reset also fires unconditionally when CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR is truthy, in which case it is silent - the `if(!d)` arm is what emits the notice and the telemetry; (b) the notice builder trims whatever stderr preceded it and joins with a single newline, which is what produces the two rendered forms measured in BASH-006. src/bash_mutations/cwd.rs 13-18 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-005",
      "area": "bash-shell",
      "behavior": "With the environment variable `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` truthy (`1`, `true`, `yes`, `on`, lowercased and trimmed) Claude Code resets the shell cwd on EVERY Bash call and emits NO notice; the variable sits in the settings.json `env` allowlist, so it is settable per project.",
      "depends": "csift resolves operands per record instead of predicting an ending cwd across calls, so a session run under this setting still resolves correctly; a tracker that carried a predicted cwd forward would be silently wrong for every such session, with no notice available to detect it.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "17-18",
          "snippet": "//!   the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//!   reset happens on every call and silently."
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary for 2.1.258 grepping `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR`: expect one settings-env allowlist entry plus one truthiness-gated reset branch. Counting rule: one string occurrence per site.",
      "located": {
        "claude_code": "2.1.237",
        "csift": null,
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,260}CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR.{0,260}'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Le\\(t\\)\\{if\\(!t\\).{0,180}'",
          "observed": "5 occurrences. (1) the reader: `function Rpr(){return Le(process.env.CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR)}`. (2) the truthiness helper, verbatim: `function Le(t){if(!t)return!1;if(typeof t===\"boolean\")return t;let e=String(t).toLowerCase().trim();return[\"1\",\"true\",\"yes\",\"on\"].includes(e)}`. (3) an exports map entry `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR:()=>WL`. (4) an alphabetically sorted settings env allowlist array between \"BASH_MAX_TIMEOUT_MS\" and \"CLAUDE_CODE_API_KEY_HELPER_TTL_MS\". (5) a second grouped env array. The gated reset branch is `if(d||r!==o&&!e_(r,n)){try{du(o,e)}catch{return!0}if(!d)return s(\"tengu_bash_tool_reset_to_original_dir\",{}),!0}` with `d=Rpr()` - when d is truthy the reset runs on every call and the function falls through to `return!1`, so no notice and no telemetry are emitted.",
          "rule": "One string occurrence per site; truthiness set read verbatim from the helper; the silence is read off the `if(!d)` guard that gates the only notice-producing return.",
          "note": "Every element of the claim - the exact truthy set (1/true/yes/on, lowercased and trimmed), the every-call reset, the silence, and the settings env allowlist membership - is confirmed verbatim in the 2.1.258 binary. Not exercised end-to-end on disk: no corpus session ran with the variable set (it is unset in this environment), and it cannot be injected from inside a Bash call because Claude Code reads it from its own process env. One lane caveat, not a correction to the claim: the reset call site is itself behind the pin flag, so in a subagent lane the branch is never reached (the cwd never advances there anyway). src/bash_mutations/cwd.rs 17-18 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-006",
      "area": "bash-shell",
      "behavior": "The cwd-reset notice has exactly two rendered forms - it either starts the tool_result content (5 records, when stdout was empty) or is preceded by a single newline (5249) - and it lands in BOTH the tool_result content and `toolUseResult.stderr`: measured over 5,254 carrying records, 5,254 carried it in both places, 0 content-only and 0 stderr-only. In `toolUseResult.stderr` the stored form is invariably one leading newline then the sentence (5,254/5,254). The path inside the notice equals the carrying record's own `cwd` in 5,254/5,254 cases. It is the last line of the rendered content in 5,122/5,254 - the remainder are followed by the `staleReadFileStateHint` sentence or a background block.",
      "depends": "csift reads the `cwd` field rather than parsing the notice, so a wording change costs nothing while a change to the `cwd` field would break every bash operand resolution.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "15-18",
          "snippet": "//!   to the original directory happens only when the shell ends OUTSIDE the original\n//!   cwd and the `/add-dir` set, and it prints \"Shell cwd was reset to <path>\" into\n//!   the tool result. With `CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR` set truthy, the\n//!   reset happens on every call and silently."
        }
      ],
      "instrument": "`rg -c 'Shell cwd was reset to' ~/.claude/projects --glob '*.jsonl'`, then for each hit compare the notice's path to the carrying record's `cwd` and check whether the same text also appears in `toolUseResult.stderr`. Counting rule: one record per matching line; equality counted per record carrying the notice.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "src/bash_mutations/cwd.rs module doc; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; keep only lines containing the byte string 'Shell cwd was reset to'; json-parse; a record COUNTS only when re.compile(r'(?:^|\\\\n)Shell cwd was reset to .+$', re.M) matches inside toolUseResult.stderr; then check the same regex against the concatenated tool_result block text, classify the character run before the match, and compare the path in the notice with the record's own top-level cwd\\\"",
          "observed": "5254 records carry a genuine notice in toolUseResult.stderr. In 5254/5254 the stderr form is exactly one leading newline then the sentence, with nothing before it. In 5254/5254 the notice also appears in the rendered tool_result content: 5249 preceded by a newline, 5 starting the content (that is exactly two textual forms). stderr-only 0. The path inside the notice equals the carrying record's own cwd in 5254/5254. The notice is the LAST line of the rendered content in 5122/5254; the other 132 are followed by a later section, dominated by the staleReadFileStateHint sentence '[This command modified N file(s) you have previously read: ...]' with a few background blocks. All 5254 are top-level-lane records; versions include 10 at 2.1.258, 22 at 2.1.257, 15 at 2.1.252, 37 at 2.1.251. A looser filter that accepted the byte string anywhere added 8 'content-only' records, all of them tool output or prose quoting the sentence rather than notices.",
          "rule": "One record per notice, admitted only when the sentence starts a line inside toolUseResult.stderr; both-places, path-equality and last-line are each counted per admitted record.",
          "note": "The claim's shape survives at a larger n; three numbers and one wording need fixing. (a) The single 'content-only' record was a false positive of a byte-level filter: with the sentence required to start a line inside toolUseResult.stderr, both-places is 100%. (b) Path equality is now 5254/5254 rather than a 4229-record subset. (c) 'as its last line' is only true 97.5% of the time - the render order is stdout, stderr, background block, staleReadFileStateHint, rate-limit hint, joined by newlines, so anything after the stderr section pushes the notice off the end. Do not key a parser on last-line position. src/bash_mutations/cwd.rs 15-18 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-007",
      "area": "bash-shell",
      "behavior": "Claude Code's post-command cwd update lands between the tool_use record and its result record, so the TOOL_USE record carries the spawn (pre-command) directory while the RESULT record can already carry the post-command value: measured 52 of 119,070 Bash use/result pairs (0.0437%), with the neighbouring records agreeing with the tool_use side in 52/52 and with the result side in 46/52.",
      "depends": "csift's ANCHOR LAW reads `cwd` off the tool_use record everywhere (`files`, `recover`, stale-hint path resolution); anchoring on the result record instead silently mis-joins roughly one operand in four hundred, with no error.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "19-21",
          "snippet": "//! - ANCHOR LAW: read the `cwd` off the TOOL_USE record, not the result record. The\n//!   post-command cwd update is asynchronous and in about 0.24% of calls the result\n//!   record still carries the pre-command value."
        }
      ],
      "instrument": "Pair each Bash `tool_use` block id with its `tool_result` carrier in one transcript and compare the two records' `cwd` fields. Counting rule: disagreeing pairs divided by all Bash use/result pairs in the file (expect about 0.24%).",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; SPEC.md section 4.9; src/bash_mutations/cwd.rs ANCHOR LAW; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; per transcript map every Bash tool_use block id to its carrying record's top-level cwd; when a tool_result block quotes that id, compare the two records' cwd; for each disagreement also read the nearest preceding and nearest following user|assistant record's cwd\\\"",
          "observed": "119070 Bash use/result pairs; 52 disagree (0.0437%), 50 in the top-level lane and 2 in subagent lanes; versions include 2.1.257 (1), 2.1.252 (1), 2.1.251 (11), 2.1.208 (18). Direction, in all 52: the nearest PRECEDING record agrees with the TOOL_USE record (52/52); the nearest FOLLOWING record agrees with the RESULT record in 46/52 and with the tool_use in the remaining 6. So the tool_use record carries the pre-command spawn directory and the result record has already picked up the post-command value.",
          "rule": "Disagreeing pairs divided by all Bash tool_use/tool_result pairs joined by tool_use_id, corpus-wide, one comparison per pair; direction decided per disagreement by which side the neighbouring records agree with.",
          "note": "The ANCHOR LAW is confirmed correct and still load-bearing, but the rationale printed beside it in src/bash_mutations/cwd.rs 19-21 has the polarity inverted: it is not that the result record lags behind, it is that the result record RUNS AHEAD. The consequence is the same and if anything sharper - anchoring on the result record would join a relative operand against the directory the command left the shell in, i.e. after its own `cd`, which is exactly the wrong directory. The rate is 5.5x lower than the recorded 0.24%: use 0.0437% (about 1 in 2,300). The cited lines exist verbatim; only the wording of the second sentence needs replacing."
        }
      ]
    },
    {
      "id": "BASH-008",
      "area": "bash-shell",
      "behavior": "A backgrounded Bash command in which ANY top-level segment's first token is `cd`/`pushd`/`popd`/`chdir` gets the notice `Session cwd remains <path>; directory changes made by the backgrounded command do not apply to subsequent commands.`, carried on `toolUseResult.backgroundCwdHint` and appended to the background block of the result content (measured 1,204 records; 1,204/1,204 match the sentence exactly, appear in the rendered content, and name the carrying record's own cwd).",
      "depends": "csift treats a backgrounded command as never advancing the tracked cwd, matching the harness; no csift surface parses the hint, so a rename is not load-bearing, but the underlying suppression is.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "10-12",
          "snippet": "//! - The runner appends `&& pwd -P >| <tmp>/claude-<id>-cwd` to the command and reads\n//!   the file back afterward. Because the read-back is `&&`-chained, a command that\n//!   exits non-zero, and any backgrounded command, never advances the tracked cwd."
        }
      ],
      "instrument": "`rg -c 'backgroundCwdHint' ~/.claude/projects --glob '*.jsonl'`; expect a four-digit count in a large corpus. Counting rule: one occurrence per Bash result object carrying the key.",
      "located": {
        "claude_code": "2.1.237",
        "csift": null,
        "source": "dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; keep lines containing 'backgroundCwdHint'; json-parse; require toolUseResult to be an object carrying the key; match the value against ^Session cwd remains (.+); directory changes made by the backgrounded command do not apply to subsequent commands\\\\.$; compare group 1 with the record's own cwd; check whether the same string appears in the rendered tool_result content\\\"   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,700}backgroundCwdHint.{0,300}'",
          "observed": "1204 records carry toolUseResult.backgroundCwdHint. 1204/1204 match the sentence exactly, 1204/1204 also appear inside the rendered tool_result content, and the path in the hint equals the carrying record's own cwd in 1204/1204. Versions include 3 at 2.1.258 and 3 at 2.1.251. Binary: the value is built as `hn=ke.backgroundTaskId&&S9(e.command)?`Session cwd remains ${ne()}; directory changes made by the backgrounded command do not apply to subsequent commands.`:void 0`, appended to the background block (`if(I)_e+=\"\\n\"+I`) which is then joined into `[fe,me,_e,j,W].filter(Boolean).join(\"\\n\")`. The trigger predicate is `function S9(e){return ep(e).some((n)=>_O(n.trim()))}` over `function _O(e){let n=Uh(uC(e))[0];return n===\"cd\"||n===\"pushd\"||n===\"popd\"||n===\"chdir\"}`. The tool schema describes the field as \"Model-facing note that the session cwd was not changed by a backgrounded command containing a directory-change builtin (cd/pushd/popd/chdir)\".",
          "rule": "One occurrence per Bash result object carrying the key; sentence match, content presence and path equality each counted per carrying record.",
          "note": "Only the trigger wording needed fixing: it is not 'whose text starts with' a directory-change builtin - the command is split into top-level segments and ANY segment whose first token is cd/pushd/popd/chdir arms the hint, so `make build && cd /tmp` in a backgrounded command is enough. The four-digit corpus count the claim predicted is exactly right (1,204). src/bash_mutations/cwd.rs 10-12 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-009",
      "area": "bash-shell",
      "behavior": "A Bash result's `toolUseResult` object carries the always-present key set `{stdout, stderr, interrupted, isImage, noOutputExpected}` (44,340/44,340 objects) plus at least eleven observed optional keys - `backgroundTaskId`, `backgroundCwdHint`, `staleReadFileStateHint`, `gitOperation`, `persistedOutputPath`, `persistedOutputSize`, `returnCodeInterpretation`, `dangerouslyDisableSandbox`, `timedOutAfterMs`, `assistantAutoBackgrounded`, `backgroundEndsWithFinalResponse` (2.1.258 can also emit `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `ghRateLimitHint`) - and no structured field naming the paths the command touched: the only path-valued key, `persistedOutputPath`, names the externalized stdout file, and modified-file names appear only inside the prose of `staleReadFileStateHint`. The object is absent entirely on 73,006 subagent-lane results and is a bare string on 1,792 error-path results. The rendered result content is assembled as stdout, stderr, the background block, `staleReadFileStateHint` and the rate-limit hint, joined by newlines.",
      "depends": "csift keeps `toolUseResult` UNPARSED as a `RawValue` and reads only small named fields through `Record::tur_probe`, so a renamed key silently removes a `recover` freshness boundary or a `search --resolve-persisted` pointer with no error. Because the object names no path, bash file attribution is a lexical parse of `input.command` and is flagged `(heuristic)` on every surface - text, JSON, help and SKILL.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "123-124",
          "snippet": "    #[serde(default, rename = \"toolUseResult\")]\n    pub tool_use_result: Option<Box<serde_json::value::RawValue>>,"
        },
        {
          "path": "src/model/record.rs",
          "lines": "306-308",
          "snippet": "    #[serde(rename = \"persistedOutputPath\")]\n    pub(crate) persisted_output_path: Option<serde_json::Value>,\n    pub(crate) status: Option<serde_json::Value>,"
        }
      ],
      "instrument": "python over ~/.claude/projects: for every Bash `tool_result` carrier, union the top-level keys of `toolUseResult`; expect the five always-present keys plus the six optional ones, and no path key. Counting rule: one key set per Bash result record, unioned corpus-wide.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.6; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; per transcript collect Bash tool_use block ids; for every tool_result quoting one, record the python type of toolUseResult, union its top-level keys, count records missing any of stdout/stderr/interrupted/isImage/noOutputExpected, and where both a non-empty stderr and a staleReadFileStateHint are present check which appears first in the rendered block text\\\"   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'mapToolResultToToolResultBlockParam\\(\\{interrupted.{0,2400}'",
          "observed": "119138 Bash results seen: 44340 carry an object toolUseResult, 1792 carry a bare string, 73006 carry none (the subagent lane). Of the 44340 objects, 0 are missing any of the five core keys. Key census: stdout/stderr/interrupted/isImage/noOutputExpected 44340 each; then backgroundTaskId 2980, backgroundCwdHint 1204, staleReadFileStateHint 1077, gitOperation 827, persistedOutputPath 400, persistedOutputSize 400, returnCodeInterpretation 249, dangerouslyDisableSandbox 102, timedOutAfterMs 59, assistantAutoBackgrounded 21, backgroundEndsWithFinalResponse 8 - eleven optional keys, not six. 2.1.258 additionally constructs backgroundedByUser, backgroundedByTurnAbort, backgroundedToDeliverMessage and ghRateLimitHint on the same object. Render assembly, verbatim from the binary: `return{tool_use_id:z,type:\"tool_result\",content:[fe,me,_e,j,W].filter(Boolean).join(\"\\n\"),is_error:e}` where fe is stdout (or the persisted-output pointer), me is stderr, _e is the background block, j is staleReadFileStateHint and W is ghRateLimitHint. On disk stderr precedes staleReadFileStateHint in 134/134 records carrying both.",
          "rule": "One key set per Bash tool_result record whose toolUseResult is an object, unioned corpus-wide; order checked once per record carrying both a non-empty stderr and a stale-read hint.",
          "note": "The always-present five and the render assembly are exactly right (the latter now confirmed verbatim in the binary, with the rate-limit hint identified as ghRateLimitHint). Three corrections: the optional set is eleven observed rather than six - notably backgroundCwdHint was already ledgered as BASH-008 but missing from this key list; toolUseResult is not always an object (bare string on 1,792 error-path results, absent on the whole subagent lane); and 'NO path field of any kind' overstates it - persistedOutputPath is a path, just not an operand path. csift's design consequence is unchanged and if anything reinforced: no key names the mutated files, so bash file attribution stays a lexical parse of input.command. src/model/record.rs 116-117 and 299-301 exist verbatim."
        }
      ]
    },
    {
      "id": "BASH-010",
      "area": "bash-shell",
      "behavior": "`toolUseResult` separates `stdout` from `stderr`, and when stderr is empty and no other rendered section is present the tool_result block text equals `stdout` byte-exactly - no wrapper decoration and no line-number gutter (measured 34,246/34,246 such results corpus-wide, 0 exceptions). When stdout is empty the block instead carries the synthetic text `(Bash completed with no output)`. The renderer does apply a leading-blank-line strip and a trailing trim to stdout, which is a no-op on every stored value measured. `cat -n` and `nl` are not absent from real sessions (563 and 96 of 119,190 Bash commands respectively), so a read anchor must gate on the command shape rather than assume no gutter.",
      "depends": "csift's `recover` read anchors take a gated command's stdout as the file window verbatim, so a wrapper prefix or a gutter would splice decoration into a reconstructed file; the anchor therefore also demands a non-error, non-interrupted result.",
      "code": [
        {
          "path": "src/recover/bash_anchors.rs",
          "lines": "5-11",
          "snippet": "//! The lexical classifier decides WHAT a command shape means; this layer supplies\n//! the transcript-side gates:\n//! - the command's RESULT must not be an error (`failed_ids` - a failed write never\n//!   landed, a failed read proves nothing);\n//! - a READ anchor additionally needs the top-level `toolUseResult` echo with EMPTY\n//!   stderr, `interrupted` false, and NO persisted-output pointer (an externalized\n//!   stdout is not the inline text). The echo is a per-LANE fact, not a per-level one"
        }
      ],
      "instrument": "python over one transcript: for Bash results with empty `toolUseResult.stderr`, compare `toolUseResult.stdout` to the tool_result block's rendered text. Counting rule: one byte comparison per Bash result with empty stderr.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.7; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"walk ~/.claude/projects; for every Bash tool_result whose toolUseResult is an object with stderr=='' and a non-empty stdout and no persistedOutputPath, no backgroundTaskId, interrupted falsey, no staleReadFileStateHint and no returnCodeInterpretation, compare toolUseResult.stdout byte-for-byte with the concatenated text of the tool_result block, and also against stdout with leading blank lines stripped and trailing whitespace trimmed\\\"   ;   a second pass counting Bash tool_use commands containing 'cat -n' or a top-level segment whose first token is 'nl'",
          "observed": "34246 comparisons: 34246 byte-exact, 0 needing the leading-blank-line strip plus trimEnd, 0 differing. The binary does normalize on the way out - `let fe=n;if(n)fe=n.replace(/^(\\s*\\n)+/,\"\"),fe=fe.trimEnd()` - so the equality is telling us the stored stdout is already in that normal form. Separately, when stdout is empty the rendered text is the synthetic string `(Bash completed with no output)` rather than an empty block (348 such records). Line-numbering commands are NOT absent from the corpus: of 119,190 Bash commands, 563 contain `cat -n` and 96 have `nl` as a segment head.",
          "rule": "One byte comparison per Bash result with an object toolUseResult, empty stderr, non-empty stdout and none of the other rendered sections present; the command census counts one command per Bash tool_use block.",
          "note": "The load-bearing half is confirmed at 1,600x the original sample size: nothing is spliced into the block, so recover can take a gated command's stdout as the file window verbatim. Two additions the anchor layer should know: (a) the empty-stdout case renders a synthetic sentence, so an anchor that reads the BLOCK text rather than toolUseResult.stdout would splice `(Bash completed with no output)` into a reconstructed empty file - reading the echo, as src/recover/bash_anchors.rs already requires, avoids this; (b) the parenthetical about `cat -n`/`nl` describes the 1,085-command sample only and does not generalise - both appear in the wider corpus, which is a reason the shape gate (plain `cat`/`head -n N`/`sed -n 'A,Bp'`) is doing real work. src/recover/bash_anchors.rs 5-13 exists verbatim."
        }
      ]
    },
    {
      "id": "BASH-011",
      "area": "bash-shell",
      "behavior": "`toolUseResult.returnCodeInterpretation` is not a generic non-zero marker. Claude Code emits it only for a CLOSED command-to-interpretation table (grep/rg/egrep/fgrep -> 'No matches found', find -> 'Some directories were inaccessible', diff -> 'Files differ', test and [ -> 'Condition is false', plus `git grep`/`git diff` routed to the same two), and only at exit code exactly 1, which that table classifies as NON-error (isError is n>=2). Every other non-zero exit falls to the default interpreter, which marks the call an error and writes no such field. The half that survives is the second: absence does not imply exit zero -- 91 of 91 Bash results flagged is_error:true carried no field. Measured share 30/4599 = 0.65% of Bash results in scope.",
      "depends": "csift never infers exit status from the absence of that key; `recover`'s content-anchor gates demand a positively clean result (non-error AND empty stderr) rather than trusting a missing field.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "18-22",
          "snippet": "//!   whole command to a CLEAN result (exit ok AND empty stderr: a failing\n//!   cat/tee/echo always writes stderr, so a clean echo proves the write landed even\n//!   in a `;`/newline chain whose exit code only reflects the LAST command). The\n//!   anchor's own segment must also be free of substitutions/subshells, and no OTHER\n//!   part of the command may touch the same resolved path (`same_path_hits`)."
        }
      ],
      "instrument": "`rg -c 'returnCodeInterpretation' ~/.claude/projects --glob '*.jsonl'` against the count of Bash result records. Counting rule: one occurrence per Bash `toolUseResult` object.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 $B | rg -o '.{0,500}Files differ.{0,200}'  AND  strings -n 6 $B | rg -o '.{0,400}Semantic interpretation for non-error exit codes.{0,60}'  AND an inline python3 census over $P/*.jsonl pairing every Bash tool_use id to its tool_result carrier and reading toolUseResult",
          "observed": "Binary: the field's own schema text is \"Semantic interpretation for non-error exit codes with special meaning\". The producer is a closed table -- x$=(e)=>(n,r,o)=>({isError:n>=2,message:n===1?e:void 0}) and Wzo=new Map([[\"grep\",x$(\"No matches found\")],[\"rg\",x$(\"No matches found\")],[\"egrep\",x$(\"No matches found\")],[\"fgrep\",x$(\"No matches found\")],[\"find\",x$(\"Some directories were inaccessible\")],[\"diff\",x$(\"Files differ\")],[\"test\",x$(\"Condition is false\")],[\"[\",x$(\"Condition is false\")]]) -- with a default interpreter jzo=(e,n,r)=>({isError:e!==0,message:e!==0?`Command failed with exit code ${e}`:void 0}) and a git arm returning the grep/diff interpreter for `git grep`/`git diff`. Corpus: 4599 Bash tool_result carriers, 30 carry returnCodeInterpretation (0.65%); the ONLY two distinct values are 'No matches found' (29) and 'Files differ' (1). 123 carriers have non-empty toolUseResult.stderr and 91 carry is_error:true -- 123/123 and 91/91 of those carry NO returnCodeInterpretation.",
          "rule": "One key set per Bash tool_result record (a tool_result block whose tool_use_id resolves to a tool_use block named Bash), counted over every top-level transcript in $P. Ratio = records carrying the key over all Bash tool_result records. The absence half is counted as: Bash results with is_error:true that carry the key (expect 0).",
          "note": "Not Claude Code drift: the same command-to-interpretation table is present in 2.1.229, 2.1.234, 2.1.241 and 2.1.252 as well as 2.1.258 (one match each for the `[\"grep\",...(\"No matches found\")]` entry), so the ledger wording was wrong when written rather than overtaken. The csift consequence in the depends clause is unaffected and in fact strengthened: since the field is emitted for a narrow non-error subset only, inferring exit status from its absence would be wrong in both directions, and recover's positively-clean gate (non-error AND empty stderr) is the right instrument. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 18-22."
        }
      ]
    },
    {
      "id": "BASH-012",
      "area": "bash-shell",
      "behavior": "The omission belongs to the WORKFLOW subagent lane specifically, not to subagent transcripts as a class. A Bash tool_result under <session>/subagents/workflows/wf_*/agent-*.jsonl carries no top-level toolUseResult echo at all (0 of 1302 at CC 2.1.258; 0 of 3867 across all versions in scope). A Bash tool_result in the two BUILT-IN-location lanes -- <session>/subagents/agent-*.jsonl, covering both one-shot Task/Agent subagents and named teammates -- does carry the echo, at 283 of 286 (99.0%) at CC 2.1.258. So stdout/stderr/interrupted exist in built-in subagent lanes as well as top-level ones, and are missing only in the workflow lane.",
      "depends": "`recover`'s READ content anchors are gated on the ECHO ITSELF, not on the lane: `gated_stdout` admits a carrier only when its top-level `toolUseResult` carries a `stdout` string with empty stderr, `interrupted` false and no `persistedOutputPath`; no code on the anchor path inspects the transcript's lane. So read anchors reach every lane that carries the echo - the main lane and both built-in-location subagent shapes (one-shot Task/Agent and teammate) - and are unreachable only in the WORKFLOW lane, which carries none. WRITE anchors, being input-side, work in every lane.",
      "code": [
        {
          "path": "src/recover/bash_anchors.rs",
          "lines": "279-286",
          "snippet": "/// The COMPLETENESS-gated stdout of a Bash result: the FIRST carrier record in this\n/// turn answering the tool_use id, whose `toolUseResult` echoes the command output.\n/// `None` when that carrier lacks the echo (workflow lanes never carry it; built-in\n/// and teammate lanes and the main lane do - the module doc has the measurement),\n/// stderr is non-empty, the command was interrupted, or the output was externalized\n/// to a persisted file (the inline text is not the whole stdout then). The gate is\n/// data-driven: no lane predicate, and no second carrier is tried.\nfn gated_stdout("
        },
        {
          "path": "src/recover/bash_anchors.rs",
          "lines": "9-15",
          "snippet": "//! - a READ anchor additionally needs the top-level `toolUseResult` echo with EMPTY\n//!   stderr, `interrupted` false, and NO persisted-output pointer (an externalized\n//!   stdout is not the inline text). The echo is a per-LANE fact, not a per-level one\n//!   (re-measured at CC 2.1.258, v0.10.1): WORKFLOW lanes never carry it (0 of 3867\n//!   Bash results), built-in Task/Agent and teammate lanes carry it 97-99% of the\n//!   time, the main lane always - so read anchors reach every lane that carries the\n//!   echo, by construction, while WRITE anchors (input-side) work in every lane;"
        },
        {
          "path": "src/recover/bash_anchors.rs",
          "lines": "300-314",
          "snippet": "        let tur = rec.tool_use_result_value()?;\n        let stdout = tur.get(\"stdout\").and_then(serde_json::Value::as_str)?;\n        let stderr_clean = tur\n            .get(\"stderr\")\n            .and_then(serde_json::Value::as_str)\n            .is_none_or(|s| s.trim().is_empty());\n        let interrupted = tur\n            .get(\"interrupted\")\n            .and_then(serde_json::Value::as_bool)\n            .unwrap_or(false);\n        let persisted = tur.get(\"persistedOutputPath\").is_some();\n        if stderr_clean && !interrupted && !persisted {\n            return Some((*line_no, rec.timestamp.clone(), stdout.to_string()));\n        }\n        return None;"
        }
      ],
      "instrument": "`rg -c '\"interrupted\":' <a top-level transcript>` versus the same grep on one of its subagent transcripts (expect zero on the subagent). Counting rule: Bash tool_result records carrying the echo, per lane.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "src/recover/bash_anchors.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "inline python3 census over $P/*/subagents/**/*.jsonl (journal.jsonl excluded), splitting each transcript by on-disk shape -- 'workflow' when the path contains /subagents/workflows/, else 'builtin' -- pairing every Bash tool_use id to its tool_result carrier and testing whether that carrier has a top-level toolUseResult object; re-run filtered to records whose `version` field is exactly 2.1.258. Cross-checked live against this verifier's own workflow-lane transcript resolved with `csift whoami @trap:<marker>`.",
          "observed": "Restricted to records stamped version 2.1.258: builtin subagent lanes 283 Bash tool_results WITH a toolUseResult object vs 3 without (99.0% carry the echo); workflow subagent lanes 0 with vs 1302 without (0.0%). Across all versions in scope: builtin 2547 with / 86 without (96.7%) -- split further as built-in Task lanes 2222 with / 83 without and teammate lanes 325 with / 3 without -- against workflow 0 with / 3867 without. The keys observed on a built-in subagent Bash result are ['interrupted','isImage','noOutputExpected','stderr','stdout']. Live control: this verifier's own workflow lane at 2.1.258 showed 15 of 15 Bash tool_results with no toolUseResult (and 1 Read likewise); the top-level parent transcript in the same session carried the echo on 2193 of 2193 Bash results.",
          "rule": "One observation per Bash tool_result record (tool_result block whose tool_use_id resolves to a Bash tool_use block), bucketed by the carrying transcript's on-disk lane shape. echo_rate = records with a dict-valued top-level toolUseResult over all Bash tool_results in that bucket.",
          "note": "This is a decisive refutation of the claim as written, reproducible in one command on current Claude Code, and it is not a version effect -- built-in lanes carried the echo continuously from 2.1.191 through 2.1.258 while workflow lanes never did. The csift consequence is a conservative miss, not a correctness bug: recover's READ content anchors are gated to lanes carrying the echo and are currently declared top-level-only, so read anchors that built-in Task/Agent and teammate lanes could legitimately support are being forgone. WRITE anchors are input-side and are unaffected in every lane. The gate itself is sound wherever it fires; only its stated reach is too narrow. Code site confirmed verbatim at src/recover/bash_anchors.rs lines 9-13."
        }
      ]
    },
    {
      "id": "BASH-013",
      "area": "bash-shell",
      "behavior": "Claude Code stores a Bash command verbatim in the tool_use block's `input.command`, so a quoted-delimiter heredoc's body sits byte-exact inside that string; 444 of 456 measured heredocs (97.4%) use a quoted delimiter, which makes the body a literal already present in the transcript.",
      "depends": "csift's `recover` content anchors take a quoted-heredoc body as a full write anchor, and admit an unquoted delimiter only when the body is free of `$`, backticks and backslashes. If the harness ever normalized or truncated `input.command`, the anchor would fabricate file bytes.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "4-8",
          "snippet": "//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file."
        },
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "30-31",
          "snippet": "//! - An unquoted heredoc delimiter admits the body only when the body is free of\n//!   `$`, backticks, and backslashes (bash would expand them)."
        }
      ],
      "instrument": "python over one project's transcripts: for Bash tool_use blocks whose `input.command` contains `<<`, count how many use a quoted delimiter (`<<'X'` or `<<\"X\"`). Counting rule: one heredoc per `<<` occurrence in a command string.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.7; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "LIVE: ran a Bash call writing a quoted-delimiter heredoc (`cat > <tmpfile> <<'HDEOF'`) whose body contains $HOME, a backtick pair, a literal backslash-n, a tab and both quote kinds; then an inline python3 that re-reads the recording transcript, extracts the heredoc body from that tool_use block's input.command with re.search(r\"<<'HDEOF'\\n(.*?)\\nHDEOF\\n\", cmd, re.S), and md5-compares it to the bytes on disk. CENSUS: inline python3 over $P/*.jsonl matching <<-?\\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*)) in every Bash tool_use input.command, skipping <<< here-strings.",
          "observed": "Live: transcript body 72 bytes, disk file 72 bytes, transcript md5 72806c7cf4d3b5cbe842af305424d1a7, disk md5 72806c7cf4d3b5cbe842af305424d1a7, BYTE-EXACT True -- the expansion-triggering characters survived verbatim on both sides. Census: 4596 Bash tool_use blocks scanned, 775 of them contain at least one heredoc opener, 978 openers total; 949 quoted (97.0%), 29 bare (3.0%).",
          "rule": "One heredoc per << occurrence in a Bash tool_use input.command (<<< here-strings excluded), deduplicated by tool_use block id so a record repeated across the file counts once. Quoted = the delimiter is wrapped in single or double quotes.",
          "note": "Both halves confirmed by instrument. The byte-exactness half is the load-bearing one for recover's write anchors and it was tested directly rather than inferred: a body deliberately loaded with $, backtick, backslash, tab and quotes round-tripped through input.command with an identical md5, so Claude Code performs no normalization, escaping or truncation of the command string. Code sites confirmed verbatim at src/bash_mutations/anchors.rs lines 4-8 and 30-31."
        }
      ]
    },
    {
      "id": "BASH-014",
      "area": "bash-shell",
      "behavior": "The ssh share does not reproduce: measured 0.20% of quoted-heredoc-bearing Bash records corpus-wide (26 of 13101) and 0.9% of openers in one project directory (9 of 978), against the claimed ~16%. The dominant non-file-content heredoc shape is the INTERPRETER heredoc, not the ssh payload -- 615 of 978 openers (62.9%) in scope are fed to python3, versus 279 to cat or tee. Corpus-wide the same ordering holds (2478 cat/tee-bearing records against 423 interpreter-bearing ones under narrower adjacency, with ssh two orders of magnitude below both).",
      "depends": "csift admits a heredoc as a write anchor only when its CONSUMER is `cat` or `tee`, which excludes both shapes and enforces nesting depth 0 by construction; admitting nested or interpreter heredocs is the single largest fabrication trap measured.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "26-29",
          "snippet": "//! - A heredoc anchors only when its CONSUMER is `cat` or `tee` (a plain local file\n//!   write). An interpreter heredoc (`python3 <<EOF`) is a SCRIPT, not file content,\n//!   and an `ssh`-fed heredoc writes on a REMOTE filesystem - the consumer gate\n//!   excludes both, which also enforces nesting depth 0 by construction."
        }
      ],
      "instrument": "`csift files <target> --by timeline` on a heredoc-heavy session, cross-checked against the raw commands: local heredoc writes are attributed, ssh-nested ones are not. Counting rule: one write per heredoc opener; nesting depth counted per command.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search \"<<-?'[A-Za-z_]\" -t agent.tool.use --count-by tool  AND  csift search \"ssh [^\\n]{0,120}<<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool  AND  csift search \"(python3?|node|bash|sh|perl|ruby|psql|jq) <<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool  AND  csift search \"(cat|tee)( -a)? [^\\n]{0,80}<<-?'?[A-Za-z_]\" -t agent.tool.use --count-by tool (all corpus-wide over ~/.claude/projects), plus the per-opener inline python3 consumer census over $P/*.jsonl",
          "observed": "Corpus-wide, Bash key only: 13101 records carry a quoted heredoc opener; 26 records carry an ssh-fed heredoc opener = 0.20%; 423 carry an interpreter-fed opener; 2478 carry a cat/tee-fed opener. Per-opener in $P: of 978 openers, the resolved consumer token is python3 on 615 (62.9%), cat on 276 (28.2%), python on 18, bash on 9, tee on 3; only 9 openers (0.9%) have `ssh` anywhere on the opener's own line.",
          "rule": "csift figures count RECORDS matching the regex under -t agent.tool.use, bucketed by the tool key (Bash row only), so a record with two heredocs counts once -- they are share-of-records, not share-of-openers. The python figures count one opener per << occurrence and resolve the consumer as the last recognized interpreter/writer token on the opener's logical line.",
          "note": "Claude Code's behavior has not changed -- heredocs are still stored verbatim in input.command and an ssh-fed one still targets a remote filesystem -- so this is a corpus-statistics correction, not drift. The depends clause survives intact and is if anything better supported: the single cat/tee consumer gate excludes the interpreter shape (the large trap, 62.9% of openers) and the ssh shape (the small one) by the same rule, and enforces nesting depth 0 by construction. Restating the claim around the interpreter share rather than the ssh share would make it both true and more load-bearing. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 26-29."
        }
      ]
    },
    {
      "id": "BASH-015",
      "area": "bash-shell",
      "behavior": "Claude Code hoists exactly one deterministic bash-danger class - `dangerous-rm`, an `rm`/`rmdir` whose target is a bare `$VAR/...` or `${VAR}/...` - to a human approval prompt EVEN under bypass permissions, as a `classifierApprovable:false` safety check. The classifier is a purely static regex path with no LLM and no filesystem access, and it is PURELY LEXICAL: it never checks whether the variable is actually empty.",
      "depends": "csift ports the classifier 1:1 so `agents` can split a frozen lane's `pending_classification` into `escalation-blocked` - the one state the jsonl can positively confirm - versus `awaiting-execution`; \"improving\" the port to resolve variables would diverge from the real hoist decision and mispredict whether Claude Code actually blocks.",
      "code": [
        {
          "path": "src/bash_danger.rs",
          "lines": "1-6",
          "snippet": "//! Faithful port of Claude Code's `dangerous-rm` bash classifier - the ONE deterministic\n//! bash-danger class CC hoists to a human approval prompt EVEN under bypass-permissions (a\n//! `classifierApprovable:false` safetyCheck). Used by `agents` to tell a frozen lane that is\n//! **escalation-blocked** (a pending Bash tool_use CC would hoist → waiting for a human \"Yes\")\n//! apart from one merely **awaiting-execution** (a slow tool) - a distinction the jsonl alone\n//! otherwise can't make (the escalation lives only in CC process memory; see `subagent.rs`)."
        },
        {
          "path": "src/bash_danger.rs",
          "lines": "8-14",
          "snippet": "//! **Extracted 1:1 from the CC 2.1.193 Mach-O** (function `Ywa` + the `egp`/`Zhp` regexes),\n//! re-grepped at port time (`Dangerous rm operation` / `possibly-empty variable path` strings +\n//! the two regexes verbatim). CC flags `rm`/`rmdir` whose target is a bare `$VAR/…` / `${VAR}/…`.\n//! It is PURELY LEXICAL: it does NOT check whether the variable is actually empty - CC knowingly\n//! accepts that false-positive rate (it would rather over-prompt on `rm $VAR/…`). We MIRROR that\n//! faithfully; do NOT \"improve\" it to resolve variables, or csift's verdict diverges from CC's\n//! real hoist decision and we'd mispredict whether CC actually blocks."
        }
      ],
      "instrument": "Grepping the literal 'Dangerous rm operation' no longer finds anything -- the reason string is assembled from a template. Grep `possibly-empty variable path` (still literal, two forms) or the template `Dangerous ${` instead.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.9; src/bash_danger.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 $B | rg -n 'Dangerous rm operation|possibly-empty variable path'  AND  strings -n 6 $B | rg -o 'Dangerous [^\"`,]{0,60}operation[^\"`,]{0,50}' | sort -u  AND  strings -n 6 $B | rg -o 'function sF\\(.{0,400}'  AND  strings -n 6 $B | rg -o 'dangerousRemoval:\\{[^}]*\\}'  AND  strings -n 6 $B | rg -o 'circuitBreaker:\"[a-zA-Z]+\"' | sort -u  AND  strings -n 6 $B | rg -o 'function Po\\(.{0,300}'",
          "observed": "The safety check is built verbatim by: function sF(e,n,r){return{behavior:\"ask\",message:n,decisionReason:{type:\"safetyCheck\",reason:`Dangerous ${e} operation ${r}`,classifierApprovable:!1,circuitBreaker:\"dangerousRemoval\"},suggestions:[]}}. The bypass table reads dangerousRemoval:{bypassImmune:!0,classifierRouted:!0} and is consumed by function TUe(e){return e.circuitBreaker!==void 0&&_lr[e.circuitBreaker]?.bypassImmune===!0}; the only other bash-reachable breakers are backgroundOperator:{bypassImmune:!1,classifierRouted:!0} and suspiciousWindowsPath:{bypassImmune:!1,classifierRouted:!0}. The matcher is pure regex with no emptiness resolution: function hnt(e){if(!e.includes(\"$\")||!/\\brm(?:dir)?\\b/.test(e))return null; ... if(yto.test(I))return{command:_,target:I}}. The strings 'on possibly-empty variable path: ' and 'on possibly-empty variable path inside command substitution: ' are both present. A literal 'Dangerous rm operation' is NOT present (rg -c returned no match); the four template forms present are 'Dangerous ${e} operation ${r}', 'Dangerous ${_} operation detected: '${v}'', 'Dangerous ${e} operation detected: '${U}'' and 'Dangerous ${F.command} operation detected inside command substitution: '${F.target}''. The surrounding removal checker _9 opens with let{resolvedPath:v}=Po(ce(),r), and function Po(e,n){...try{let i=e.realpathSync(n);return{resolvedPath:i,isSymlink:i!==n,isCanonical:!0}}catch...}.",
          "rule": "String presence or absence in the 2.1.258 binary, one occurrence per distinct site; the bypass verdict is read off the single _lr breaker table entry for dangerousRemoval, and the fs verdict off whether a realpath call is reachable from the checker's entry.",
          "note": "The load-bearing core all holds on current Claude Code and was confirmed by instrument rather than by reading: classifierApprovable:!1 is verbatim in sF; dangerousRemoval is the ONE bash-reachable breaker with bypassImmune:!0, so it is still the single class hoisted to a human even under bypass permissions (backgroundOperator and suspiciousWindowsPath are both bypassImmune:!1); and the matcher is purely lexical with no emptiness check, exactly as the port mirrors. Code sites confirmed verbatim at src/bash_danger.rs lines 1-6 and 8-14."
        }
      ]
    },
    {
      "id": "BASH-016",
      "area": "bash-shell",
      "behavior": "The hoist decision is two regexes: a clause head accepting optional `VAR=val` assignments, an optional backslash and an optional `path/` prefix before `rm` or `rmdir` at a word boundary, and a removal TARGET matching a bare `$VAR` or `${VAR}` (optionally double-quoted) immediately followed by `/` and then one of `*`, `$`, `/`, a quote, or end of string.",
      "depends": "csift's `EGP` and `ZHP` are verbatim ports of those two regexes, with the two lookaround-using preprocessing transforms hand-ported because the Rust regex crate has no lookaround; drift here changes which frozen lanes csift calls escalation-blocked.",
      "code": [
        {
          "path": "src/bash_danger.rs",
          "lines": "28-33",
          "snippet": "/// `egp` (verbatim): a clause head - optional `VAR=val ` assignments, optional backslash, optional\n/// `path/` prefix, then `rm`/`rmdir` at a word boundary. Capture group 1 = `rm` | `rmdir`.\nstatic EGP: LazyLock<Regex> = LazyLock::new(|| {\n    Regex::new(r\"^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*/)?(rm|rmdir)(?:\\s|$)\")\n        .expect(\"egp\")\n});"
        },
        {
          "path": "src/bash_danger.rs",
          "lines": "35-41",
          "snippet": "/// `Zhp` (verbatim): a removal TARGET that is a bare `$VAR`/`${VAR}` (optional surrounding `\"`)\n/// immediately followed by `/` then one of `* $ / \" ' <end>` - i.e. `$VAR/…`. This is what makes\n/// `\"$SCRATCH/$f\"` dangerous. Lexical only (no emptiness check).\nstatic ZHP: LazyLock<Regex> = LazyLock::new(|| {\n    Regex::new(r#\"^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?/(?:\\*|\\$|/|[\"']|$)\"#)\n        .expect(\"Zhp\")\n});"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary for 2.1.258 grepping `rm|rmdir` surfaces the same two regex bodies; the port is pinned by the unit tests in `src/bash_danger.rs`. Counting rule: a command is dangerous when the head regex matches a clause and the target regex matches one of that clause's operands.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.7.0",
        "source": "AGENTS.md section 3.9; src/bash_danger.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 $B | rg -o '\\^\\(\\?:\\[A-Za-z_\\]\\[A-Za-z0-9_\\]\\*.{0,70}rm\\|rmdir.{0,15}' | sort -u  AND  strings -n 6 $B | rg -o '\\^\"\\?\\\\\\$\\(\\?:.{0,90}' | sort -u  AND  sed -n '28,41p' src/bash_danger.rs",
          "observed": "Binary head regex: ^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*\\/)?(rm|rmdir)(?:\\s|$) -- bound in source as _to. Binary target regex: ^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?\\/(?:\\*|\\$|\\/|[\"']|$) -- bound as yto. csift EGP: ^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*/)?(rm|rmdir)(?:\\s|$). csift ZHP: ^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?/(?:\\*|\\$|/|[\"']|$). Each binary body appears twice in the strings output (the standalone string and the source-line context). The use site is: let f=d.match(_to); ... let _=f[1]===\"rmdir\"?\"rmdir\":\"rm\" ... if(yto.test(I))return{command:_,target:I}.",
          "rule": "Character-for-character equality of the two regex bodies after removing JavaScript regex-literal escaping of the forward slash (\\/ in the binary, / in Rust), which is a lexical artifact of the literal syntax and not a pattern difference. Capture-group semantics checked at the use site: group 1 of the head regex selects rm vs rmdir; the target regex is applied to each of that clause's operands.",
          "note": "Both regexes are still byte-identical to the ported EGP/ZHP on current Claude Code, and the described semantics match the use site: the head regex accepts leading VAR=val assignments, an optional backslash and an optional path/ prefix before rm or rmdir at a word boundary, and the target regex accepts a bare $VAR or ${VAR}, optionally double-quoted, immediately followed by / and then one of * $ / a quote or end-of-string. Code sites confirmed verbatim at src/bash_danger.rs lines 28-33 and 35-41."
        }
      ]
    },
    {
      "id": "BASH-017",
      "area": "bash-shell",
      "behavior": "By 2.1.228 the dangerous-rm classifier had evolved past the generation csift ported: it strips `$(...)` and `(...)` groups to a FIXPOINT (the port is single-pass), and a tree-sitter pass bails to explicit approval when a command carries more than 64 command substitutions.",
      "depends": "csift's escalation-blocked prediction can therefore diverge from current Claude Code on those command shapes; the divergence is recorded in-code as a staleness note and a port-refresh follow-up rather than silent drift. The same lexical bash classifier deliberately does not run on Windows `PowerShell` records, where a pending lane classifies `awaiting-execution`.",
      "code": [
        {
          "path": "src/bash_danger.rs",
          "lines": "84-91",
          "snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary for 2.1.258, reading the decompiled logic around `Dangerous rm operation` for the fixpoint strip loop and the substitution-count bail, then comparing against `src/bash_danger.rs`. Counting rule: command substitutions per command string versus the 64 threshold; presence or absence of the fixpoint loop on each side.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.4",
        "source": "SPEC.md section 6 v0.7.4 ledger item 3; src/bash_danger.rs staleness note"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 $B | rg -o 'function hnt\\(.{0,900}'  AND  strings -n 6 $B | rg -o 'async function Bno\\(.{0,1200}'  AND  strings -n 6 $B | rg -c '\"PowerShell\"'  AND  strings -n 6 $B | rg -o 'circuitBreaker:\"suspiciousWindowsPath\".{0,60}'  AND  sed -n '84,91p' src/bash_danger.rs",
          "observed": "Fixpoint strip, verbatim inside hnt: for(let o=\"\";o!==r;)o=r,r=r.replace(/\\$\\([^()]*\\)/g,\" \").replace(/(?<!\\$)\\([^()]*\\)/g,\" \"); -- an unbounded loop that strips BOTH $(...) and non-$ (...) groups. A second, bounded fixpoint lives in the AST pass: for(let W=\"\",z=0;W!==B&&z<16;z++)W=B,B=B.replace(/\\$\\([^()]*\\)/g,\"__CMDSUB__\");. Substitution bail, verbatim: if(o.length>64){if(/\\brm(?:dir)?\\b/.test(e.text))return sF(\"rm\",`This command contains ${o.length} command substitutions \\u2014 too many to analyze for catastrophic removals. This requires explicit approval.`,`\\u2014 too many command substitutions to analyze (${o.length})`);return null} -- o is filled by a tree walk collecting nodes whose type is command_substitution or process_substitution. PowerShell separation: the literal \"PowerShell\" appears 13 times; a distinct PowerShell destructive-pattern table exists beside the bash path (entries keyed (Remove-Item|rm|del|rd|rmdir|ri) with -Recurse and/or -Force, categories remove_item_recursive_force / remove_item_recursive / remove_item_force) along with a deny path `Remove-Item on system path '${e}' is blocked.` carrying its own classifierApprovable:!1.",
          "rule": "Presence or absence of the fixpoint loop and of the substitution-count bail on each side; the threshold is read off the literal comparison o.length>64 where o is the collected substitution-node list, one entry per command_substitution or process_substitution node in the parsed command.",
          "note": "The claim's substance holds on current Claude Code: both divergences the staleness note records are still present at 2.1.258, so csift's port remains single-pass against a fixpoint stripper and knows nothing of the substitution-count bail, and the recorded divergence is real rather than resolved. The PowerShell half of the depends clause also holds -- Claude Code ships a separate PowerShell tool with its own destructive-pattern classifier, so declining to run the lexical bash classifier on PowerShell records matches the harness rather than under-reading it. Code site confirmed verbatim at src/bash_danger.rs lines 84-91."
        }
      ]
    },
    {
      "id": "BASH-018",
      "area": "bash-shell",
      "behavior": "A pending tool approval lives ONLY in Claude Code process memory - it is in neither the transcript nor any control file - so on disk an escalation-blocked lane, a slow tool and a wedged one share ONE signature: the assistant's `tool_use` record (`stop_reason:\"tool_use\"`) is the last record, with no `tool_result` for its `tool_use_id`, and the block carries only `{type, id, name, input}` with no permission or escalation field.",
      "depends": "csift's lifecycle forces `status:\"running\"` whenever the newest meaningful record is an unreturned `tool_use` (never `completed`) and refuses to distinguish slow from wedged; `status`/`wait` state outright that the pending permission prompt is invisible.",
      "code": [
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "35-40",
          "snippet": "    // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n    // terminates with a visible assistant message (a clean finish), AND whether the lane is\n    // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n    // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n    // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n    // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
        },
        {
          "path": "src/live/tail.rs",
          "lines": "3-5",
          "snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),"
        }
      ],
      "instrument": "`csift search \"\" <target> --count-by pairing` (the `pending` bucket) plus `csift agents <target> --format json | jq 'select(.kind==\"agent\" and .status==\"running\") | {pending_tool_name, pending_classification, pending_since_utc}'`. Counting rule: one record per pairing bucket; one node per lane.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.9; SKILL.md status honesty limits"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c 'json.load(open(one ~/.claude/sessions/<pid>.json)); print top-level keys'  AND  rg -io 'pending[A-Za-z]*|permission[A-Za-z]*|approval[A-Za-z]*|toolUse[A-Za-z]*' ~/.claude/sessions/*.json | sort -u  AND  rg -il 'pendingApproval|permissionRequest|awaitingApproval|classifierApprovable' ~/.claude/tasks  AND an inline python3 census of tool_use block key sets over $P/*.jsonl  AND  csift status @<session>  AND  csift search \"\" @trap:<marker> --count-by pairing",
          "observed": "Session registry: the 19 top-level keys of a ~/.claude/sessions/<pid>.json are pid, sessionId, cwd, startedAt, procStart, version, peerProtocol, peerFeatures, kind, entrypoint, pidDomain, messagingSocketPath, name, nameSource, nameSince, updatedAt, status, statusUpdatedAt, bridgeSessionId -- the case-insensitive rg for pending/permission/approval/toolUse across every file in ~/.claude/sessions returned nothing, and the rg across ~/.claude/tasks (167 task directories) returned nothing. Block census: 7798 of 7798 tool_use blocks carry exactly ONE key set, ('caller','id','input','name','type'); no permission, escalation, isEscalated or requires_approval key appears on any block. Live: with this verifier's own lane frozen mid-call, csift status printed `child <agent-id>  in-flight  unreturned Bash call (0s ago)` and `tail  no pending call; last stop_reason tool_use`, and csift search \"\" @trap:<marker> --count-by pairing on that same lane printed `88 paired` / `1 pending`.",
          "rule": "One key set per tool_use block, unioned across every top-level transcript in $P; a claim of an approval-state field is refuted only if zero blocks carry one. For the control files, one grep per file over the whole of ~/.claude/sessions and ~/.claude/tasks. The pairing census counts one record per bucket, so the pending bucket is the count of tool_use records whose id has no tool_result in scope.",
          "note": "Every half was confirmed by an instrument that ran. The signature is exactly as described and was reproduced live rather than reconstructed: a frozen lane appears on disk only as a trailing unreturned tool_use, and csift reports it as `pending` on the pairing axis and `in-flight` in status, with no field anywhere on the record, in the session registry, or under ~/.claude/tasks that would separate an escalation-blocked lane from a slow or wedged one. The three-states-one-signature honesty constraint therefore still binds. Code sites confirmed verbatim at src/subagent/lifecycle.rs lines 35-40 and src/live/tail.rs lines 3-5."
        }
      ]
    },
    {
      "id": "BASH-019",
      "area": "bash-shell",
      "behavior": "Claude Code lexically recognizes read-shaped Bash commands and SEEDS its readFileState from them, so a bash `cat <file>` legalizes a later Write of that file; a Bash command never invalidates readFileState.",
      "depends": "csift's `recover` treats a gated bash `cat`/`head -n N`/`sed -n 'A,Bp'` stdout as a read anchor for the same reason the harness does, and its freshness reasoning assumes bash writes create staleness rather than clearing it.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "12-14",
          "snippet": "//! ADMISSION LAWS (each refusal falls back to today's behavior, never a wrong anchor):\n//! - A READ anchor demands a SINGLE simple segment: a compound command's stdout is a\n//!   concatenation nothing can attribute to one file."
        }
      ],
      "instrument": "Find in the corpus a Write that immediately follows a bash `cat` of the same path with no intervening Read and no rejection (`is_error:true`). Counting rule: one witness sequence per session.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 4.9; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 $B | rg -o 'async function hWn\\(.{0,1400}'  AND  strings -n 6 $B | rg -o 'function Jzo\\(.{0,1500}'  AND  strings -n 6 $B | rg -o '.{0,300}returnCodeInterpretation.{0,200}' (for the call site)",
          "observed": "Call site, verbatim on the Bash result path: if(!me&&!nt&&!ke.backgroundTaskId)await hWn(e.command,n.readFileState,C.signal,ke.code,...) -- the command string and the readFileState map are handed to the seeder, skipped only when the result was interrupted, was an image, or was backgrounded. Seeder, verbatim: async function hWn(e,n,r,o,d){let f=Jzo(e).filter((v)=>!v.requiresExitZero||o===0);if(f.length===0)return;let _=ce();await Promise.all(f.map(async(v)=>{let C=ct(v.filePath);if(n.get(C))return;try{let I=await _.stat(C);if(I.size>10485760)return;if(r.aborted)return;let F=await _.readFile(C,{encoding:\"utf8\"}),B=AA(F),U=dqo(B,v);if(U===null)return;n.set(C,{content:U.content,timestamp:Math.floor(I.mtimeMs),offset:U.offset,limit:U.limit,...})}catch{}}))}. Recognizer: function Jzo(e){if(/[|<>]/.test(e))return[]; ...} dispatching to a sed arm that requires -n/--quiet/--silent and an A,Bp or Np expression while rejecting -i/--in-place/-e, and to a table eqo=new Map([[\"cat\",new Set([\"-n\",\"--number\"])],[\"nl\",new Set],[\"bat\",new Set([\"-n\",\"--number\",\"-p\",\"--plain\"])],[\"batcat\",new Set([\"-n\",\"--number\",\"-p\",\"--plain\"])]]) that accepts exactly one non-flag operand.",
          "rule": "Presence of a write into the readFileState map (n.set) and absence of any delete or invalidate on that map anywhere in the Bash result path, read off the seeder body; the read shapes are enumerated from the recognizer's dispatch arms rather than assumed.",
          "note": "Both halves confirmed at the binary. The seeding half is verbatim -- n.set(C,{content,timestamp,offset,limit}) writes the readFileState entry a later Write or Edit gates on. The never-invalidates half is confirmed structurally by the guard `if(n.get(C))return`: an existing entry is left untouched, and the function contains no delete or clear of the map at all, so a Bash command can only add freshness, never remove it. Two further gates worth recording alongside: a file larger than 10485760 bytes is skipped, and the recognizer bails outright (returns the empty list) when the command contains |, < or >, so a piped or redirected cat seeds nothing. That last gate is a close structural analogue of csift's own single-simple-segment admission law for read anchors. Code site confirmed verbatim at src/bash_mutations/anchors.rs lines 12-14."
        }
      ]
    },
    {
      "id": "BASH-020",
      "area": "bash-shell",
      "behavior": "Claude Code routinely runs Bash commands that mutate files whose names never appear in the command text - whole-tree formatters (`cargo fmt --all`), package managers, archive extraction and patch application - so the mutated file set exists only in the tool's own discovery, the lockfile layout, or the archive/diff contents.",
      "depends": "csift emits these as CLASS MARKERS (`fmt:cargo`, `pkg:npm`, `extract:tar`, `interp:python`, `git:<sub>`) that are never resolved, joined or path-matched, and `recover` counts them per window as opaque mutating activity so a window is never falsely reported clean; a formatter that DOES name operands emits ordinary path rows instead, and dry-run forms emit nothing.",
      "code": [
        {
          "path": "src/bash_mutations/classes.rs",
          "lines": "1-11",
          "snippet": "//! Mutating-CLASS markers: commands known to rewrite files without naming them.\n//!\n//! A formatter (`cargo fmt`), a package manager (`npm install`), an archive extraction\n//! (`tar -xf`), and a patch application all mutate real files whose names never appear\n//! in the command text: the file set comes from the tool's own discovery, the lockfile\n//! layout, or the archive/diff contents. No lexical parser can name those files, and\n//! fabricating names would break the precision contract. So these commands are\n//! reported as CLASS MARKERS in the `git:<sub>` style: a pseudo-path (`fmt:cargo`,\n//! `pkg:npm`, `extract:tar`) that flags WHAT KIND of mutation ran, deliberately not\n//! WHICH files. Markers are never resolved, joined, or matched against a `--file`;\n//! `recover` counts them per window as opaque mutating activity."
        }
      ],
      "instrument": "`csift recover <target> --file <path> --coverage` on a session that ran a formatter: the opaque-command disclosure names class markers with a per-window count. Counting rule: one marker per parsed mutating-class segment.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; src/bash_mutations/classes.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift files @<session> --by timeline --no-subagents | rg -o '(fmt|pkg|extract|interp|git):[a-z-]+' | sort | uniq -c | sort -rn  AND  csift files @<session> --by file --format json --no-subagents | rg -o '\"path\":\"(fmt|pkg|extract|interp|git):[^\"]*\"' | sort | uniq -c  AND  csift recover @<session> --file .claude/settings.json --coverage --no-subagents",
          "observed": "Timeline over one real session emitted 38 class-marker rows: interp:python x18, git:commit x6, git:add x6, pkg:npm x4, git:stash x3, fmt:prettier x1. The per-file rollup carries each as a `path` value -- \"path\":\"pkg:npm\", \"path\":\"interp:python\", \"path\":\"fmt:prettier\", \"path\":\"git:stash\", \"path\":\"git:commit\", \"path\":\"git:add\" -- and none of them appears as a filesystem path in any row. The recover coverage disclosure printed, verbatim: `opaque in window: 26 mutating-class command(s) whose file set is not in the command text (fmt:prettier, git:stash x3, interp:python x18, pkg:npm x4)`, followed by per-line, per-turn, timestamped rows and a `(+21 more; use the search below)` elision with the exact continuation search.",
          "rule": "One marker per parsed mutating-class command segment, counted from the chronological timeline so a compound command contributes one row per mutating segment; the recover figure is the count of such segments falling inside the reconstruction window for the named file.",
          "note": "Both halves confirmed by running csift against a real session. The harness half is established by the markers themselves: each row is a real Bash command Claude Code ran whose mutated file set is absent from the command text, and 38 such commands occur in one session. The csift half is confirmed on both surfaces -- the markers are emitted as pseudo-paths and never resolved, joined or matched against a real path, and recover counts them per window under an explicit opaque heading with a per-marker breakdown, so a window containing only such commands is disclosed as opaque rather than reported clean. The elision itself reports its own drop count and hands back the exact continuation command, matching the no-silent-truncation invariant. Code site confirmed verbatim at src/bash_mutations/classes.rs lines 1-11."
        }
      ]
    },
    {
      "id": "BASH-021",
      "area": "bash-shell",
      "behavior": "Claude Code writes Windows paths into structured tool fields in native form (`C:\\...`), so besides a leading `/` the only absolute operand shapes are a `<letter>:` drive prefix and a `\\\\`-led UNC path.",
      "depends": "csift's `is_absolute_shell_path` accepts all three shapes before deciding whether to join an operand to the record `cwd`; treating `C:\\...` as relative would join a Windows absolute path onto a cwd and fabricate a path in every `files`/`recover` row.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "209-215",
          "snippet": "pub fn is_absolute_shell_path(p: &str) -> bool {\n    if p.starts_with('/') || p.starts_with(\"\\\\\\\\\") {\n        return true;\n    }\n    let b = p.as_bytes();\n    b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && matches!(b[2], b'/' | b'\\\\')\n}"
        }
      ],
      "instrument": "The unit tests beside `src/bash_mutations/` pin `is_absolute_shell_path` for `/a`, `C:/a`, `C:\\a` and a `\\\\`-led UNC path; on a real Windows session, confirm Edit/Write `filePath` values use the drive form. Counting rule: one path per structured tool input field.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -Fx '^(?:[a-zA-Z]:[\\\\/]|\\\\\\\\)'  (and the two sibling predicates); then a python json census of every structured tool-input path field (file_path / filePath / notebook_path / path) over a Windows-recorded Claude Code 2.1.258 transcript tree (5 .jsonl files, encoded project dir of drive-letter shape) copied onto this machine; then cargo test is_absolute_shell_path",
          "observed": "Binary carries three fully-qualified-path predicates that enumerate exactly the drive-letter and UNC forms: '^(?:[a-zA-Z]:[\\\\/]|\\\\\\\\)' (its string table continues ': not a fully qualified path'), '^(?:[A-Za-z]:[\\\\/]|[\\\\/]{2})' and '^(?:[A-Za-z]:\\\\|\\\\\\\\)'; the Write tool schema string reads 'The absolute path to the file to write (must be absolute, not relative)'. Windows transcripts: 55 of 55 structured tool-input path values are native drive form matching ^[A-Za-z]:\\\\ (Read file_path 32, Edit file_path 10, Write file_path 9, Grep path 4); 0 were POSIX-ified or relative. All 389 cwd-carrying records in that tree stamp one native drive-form cwd. cargo test: 1 passed, 0 failed.",
          "rule": "One path per structured tool input field. Shape buckets: leading '/', ^[A-Za-z]:[\\\\/], leading '\\\\\\\\', else relative. Counted over every tool_use block in the tree.",
          "note": "Code site confirmed verbatim at src/bash_mutations/cwd.rs:209-215, unchanged. The drive-letter arm is confirmed on real Windows-recorded data at the current Claude Code version; the UNC arm is confirmed only from the binary's own predicates, since no '\\\\\\\\'-led operand occurred in the observed Windows session. A session on a UNC-mounted share would settle that arm on disk."
        }
      ]
    },
    {
      "id": "BASH-022",
      "area": "bash-shell",
      "behavior": "Real Bash command shapes are dominated by a leading absolute `cd`: across 118,900 measured commands, 61.16% began with `cd <absolute>` - though only 7.87% of all commands cd'd to the directory the shell was already tracking - 2.65% were backgrounded through the Bash tool's `run_in_background` field and none by a trailing top-level `&`, `pushd`/`popd`/`chdir` appeared zero times as a shell verb against 79,911 `cd` verbs, and a `cd` inside a `( )` subshell appeared twice.",
      "depends": "csift still models `pushd`/`chdir` as a `cd` and `popd` as an unknowable restore, but this distribution is why per-command lexical tracking suffices in practice instead of a full shell simulation.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "111",
          "snippet": "    /// `cd`/`pushd`/`popd`. Call AFTER stamping the segment's mutations."
        },
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "130-132",
          "snippet": "            // `popd` restores a directory this layer does not model.\n            \"popd\" => self.set(CwdAt::Unknown),\n            \"cd\" | \"pushd\" | \"chdir\" => self.apply_cd(operands),"
        }
      ],
      "instrument": "python over a cached set of transcripts: for every Bash tool_use, classify the first top-level segment and count leading `cd <abs>` shapes, backgrounded launches and `pushd`/`popd` tokens. Counting rule: one command per tool_use block; segments split on top-level `;`, `&&`, `||`, `|`, `&` and newline outside quotes and heredocs.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 walk of every *.jsonl under ~/.claude/projects: for each Bash/PowerShell tool_use block, mask quoted spans and heredoc bodies, split top-level segments on ';', '&&', '||', '|', '&' and newline outside quotes and parens, then classify the first segment's verb and census every cd-family verb and its paren depth",
          "observed": "118,900 Bash/PowerShell tool_use commands. Leading 'cd <absolute>': 72,718 = 61.16% (claim: 42.8%). Of those, target equal to the record's own cwd: 9,362 = 7.87% of all commands. Backgrounded by a trailing top-level '&': 0 = 0.00%; backgrounded via the Bash tool's run_in_background field: 3,150 = 2.65% (claim: 9.21%). cd-family segment verbs: cd 79,911 (claim: 11,974), pushd 0, popd 0, chdir 0 (claim: 0 / 0 / 1). cd inside a ( ) subshell: 2 (claim: 1). All 39 Bash commands whose text contains pushd, popd or chdir carry the word inside a heredoc'd script body, never as a shell verb. The corpus is live: the same walk 20 minutes later counted 119,110.",
          "rule": "One command per Bash/PowerShell tool_use block. A segment verb is the first token after stripping env assignments and the prefixes command/builtin/exec/env/nohup/time/sudo. A leading 'cd' target is the first non-flag operand; absolute = leading '/', '\\\\\\\\', or ^[A-Za-z]:[/\\\\].",
          "note": "Both code sites confirmed verbatim at src/bash_mutations/cwd.rs:111 and 130-132, unchanged. The structural conclusion holds and is stronger than claimed - pushd/popd/chdir are now categorically absent as shell verbs, and subshell cd stays vanishingly rare - so per-command lexical tracking still suffices. Every number needed correction, and one sub-clause did not survive: the claim characterized the leading absolute cd as landing the shell 'where it already was', but only 12.9% of leading-absolute-cd commands (9,362 of 72,718) target the directory the record cwd already names. Backgrounding also moved carrier rather than frequency: the trailing-'&' form has disappeared entirely in favour of the tool's own run_in_background field."
        }
      ]
    },
    {
      "id": "BASH-023",
      "area": "bash-shell",
      "behavior": "Across 17,361 measured mutation-target operands, resolution classes were 38.40% typed absolute, 52.65% relative - of which only 24.30% resolve at the zero-inference `cwd-joined` class and 75.70% need the lexical `cd-tracked` class - and 8.95% dynamic (`$VAR` or a glob, never resolvable); the `cd-tracked` class agrees with Claude Code's own modified-file hints 99.76% of the time (411 of 412 joinable entries, the one exception being a path-normalization artifact rather than a wrong answer).",
      "depends": "csift labels every resolved operand with an explicit `Resolution` class (`absolute`, `cwd-joined`, `cd-tracked`, `unresolved`) and keeps a dynamic operand VERBATIM rather than guessing; collapsing the classes would hide which `files`/`recover` rows involved inference.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "30-39",
          "snippet": "//! - `Absolute`: the operand was typed absolute; used as-is.\n//! - `CwdJoined`: a relative operand before any `cd`; joined to the record's own `cwd`\n//!   field. That value is data Claude Code wrote, so the join involves no inference.\n//! - `CdTracked`: a relative operand after one or more literal in-command `cd`s;\n//!   the join uses the lexically tracked directory. This is inference: a `cd` that\n//!   failed at runtime is invisible here. Measured against Claude Code's own\n//!   modified-file hints, this class resolves correctly in about 99.7% of cases.\n//! - `Unresolved`: the operand (or a `cd` on the way to it) carries `~`, `$VAR`, a\n//!   substitution, or the cwd is otherwise unknowable. The operand is kept VERBATIM\n//!   and callers must disclose it as unresolved, never treat it as a full path."
        }
      ],
      "instrument": "`csift files <target> --format json | jq -r 'select(.kind==\"mutation\") | .resolution' | sort | uniq -c` gives the class distribution for one session; the 99.7% figure comes from joining `cd-tracked` rows against the same result's `toolUseResult.staleReadFileStateHint` paths. Counting rule: one class per mutation row.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9; src/bash_mutations/cwd.rs resolution classes; dev session 2026-08-22"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift files --by timeline --format json (no target, all projects) piped to a python census of the `resolution` field; then a python join of the cd-tracked rows against every toolUseResult.staleReadFileStateHint file list in the same corpus",
          "observed": "27,744 bash mutation rows. 10,383 carry resolution null (class-marker pseudo-paths such as interp:/git:/fmt:), leaving 17,361 resolvable operands: absolute 6,667 = 38.40% (claim 19.75%), cd-tracked 6,920 = 39.86%, cwd-joined 2,221 = 12.79%, unresolved 1,553 = 8.95% (claim 17.47%). Relative subtotal 9,141 = 52.65% (claim 62.79%), split cwd-joined 24.30% / cd-tracked 75.70% (claim 84.21% / 15.70% - inverted). Accuracy join: 1,077 records carry toolUseResult.staleReadFileStateHint, 884 parse to a file list, 371 of those calls produced at least one cd-tracked row; of the 412 hint entries sharing a basename with a cd-tracked row, 411 = 99.76% have csift's absolute path ending in the hint's full relative path. The single exception is a '../../'-normalization artifact where csift's resolved path is in fact correct.",
          "rule": "One class per mutation row emitted by `csift files --by timeline`; percentages taken over rows with a non-null resolution (null rows are class markers, not file operands). Accuracy: one hint entry per joinable file; agreement = csift's absolute path ends with the hint's relative path.",
          "note": "Code site confirmed verbatim at src/bash_mutations/cwd.rs:30-39, unchanged, and its 'about 99.7%' figure reproduces almost exactly at 99.76% - that half of the claim needs no edit. The distribution half inverted: the claim's reassuring reading, that most relative operands land in the zero-inference cwd-joined class, is no longer true - the inference-bearing cd-tracked class now dominates relative operands about 3:1 where the claim had cwd-joined dominating about 5:1. This is consistent with and explained by the BASH-022 shift: leading absolute `cd` rose from 42.8% to 61.16%, so far more operands now sit after a cd. The practical consequence is that a larger share of files/recover rows now rest on inference, which raises the value of the explicit Resolution class label rather than undermining it."
        }
      ]
    },
    {
      "id": "BASH-024",
      "area": "bash-shell",
      "behavior": "8.39% of 119,110 measured Bash calls mutate a persistent file, and across 397 real `sed -i` invocations (292 commands) `sed -i` yielded ZERO deterministic literal-subset content anchors - even the 55 invocations whose s/// OLD and NEW are both fully literal, because the substitution is conditional on prior content and sed exits 0 whether or not it matched.",
      "depends": "csift's bash content anchors deliberately exclude `sed -i`, `tail`, variable targets and interpreter/ssh heredocs; the bash resolution work raised full-target attribution from 27.1% to a measured 77.0% ceiling.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "1-10",
          "snippet": "//! Content-ANCHOR classification of one Bash command (the recover layer's v0.9.4\n//! \"bash reads are reads, bash writes are writes\" upgrade).\n//!\n//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file.\n//! Everything else stays in the boundary/heuristic lanes - correctness first, but\n//! honesty about the decidable subset is not surrender on it."
        }
      ],
      "instrument": "Collect every `sed -i` Bash command in the corpus (`csift search 'sed -i' <scope> -t agent.tool.use`) and inspect each for an extractable literal old/new pair. Counting rule: commands carrying a literal replacement subset over all `sed -i` commands; mutating calls over all Bash calls.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger item 1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift files --by timeline --format json (no target, all projects) piped to a python count of distinct (session_id, line) bash mutation rows with a non-null resolution, divided by the python-walk count of Bash/PowerShell tool_use blocks under ~/.claude/projects; then a python extraction of every `sed -i` invocation, its script argument, and its paired tool_result",
          "observed": "9,996 distinct Bash calls produced at least one real-path bash mutation row, out of 119,110 Bash/PowerShell tool_use commands = 8.39% (claim: 19.4% of 22,410). Counting class-marker pseudo-path rows as mutations raises it to 13,563 = 11.39%. sed -i: 292 commands, 277 distinct tool_use ids, 397 `sed -i` invocations. Script shapes: 182 s/// with regex metacharacters in both OLD and NEW, 118 non-s/// scripts (d/a/i/p/range), 55 with OLD and NEW both fully literal, 23 OLD-only metacharacters, 8 NEW-only, 11 unparseable. Paired results: 6 errored, 286 returned non-empty stdout - and every non-empty body is another command's output in the same compound (formatter, test runner, version control), never the edited file's bytes. ZERO of the 397 invocations yields a deterministic literal-subset content anchor.",
          "rule": "Mutating call = a distinct (session_id, line) carrying at least one bash mutation row with a non-null resolution, over all Bash/PowerShell tool_use blocks in the corpus. A content anchor = a shape from which file bytes are known from the transcript alone; a `sed -i` substitution is conditional on prior content and so can never qualify.",
          "note": "Code site confirmed verbatim at src/bash_mutations/anchors.rs:1-10, unchanged. The `sed -i` half of the claim reproduces exactly, and the code shows why structurally rather than incidentally: anchors.rs admits `sed` only under `\"sed\" if words.len() == 4 && words[1].orig == \"-n\"` (src/bash_mutations/anchors.rs:310), the windowed-read idiom, so `sed -i` can never reach the anchor path at all. The mutation rate fell from a claimed 19.4% to a measured 8.39% on a corpus roughly five times larger, so the claim's rate needed correction. Not re-measured here: the `depends` field's attribution figures (27.1% raised to a 77.0% ceiling) - no instrument was run against them, and they should be treated as unverified until one is."
        }
      ]
    },
    {
      "id": "BASH-025",
      "area": "bash-shell",
      "behavior": "Whether a `Bash` tool_result carries the structured `toolUseResult` OBJECT is a per-LANE and per-VERSION fact, not a property of the tool: a WORKFLOW-agent lane carries it on NONE of its 70,777 Bash results at any version, the current 2.1.258 included (0 of 5,100); a BUILT-IN Task/Agent or teammate lane carries it on none of the 6,837 results written by 2.1.156 through 2.1.177 and on 92.8-100% per version from 2.1.179 onward (12,752 of 19,943 corpus-wide); the main lane carries it on 31,944 of 32,489. The non-object remainder in every lane is either no `toolUseResult` field at all or a STRING-valued one carrying a failed call's error text.",
      "depends": "csift's `recover` READ content anchors demand that object echo (a `stdout` string, empty stderr, `interrupted` false, no persisted-output pointer), so a bash read anchor is reachable in the main lane and in a post-2.1.179 built-in or teammate lane but never in a workflow lane or an archived pre-2.1.179 subagent lane; WRITE anchors are input-side and work in every lane. Both blanket readings - `subagent lanes never carry it` and `every current lane carries it` - mis-gate a whole class of transcripts.",
      "code": [
        {
          "path": "src/recover/bash_anchors.rs",
          "lines": "12-15",
          "snippet": "//!   (re-measured at CC 2.1.258, v0.10.1): WORKFLOW lanes never carry it (0 of 3867\n//!   Bash results), built-in Task/Agent and teammate lanes carry it 97-99% of the\n//!   time, the main lane always - so read anchors reach every lane that carries the\n//!   echo, by construction, while WRITE anchors (input-side) work in every lane;"
        }
      ],
      "instrument": "Same scan, re-measured 2026-09-02 later in the day: workflow 0 object / 947 string / 69,830 absent; built-in-or-teammate 12,752 object / 336 string / 6,855 absent, every object echo at 2.1.179 or later and zero at 2.1.156-2.1.177; main 31,944 object / 545 string / 0 absent; over 67 top-level and 7,584 subagent transcripts.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "src/recover/bash_anchors.rs module doc; measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.179",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 one-pass census over ~/.claude/projects (walk every *.jsonl except journal.jsonl; per file first register the ids of every `tool_use` block whose name is Bash or PowerShell, then for every later `tool_result` block whose tool_use_id is registered tally the lane, the JSON type of the carrying record's top-level `toolUseResult`, and that record's `version`; lane = workflow when the path has a `subagents/workflows/` component, builtin_or_teammate for any other `subagents/` component, else main)",
          "observed": "7,671 transcripts scanned: 67 top-level, 20 elicitation sidecars (0 Bash results), 7,584 subagent. WORKFLOW lane 0 object / 947 string / 69,830 absent = 70,777 Bash tool_result blocks - and 0 object of the 5,100 results written by the CURRENT 2.1.258 (47 string, 5,053 absent), so the zero is live, not archival. BUILTIN-OR-TEAMMATE lane 12,752 object / 336 string / 6,855 absent = 19,943; object count is 0 at 2.1.156 (0 of 956), 2.1.159 (0 of 3,151), 2.1.170 (0 of 15) and 2.1.177 (0 of 2,715) = 6,837 results, and the first object echo appears at 2.1.179 (16 of 16); from 2.1.179 onward the per-version object share runs 92.8% (2.1.199, 885 of 954) to 100%, and is 99.0% (283 of 286) at 2.1.258. MAIN lane 31,944 object / 545 string / 0 absent = 32,489, of which 341 object / 2 string at 2.1.258. Code site src/recover/bash_anchors.rs lines 12-15 matches the claim's snippet verbatim; the read-anchor gate it documents is implemented at lines 300-309 (`stderr_clean && !interrupted && !persisted` over the `toolUseResult` object).",
          "rule": "One tally per `tool_result` BLOCK whose `tool_use_id` names a Bash/PowerShell `tool_use` registered earlier in the SAME transcript file. kind = object | string | absent, by the JSON type of the carrying record's top-level `toolUseResult`. Version buckets use the carrying record's own `version`. Percentages are object / (object + string + absent) within one version and lane.",
          "note": "The structural claim reproduced exactly: workflow-lane object count is still zero at every version including the current one, the built-in lane's first object echo is still 2.1.179, and the builtin (12,752 of 19,943), 2.1.156-2.1.177 (6,837) and main (31,944 of 32,489) totals are byte-identical to the ledger's. Two numbers needed correction. (1) The workflow denominator grew from 70,046 to 70,777 (942 -> 947 string, 69,104 -> 69,830 absent) because the corpus itself grew between the two measurements on the same date; the object count stayed 0. (2) The per-version floor from 2.1.179 onward is 92.8% (2.1.199), not 93%. Also worth reconciling: the code doc at bash_anchors.rs:12-15 cites `0 of 3867 Bash results` for the workflow lane, a narrower scope than this whole-corpus 70,777 - both are zero, but the two figures are not the same census and a reader may take the smaller one for the corpus total."
        }
      ]
    },
    {
      "id": "BASH-026",
      "area": "bash-shell",
      "behavior": "The Bash echo's OPTIONAL key set drifts with the harness version and is not closed at any one version: 2.1.258's toolUseResult constructor emits `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `backgroundEndsWithFinalResponse`, `backgroundCwdHint` and `gitOperation` beside the keys most records show, and its tool_result BLOCK mapper additionally destructures `structuredContent`. Four of those - `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `structuredContent` - occur on no record anywhere in the corpus; `backgroundEndsWithFinalResponse` occurs on 8, all in a BUILT-IN/teammate subagent lane at 2.1.231 (always true, always beside a `backgroundTaskId`), which is why a top-level-only census reads zero for it. Conversely `assistantAutoBackgrounded` occurs on 21 corpus records - always `false`, always beside a `backgroundTaskId` - confined to versions 2.1.156, 2.1.159 and 2.1.177, and is absent from the 2.1.258 binary entirely: it is the only one of these names with zero binary hits.",
      "depends": "csift keeps `toolUseResult` UNPARSED as a `RawValue` and reads only small named fields through the typed probe, whose every field is an `Option<Value>` accepting any JSON type, so an added key and a retired one both parse without error - that tolerance is what lets one binary read a corpus spanning a hundred harness releases. A surface that enumerated this key set from one version's transcripts would be wrong for exactly the archived sessions a forensic tool exists to read.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "293-299",
          "snippet": "/// Typed probe of the SMALL `toolUseResult` fields the hot paths consult (see\n/// [`Record::tur_probe`]). Every field is an `Option<Value>` - a tiny scalar/map tree\n/// that accepts ANY JSON type, so one oddly-typed field can never fail the whole probe\n/// (each accessor then applies the same `as_str`/`as_bool`/`as_object` coercion the\n/// former `.get(…)` chains did - byte-identical semantics). Crucially, the blob's HUGE\n/// unlisted values (file bodies, stdout echoes, structured patches) are skipped by\n/// serde's ignore path without ever being allocated."
        }
      ],
      "instrument": "Corpus census widened from top-level transcripts to EVERY *.jsonl under ~/.claude/projects (7,831 files, 1,312,236 lines), one tally per line containing the quoted key name. Measured 2026-09-02: backgroundedByUser 0, backgroundedByTurnAbort 0, backgroundedToDeliverMessage 0, structuredContent 0, backgroundEndsWithFinalResponse 8, assistantAutoBackgrounded 21.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'noOutputExpected:Lqo\\(e\\.command\\).{0,600}' and | rg -o 'ResultBlockParam\\(\\{interrupted:.{0,320}' and | rg -c 'assistantAutoBackgrounded'; (b) python3 byte census over ~/.claude/projects counting, per line, occurrences of each quoted key name; (c) python3 per-key tally over the main-lane Bash object echoes from the BASH-025 scan.",
          "observed": "(a) the 2.1.258 toolUseResult constructor is `noOutputExpected:Lqo(e.command),backgroundTaskId:ke.backgroundTaskId,backgroundedByUser:ke.backgroundedByUser,backgroundedByTurnAbort:ke.backgroundedByTurnAbort,backgroundedToDeliverMessage:ke.backgroundedToDeliverMessage,timedOutAfterMs:ke.timedOutAfterMs,backgroundEndsWithFinalResponse:fn,backgroundCwdHint:hn,dangerouslyDisableSandbox:...,persistedOutputPath:Ot,persistedOutputSize:gn,staleReadFileStateHint:An,ghRateLimitHint:pn,gitOperation:xn}`; the separate tool_result BLOCK mapper destructures `{interrupted,stdout,stderr,isImage,backgroundTaskId,backgroundedByUser,backgroundedToDeliverMessage,timedOutAfterMs,backgroundEndsWithFinalResponse,backgroundCwdHint,structuredContent,persistedOutputPath,persistedOutputSize,staleReadFileStateHint,ghRateLimitHint}`; `assistantAutoBackgrounded` = 0 matching lines. (b) over 7,831 jsonl files / 1,312,236 lines: \"backgroundedByUser\" 0, \"backgroundedByTurnAbort\" 0, \"backgroundedToDeliverMessage\" 0, \"structuredContent\" 0, \"backgroundEndsWithFinalResponse\" 8, \"assistantAutoBackgrounded\" 21, \"backgroundCwdHint\" 1204, \"gitOperation\" 827, \"returnCodeInterpretation\" 251. The 8 `backgroundEndsWithFinalResponse` records are all `type:\"user\"` tool_result carriers in a BUILT-IN/teammate subagent lane at version 2.1.231, value always true, each beside a `backgroundTaskId`. (c) of the 31,944 main-lane object echoes: stdout/stderr/interrupted/isImage/noOutputExpected 31,944 each, backgroundTaskId 2,593, backgroundCwdHint 1,052, staleReadFileStateHint 969, gitOperation 708, persistedOutputPath 235, persistedOutputSize 235, returnCodeInterpretation 159, dangerouslyDisableSandbox 66, timedOutAfterMs 31, assistantAutoBackgrounded 21 (all false, all beside a backgroundTaskId, versions 2.1.156 x2 / 2.1.159 x6 / 2.1.177 x13). Code site src/model/record.rs lines 292-298 matches the claim's snippet verbatim.",
          "rule": "(b) one tally per LINE containing the quoted key name, over every *.jsonl under ~/.claude/projects (a key occurring twice on one line counts once); (c) one tally per KEY PRESENT in the `toolUseResult` object of a main-lane Bash echo, denominator 31,944 echoes.",
          "note": "A partial refutation landed. The claim's `none of those five occurs on any record in the corpus` is true only under its own stated counting rule (`over every top-level transcript`); widening the same census to the subagent lanes finds `backgroundEndsWithFinalResponse` on 8 records. The claim's headline point survives and is strengthened: the key set really is open at any one version, and the drift runs in BOTH directions AND across lanes, so a surface enumerating it from one version's top-level transcripts would be wrong twice over. The `assistantAutoBackgrounded` half is exact - 21 records, the three named versions, always false beside a backgroundTaskId, and the only one of the nine names checked that scores 0 in the 2.1.258 binary (`backgroundCwdHint` 4, `gitOperation` 6, `returnCodeInterpretation` 8, `structuredContent` 24, `backgroundEndsWithFinalResponse` 6, `backgroundedByTurnAbort` 8, `backgroundedToDeliverMessage` 10, `backgroundedByUser` 14 matching lines). Two further live keys the claim does not name are worth adding to any future enumeration: `backgroundCwdHint` (1,204 corpus lines) and `gitOperation` (827)."
        }
      ]
    },
    {
      "id": "BASH-027",
      "area": "bash-shell",
      "behavior": "A FOREGROUND Bash command that outruns its timeout is not killed: Claude Code moves it to the background, mints a `backgroundTaskId`, records the elapsed limit as `toolUseResult.timedOutAfterMs` (its schema description reads `Set when the command hit its timeout and was auto-backgrounded; the timeout value in ms`), returns `interrupted:false` with EMPTY `stdout` and `stderr`, and writes the tool_result text `Command did not complete within its <N>s timeout and was moved to the background (ID: <id>). Output is being written to: <path>`. Measured on 31 such results: all 31 carry a `backgroundTaskId`, all 31 have empty stdout and stderr, none is marked interrupted.",
      "depends": "csift's `status` BACKGROUND scan mints a shell task only from a `tool_use` whose `input.run_in_background` is true, and its raw-byte candidate needles cover `Command running in background` but not the moved-to-background wording, so a timeout-promoted task is missing from the very section that exists to name still-running work. `recover` is hit differently: the promoted call's echo carries empty stdout, so a bash READ content anchor can never gate on it - the command's output exists only in the harness task output file.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "25-33",
          "snippet": "            [\n                &b\"run_in_background\"[..],\n                b\"Command running in background\",\n                b\"async_launched\",\n                b\"task-notification\",\n                b\"stopped by the user\",\n                b\"\\\"Monitor\\\"\",\n                b\"Monitor started\",\n            ]"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary (2.1.258) for the optional schema member `timedOutAfterMs` with its description and for the literal `was moved to the background (ID: `. Corpus: for every Bash echo carrying `timedOutAfterMs`, read `interrupted`, `backgroundTaskId`, `stdout`, `stderr` and the tool_result block's text. Counting rule: one tally per Bash echo carrying the key, over every top-level transcript. Measured 2026-09-02: 31 results, 31 with a `backgroundTaskId`, 31 with empty stdout and stderr, 0 interrupted, 31 whose text carries the moved-to-background sentence.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.210",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'timedOutAfterMs:[A-Za-z()]*\\.optional\\(\\)\\.describe\\(\"[^\"]*\"' and | rg -o '.{0,120}was moved to the background \\(ID: .{0,140}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo whose `toolUseResult` carries `timedOutAfterMs`, read `interrupted`, `backgroundTaskId`, `stdout`, `stderr` and the tool_result block's text; (c) controlled A/B fixture under `--claude-home <tmpdir>`: two synthetic single-turn sessions, identical except that one Bash `tool_use` sets `run_in_background:true` and the other sets nothing, both answered by a `toolUseResult` carrying a `backgroundTaskId`; ran `csift status @11111111 --claude-home <tmpdir>` and `csift status @22222222 --claude-home <tmpdir>`.",
          "observed": "(a) schema member verbatim: `timedOutAfterMs:A().optional().describe(\"Set when the command hit its timeout and was auto-backgrounded; the timeout value in ms\")`; the text template verbatim: `Command did not complete within its ${Math.max(1,Math.round(d/1000))}s timeout and was moved to the background (ID: ${e}). Output is being written to: ${n}.`, one arm of a four-way selector whose other arms are `Command was manually backgrounded by user with ID: `, `Command was moved to the background (ID: ...) so that a message that arrived while it was running can reach you; it was not interrupted.` and the default `Command running in background with ID: `. (b) 31 echoes carry `timedOutAfterMs`; 31 of 31 carry a `backgroundTaskId`; 31 of 31 have empty stdout AND empty stderr; 0 are `interrupted:true`; 31 of 31 have the moved-to-background sentence in the tool_result text; 0 of 31 came from a `tool_use` that set `run_in_background`. Rendered second counts: 120s x20, 180s x1, 300s x4, 420s x1, 600s x5. (c) the explicit fixture prints `verdict idle-background-open`, `background 1 open; 0 completed...` and a `bg shell bfxexplicit` row; the timeout-promoted fixture prints `verdict idle-eot` with NO background section and no bg row at all.",
          "rule": "(b) one tally per Bash `tool_result` block whose carrying record's `toolUseResult` object contains the key `timedOutAfterMs`, over the 67 top-level transcripts; stdout/stderr counted empty when the field is absent or the empty string. (c) one status invocation per fixture session; a task is 'entered the BACKGROUND section' iff it appears as a `bg` row in text or in the JSON verdict's `background.tasks[]`.",
          "note": "Every number and every quoted string reproduced exactly. The dependency is also confirmed, and by a stronger instrument than the ledger cites: the A/B fixture isolates `input.run_in_background` as the single differing byte and shows csift calling the timeout-promoted session `idle-eot` - truly stopped - while a shell of its own is still running in the harness background. On a real session carrying a 2.1.257 timeout promotion, `csift status --format json` likewise returned that promotion's task id in no `background.tasks[]` row (the id surfaces only inside the `last` excerpt, as the completion `<task-notification>`). One incidental repo observation: the doc comment above the needle array in src/live/background_scan.rs calls it `The five raw-byte needles` while the array holds seven; the claim's snippet at lines 25-33 is verbatim."
        }
      ]
    },
    {
      "id": "BASH-028",
      "area": "bash-shell",
      "behavior": "The foreground Bash timeout is the call's own `input.timeout` clamped to a ceiling of 600,000 ms, defaulting to 120,000 ms when the call names none (the binary carries the pair as adjacent constants `120000` and `600000`). Both bounds are overridable per environment by `BASH_DEFAULT_TIMEOUT_MS` and `BASH_MAX_TIMEOUT_MS`, both on the settings `env` allowlist, and on a main-agent lane that can auto-background `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS` lowers the effective limit further to `min(requested, max(env, 2000))`.",
      "depends": "csift never predicts how long a lane may legitimately sit at an unreturned Bash `tool_use`: `agents` reports `pending_classification: awaiting-execution` and hands the caller `pending_since_utc` to weigh. The ceiling is what makes that weighing possible on a lane where auto-backgrounding applies - past ten minutes the harness's own timeout path would have returned a background promotion, so continued silence points at a blocked or abandoned lane rather than a slow command.",
      "code": [
        {
          "path": "src/cli/agents_args.rs",
          "lines": "107-110",
          "snippet": "        STALENESS: `pending_classification: awaiting-execution` means slow OR wedged OR \\\n        abandoned; jsonl cannot tell them apart, and at corpus scale a lane pending for \\\n        hours/days is overwhelmingly \\\"parent session ended, nobody is coming back\\\", not \\\n        in-flight work: weigh `pending_since_utc` against now yourself; \\"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary (2.1.258) for the adjacent constant pair `120000,` `600000` used by the two timeout resolvers (`BASH_DEFAULT_TIMEOUT_MS` returns the env value when positive else the 120000 constant; `BASH_MAX_TIMEOUT_MS` returns `Math.max(env, default)` else `Math.max(600000, default)`), and for the auto-background clamp `Math.min(requested, Math.max(env, 2000))` gated on `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS`. Corpus: join each Bash echo carrying `timedOutAfterMs` to its `tool_use` in the same transcript and tabulate `(input.timeout, timedOutAfterMs)`. Counting rule: one pair per timed-out call over every top-level transcript. Measured 2026-09-02, 31 pairs: no timeout named -> 120000 (20 calls); 180000 -> 180000; 300000 -> 300000 (4); 420000 -> 420000; 600000 -> 600000; 900000 -> 600000 (3); 2400000 -> 600000.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.210",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var Ivo=2000;function xBt\\(.{0,500}' and | rg -o '.{0,220}BASH_DEFAULT_TIMEOUT_MS.{0,220}' and | rg -o '.{0,400}liedMs:Ne\\}\\)'; (b) python3 over ~/.claude/projects: join each top-level Bash echo carrying `timedOutAfterMs` to its `tool_use` in the same transcript and tabulate (input.timeout, timedOutAfterMs).",
          "observed": "(a) the resolvers verbatim: `var Rvo=120000,Pvo=600000;function DTe(e=process.env){let n=e.BASH_DEFAULT_TIMEOUT_MS;if(n){let r=nl(n);if(!isNaN(r)&&r>0)return r}return Rvo}function Jze(e=process.env){let n=e.BASH_MAX_TIMEOUT_MS;if(n){let r=nl(n);if(!isNaN(r)&&r>0)return Math.max(r,DTe(e))}return Math.max(Pvo,DTe(e))}`; the clamp itself verbatim: `let{command:we,description:Ie,timeout:Pe,run_in_background:De}=e,Ne=Math.min(Pe||nme(),sZ(),r?.maxTimeoutMs??1/0);if(Pe&&Ne<Pe)o?.({key:\"timeout_clamped\",requestedMs:Pe,appliedMs:Ne})` (nme = the 120000 default resolver, sZ = the 600000 max resolver); the auto-background clamp verbatim: `var Ivo=2000;function xBt({requestedTimeoutMs:e,isMainAgent:n,canAutoBackground:r,env:o=process.env}){if(!n||!r)return e;let d=o.CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS;if(!d)return e;let f=nl(d);if(isNaN(f)||f<=0)return e;return Math.min(e,Math.max(f,Ivo))}`; the tool schema renders the ceiling live as `timeout:$M(A().optional()).describe(`Optional timeout in milliseconds (max ${sZ()})`)`; both `BASH_DEFAULT_TIMEOUT_MS` and `BASH_MAX_TIMEOUT_MS` sit in the quoted name Set consulted by the settings-`env` allowlist predicate, whose caller does `for(let{key:e,value:o}of n.values())if(aUe(e,o))process.env[e]=o` after `filterSettingsEnv`. (b) 31 pairs: no timeout named -> 120000 (20 calls); 180000 -> 180000 (1); 300000 -> 300000 (4); 420000 -> 420000 (1); 600000 -> 600000 (1); 900000 -> 600000 (3); 2400000 -> 600000 (1).",
          "rule": "One (input.timeout, timedOutAfterMs) pair per Bash echo carrying `timedOutAfterMs`, joined by tool_use id inside the same transcript, over the 67 top-level transcripts; every one of the 31 resolved in-file.",
          "note": "Holds on both instruments, and the binary supplies the mechanism the ledger inferred from the pairs: the clamp is literally `Math.min(input.timeout || default(120000), max(600000), ...)`, and it emits a `timeout_clamped` telemetry key carrying requestedMs and appliedMs. Two details worth folding into a future revision rather than corrections, since nothing in the claim is wrong: the min has a THIRD term `r?.maxTimeoutMs ?? Infinity` (a per-caller ceiling that can cut below 600000), and `Jze` returns `Math.max(600000, default)`, so raising BASH_DEFAULT_TIMEOUT_MS above 600000 raises the ceiling with it rather than producing a default above the max. `CLAUDE_CODE_AUTO_BACKGROUND_TIMEOUT_MS` is read from the process env but is NOT in the settings-env allowlist Set that carries the two BASH_* names."
        }
      ]
    },
    {
      "id": "BASH-029",
      "area": "bash-shell",
      "behavior": "A `backgroundTaskId` on a Bash echo does not mean the call asked to be backgrounded: 2.1.258 distinguishes FIVE background triggers in its own telemetry - user, turn_abort, deliver_message, timeout and explicit - selected from `backgroundedByUser`, `backgroundedByTurnAbort`, `backgroundedToDeliverMessage`, `timedOutAfterMs`, else explicit. Measured over 2,593 backgrounded Bash results, 60 (2.31%) came from a `tool_use` that never set `run_in_background`: 31 carry `timedOutAfterMs`, 21 carry the retired `assistantAutoBackgrounded:false`, and 8 name no trigger at all while still rendering the ordinary `Command running in background with ID:` text.",
      "depends": "csift's background scan keys a shell LAUNCH on `input.run_in_background == true` and only then joins a carrier to it by tool_use id, so those 60 results never enter the BACKGROUND section: `status` under-reports off-turn shell work by exactly the non-explicit trigger classes, and silently. The id join itself is sound; the launch predicate is the narrow part.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "49-59",
          "snippet": "            Block::ToolUse {\n                id: Some(id),\n                name: Some(name),\n                input: Some(input),\n                ..\n            } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n                && input\n                    .get(\"run_in_background\")\n                    .and_then(serde_json::Value::as_bool)\n                    == Some(true))\n                || name == \"Monitor\" =>"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary (2.1.258) for the background-acknowledgement telemetry event, whose trigger expression maps `backgroundedByUser` -> user, `backgroundedByTurnAbort` -> turn_abort, `backgroundedToDeliverMessage` -> deliver_message, `timedOutAfterMs` -> timeout, else explicit. Corpus: for every Bash echo carrying a `backgroundTaskId`, join the result block's `tool_use_id` back to its `tool_use` in the same transcript and read `input.run_in_background`. Counting rule: one tally per backgrounded Bash echo, over every top-level transcript; every launch resolved in-file. Measured 2026-09-02: 2,593 backgrounded results, 2,533 with `run_in_background:true`, 60 without (31 `timedOutAfterMs`, 21 `assistantAutoBackgrounded`, 8 with neither).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'tengu_bash_task_ack.{0,400}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo carrying a `backgroundTaskId`, join the result block's tool_use_id back to its `tool_use` in the same transcript, read `input.run_in_background`, and for the non-explicit ones bucket by trigger and by the leading sentence of the tool_result text.",
          "observed": "(a) the trigger expression verbatim: `tengu_bash_task_ack\",{trigger:Bn.backgroundedByUser?b(\"user\"):Bn.backgroundedByTurnAbort?b(\"turn_abort\"):Bn.backgroundedToDeliverMessage?b(\"deliver_message\"):Bn.timedOutAfterMs!==void 0?b(\"timeout\"):b(\"explicit\"),ends_with_final_response:Bn.backgroundEndsWithFinalResponse===!0,shell:b(\"bash\")}` - with an identical PowerShell twin ending `shell:b(\"powershell\")`. (b) 2,593 backgrounded Bash echoes; 2,533 from a `tool_use` with `run_in_background:true`; 60 (2.31%) without - and none of those 60 set the field to false, all 60 omit it entirely. Trigger split of the 60: 31 carry `timedOutAfterMs`, 21 carry `assistantAutoBackgrounded:false`, 8 carry neither. Text split: the 21 and the 8 both render `Command running in background with ID`, the 31 render `Command did not complete within its <N>s timeout and was moved to the ...`. The 60 span 17 versions from 2.1.156 to 2.1.257.",
          "rule": "One tally per Bash `tool_result` block whose carrying record's `toolUseResult` object has a non-null `backgroundTaskId`, over the 67 top-level transcripts; the launch is the `tool_use` with the matching id in the same file (all 60 resolved in-file); text bucket = the substring of the tool_result text before its first colon.",
          "note": "Exact on every number: 2,593 / 2,533 / 60, and 31 / 21 / 8. The binary gives the five triggers verbatim in the order the claim states, and adds that the same five-way selector is duplicated for the PowerShell tool, so the narrow-launch-predicate consequence applies to `PowerShell` records identically. The dependency is confirmed by construction and by the fixture used for BASH-027: csift's `ingest_launches` can only mint a task from a `tool_use` whose `input.run_in_background` is true (or a Monitor), so a result whose launch omits the field enters no BACKGROUND row no matter which of the four non-explicit triggers produced it - and the 8 no-trigger cases are the sharpest form, since their text is byte-identical to an explicit launch's yet csift still mints nothing."
        }
      ]
    },
    {
      "id": "BASH-030",
      "area": "bash-shell",
      "behavior": "A Bash call can opt out of Claude Code's sandbox per call, and the decision is recorded on BOTH sides of the call: the `tool_use` carries `input.dangerouslyDisableSandbox` and the result echoes the same boolean back under the same name. Measured on 66 echoes carrying the key - 63 `true` and 3 `false` - the launching `tool_use` carried the identical value in all 66.",
      "depends": "csift models neither side: the lexical layers (the dangerous-rm escalation prediction and the operand attribution that reads `input.command`) run identically on a sandboxed and an unsandboxed command, and `--count-by tool` reports one `Bash` key regardless - so a call's recorded sandbox posture reaches a consumer only through the raw line (`csift show --raw`).",
      "code": [
        {
          "path": "src/bash_mutations/entry.rs",
          "lines": "5-15",
          "snippet": "/// One heuristically-detected Bash file mutation. `verb` is from the fixed allowlist\n/// below (it is the lexical command/operator that touched the path), and `path` is the\n/// operand exactly as it appeared (quote-stripped, otherwise verbatim).\n#[derive(Debug, Clone, PartialEq, Eq)]\npub struct BashMutation {\n    pub path: String,\n    pub verb: &'static str,\n    /// The shell cwd in effect at this operand's segment (see [`cwd`] for the\n    /// tracked-cwd mechanism and the resolution classes built on it).\n    pub cwd_at: CwdAt,\n}"
        }
      ],
      "instrument": "`strings` over the installed Claude Code binary (2.1.258) for `dangerouslyDisableSandbox` (26 matching output lines, including the result-mapper member). Corpus: for every Bash echo carrying the key, join the result block's `tool_use_id` back to its `tool_use` and compare the echoed boolean with `input.dangerouslyDisableSandbox`. Counting rule: one tally per Bash echo carrying the key, over every top-level transcript. Measured 2026-09-02: 66 echoes, 63 true / 3 false, input value equal to the echoed value in 66 of 66.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.210",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'dangerouslyDisableSandbox' and | rg -o 'noOutputExpected:Lqo\\(e\\.command\\).{0,600}'; (b) python3 over ~/.claude/projects: for every top-level Bash echo carrying `dangerouslyDisableSandbox`, join the result block's tool_use_id back to its `tool_use` and compare the echoed boolean with `input.dangerouslyDisableSandbox`; (c) rg -n -i 'dangerouslyDisableSandbox|useSandbox' src/ in the csift repo.",
          "observed": "(a) 26 matching output lines, among them the constructor member that performs the copy verbatim: `dangerouslyDisableSandbox:\"dangerouslyDisableSandbox\" in e ? e.dangerouslyDisableSandbox : void 0` - the echo is written from the tool INPUT by an `in` test, which is why a `false` is echoed rather than dropped. (b) 66 echoes carry the key, 63 true and 3 false; the launching `tool_use` carried the identical value in 66 of 66; the 66 span versions 2.1.210 (2), 2.1.211 (36), 2.1.218 (1), 2.1.219 (1), 2.1.231 (2), 2.1.233 (24). (c) zero hits - the string appears nowhere in csift's src/. Code site src/bash_mutations/entry.rs lines 5-15 matches the claim's snippet verbatim.",
          "rule": "One tally per Bash `tool_result` block whose carrying record's `toolUseResult` object contains the key `dangerouslyDisableSandbox`, over the 67 top-level transcripts; equality compared as JSON booleans against the joined `tool_use`'s `input.dangerouslyDisableSandbox`.",
          "note": "Exact: 66 echoes, 63 true / 3 false, input equal to echo in 66 of 66. The binary explains the shape the corpus shows - the copy is guarded by `\"dangerouslyDisableSandbox\" in e`, so the key is present on the echo exactly when the call named it, at either value, and absent otherwise; that is what makes the two sides agree 66 of 66 by construction rather than by coincidence. The dependency also holds under a direct grep: csift models neither side, so the recorded sandbox posture is reachable only through the raw line."
        }
      ]
    },
    {
      "id": "CLS-001",
      "area": "classification",
      "behavior": "The label taxonomy Claude Code's record shapes require is exactly 33 leaf classes across three roles (user, agent, harness), and `agent.thinking.narration` is the first leaf whose dotted path another leaf (`agent.thinking`) prefixes.",
      "depends": "`Class::ALL` is the single source of truth behind `-t` selector derivation, the clap value parser, the `--help` leaf list, the census keys and the zero-match probe, and a drift-guard unit test pins its length at 33; the prefix relationship forces dot-SEGMENT prefix selection rather than plain string prefixing, so `-t agent.thinking` selects both leaves and pure reasoning needs `-T agent.thinking.narration`. A record shape added without its leaf is unreachable; a leaf added without updating the pinned count fails the gate.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "203-208",
          "snippet": "    pub const ALL: &'static [Class] = &[\n        Class::UserMessage,\n        Class::UserAnswer,\n        Class::UserRejection,\n        Class::UserUnsent,\n        Class::UserQueued,"
        }
      ],
      "instrument": "`csift search '' -t nosuchleaf` errors listing every valid selector - count the listed leaves, and compare with the `Class::ALL.len()` assertion in the drift-guard unit test. Counting rule: one per entry of `Class::ALL`.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "SPEC.md section 5; AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' -t nosuchleaf ; csift search '' -t agent.thinking --count-by label <csift-project-dir> ; csift search '' -t agent.thinking.narration --count-by label <csift-project-dir> ; sed -n '193,228p' src/model/taxonomy.rs ; cargo test --bin csift class",
          "observed": "The clap value-parser error lists 44 selectors, of which 33 are full dotted leaf paths: 5 under user, 5 under agent plus 3 under agent.communication, 20 under harness. src/model/taxonomy.rs lines 193-226 hold a Class::ALL array with exactly those 33 entries, and src/model/tests/classify_support.rs:83 carries assert_eq!(n, 33, \"Class::ALL leaf count drifted\"); the model::tests suite runs green. Segment-prefix selection measured: -t agent.thinking returned 2 census keys, agent.thinking 10192 + agent.thinking.narration 888 = 11080 records; -t agent.thinking.narration returned 1 key, 888 records.",
          "rule": "One leaf per entry of Class::ALL. A selector printed by the error counts as a leaf only if it is a full dotted path present in Class::ALL; bare roles and mid-path prefixes (user, agent, agent.tool, harness.meta, ...) are excluded. Census records are counted once per surviving leaf.",
          "note": "agent.thinking is the only leaf in Class::ALL whose dotted path is a segment-prefix of another leaf, and the two-key census under -t agent.thinking is the direct proof that selection is dot-segment rather than plain string prefixing. Code site verified verbatim at src/model/taxonomy.rs:193-198."
        }
      ]
    },
    {
      "id": "CLS-002",
      "area": "classification",
      "behavior": "`isMeta` is an AUTHORSHIP flag and `isVisibleInTranscriptOnly` a summary DISPLAY flag; neither reports whether the model received the content. The only measured visibility instrument is the presence of a `message{}` field: zero of 24 non-record line types carry one, and every `user`/`assistant` record does.",
      "depends": "csift's bare-role selectors expand to LLM-visible leaves using `Class::llm_visible`, keyed on the `message{}` instrument rather than on `isMeta`, `isVisibleInTranscriptOnly`, parentUuid threading or `compactMetadata.preservedMessages` membership; misreading `isMeta` as visibility poisoned a real last-human-touch hook.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "167-170",
          "snippet": "    ///   all - pure compaction metrics. (The compaction SUMMARY is visible: the\n    ///   DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n    ///   summaries, not a delivery flag, and `isMeta` is an authorship flag -\n    ///   neither is a visibility instrument.)"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "172-174",
          "snippet": "    /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n    /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n    /// over every non-record line type in the corpus; every user/assistant record"
        }
      ],
      "instrument": "`csift stats <target> --format json | tail -1 | jq .line_types` enumerates the file's line types, then `csift show <target> --line <n> --raw | jq 'has(\"message\")'` on one line of each type. Counting rule: one probe per distinct top-level `type`.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "CHANGELOG.md 0.9.4 and 0.10.0; src/model/taxonomy.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift stats --format json (line_types summed over every row) ; a python probe reading every line of 26 top-level transcripts and counting isinstance(record.get('message'), dict) per top-level type ; a byte-regex census of \"subtype\" on system lines across all 64 top-level transcripts",
          "observed": "16 distinct top-level line types corpus-wide: attachment 932244, assistant 855488, user 454942, last-prompt 58394, permission-mode 58276, mode 58098, ai-title 57440, queue-operation 26984, system 25362, agent-name 24546, file-history-snapshot 8102, file-history-delta 3926, atis-latch 1522, bridge-session 1384, fork-context-ref 66, cost-state 30. 11 distinct system subtypes: stop_hook_summary 5635, turn_duration 4623, away_summary 1628, scheduled_task_fire 459, compact_boundary 229, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, model_refusal_no_fallback 2, informational 1. Over the 26 probed transcripts (chosen to cover all 14 non-record top-level types, including the four rarest): every non-record type scored 0 records carrying a message{} object; assistant scored 17793/17793 and user 9269/9269.",
          "rule": "Non-record line types = the 13 non-record top-level types other than system, plus the 11 system subtypes = 24. One message{}-presence probe per distinct type/subtype; presence means the top-level message field deserializes to a JSON object.",
          "note": "The claim's number 24 reproduces exactly under that expansion rule. Without expanding system into its subtypes the corpus shows 14 non-record top-level types, so the rule has to travel with the number. The instrument the claim proposes (csift show --raw plus jq has(\"message\")) works but is one probe per type; the python pass gives the same answer with full coverage instead of a single sampled line. Code sites verified verbatim at src/model/taxonomy.rs:159-162 and 164-166."
        }
      ]
    },
    {
      "id": "CLS-003",
      "area": "classification",
      "behavior": "Claude Code injects background/automation completions as a type:\"user\", non-isMeta (the key is absent entirely), STRING-content record wrapped in <task-notification>...</task-notification>. Only <task-id> and <summary> are universal (100.0% of 2797 measured); <tool-use-id>, <status> and <output-file> ride together on 70% (every non-monitor kind), <event> on 29.9% (monitor kinds only) and <result> on 11.6%. It passes every genuine-user gate, so on disk it looks exactly like a human turn.",
      "depends": "csift lets the pulse OPEN a turn but reparents it to `harness.notification.<kind>` and renders `Record::automation_label()` as `[<kind> <task-id> <status>] <summary>` instead of dumping the raw XML wrapper; hardcoding the literal `workflow` for every trigger mislabeled 81% of them on one captured session (85 background-command + 2 agent pulses). A pulse carrying a `<result>` is a background-agent REPORT, so it additionally carries `agent.communication.inbox` (child to self).",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "120-127",
          "snippet": "/// Prefix of a `<task-notification>…</task-notification>` user record - a MACHINE-INJECTED\n/// automation trigger (a background-command / workflow / spawned-task completion notice CC\n/// inserts as a `type:\"user\"`, non-`isMeta`, STRING-content record). It LOOKS like a human\n/// turn to [`Record::is_genuine_user`] (it passes every gate), so it DOES open a turn - but\n/// it is an automation pulse, not the operator's prose. [`Record::automation_trigger`]\n/// classifies it so surfaces can LABEL the segment (`[workflow <id> completed] <summary>`)\n/// instead of dumping the raw `<task-id>`/`<output-file>`/`<status>` XML wrapper.\npub const TASK_NOTIFICATION_PREFIX: &str = \"<task-notification>\";"
        }
      ],
      "instrument": "csift search '' -t harness.notification --count-by label, cross-checked against a raw census of records whose origin.kind == \"task-notification\". Do NOT use csift search '<task-notification>' --count-by label: a notification-classified record's matchable text is the rendered automation label, not the raw XML, so that query returns 423 records corpus-wide and ZERO of them under harness.notification.* - they are all prose mentions of the tag in other records.",
      "located": {
        "claude_code": null,
        "csift": "0.2.0",
        "source": "SPEC.md section 5.1; AGENTS.md section 3.3; src/model/automation.rs comment; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python over all 64 top-level transcripts selecting records whose origin.kind == \"task-notification\" and whose message.content is a string starting with <task-notification>, counting inner-tag presence ; csift search '<task-notification>' --count-by label ; csift search '' -t harness.notification --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}(tool-use-id|output-file).{0,60}'",
          "observed": "2807 records carry origin.kind == task-notification; 2797 of them open with the XML wrapper. Inner-tag presence over those 2797: <task-id> 2797 (100.0%), <summary> 2797 (100.0%), <tool-use-id> 1957 (70.0%), <status> 1962 (70.1%), <output-file> 1957 (70.0%), <event> 835 (29.9%), <result> 325 (11.6%). Shape is uniform at 2807/2807: type user, message.role user, message.content a STRING, userType external, isSidechain false, and no isMeta key present. The 2.1.258 binary carries the tag constants verbatim: 's7t=\"fork-source\",_p=\"task-notification\",Use=\"task-id\",jdr=\"tool-use-id\",qBe=\"task-type\"' and 'Gdr=\"output-file\",zx=\"status\",j0=\"summary\"'. Label check: csift search '' -t harness.notification --count-by label returns 2810 hits (background-command 1519, monitor 946, subagent 198, workflow 142, task 5), and a separate probe shows 49/49 <result>-bearing pulses in one project dir additionally carry agent.communication.inbox while 131/131 pulses without <result> carry only their notification leaf.",
          "rule": "One observation per RECORD whose parsed origin.kind == \"task-notification\" and whose message.content is a string; a tag counts as present if its literal open form occurs anywhere in the content. Label counts are per surviving leaf, one hit per section.",
          "note": "The record shape holds exactly as claimed and the <result> to inbox coupling reproduces. Two corrections: the claim treats <status> as one of the always-present inner tags (measured 70.1%) and <output-file> as optional (measured 70.0%, i.e. the same population as <status>), and the claim's stated instrument inverts - it counts prose mentions and finds zero real pulses. The 81% mislabel figure is historical and cannot be re-measured against the current classifier. Code site verified verbatim at src/model/markers.rs:115-122."
        }
      ]
    },
    {
      "id": "CLS-004",
      "area": "classification",
      "behavior": "A <task-notification> <summary> opens with a fixed leading classifier and that leading phrase, never the quoted command name, is the class: a Background command \"...\" pulse is always a background command even when its quoted description contains the words monitor or re-arm (measured: 644 such pulses corpus-wide, all background-command). The classifier set in Claude Code 2.1.258 is larger than the four csift names explicitly - at least Background command \"...\", Dynamic workflow \"...\", Agent \"...\", Monitor event: \"...\", Remote task \"...\", Remote task blocked on input: ..., Cloud review failed: ..., Stopped by a worker restart: ..., The container running this session was restarted before background work reported back: ..., and a consolidated N memory file... form - and every head csift does not name falls through to the task slug.",
      "depends": "`AutomationKind::from_summary` matches the leading phrase case-insensitively into the five slugs `background-command`/`workflow`/`agent`/`monitor`/`task` (anything unrecognized falls back to `task`), so `search -t harness.notification.monitor` reports the real Monitor tool's pulses only; the retired quoted-name heuristic double-booked, producing 40 `monitor` records against zero genuine Monitor pulses on one measured project. The classifier does not cover `ScheduleWakeup` wakeup-tick prompts, which are `isMeta` records that never reach it.",
      "code": [
        {
          "path": "src/model/automation.rs",
          "lines": "33-40",
          "snippet": "    /// Classify from the `<summary>`. Case-insensitive on the known leading prefixes; anything\n    /// else (or a missing summary) is [`AutomationKind::Task`]. The `monitor`/`scheduled`/`cron`\n    /// LEADING prefixes route a Monitor-tool pulse or termination notice\n    /// (`Monitor event: …` / `Monitor \"…\" …`) to [`AutomationKind::Monitor`]. A `Background\n    /// command \"…\"` pulse is ALWAYS `background-command`, whatever its quoted name says\n    /// (v0.10.0: the former quoted-name heuristic - `monitor`/`re-arm`/`liveness` in the name\n    /// routed to `Monitor` - predates the real Monitor tool and double-booked; measured on one\n    /// project it produced 40 `monitor` records against zero genuine Monitor pulses). This"
        },
        {
          "path": "src/model/automation.rs",
          "lines": "44-63",
          "snippet": "    pub fn from_summary(summary: Option<&str>) -> Self {\n        let s = summary.unwrap_or(\"\").trim_start();\n        // The classifiers are a fixed leading phrase; match the longest-distinguishing\n        // prefix case-insensitively so a `Background command \"…\"` is not mistaken for `task`.\n        let lower = s.to_ascii_lowercase();\n        if lower.starts_with(\"background command\") {\n            AutomationKind::BackgroundCommand\n        } else if lower.starts_with(\"dynamic workflow\") || lower.starts_with(\"workflow\") {\n            AutomationKind::Workflow\n        } else if lower.starts_with(\"monitor\")\n            || lower.starts_with(\"scheduled\")\n            || lower.starts_with(\"cron\")\n        {\n            AutomationKind::Monitor\n        } else if lower.starts_with(\"agent\") {\n            AutomationKind::Agent\n        } else {\n            AutomationKind::Task\n        }\n    }"
        },
        {
          "path": "src/model/automation.rs",
          "lines": "67-75",
          "snippet": "    pub fn slug(self) -> &'static str {\n        match self {\n            AutomationKind::BackgroundCommand => \"background-command\",\n            AutomationKind::Workflow => \"workflow\",\n            AutomationKind::Agent => \"agent\",\n            AutomationKind::Monitor => \"monitor\",\n            AutomationKind::Task => \"task\",\n        }\n    }"
        }
      ],
      "instrument": "`csift search '' <project-dir> --count-by label | rg 'harness.notification'` cross-checked against a raw census of summary heads, `rg -o '<summary>[^<]*' <transcript> | cut -c1-30 | sort | uniq -c`. Counting rule: one record per pulse, bucketed by the leading summary phrase; a project whose summaries never open with Monitor/Scheduled/cron must report zero monitor records.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 4b; AGENTS.md section 3.3; CHANGELOG.md 0.10.0; src/model/automation.rs comment; SPEC.md v0.10.0 ledger; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t harness.notification.background-command --format json, filtering rendered excerpts whose quoted name matches /monitor|re-arm|rearm|liveness/i ; a raw python census of the 2797 XML-wrapped pulses bucketed by the leading phrase of <summary> ; csift search '' -t harness.notification --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'summary:`[^`]{0,90}'",
          "observed": "644 background-command pulses corpus-wide carry monitor / re-arm / liveness inside their quoted command name and every one classifies background-command, not monitor (top specimens are 53, 27, 20, 19 and 16 records of re-arm-named ticks). Independent raw census by leading summary phrase: background-command 1517, monitor 946, agent 187, workflow 142, task 5 (total 2797); csift's own leaf census over the same corpus: background-command 1519, monitor 946, subagent 198, workflow 142, task 5 (total 2810) - the +11 on the subagent leaf is the 10 plain-text agents-stopped notices plus one, and the leading-phrase buckets otherwise agree record-for-record. The 2.1.258 binary carries the classifier constants: 'Gdr=\"output-file\",zx=\"status\",j0=\"summary\",Lke=\"Background command \",Htt=\\'Agent \"\\',mIn=\"finished\",qdr=`\" ${mIn}`,mkt=\\'Remote task \"\\'' plus the templates summary:`Dynamic workflow \"${l}\" completed` / `Dynamic workflow \"${l}\" failed: ${Y}` and summary:`Monitor event: \"${Bt(e)}\"`.",
          "rule": "One record per pulse, bucketed by the leading phrase of its <summary> matched case-insensitively; a pulse counts as quoted-name-contaminated if /monitor|re-arm|rearm|liveness/i matches inside the quotes that follow the classifier.",
          "note": "The mechanism holds and is now measured at 644 records rather than the 40 the code comment cites for one project. The refinement is that the enumeration of leading classifiers is not exhaustive against 2.1.258: the binary carries at least six further summary heads that csift's from_summary absorbs into its task fallback, and a mIn=\"finished\" / qdr=`\" ${mIn}` pair meaning a summary can also read Background command \"...\" finished. Nothing misroutes today - the fallback is correct - but the ledger wording should say the four named classifiers plus a task fallback rather than implying the four are the whole set. Code sites verified verbatim at src/model/automation.rs:33-40, 44-63 and 67-75."
        }
      ]
    },
    {
      "id": "CLS-005",
      "area": "classification",
      "behavior": "A Monitor / cron cadence pulse carries its real outcome in an `<event>` payload (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`) and usually carries NO `<status>` tag at all, unlike a completion pulse.",
      "depends": "csift's `automation_label` fills the status slot in the fallback order `<status>`, then `<event>`, then the literal `completed`, so a monitor pulse renders the event it actually carries instead of a fabricated outcome the record does not hold.",
      "code": [
        {
          "path": "src/model/automation.rs",
          "lines": "93-97",
          "snippet": "    /// The `<event>` payload, if present - where a Monitor / ScheduleWakeup pulse carries its\n    /// real outcome (`STAGE2_OUTPUT_READY`, `[Monitor timed out - re-arm if needed.]`). Often\n    /// the only outcome signal on a Monitor pulse (which usually has no `<status>`), so the\n    /// label falls back to it instead of fabricating `completed`.\n    pub event: Option<String>,"
        }
      ],
      "instrument": "`grep -o '<event>[^<]*' ~/.claude/projects/*/*.jsonl | sort | uniq -c` for the event census, then `csift search '' @<session> -t harness.notification.monitor --max-count 3` and read the rendered label: the status slot must show the event payload, not `completed`. Counting rule: one rendered label per pulse record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "SPEC.md section 5.1; AGENTS.md section 3.3; src/model/automation.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "a raw python census over the 2797 XML-wrapped pulses tabulating (kind by leading summary phrase, '<status>' in content, '<event>' in content) ; csift search '' -t harness.notification.monitor --max-count 8 (reading the rendered label) ; csift search '' -t harness.notification --format json parsing the status slot out of each rendered label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Monitor timed out[^\"]{0,40}'",
          "observed": "Of 946 monitor pulses corpus-wide, 835 (88.3%) carry an <event> and NO <status>, and 111 (11.7%) carry a <status> and no <event>. <event> occurs on ZERO of the 1851 non-monitor pulses (background-command 1517, agent 187, workflow 142, task 5 - all <status>, no <event>). Rendered labels put the event payload in the status slot: '[monitor <id> [Monitor timed out \\u2014 re-arm if needed.]] Monitor event: \"...\"' and '[monitor <id> STEP=[113/124...]'; the rendered census counts exactly 111 monitor records whose slot reads 'completed', matching the 111 <status>-bearing pulses one for one. The 2.1.258 binary carries the literal 'Monitor timed out \\u2014 re-arm if needed.]'.",
          "rule": "One rendered label per pulse record; a pulse is monitor-kind if its <summary> opens case-insensitively with monitor, scheduled or cron. Slot equals 'completed' is counted only on an exact match of that word.",
          "note": "The fallback order never fabricates: the count of monitor pulses rendering 'completed' (111) equals the count that actually carry a <status> tag (111), so the remaining 835 render the <event> they really hold. The doc comment's example payload STAGE2_OUTPUT_READY was not observed in this corpus; the event payloads seen are step/progress strings, terminal markers and the timed-out sentinel - the example is illustrative, not a claim. Code site verified verbatim at src/model/automation.rs:93-97."
        }
      ]
    },
    {
      "id": "CLS-006",
      "area": "classification",
      "behavior": "Claude Code CONCATENATES several harness sections into ONE `type:\"user\"` record: a batched record can hold multiple `<task-notification>` pulses and/or several inbound peer sections of mixed kind (prose, `idle_notification`, `teammate_terminated`), each closed by `</task-notification>`, `</teammate-message>` or `</agent-message>`, and a peer tag can also be QUOTED inside a notification's `<result>` span.",
      "depends": "csift recognizes a section only at a BOUNDARY (content start, immediately after the relay preamble, or right after a prior section's close tag), scans ALL sections and UNIONs their labels with notification precedence over a peer tag quoted inside a notification span, mirrors that masking exactly in the per-section text render so classification and render never drift, and emits one hit per section; a plain `contains` check mislabels any genuine message that merely quotes a tag mid-prose - common in csift's own development sessions.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "120",
          "snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "242-245",
          "snippet": "        // BATCHED mixed-family sections: a `<task-notification>` automation pulse and/or an\n        // inbound peer message can be concatenated in ONE record. Scan ALL sections and UNION\n        // their labels, with notification precedence over a peer tag quoted inside a\n        // notification span (P1c M3). When ≥1 section matches, that fully classifies the record."
        }
      ],
      "instrument": "`csift search '<task-notification>' -t user.message -c` must be far smaller than `rg -c '<task-notification>' <same scope>` (rg counts raw occurrences, csift counts boundary-anchored sections), and a record with two notifications must yield two hits under `csift search '' -t harness.notification --format json`. Counting rule: one record, N sections, labels unioned.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 5.2; src/model/classify.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python over one project dir's 23 top-level transcripts counting, per string-content record, the number of </task-notification> + </teammate-message> + </agent-message> closes ; rg -o --no-filename '<task-notification>' *.jsonl | wc -l and rg -c '<task-notification>' *.jsonl in that same dir ; csift search '<task-notification>' -t user.message -c ; csift show @<id> --line <n> --format json on one 8-section record",
          "observed": "253 string-content records carry at least one section close; 319 sections total; 22 records carry 2 or more, histogram {2:6, 3:6, 4:2, 5:4, 7:2, 8:2}. Close-tag census: </task-notification> 181, </teammate-message> 135, </agent-message> 3. Mixed payload kinds observed inside one record: prose, {\"type\":\"idle_notification\"...} and {\"type\":\"teammate_terminated\"...}, all behind the relay preamble 'Another Claude session sent a message: '. A <task-notification> open tag was observed INSIDE a <teammate-message> span (tag sequence teammate-message, /teammate-message, teammate-message, task-notification, /teammate-message). Boundary anchoring: rg reports 1557 lines and 2043 raw occurrences of the open tag in that one project dir, while csift reports exactly 1 user.message record mentioning it corpus-wide. The 8-section record emits 8 units, every unit carrying the unioned label set ['agent.communication.signal','agent.communication.inbox'], split 5 signal / 3 inbox.",
          "rule": "One record, N sections, labels unioned: sections counted by close tags in message.content; emitted units counted as JSON rows whose line equals the record's line.",
          "note": "Every part of the claim reproduced, including the quoted-tag-inside-a-notification case that a plain contains check would mislabel. The 1557-vs-1 gap is the size of the error boundary anchoring avoids, and the surviving 1 user.message hit is the correct outcome - a genuine message that quotes the tag mid-prose. Code sites verified verbatim at src/model/peer.rs:120-127 and src/model/classify.rs:242-245."
        }
      ]
    },
    {
      "id": "CLS-007",
      "area": "classification",
      "behavior": "Every member of the stopped/orphan notice family shares one record shape: type:\"user\", userType:\"external\", isSidechain:false, origin:{\"kind\":\"task-notification\"}, NO isMeta key at all, and a message.content that is a STRING (never an array). promptSource is \"system\" on the large majority but is not universal - of 2807 measured records it is \"system\" on 2685 and absent on 122. Of those 2807, 2797 were XML-wrapped and 10 were plain text.",
      "depends": "csift classifies these by CONTENT shape (the `<task-notification>` prefix, or the agents-stopped templates) rather than by `origin`/`promptSource`, so a plain-text notice with no recognizable template still classifies as the human; `origin.kind` and `promptSource` are the authoritative discriminator csift does not yet read.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "325-329",
          "snippet": "        // The harness's agents-stopped notice (v0.10.0): a kill notice, not the human.\n        if is_agents_stopped_notice(s) {\n            push_unique(out, Class::NotificationSubagent);\n            return;\n        }"
        }
      ],
      "instrument": "`rg -l '\"origin\":\\{\"kind\":\"task-notification\"\\}' ~/.claude/projects --glob '*.jsonl'` then, for each matching record, test whether `message.content.lstrip().startswith('<task-notification>')`. Counting rule: one observation per RECORD whose parsed `origin.kind == \"task-notification\"`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python over all 64 top-level transcripts selecting records whose parsed origin.kind == \"task-notification\", tabulating (type, userType, isSidechain, promptSource, whether the isMeta key exists, isMeta value) and whether message.content lstrip-starts with the XML wrapper ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'promptSource|\"kind\":\"task-notification\"|task-notification\"\\}'",
          "observed": "2807 records carry origin.kind == task-notification (the claim says 2700). 2797 are XML-wrapped and 10 are plain text (the claim says 2683 and 17). message.content is a python str on 2807/2807 - never an array. type is user, userType is external, isSidechain is false and the isMeta key is ABSENT on 2807/2807. promptSource is NOT uniform: \"system\" on 2685 records (95.7%) and the key is absent on 122 (4.3%). All 10 plain-text specimens are agents-stopped notices, 9 plural and 1 singular. The binary carries origin:{kind:\"task-notification\",source:...} and mode:\"task-notification\" at the enqueue sites, and 33 occurrences of promptSource.",
          "rule": "One observation per RECORD whose parsed origin.kind == \"task-notification\"; XML-wrapped means message.content.lstrip() starts with '<task-notification>'; a field is 'present' only when the key exists in the record object.",
          "note": "The shape claim holds on five of its six fields at 100%, and the wrapped/plain split reproduces at the same order (99.6% wrapped here vs 99.4% claimed). Two corrections: the totals have moved (2807/2797/10 rather than 2700/2683/17 - the corpus grew) and promptSource is a 95.7% field, not a universal one, so it is a weaker discriminator than the claim's depends paragraph implies while origin.kind is exact at 2807/2807. Code site verified verbatim at src/model/classify.rs:325-329."
        }
      ]
    },
    {
      "id": "CLS-008",
      "area": "classification",
      "behavior": "When background agents are killed from the UI, Claude Code writes a plain-STRING type:\"user\" notice in one of two templates - the singular Background agent \"<desc>\" was stopped by the user. or the plural N background agents were stopped by the user: \"<desc>\", \"<desc>\". - naming a count and truncated prompt prefixes but NEVER an agent id (measured 0 of 10 records carry a >=16-hex token), and it is delivered through enqueuePendingNotification so it triggers no generation (no assistant record follows any of the 10).",
      "depends": "csift treats the notice as a synthetic marker so it is excluded from `is_genuine_user` and never opens a turn, classifies it `harness.notification.subagent`, and renders `[subagent stopped] <notice>`; without the exclusion each kill inflates the human-turn count (9 corpus specimens were previously read as `user.message`). Because the notice names no id, `csift status` can report that agents were stopped but not WHICH, and the raw needle `stopped by the user` doubles as the verifiable synth marker guarding the whole-file prefilter gate.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "60-66",
          "snippet": "pub fn is_agents_stopped_notice(content: &str) -> bool {\n    let s = content.trim_start();\n    // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n    // the user.` (no count).\n    if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n        return true;\n    }"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "72-79",
          "snippet": "    (rest.starts_with(\" background agent was stopped by the user\")\n        || rest.starts_with(\" background agents were stopped by the user\"))\n        && rest.contains(AGENTS_STOPPED_MARKER)\n}\n\n/// The raw-byte marker the agents-stopped notice always carries (the synth-marker\n/// needle for its fabricated `[subagent stopped]` label prefix).\npub const AGENTS_STOPPED_MARKER: &str = \"stopped by the user\";"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "195-197",
          "snippet": "        let note = format!(\n            \"{count} background agent(s) were stopped by the user at {} - the notice names \\\n             no id, so csift cannot mark which agents it stopped\","
        }
      ],
      "instrument": "csift search 'stopped by the user' --count-by label, asserting the user.message key is absent and that the harness.notification.subagent count equals the on-disk notice count. Do NOT assert that every matched record lands under harness.notification.subagent: the phrase also occurs in ordinary prose, and corpus-wide the query returns 242 records across 9 labels of which only 10 are notices (agent.tool.result 122, agent.tool.use 60, agent.thinking 29, agent.communication.inbox 10, harness.notification.subagent 10, agent.message 8, agent.communication.sent 4, harness.compaction.summary 2, harness.meta.attachment 1).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 4a; AGENTS.md section 3.3a; src/model/markers.rs comment; CHANGELOG.md 0.10.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,140}background agents? (was|were) stopped by the user.{0,120}' ; python over all 64 top-level transcripts matching either notice template at content start, testing each for a >=16-hex token and recording the type of the next record in the file ; csift search 'stopped by the user' --count-by label ; rg -c 'stopped by the user' on one project dir's top-level transcripts",
          "observed": "The binary carries the generator verbatim: 'let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\"' - both templates exact, the map is over descriptions (kn is built from Ko.description) so no id can appear, and enqueuePendingNotification is the delivery path. On disk: 10 notice records corpus-wide, 0 of them containing a >=16-hex token, and the record following a notice is never an assistant record (file-history-snapshot 3, user 3, attachment 5). csift labels all 10 harness.notification.subagent and 0 user.message. Scoped rg on one project dir reports 76 lines containing the phrase.",
          "rule": "A record counts as a notice only if its trimmed content starts with 'Background agent \"' and contains ' was stopped by the user', or matches ^\\d+ background agents? (were|was) stopped by the user. rg counts raw lines; csift counts records whose content matches at content start.",
          "note": "The behavior is confirmed at the binary level, which is stronger evidence than the corpus alone. Corrections: the specimen count is now 10 rather than 9, and the claim's instrument overstates - the phrase appears in prose 232 times in this corpus, so the assertion that survives is 'zero under user.message' plus 'notice count equals subagent-leaf count', not 'every record'. Two adversarial findings worth recording: a loose contains-based probe finds an 11th candidate that is a compaction summary quoting the phrase, and csift's stricter start-anchored template correctly rejects it; and the binary carries a SEPARATE template `Task \"${e.description}\" (${e.taskId}) was stopped by the user.` written with isMeta:!0 which DOES name an id and which is_agents_stopped_notice does not match (harmless today because isMeta records are excluded anyway, but it means 'the notice never names an id' is true only of the Background agent family). Code sites verified verbatim at src/model/markers.rs:55-61 and 67-74 and src/live/background_scan.rs:205-207."
        }
      ]
    },
    {
      "id": "CLS-009",
      "area": "classification",
      "behavior": "An asynchronous background Agent spawn's tool_result is a LAUNCH ACK, not the child's report: its text opens 'Async agent launched successfully' and it carries structured toolUseResult.{isAsync:true, status:\"async_launched\"} under the spawn's own tool_use_id (272 records corpus-wide, both signals on all of them); the child's actual report arrives LATER as a <task-notification> pulse carrying a <result>. The structured status:\"async_launched\" marker is broader than this family - a further 174 records are background WORKFLOW launches carrying that status with no isAsync key and the different ack text 'Workflow launched in background. Task ID: <id>'.",
      "depends": "csift prefers the structured signal and falls back to the text prefix (`Record::is_async_launch_ack`), labeling the ack `agent.tool.result` ONLY and excluding it from subagent-return detection, while the later `<result>`-bearing pulse is what additionally carries `agent.communication.inbox` with direction child to self; conflating them attributes the child's report to the launch instant.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "180-185",
          "snippet": "/// The leading sentence of an ASYNC/background `Agent` spawn's launch-confirmation tool_result\n/// (`\"Async agent launched successfully.\\nagentId: …\"`). This is a launch ACK, NOT the child's\n/// report - the report arrives LATER via the `<task-notification>` `<result>` pulse (G1 → inbox).\n/// On disk the ack also carries the structured `toolUseResult.{isAsync:true, status:\"async_launched\"}`\n/// shape ([`Record::is_async_launch_ack`] prefers the structured signal, falls back to this prefix).\npub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "81-88",
          "snippet": "    pub(crate) fn is_async_launch_ack(&self) -> bool {\n        if let Some(probe) = self.tur_probe() {\n            if probe.status.as_ref().and_then(serde_json::Value::as_str) == Some(\"async_launched\")\n                || probe.is_async.as_ref().and_then(serde_json::Value::as_bool) == Some(true)\n            {\n                return true;\n            }\n        }"
        }
      ],
      "instrument": "`jq -r 'select(.toolUseResult.status==\"async_launched\") | .toolUseResult.agentId' <file> | wc -l` versus `grep -c 'Async agent launched successfully' <file>`, then `csift search 'Async agent launched' --format json | jq -r '.label'` must be `agent.tool.result` on every row and never `agent.communication.inbox`. Counting rule: one ack per launch, one inbox record per `<result>` pulse.",
      "located": {
        "claude_code": null,
        "csift": "0.4.0",
        "source": "SPEC.md section 5.2; AGENTS.md section 3.3; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python over all 64 top-level transcripts counting records with toolUseResult.status == \"async_launched\" or toolUseResult.isAsync == true versus records whose tool_result text opens 'Async agent launched successfully', tabulating the toolUseResult key sets ; csift show --line <list> --format json over every such record in one project dir, censusing the labels array ; a paired census of pulses with and without <result> in that same dir ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Async agent launched successfully'",
          "observed": "446 records corpus-wide carry the structured signal. 272 of them carry BOTH isAsync:true and a tool_result text opening 'Async agent launched successfully' (co-occurrence 272/272, key set agentId/canReadOutputFile/description/isAsync/outputFile/prompt/resolvedModel/status). The other 174 carry status:\"async_launched\" with NO isAsync key, a different key set (runId/scriptPath/status/summary/taskId/taskType/transcriptDir/workflowName) and a different ack text opening 'Workflow launched in background. Task ID: <id>'. All 86 Agent-spawn acks in one project dir label exactly ['agent.tool.result'] - never agent.communication.inbox. In that same dir 49 pulses carry a <result> and 49/49 additionally carry agent.communication.inbox (94 rows subagent+inbox, 4 rows workflow+inbox = 49 records at 2 label views each), while 131/131 pulses without a <result> carry only harness.notification.background-command. The binary carries the literal 'Async agent launched successfully'.",
          "rule": "One ack per launch: a record counts as an Agent-spawn ack if toolUseResult.isAsync is true; as a workflow launch if status is async_launched and isAsync is absent. One inbox record per <result>-bearing pulse. csift show emits one JSON row per label view, so a dual-labeled record yields two rows - divide by the label count to get records.",
          "note": "The classification behavior is confirmed at 86/86 acks labelling agent.tool.result only, and the <result>-pulse coupling at 49/49 versus 131/131. The refinement is scope: the claim describes status:\"async_launched\" as the Agent ack's structured signature, but it covers two ack families (446 records = 272 Agent + 174 workflow). csift's is_async_launch_ack ORs the two signals so it catches both, which is wider than the claim's wording describes and is not itself a bug - it just means the marker cannot be read as an Agent-spawn discriminator. Code sites verified verbatim at src/model/markers.rs:167-172 and src/model/classify.rs:81-88."
        }
      ]
    },
    {
      "id": "CLS-010",
      "area": "classification",
      "behavior": "A `SendMessage` tool_use and a `Task`/`Agent`/`Workflow` spawn tool_use are COMMUNICATIONS between sessions, not merely tool calls: the spawn addresses a child, and `SendMessage` addresses a named peer.",
      "depends": "csift dual-labels them agent.tool.use plus agent.communication.{sent,signal} and emits the record ONCE under the richer communication view, with a from / to direction. The direction source differs by tool: a SendMessage reads input.to, while a spawn addresses its child by the spawn target's name or id and a Workflow spawn has no target to name yet, so 169 of 1819 sent hits (9.3%) render to as '?'. The owner's own id renders as the literal 'self'.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "214-216",
          "snippet": "        Class::CommInbox,\n        Class::CommSent,\n        Class::CommSignal,"
        }
      ],
      "instrument": "`csift search '' -t agent.communication --count-by label --format json @<a team session>`, cross-checked with `rg -o '\"name\":\"SendMessage\".{0,120}' <transcript> | rg -o '\"type\":\"[a-z_]+\"'`. Counting rule: one per matched record, deduped to the richest label.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 5.2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t agent.communication --count-by label ; csift search '' -t agent.communication --format json censusing (label, tool_name), the labels array and the from/to pair ; csift search '' -t agent.tool.use --count-by tool ; csift show @<id> --line <n> --format json on one SendMessage record ; a python read of that record's raw tool_use block",
          "observed": "Corpus-wide the communication leaves hold 10804 records (inbox 8644, sent 1819, signal 400). 1818 records carry the label SET ('agent.tool.use','agent.communication.sent') and emit under the comm view. Tool split for sent: Agent 916, SendMessage 725, Workflow 178; signal from SendMessage 79. Under -t agent.tool.use --count-by tool, SendMessage counts 804 = 725 + 79 exactly, so every SendMessage tool_use is dual-labeled. Dedup measured on one record: csift show returns exactly 1 row, label agent.communication.sent, labels ['agent.tool.use','agent.communication.sent']. Direction fields are the hit keys from and to, not a nested object: the raw block's input keys are ['content','message','recipient','to','type'] and its input.to value is rendered verbatim as the hit's to while from renders the literal 'self'. Inbox hits render <peer> to self (top peers 3512, 976, 631, 544, 433 records; a named lead 428). to-shape by spawning tool: Agent 835 name-embedded child id / 79 bare name / 1 bare hex / 1 unknown, SendMessage 589 bare name / 136 name-embedded id, Workflow 168 unknown / 10 bare name.",
          "rule": "One per matched record, deduped to the richest surviving label; census keys pass the active -t filter. A record's label set is read from the labels array, so a dual-labeled record contributes one record and two labels.",
          "note": "The behavior claim reproduces exactly, including the once-only emission and the self literal. The single correction is the direction provenance: input.to is the source for SendMessage only (725 sent + 79 signal), while the 916 Agent and 178 Workflow spawns take their to from the spawn target, and every Workflow spawn but ten renders '?' because the child id does not exist at spawn time. Code site verified verbatim at src/model/taxonomy.rs:204-206."
        }
      ]
    },
    {
      "id": "CLS-011",
      "area": "classification",
      "behavior": "A `<teammate-message ...>` body that is a JSON object `{\"type\":\"<sig>\"}` is a control SIGNAL - observed types are `idle_notification` (334), `task_assignment` (49), `shutdown_request` (36), `teammate_terminated` (25) and `shutdown_approved` (25) - while a prose body is a plain message; a `SendMessage` whose `input.type` (or nested `message.type`) is anything but `message`/`direct` is likewise a signal (observed outbound: `shutdown_request` 38, `shutdown_response` 41). Every prose `SendMessage` observed carried the explicit `input.type == \"message\"`; the `direct` value and an absent `type` are tolerated by the predicate but were never seen on disk.",
      "depends": "`agent.communication.signal` versus `agent.communication.inbox` splits on exactly this, so a shutdown request is not counted as peer prose.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "13-15",
          "snippet": "    /// The control-signal type when the body is `{\"type\":\"<sig>\"}` (idle_notification /\n    /// shutdown_request / teammate_terminated / shutdown_approved / …); `None` for prose.\n    pub signal_type: Option<String>,"
        },
        {
          "path": "src/model/peer.rs",
          "lines": "269-272",
          "snippet": "pub(crate) fn send_message_is_signal(input: Option<&serde_json::Value>) -> bool {\n    let Some(input) = input else {\n        return false;\n    };"
        }
      ],
      "instrument": "`csift search '' -t agent.communication.signal --format json | jq -r .excerpt | rg -o '\"type\":\"[a-z_]+\"' | sort | uniq -c`. Counting rule: one record per signal-bodied section.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/model/peer.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'idle_notification|shutdown_request|teammate_terminated|shutdown_approved|shutdown_response' | sort | uniq -c   AND   csift search '' -t agent.communication.signal --raw | python3 (pull the JSON body of every <teammate-message>/<agent-message> section and every SendMessage tool_use input.type)   AND the same census on -t agent.communication.sent",
          "observed": "Binary 2.1.258: idle_notification x6, shutdown_request x18, shutdown_response x16, shutdown_approved x7, teammate_terminated x5. Corpus: 394 records classify agent.communication.signal. Inbound signal-bodied sections: idle_notification 334, task_assignment 49, shutdown_request 36, teammate_terminated 25, shutdown_approved 25. Outbound SendMessage input.type: shutdown_request 38, shutdown_response 41. Records with neither shape: 0. The prose leaf agent.communication.sent carries 711 SendMessage tool_use blocks and input.type is the literal \"message\" on all 711 (0 \"direct\", 0 absent).",
          "rule": "One count per signal-bodied <teammate-message>/<agent-message> section and per SendMessage tool_use block, taken over the records csift classifies agent.communication.signal; the split is checked by running the identical census over agent.communication.sent.",
          "note": "Two signal types the claim omits are live and not rare: task_assignment (49 records) and shutdown_response (41 blocks, outbound only). The split predicate itself is clean - 711/711 prose sends carry type \"message\" and 0 signal-bodied records fell through to the prose leaf."
        }
      ]
    },
    {
      "id": "CLS-012",
      "area": "classification",
      "behavior": "The tools that spawn a subagent are named exactly `Task`, `Agent` and `Workflow` - all three registered in 2.1.258, though only `Agent` (897 blocks) and `Workflow` (177) were exercised in this corpus and `Task` appears 0 times. A peer message is sent with `SendMessage`, whose recipient rides `input.to`; `input.recipient` is not an alternative but a duplicate alias - both keys were present and byte-equal on all 790 observed blocks. A named/teammate spawn carries `input.name` (291 of 897 Agent blocks) and a typed spawn `input.subagent_type` (853 of 897); a `Workflow` spawn carries `name`/`script`/`scriptPath` and never `subagent_type`.",
      "depends": "Communication direction (self to child, self to the named recipient) and the whole topology join key off these literal names; a rename breaks `agents`, `search -t agent.communication.sent`, and the teammate name-join.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "262-264",
          "snippet": "pub(crate) fn is_spawn_tool_name(name: &str) -> bool {\n    matches!(name, \"Task\" | \"Agent\" | \"Workflow\")\n}"
        },
        {
          "path": "src/model/peer.rs",
          "lines": "304-306",
          "snippet": "pub(crate) fn spawn_target_name(input: Option<&serde_json::Value>) -> Option<String> {\n    let input = input?;\n    for key in [\"name\", \"subagent_type\"] {"
        }
      ],
      "instrument": "`csift search '' @<session> --count-by tool` lists every tool name seen. Counting rule: one count per tool_use block, keyed by `name`.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/model/peer.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\"Bash\",\"BashOutput\"[^\\]]{0,600}' | sort -u   AND   rg -o 'var yt=\"Agent\",bnr=\"Launch a new agent to handle complex, multi-step tasks\"'   AND   csift search '' -t agent.communication --raw | python3 (count tool_use names, spawn input keys, SendMessage recipient keys)",
          "observed": "Binary tool registry array contains, adjacent: \"Agent\",\"Task\",\"Workflow\",\"Skill\", and \"SendMessage\" later in the same array. Verbatim: `var yt=\"Agent\",bnr=\"Launch a new agent to handle complex, multi-step tasks\"`; `o_=\"Task\"` sits beside `hj=\"fork\"` and `Enr=\"agent:builtin:fork\"`. Corpus tool_use blocks on the agent.communication leaves: Agent 897, Workflow 177, SendMessage 790, Task 0. Agent input keys: prompt 897, description 897, subagent_type 853, model 616, name 291, run_in_background 266. Workflow input keys: script 135, args 55, description 40, scriptPath 32, name 10, resumeFromRunId 10, run_in_background 2 - no subagent_type. SendMessage: `to` present on 790/790 AND `recipient` present on 790/790, and the two values are equal on all 790.",
          "rule": "One count per tool_use block keyed by `name`, and one count per input key present on that block, over every record csift classifies under the agent.communication role prefix.",
          "note": "The `input.recipient` fallback never fired alone - it is a redundant alias on every block, so a reader keyed only on `to` loses nothing today. `Task` is registered but dormant in this corpus, so csift's three-name matcher is a superset, not dead code."
        }
      ]
    },
    {
      "id": "CLS-013",
      "area": "classification",
      "behavior": "Claude Code injects a fixed continuation marker as an `isMeta` `type:\"user\"` record whose content is exactly `Continue from where you left off.` when resuming a session. Current measurement (CC 2.1.258, whole corpus, csift 0.10.0): 6 records classify `harness.schedule.continuation`; 470 records contain the phrase anywhere, the rest being prose that discusses the marker.",
      "depends": "csift classifies it `harness.schedule.continuation` and excludes it from `is_genuine_user`, so a resumed session's human-turn count is not inflated by one per resume and resume points stay enumerable; without the exact-content rule the record falls through the isMeta gate unlabeled.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "187-190",
          "snippet": "/// The fixed harness-injected continuation marker (GOLD §5) - `harness.schedule.continuation`.\n/// A `type:\"user\"` (`isMeta`) record CC injects to resume a session from where it left off.\n/// Verified across real `~/.claude/projects` data (522 occurrences), exact content.\npub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
        }
      ],
      "instrument": "csift search '' -t harness.schedule.continuation --count-by label --format json | tail -1 (matched_records = the classified set), then csift search '' -t harness.schedule.continuation --raw and confirm every record is type=user / isMeta=true / content exactly the marker.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.4.0",
        "source": "AGENTS.md section 3.3a; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Continue from where you left off\\.?' | sort | uniq -c   AND   csift search '' -t harness.schedule.continuation --count-by label --format json | tail -1   AND   csift search '' -t harness.schedule.continuation --raw | python3 (print type/isMeta/role/content per record)   AND   csift search 'Continue from where you left off' --count-by label --format json | tail -1",
          "observed": "Binary 2.1.258 carries the exact string 6 times. Corpus: matched_records 6 under harness.schedule.continuation; all 6 are type=user, isMeta=True, role=user, and message.content is a single text block whose text is exactly 'Continue from where you left off.'. The same phrase appears anywhere in 470 records overall (335 agent.tool.result, 65 agent.tool.use, 23 agent.message, ...), i.e. 464 of them are prose mentions.",
          "rule": "Records, not lines: matched_records from the --count-by label summary. The classified figure counts records whose reconstructed content STARTS with the marker (csift's `trim_start().starts_with`); the 470 figure counts records containing the phrase anywhere.",
          "note": "The record SHAPE reproduces perfectly and the marker string is live in the 2.1.258 binary. Only the 522 count fails to reproduce under any counting rule I could construct - 6 classified records, 470 phrase-bearing records. The stale figure was almost certainly a raw substring occurrence count over a different corpus state, and it should not be carried forward without its rule."
        }
      ]
    },
    {
      "id": "CLS-014",
      "area": "classification",
      "behavior": "The harness distinguishes a FIRED autonomous-loop / `ScheduleWakeup` timer tick - an `isMeta` `type:\"user\"` record opening `# Autonomous loop check`, whose next line opens `You're being invoked on a timer while the user is away or occupied.` (straight ASCII apostrophe) - from the loop DRIVER ticks `# Autonomous loop tick (dynamic pacing)` and `Run the autonomous check using the loop instructions established earlier in this conversation.`. TWO loop sentinels exist in 2.1.258, not one: `<<autonomous-loop-dynamic>>` (always used by `ScheduleWakeup`) and `<<autonomous-loop>>` (CronCreate-based autonomous loops); the binary explicitly warns not to confuse them.",
      "depends": "csift routes the fired-timer forms to `harness.schedule.wakeup` and the driver forms to `harness.meta.loop`, matching the wakeup arm FIRST so `check` never falls through to `tick`; conflating them merges the scheduler with the driver in every census and makes a monitoring transcript unreadable. A generic cron tick's injected prompt is operator-authored free text with no universal marker and is deliberately left unlabeled rather than mislabeled.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "204-211",
          "snippet": "/// The header of the harness-injected FIRED autonomous-loop / `ScheduleWakeup` timer tick (P1c\n/// M2a / oracle D12) - `harness.schedule.wakeup`. When the timer FIRES, the harness injects an\n/// `isMeta` `type:\"user\"` record whose content opens `# Autonomous loop check\\n\\nYou're being\n/// invoked on a timer …`. DISTINCT from the `meta.loop` DRIVER ticks\n/// ([`AUTONOMOUS_LOOP_TICK_PREFIX`] = `# Autonomous loop tick` / [`AUTONOMOUS_CHECK_MARKER`]):\n/// `check` ≠ `tick`, so the two prefixes never collide. The wakeup arm is matched BEFORE the\n/// meta.loop arm in [`Record::classify`], so the fired tick routes to `schedule.wakeup`.\npub const SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX: &str = \"# Autonomous loop check\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "229-233",
          "snippet": "/// `harness.meta.loop` markers (GOLD §2, edge-fixtures G2) - autonomous-loop drivers (distinct\n/// from the [`SCHEDULE_WAKEUP_MARKER`] sentinel, which stays `harness.schedule.wakeup`).\npub const AUTONOMOUS_LOOP_TICK_PREFIX: &str = \"# Autonomous loop tick\";\n/// See [`AUTONOMOUS_LOOP_TICK_PREFIX`] - matched anywhere (it can sit mid-prompt).\npub const AUTONOMOUS_CHECK_MARKER: &str = \"Run the autonomous check\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "202",
          "snippet": "pub const SCHEDULE_WAKEUP_MARKER: &str = \"<<autonomous-loop-dynamic>>\";"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "297-303",
          "snippet": "        if s.contains(SCHEDULE_WAKEUP_MARKER)\n            || s.starts_with(SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX)\n            || s.contains(SCHEDULE_WAKEUP_TIMER_MARKER)\n        {\n            push_unique(out, Class::ScheduleWakeup);\n            return;\n        }"
        }
      ],
      "instrument": "`rg -c \"You're being invoked on a timer\" ~/.claude/projects/*/*.jsonl` and `rg -c '# Autonomous loop tick' <same>`, then `csift search '' -t harness.schedule.wakeup -c` and `-t harness.meta.loop -c`. Counting rule: one per raw occurrence for rg, one per classified record for csift; the two csift counts must not overlap.",
      "located": {
        "claude_code": null,
        "csift": "0.4.0",
        "source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n -A4 '^# Autonomous loop check$'   AND   rg -o '.{0,100}# Autonomous loop tick.{0,180}'   AND   rg -o '.{0,140}<<autonomous-loop>>.{0,160}'   AND   csift search '' -t harness.schedule.wakeup --count-by label --format json | tail -1   AND   csift search '' -t harness.meta.loop --count-by label --format json | tail -1   AND   csift search '' -t harness.schedule.wakeup --raw",
          "observed": "Binary: `# Autonomous loop check` appears twice, each immediately followed by a line opening `You're being invoked on a timer while the user is away or occupied.` (straight ASCII apostrophe, two prompt variants). The driver is `# Autonomous loop tick (dynamic pacing)`, built by a function returning a template that opens `# Autonomous loop tick`; `Run the autonomous check using the loop instructions established earlier in this conversation.` appears 5 times. Verbatim: `var wa=\"ScheduleWakeup\",zAe=\"<<autonomous-loop>>\",roe=\"<<autonomous-loop-dynamic>>\"`, with an adjacent in-binary note `(There is a similar ${\"<<autonomous-loop>>\"} sentinel for CronCreate-based autonomous loops; do not confuse the two - ${\"ScheduleWakeup\"} always uses the -dynamic variant.)`. Corpus: matched_records 1 for harness.schedule.wakeup and 0 for harness.meta.loop; the single wakeup record is type=user, isMeta absent, role=user, plain operator prose that merely quotes the sentinel mid-text.",
          "rule": "Binary: one count per distinct string occurrence in `strings -n 6` output. Corpus: matched_records from each leaf's --count-by label summary; the two leaves must not overlap, and a true fired tick would be an isMeta record whose content STARTS with the header.",
          "note": "Two problems the instruments surfaced. (1) csift models only the `-dynamic` sentinel, so a fired CronCreate autonomous-loop tick carrying `<<autonomous-loop>>` would classify unlabeled; the header and body-sentence arms would still catch it if the runtime resolves the sentinel to the standard prompt, but the sentinel arm alone would not. (2) The wakeup arm in src/model/classify.rs:297 uses `s.contains(SCHEDULE_WAKEUP_MARKER)` and is NOT gated on isMeta, so the sole corpus record under harness.schedule.wakeup is a false positive: a genuine non-meta operator prompt that quotes the sentinel loses its user.message label entirely. No true fired tick exists anywhere in this corpus, so only the binary decides the emission half of the claim."
        }
      ]
    },
    {
      "id": "CLS-015",
      "area": "classification",
      "behavior": "Arming a `ScheduleWakeup` is an ordinary tool CALL, while the FIRED tick is a separate harness-injected prompt; a custom-prompt tick lands as an `isMeta` record.",
      "depends": "csift classifies the arming call `agent.tool.use` and only the fired tick `harness.schedule.wakeup`, so a search for scheduled work must look at both surfaces; expecting the arming call under `harness.schedule.*` returns a definitive empty.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "293-299",
          "snippet": "        // harness.schedule.wakeup: the FIRED autonomous-loop / ScheduleWakeup timer tick. Three\n        // fixed markers (P1c M2a): the `<<autonomous-loop-dynamic>>` sentinel, the `# Autonomous\n        // loop check` header, and the `You're being invoked on a timer` body sentence. Matched\n        // BEFORE the meta.loop arm - `check` ≠ `tick`, so the loop-DRIVER prefix never collides.\n        if s.contains(SCHEDULE_WAKEUP_MARKER)\n            || s.starts_with(SCHEDULE_WAKEUP_LOOP_CHECK_PREFIX)\n            || s.contains(SCHEDULE_WAKEUP_TIMER_MARKER)"
        }
      ],
      "instrument": "`csift search 'ScheduleWakeup' --count-by label` (arming records key under `agent.tool.use`) and `csift search \"You're being invoked on a timer\" --count-by label` (fired ticks key under `harness.schedule.wakeup`). Counting rule: one record per key.",
      "located": {
        "claude_code": null,
        "csift": "0.2.0",
        "source": "SKILL.md wrong-assumption row on ScheduleWakeup"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search 'ScheduleWakeup' --count-by label   AND   csift search 'ScheduleWakeup' --count-by tool   AND   csift search 'ScheduleWakeup' -t harness.schedule -c   AND   a python3 scan of ONE session jsonl pairing every ScheduleWakeup tool_use input.prompt against the content of every later type:\"user\" record in the same file",
          "observed": "Corpus-wide, records matching 'ScheduleWakeup' key under agent.tool.use 794 / agent.tool.result 595 / agent.thinking 173 / agent.message 133 and ZERO under any harness.schedule.* leaf; `-t harness.schedule -c` and `-t harness.schedule.wakeup -c` both print 0. In one session file (123,044 records): 556 ScheduleWakeup tool_use blocks, all 556 with a non-empty input.prompt, 535 distinct prompt strings; 443 of those distinct prompts reappear VERBATIM as the content of a type:\"user\" record in the same file, across 456 such records, of which 456/456 have isMeta true. Only 2 of the 556 arming prompts were the `<<autonomous-loop-dynamic>>` sentinel; the rest were operator-authored free text.",
          "rule": "Arming side: one count per tool_use block named ScheduleWakeup. Fired side: exact string equality between an arming block's input.prompt and a later user record's message.content, counted once per matching record; isMeta read off that record.",
          "note": "The pairing is the strongest instrument in this batch: 456 of 456 fired ticks are isMeta true, and the arming calls never reach a harness.schedule.* leaf (definitive empty, exit 0). A separate probe confirms the second half of the depends clause - searching a fired custom tick's own text returns only the 22 assistant arming tool_use blocks that carry the same string, never the isMeta fired records themselves, because a custom-prompt tick carries no universal marker and classifies to no label at all."
        }
      ]
    },
    {
      "id": "CLS-016",
      "area": "classification",
      "behavior": "Hook-injected feedback reaches the transcript as `isMeta` `type:\"user\"` records in two shapes emitted by Claude Code 2.1.258 - content opening `Stop hook feedback:` (29 records) and a `<local-command-caveat>` wrapper (155 records) - plus a third, hook-authored shape: the edit-retry notice `The last Edit failed because the target file was modified`, which is NOT a string in the 2.1.258 binary and whose single corpus instance rides inside a `Stop hook feedback:` body. All three land as isMeta user records; the edit-retry marker is matched ANYWHERE precisely because it nests.",
      "depends": "csift classifies all three `harness.meta.hook` rather than `user.message`, so a hook's own prose is never counted as an operator turn in `search -t user` or in `verbatim`; the edit-retry marker is matched ANYWHERE in the content rather than as a prefix, precisely because it nests inside a stop-hook body.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "218-224",
          "snippet": "/// `harness.meta.hook` markers (GOLD §2, edge-fixtures G2) - hook-injected feedback, NOT the\n/// operator: a stop-hook feedback message, a `<local-command-caveat>` wrapper, or the\n/// edit-failed-retry notice CC injects when an Edit's target changed under it. (These are\n/// `isMeta` user records that would otherwise fall through to `user.message`.)\npub const STOP_HOOK_FEEDBACK_PREFIX: &str = \"Stop hook feedback:\";\n/// See [`STOP_HOOK_FEEDBACK_PREFIX`] - the `<local-command-caveat>…` hook wrapper.\npub const LOCAL_COMMAND_CAVEAT_PREFIX: &str = \"<local-command-caveat>\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "227",
          "snippet": "pub const EDIT_RETRY_MARKER: &str = \"The last Edit failed because the target file was modified\";"
        }
      ],
      "instrument": "`rg -c 'Stop hook feedback:' ~/.claude/projects/*/*.jsonl` then `csift search 'Stop hook feedback' -t harness.meta.hook -c`, and confirm `-t user.message -c` on the same pattern is 0. Counting rule: one per raw occurrence for rg, one per record matching any of the three markers for csift.",
      "located": {
        "claude_code": null,
        "csift": "0.4.0",
        "source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Stop hook feedback:|local-command-caveat|The last Edit failed because the target file was modified' | sort | uniq -c   AND   rg -o '.{0,60}was modified.{0,60}'   AND   for each of the three markers: csift search '<marker>' -t harness.meta.hook --raw | python3 (count type/isMeta/role and print content heads)   AND   csift search '<marker>' --count-by label",
          "observed": "Binary: `Stop hook feedback:` x3, `local-command-caveat` x2, and ZERO occurrences of `The last Edit failed` or `target file was modified`; the current freshness-rejection wording in 2.1.258 is instead `... was modified after it was handed to you, so Reads of it no longer count`. Corpus: the `Stop hook feedback:` shape gives 29 records, all (type=user, isMeta=True, role=user); `<local-command-caveat>` gives 155 records, all (user, True, user); the edit-retry sentence gives exactly 1 record, (user, True, user), and its content head is `Stop hook feedback:\\nThe last Edit failed because the target ...`. Neither the `Stop hook feedback:` nor the `<local-command-caveat>` census contains a `user.message` key. 191 records classify harness.meta.hook on a default scan corpus-wide.",
          "rule": "One record per marker match, restricted to records csift classifies harness.meta.hook, with (type, isMeta, role) tallied per record; the user.message check is the absence of that key from the same pattern's --count-by label output.",
          "note": "The nesting prediction is confirmed exactly - the one record carrying the edit-retry sentence has content starting with `Stop hook feedback:`. What does not survive is the attribution: 2.1.258 contains no such string, so this is hook text, not a CC-injected notice. Keeping the marker is still correct (it costs nothing and catches the nested form), but the code comment claiming CC injects it should be corrected. csift's current stale-read wording in that family is `was modified after it was handed to you, so Reads of it no longer count`."
        }
      ]
    },
    {
      "id": "CLS-017",
      "area": "classification",
      "behavior": "Claude Code writes an `isMeta` pseudo-record whose content is an image reference in one of TWO prefixes - `[Image: ` (with a leading `source: <src>` field when the source is known, giving `[Image: source: ...]`, plus optional original/displayed dimension text) and `[Image source: <src>]` (the form used when display dimensions are unknown). Both end with `]` and are single-line. The harness recognises either prefix and skips such records when deciding whether a record is a real user turn, so it is neither operator prose nor a modeled harness event.",
      "depends": "csift classifies it EMPTY (no label at all) so it can never be mislabeled `user.message`; an unmodeled record yields an empty label vector rather than a crash, and the image bytes themselves are reached through `csift image`, not this record.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "320-324",
          "snippet": "        // isMeta \"[Image: source:…]\" pseudo-record (G2): EXCLUDED - emit no label rather than\n        // mislabel it user.message.\n        if s.starts_with(IMAGE_SOURCE_PREFIX) {\n            return;\n        }"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "336-343",
          "snippet": "        // M2b ROOT FIX: a genuine `user.message` is NEVER isMeta. An isMeta record that matched\n        // no marker above is a harness-injected pseudo-turn (a generic cron/monitor tick, a\n        // novel hook wrapper), NOT the operator - emit NOTHING rather than mislabel it\n        // `user.message` (the role-level isMeta gate `is_genuine_user` already applies). Only\n        // genuine, non-isMeta unmarked prose is `user.message`.\n        if !self.is_meta.unwrap_or(false) {\n            push_unique(out, Class::UserMessage);\n        }"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "235-237",
          "snippet": "/// An `isMeta` `[Image: source:…]` pseudo-record (GOLD §2, edge-fixtures G2) - EXCLUDED from\n/// the taxonomy entirely (classify yields no label), so it is never mislabeled `user.message`.\npub const IMAGE_SOURCE_PREFIX: &str = \"[Image: source:\";"
        }
      ],
      "instrument": "`rg -c '\\[Image: source:' ~/.claude/projects/*/*.jsonl` then `csift search 'Image: source' --format json | jq -r '.label' | sort -u` must not contain `user.message`, and `--count-by label` must show no key for those records. Counting rule: one per record whose content starts with the prefix.",
      "located": {
        "claude_code": null,
        "csift": "0.4.0",
        "source": "SPEC.md section 5.1; AGENTS.md section 3.3a; src/model/markers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var J9t=\"\\[Image: \",wNe=\"\\[Image source: \"'   AND   rg -o '.{0,320}every\\(\\(r\\)=>r\\.type===\"text\"&&typeof r\\.text===\"string\"&&ZGr.{0,200}'   AND   csift search '\\[Image: source:' --raw | python3 (count records whose content STARTS with the prefix)   AND   csift search '^\\[Image' --raw | python3 (prefix + type/isMeta/role tally)",
          "observed": "Binary verbatim: `var J9t=\"[Image: \",wNe=\"[Image source: \"`. The builder returns `${wNe}${i}]` when display dimensions are unknown, else `${J9t}${c.join(\", \")}]` where the first element is `source: ${i}` - so BOTH `[Image: source: ...]` and `[Image source: ...]` are constructed. The harness's own recogniser is `function ZGr(e){return(e.startsWith(J9t)||e.startsWith(wNe))&&e.endsWith(\"]\")&&!e.includes(\"\\n\")}`, used by `JGr` inside the branch `if(o.isMeta===!0){if(JGr(o))continue;return!1}` of the routine that decides whether a record is a real user turn. Corpus: 0 records whose content starts with `[Image: source:`; the 349 records whose content starts with `[Image` are all (type=user, isMeta=False, role=user) and use the unrelated `[Image #N]` pasted-image placeholder inside genuine human prompts.",
          "rule": "One record per record whose reconstructed content STARTS with the prefix; the binary half is one count per distinct string in `strings -n 6` output plus the reading of the surrounding constructor and recogniser.",
          "note": "The isMeta half of the claim is now confirmed structurally rather than by example: the branch `if(o.isMeta===!0){if(JGr(o))continue;return!1}` proves CC writes these as isMeta records and excludes them from its own user-turn test, which is exactly why csift must not label them user.message. No instance has ever landed in this corpus, so the exclusion cannot be observed on disk - and csift's single prefix is narrower than the harness's own two-prefix recogniser."
        }
      ]
    },
    {
      "id": "CLS-018",
      "area": "classification",
      "behavior": "Claude Code emits a `redacted_thinking` block type whose content is opaque, and it is not a distinct semantic class from thinking: 2.1.258 lists it beside `thinking` in its block-type allowlist and tests the two with a single predicate `function Hpe(e){return e.type===\"thinking\"||e.type===\"redacted_thinking\"}`. No such block exists anywhere in this corpus, so the folding can only be confirmed against the binary and csift's own unit fixture, not against live data.",
      "depends": "csift folds `redacted_thinking` into `agent.thinking` and renders the literal `[redacted thinking]`; an unhandled block type would fall to the tolerant `Unknown` arm and vanish from every `-t agent.thinking` census.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "346",
          "snippet": "/// A `redacted_thinking` block - encrypted/opaque reasoning CC emits in place of a visible"
        },
        {
          "path": "src/search/types.rs",
          "lines": "17",
          "snippet": "pub(crate) const REDACTED_THINKING_PLACEHOLDER: &str = \"[redacted thinking]\";"
        }
      ],
      "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,110}redacted_thinking.{0,110}' | sort -u (the deciding instrument), plus a scoped rg for '\"type\":\"redacted_thinking\"' to record that the corpus has no instance.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 5.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,110}redacted_thinking.{0,110}' | sort -u   AND   cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -c --no-filename -F '\"type\":\"redacted_thinking\"' \"$BIG\" -g '*.jsonl'   AND   csift search 'redacted' -t agent.thinking --format json | python3 (count hits whose excerpt is exactly the placeholder)",
          "observed": "Binary 2.1.258 carries `redacted_thinking` 49 times, including a block-type allowlist reading `...redacted_thinking:!0,search_result:!0,server_tool_use:!0,text:!0,...,thinking:!0,...`, the predicate `function Hpe(e){return e.type===\"thinking\"||e.type===\"redacted_thinking\"}`, the opacity test `function Ope(e){if(e.type===\"redacted_thinking\")return!0;if(e.type===\"thinking\"&&\"signature\"in e...`, and a guard `e.message.content.every((n)=>n.type===\"thinking\"||n.type===\"redacted_thinking\")`. On disk, in the largest single project dir (2.0 GB, 5,624 jsonl files): 0 lines matching `\"type\":\"redacted_thinking\"`, 20 lines merely containing the word. csift found 139 agent.thinking hits for the pattern `redacted`, of which 0 had the excerpt `[redacted thinking]`.",
          "rule": "Binary: one count per string occurrence, plus reading of each surrounding predicate. Corpus: one count per LINE containing the exact block-type key/value pair, scoped to the largest project dir; the csift check counts hits whose rendered excerpt is byte-equal to the placeholder.",
          "note": "The binary settles the semantic half decisively - CC's own code treats thinking and redacted_thinking as one predicate. The claim's stated instrument cannot run here: zero blocks on disk. Note also that a real block is unreachable by the obvious search, because it renders as `[redacted thinking]` and the raw bytes say `redacted_thinking`; only a pattern that is a substring of BOTH (e.g. `redacted`) survives the prefilter and matches the render."
        }
      ]
    },
    {
      "id": "CLS-019",
      "area": "classification",
      "behavior": "A hook's injected context lands as a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}`, where `content` is a string ARRAY in real data (one element per injected block), not a bare string.",
      "depends": "`Record::hook_additional_context_text` joins the array with a newline (tolerating a bare string) and classifies the record `harness.meta.hook`; `search --additional-context` is the only default-off gate that reaches it, so a bare-string-only reader would return nothing and make every hook-injected context invisible to search.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "336-344",
          "snippet": "    pub fn hook_additional_context_text(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }\n        let v = self.attachment_value()?;\n        let att = v.as_object()?;\n        if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"hook_additional_context\") {\n            return None;\n        }"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "350-351",
          "snippet": "    static HOOK_CONTEXT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memmem::Finder::new(b\"hook_additional_context\"));"
        }
      ],
      "instrument": "`csift search '<a phrase your hook injects>' @<session> --additional-context` must hit while the same query without the flag returns a definitive absence; then confirm the array shape with `rg -oNI '\"type\":\"hook_additional_context\",\"content\":\\[' -g '*.jsonl' | head`. Counting rule: matched records.",
      "located": {
        "claude_code": null,
        "csift": "0.7.6",
        "source": "src/model/predicates.rs comment; AGENTS.md section 3.2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -oNI --no-filename -F '\"type\":\"hook_additional_context\",\"content\":[' \"$BIG\" -g '*.jsonl' | wc -l   AND the same with '\"content\":\"'   AND   csift search '<stamp at=' -c   versus   csift search '<stamp at=' --additional-context -c   AND   csift search '<stamp at=' --additional-context --count-by label",
          "observed": "In the largest single project dir (2.0 GB, 5,624 jsonl files): 25,666 lines carry `\"type\":\"hook_additional_context\",\"content\":[` (ARRAY) and 0 lines carry `\"type\":\"hook_additional_context\",\"content\":\"` (bare string). Corpus-wide the type has 88,600 attachment records. Gate: a hook-injected phrase returns 28 exchanges on a default scan (all prose mentions) versus 1,394 exchanges with --additional-context, and those hits key 1,612 records under harness.meta.hook.",
          "rule": "One count per raw LINE matching the literal key/value/open-bracket triple, scoped to one project dir; the gate test counts exchanges from `-c` with and without the flag on the identical pattern.",
          "note": "The array shape is unanimous - 25,666 to 0 in a 2.0 GB scope. The default-off gate is real and measurable: the same pattern goes from 28 to 1,394 exchanges when --additional-context is passed, so a bare-string-only reader would indeed make every hook-injected context invisible."
        }
      ]
    },
    {
      "id": "CLS-020",
      "area": "classification",
      "behavior": "Claude Code marks a `type:\"attachment\"` record's payload with its own `type` field, and 34 distinct payload types were observed corpus-wide, dominated by `hook_success` (338,770), `hook_additional_context` (88,600), `deferred_tools_delta` (8,167), `skill_listing` (8,060), `task_reminder` (7,308), `total_tokens_reminder` (6,220), `queued_command` (2,356), `edited_text_file` (1,334), `batching_reminder_sent` (948) and `hook_cancelled` (770), down to `plan_mode` (21) and the two rarest, `opened_file_in_ide` and `plan_mode_reentry` (2 each).",
      "depends": "csift exposes the payload type as the `--count-by attachment` census key and classifies any non-hook payload `harness.meta.attachment`, with all attachment parsing gated behind `--attachments` / `--additional-context`; without the gate a default scan would parse megabyte attachment lines and blow the performance contract.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "306-309",
          "snippet": "    pub fn attachment_type(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "356-357",
          "snippet": "    static ATTACHMENT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"attachment\\\"\"));"
        }
      ],
      "instrument": "`csift search '' <project-dir> --count-by attachment` (the axis implies the `--attachments` gate), cross-checked with `rg -oNI '\"attachment\":\\{\"type\":\"[a-z_]+\"' -g '*.jsonl' | sort | uniq -c`. Counting rule: one per attachment RECORD keyed by `attachment.type` for csift, one per raw match for rg.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.8.1",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' --count-by attachment   (whole corpus: 5.1 GB, 7,748 jsonl files, 72 s wall)   cross-checked with   cd ~/.claude/projects && BIG=$(du -sk ./*/ | sort -rn | head -1 | cut -f2-); rg -oNI --no-filename '\"attachment\":\\{\"type\":\"[a-z_]+\"' \"$BIG\" -g '*.jsonl' | sed 's/.*\"type\":\"//;s/\"//' | sort | uniq -c | sort -rn   AND   csift search 'batching_reminder_sent' -c  versus  csift search 'batching_reminder_sent' --attachments --count-by label",
          "observed": "34 distinct payload types corpus-wide (the claim's count, unchanged). Fresh counts: hook_success 338,770 - hook_additional_context 88,600 - deferred_tools_delta 8,167 - skill_listing 8,060 - task_reminder 7,308 - total_tokens_reminder 6,220 - queued_command 2,356 - edited_text_file 1,334 - batching_reminder_sent 948 - hook_cancelled 770 - ultrathink_effort 594 - file 577 - compact_file_reference 551 - bash_output_audience_note 422 - date_change 394 - agent_listing_delta 226 - read_truncation_notice 164 - plan_file_reference 151 - invoked_skills 134 - workflow_keyword_request 115 - silent_turn_reminder 95 - hook_non_blocking_error 93 - remote_session_change 55 - command_permissions 48 - hook_blocking_error 30 - plan_mode_exit 23 - diagnostics 21 - plan_mode 21 - task_status 16 - auto_mode 13 - hook_system_message 5 - hook_stopped_continuation 3 - opened_file_in_ide 2 - plan_mode_reentry 2. The rg cross-check in one project dir reproduces the same ordering (hook_success 81,841, hook_additional_context 25,666, deferred_tools_delta 5,749, skill_listing 5,628, task_reminder 2,107, ...). Gate: `batching_reminder_sent` returns 8 exchanges on a default scan and 954 records under harness.meta.attachment with --attachments.",
          "rule": "One count per attachment RECORD keyed by attachment.type for csift; one count per raw match of the literal key/type pair for rg. The corpus is live and monotone-growing, so every figure is a snapshot with a timestamp, not a constant.",
          "note": "The distinct-key count 34 reproduces exactly and every named type is also a string in the 2.1.258 binary. The per-type counts are all higher than the ledger's by 0.2 to 13 percent except hook_cancelled (770) and plan_mode (21), which are byte-identical - consistent with a few hours of corpus growth on a live machine. The claim's head-of-list also skipped batching_reminder_sent (948), which outranks hook_cancelled; the corrected wording restores it. Any figure in this row needs its snapshot date attached to stay checkable."
        }
      ]
    },
    {
      "id": "CLS-021",
      "area": "classification",
      "behavior": "Claude Code 2.1.258 writes `type:\"attachment\"` records for 33+ payload kinds whose content is arbitrary nested JSON, up to at least 1699759 bytes (1.7 MB, kind `queued_command`), and they outnumber role-bearing message lines about 2.4:1 (59206 vs 25080 in one project dir). The 337988 hook_success figure is a whole-corpus count and was not re-derived here (scope rule); the rescoped equivalent is 45370 hook_success records in one project dir via `csift search '' . --count-by attachment`.",
      "depends": "csift gates the attachment keep behind an `&&`-guarded SIMD needle, so no query that omits `--attachments` / `--additional-context` / `--count-by attachment` ever runs the attachment memmem, and the matched text is the VERBATIM payload JSON - a byte substring of the raw line - so the literal prefilter and the whole-file gate stay sound with no synthesized-marker machinery. The gate is not, however, the only way an attachment line becomes a candidate: under a match-all (no `-t`) scan the D7 `compact_boundary` needle is active, and because that needle is a bare byte substring it also admits any attachment line whose payload prose happens to contain it - measured at 82 of 59206 attachment records (0.14%) in one project dir, surfacing as 64 harness.meta.attachment plus 18 harness.meta.hook.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "322-327",
          "snippet": "    pub fn attachment_payload_text(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }\n        self.attachment.as_ref().map(|raw| raw.get().to_string())\n    }"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "393",
          "snippet": "        || (needs_attachments && ATTACHMENT_FINDER.find(line).is_some())"
        }
      ],
      "instrument": "Functional: `csift search 'hook_success' . -c` (47) vs `... --attachments` (818). Cost: hyperfine --warmup 2 --runs 5 on the same pair; the gated form must be strictly cheaper (measured 253.4 ms / 442 ms user CPU vs 387.5 ms / 872 ms user CPU). Counting rule: wall seconds and user CPU seconds, and matched-exchange count for the functional half.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.8.1",
        "source": "src/search/scan.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,25}type:\"attachment\".{0,60}' | sort -u   ||   enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; count type==attachment, count records with a dict message{}, max len(json.dumps(attachment))'   ||   csift search '' . --count-by attachment   ||   csift search 'hook_success' . -c   vs   csift search 'hook_success' . -c --attachments   ||   hyperfine --warmup 2 --runs 5 \"csift search 'hook_success' . -c\" \"csift search 'hook_success' . -c --attachments\"",
          "observed": "CC 2.1.258 binary carries three distinct attachment-record writers, e.g. 'function hBn(e){return{type:\"attachment\",attachment:e,uuid:RHo(),timestamp:new Date().toISOString()}}' and '{return{attachment:e,type:\"attachment\",uuid:n.uuid(),timestamp:(e.type===\"queued_command\"||e.type=...'. One project dir under ~/.claude/projects (23 top-level .jsonl, 427 MB): 59206 attachment records vs 25080 lines carrying a role marker -> attachments outnumber message lines 2.36x. `--count-by attachment` over that dir reports 33+ distinct payload kinds, top: hook_success 45370, hook_additional_context 9909, total_tokens_reminder 2549, task_reminder 630, skill_listing 536, deferred_tools_delta 452. Largest attachment payload in that dir: 449848 bytes (kind queued_command); in a second, larger project dir (2.1 GB, 104822 attachment records): 1699759 bytes (kind queued_command) -> megabyte payloads confirmed. Gate is functional: `csift search 'hook_success' . -c` = 47, `... --attachments` = 818. hyperfine: gated 253.4 ms +/- 77.0 (user CPU 442 ms) vs --attachments 387.5 ms +/- 193.0 (user CPU 872 ms) -> the opt-in roughly doubles CPU; the gated form pays none of it. Verbatim-payload check on a synthetic transcript: with --attachments the matched/rendered text is '{\"type\": \"task_reminder\", \"content\": [{\"id\": \"1\", \"subject\": \"alpha reminder\"}]}', a byte substring of the raw line (which was written with whitespace-separated JSON, so the match is serialization-tolerant too); without the flag the same literal returns 0. LEAK MEASURED: a default no-`-t` scan of that project dir still labels 64 records harness.meta.attachment. Cause isolated: under match-all the D7 compact_boundary needle is ON, and it is a bare byte substring that occurs inside attachment payload prose. Exactly 82 attachment records in that dir contain b'compact_boundary'; splitting by payload kind gives 64 'other' (-> harness.meta.attachment, == the census figure) and 18 hook_additional_context (-> harness.meta.hook; 18 + 49 isMeta role records = the census's 67). Proven on a 4-line synthetic transcript: an attachment whose payload prose says 'document the compact_boundary record' IS admitted under no `-t` (1 harness.meta.attachment); siblings saying 'turn_duration' or nothing are NOT; under `-t user` none are.",
          "rule": "Attachment / message ratio: count lines in the 23 top-level .jsonl of ONE project dir where json.loads(line)['type']=='attachment', versus lines whose raw bytes match /\"role\"\\s*:\\s*\"(user|assistant)\"/ (csift's own stage-1 needle). Payload size: len(json.dumps(record['attachment'])) per attachment record, take the max. Payload-kind census: one record per attachment line, keyed by attachment.type (csift --count-by attachment). Timing: hyperfine wall-clock mean over 5 runs after 2 warmups, same literal, same scope, flag toggled. Leak: records whose emitted label set contains harness.meta.attachment in a flagless `csift search '' . --count-by label`, one record per line.",
          "note": "Behavior and the perf half of the dependency both hold, with numbers corrected: attachment payloads do reach megabyte scale (1.7 MB observed) but not in every project dir (max 0.45 MB in the one first measured), and the attachment/message ratio is 2.4:1, not an order of magnitude. The one substantive correction is that 'a default scan pays zero' is true of the attachment needle only. A flagless scan still parses a small fraction of attachment lines because the always-on-under-match-all compact_boundary needle is a bare value substring; this is a permissive-superset artifact (the extra records classify correctly, nothing is mislabeled), and it is invisible outside a corpus whose prose discusses transcript internals."
        }
      ]
    },
    {
      "id": "CLS-022",
      "area": "classification",
      "behavior": "The word `attachment` appears verbatim as an attachment record's `type` VALUE and as its payload key, whereas the same word inside message prose is always inside a JSON string where an adjacent quote is escaped to a backslash-quote pair.",
      "depends": "csift's attachment needle is the QUOTED form `\"attachment\"`, which is serialization-tolerant and never false-keeps on prose mentioning the word; a bare-word needle would defeat the prefilter on every csift development transcript.",
      "code": [
        {
          "path": "src/search/scan.rs",
          "lines": "352-355",
          "snippet": "    // Quoted needle: `\"attachment\"` appears verbatim as the record's `\"type\"` VALUE (and as\n    // its payload KEY); an in-content quote is escaped to `\\\"` in raw bytes, so prose that\n    // merely mentions the word never false-keeps. Serialization-tolerant (the quoted value\n    // survives a reserialize; R13)."
        }
      ],
      "instrument": "On one real transcript, count role/message lines that contain the bare word `attachment` and, of those, how many also contain the unescaped `\"attachment\"`. The second number must be 0 (measured: 587 and 0). Counting rule: one per matching LINE, restricted to records whose message.role is user or assistant.",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "src/search/scan.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); f=$(ls -S ~/.claude/projects/$enc/*.jsonl | head -1); rg -cNI '\"attachment\"' $f; rg -cNI 'attachment' $f; then python3 -c 'for each line of $f: r=json.loads(line); isrole = isinstance(r.get(\"message\"),dict) and r[\"message\"][\"role\"] in (user,assistant); tally bare = \"attachment\" in line, quoted = chr(34)+\"attachment\"+chr(34) in line'",
          "observed": "One transcript, 46827 lines. rg -cNI '\"attachment\"' = 27161; rg -cNI 'attachment' = 27818; difference 657. Decomposed: 587 role/message lines contain the bare word `attachment` and ZERO of them contain the unescaped quoted form; the remaining 70 are non-role lines that mention it without the quoted form. Line-type census of the same file: attachment 27164, assistant 6851, user 3521, plus metadata types. Escaped form: rg -cNI '\\\\\"attachment\\\\\"' returns no lines, i.e. in-content quotes are present only as the escaped pair and never as the needle bytes.",
          "rule": "Per LINE of one transcript. bare = the raw line contains the 10 bytes `attachment`; quoted = the raw line contains the 12 bytes `\"attachment\"`. A false-keep is a line that is bare-true, quoted-true, and whose record is a role-bearing message (message.role in {user, assistant}) - i.e. the quoted needle fired on prose. Count those.",
          "note": "The mechanism holds exactly as stated - the word appears unescaped only as a JSON type value or key, and an in-content quote is always the escaped pair - and the false-keep rate on prose is 0 of 587 mentions in a transcript from a corpus that discusses transcript internals constantly. Only the claim's own instrument needed replacing: a bare line-count comparison is dominated by the attachment records themselves and cannot see the property being asserted."
        }
      ]
    },
    {
      "id": "CLS-023",
      "area": "classification",
      "behavior": "Claude Code's `type:\"system\"` records carry no `message{}` and therefore no role marker, so they are invisible to a role-based byte prefilter, and the five promoted line types are rare relative to message lines.",
      "depends": "csift admits each promoted line only when an EXPLICIT `-t` selector reaches its leaf (full path, glob, or the `harness.meta` prefix) or a `show` address is present - stricter than the compaction-boundary keep, which a match-all scan admits - so a bare scan, a bare role selector, a `-T`-only filter and a flagless `--count-by label` never parse them; a zero-match run with no reaching selector emits a stderr note and JSON `gated_leaves_unreached`.",
      "code": [
        {
          "path": "src/search/scan.rs",
          "lines": "95-100",
          "snippet": "    let reach = |c: Class| args.reaches_gated(c) || address.is_some();\n    let gates = CandidateGates {\n        compact_boundary: needs_compact_boundary,\n        hook_context: needs_hook_context,\n        attachments: needs_attachments,\n        queued: reach(Class::UserQueued),"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "395-399",
          "snippet": "        || (gates.queued && QUEUED_FINDER.find(line).is_some())\n        || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n        || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n        || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n        || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())"
        }
      ],
      "instrument": "`csift search '' <project-dir> --count-by label` (no `-t`) must show no promoted leaf, while `csift search '' <project-dir> -t harness.meta --count-by label` must show them. Counting rule: one record per line admitted by the gate.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/search/scan.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,45}\"turn_duration\".{0,45}' | sort -u   ||   enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; for type==system tally subtype, whether the record has a message key, whether the raw line contains a \"role\" token'   ||   csift search '' . --count-by label   vs   csift search '' . -t harness.meta --count-by label   vs   csift search '' . -t user.queued --count-by label   vs   csift search '' . -T user --count-by label   vs   csift search '' . -t harness --count-by label   ||   csift --claude-home $FX search 'nomatchhere' @-fx --format json",
          "observed": "CC 2.1.258 constructors, all message-less: 'B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLi...'; '{return{type:\"system\",subtype:\"stop_hook_summary\",hookCount:e,hookInfos:n,hookErrors:r,ho...'; 'function DXn(e){return{type:\"system\",subtype:\"away_summary\",content:e,timestamp:new Date().toISOString(),uuid:...}'; 'o(Br,br,Ts){let qi={type:\"queue-operation\",operation:Br,timestamp:new Date().toISOString(),sessionId:Q(),...'. Corpus, 23 top-level .jsonl of one project dir: 863 system records (stop_hook_summary 410, turn_duration 284, away_summary 147, compact_boundary 17, agents_killed 3, model_refusal_fallback 1, scheduled_task_fire 1); 0 carry a `message` key; 0 contain a `\"role\"` token, so the role prefilter cannot see any of them. Rarity: the five promoted types total 2496 lines (turn_duration 284 + away_summary 147 + stop_hook_summary 410 + queue-operation 1083 + file-history-* 572) against 25080 role-bearing lines = 9.95%. Gate: flagless `--count-by label` emits 23 leaf keys and NONE of the five promoted ones. `-t harness.meta` emits harness.meta.snapshot 572, harness.meta.stop-hooks 410, harness.meta.turn-duration 284, harness.meta.away-summary 147 - each EXACTLY equal to the independent rg line count for the corresponding raw type/subtype. `-t user.queued` = 60 (of 1083 queue-operation lines; the rest are content-less or harness riders). `-T user` (exclude-only) emits none of the five. `-t harness` (bare role) also emits none of the five, consistent with Class::llm_visible gating role expansion. Zero-match diagnosis on a synthetic transcript: stderr prints 'csift: note: the gated leaves (user.queued, harness.meta.turn-duration, harness.meta.away-summary, harness.meta.stop-hooks, harness.meta.snapshot) are scanned only under an explicit -t that reaches them - this absence does not cover those lines.' and the JSON summary carries \"gated_leaves_unreached\": true; the same query with `-t harness.meta` carries \"gated_leaves_unreached\": false.",
          "rule": "message-less claim: of all lines in one project dir's top-level .jsonl whose parsed type is `system`, count those with a `message` key (expect 0) and those whose raw bytes match /\"role\"\\s*:\\s*\"(user|assistant)\"/ (expect 0). Rarity: sum the five promoted raw line counts via rg on `\"type\":\\s*\"...\"` / `\"subtype\":\\s*\"...\"`, divide by the role-marker line count. Gate: one record per line admitted; compare the leaf keys emitted by `--count-by label` with and without each selector, and cross-check each promoted leaf's count against the independent rg count of its raw line type.",
          "note": "Both halves confirmed by independent instruments. The message-less property is visible in CC 2.1.258's own record constructors (no `message` field in any of the four) and in the corpus (0 of 863 system records carry one, 0 carry a role token). The admission rule is exactly as claimed and stricter than the boundary keep: flagless, bare-role and exclude-only selectors all fail to reach the five leaves, while `harness.meta` and full leaf paths reach them and reproduce the raw line counts to the record. The unreached-leaf disclosure fires as described in both text and JSON."
        }
      ]
    },
    {
      "id": "CLS-024",
      "area": "classification",
      "behavior": "Claude Code's promoted non-record lines carry no text spelling out their rendered facts: a `turn_duration` record is {durationMs, messageCount} integers plus ids, `stop_hook_summary` is {hookCount, hookInfos, hookErrors, preventedContinuation} and `file-history-*` are paths and version numbers - 0 of 284 turn_duration, 0 of 410 stop_hook_summary and 0 of 139 file-history-delta records contain the bracketed strings csift renders. csift fabricates FOUR such forms, not three: `[turn duration: ...]`, `[stop hooks: ...]`, `[file-history delta at ...]` and `[file-history snapshot at ...]`, the last two both under the single `harness.meta.snapshot` marker `file-history-`.",
      "depends": "csift registers each line's own type/subtype bytes (`turn_duration`, `stop_hook_summary`, `file-history-`) as VERIFIABLE synthesized-text markers, active only under the explicit selector that admits the line; without a marker the whole-file prefilter gate would silently drop matches against those fabricated excerpts. The queued and away-summary leaves render VERBATIM content and need no marker.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "502-505",
          "snippet": "    // v0.10.0 promoted lines whose render FABRICATES text (key=value excerpts): the\n    // marker is the line's own type/subtype value, active only when the explicit\n    // selector admits the line (otherwise it is not a candidate at all). The queued\n    // and away-summary leaves render VERBATIM content and need no marker."
        }
      ],
      "instrument": "`csift search 'turn duration' . -t harness.meta.turn-duration -c` must return hits (measured 270 exchanges over 284 records) while a per-RECORD probe shows 0 of 284 turn_duration records contain the phrase in their raw bytes. Counting rule: exchanges for csift, records for the probe. Do NOT use a whole-file `rg -c 'turn duration'` as the negative side - it returns 56 on a transcript whose prose discusses the feature.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/search/matcher.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; rg -NI --no-filename -c 'turn duration' $d/*.jsonl | paste -sd+ - | bc   ||   python3 -c 'json-parse every line of $d/*.jsonl; bucket by (type, subtype); per bucket count records whose RAW line contains the rendered phrase'   ||   csift search 'turn duration' . -t harness.meta.turn-duration -c   ||   csift search '' . -t harness.meta.turn-duration --max-count 2   ||   csift search 'file-history delta' . -t harness.meta.snapshot --max-count 2   ||   grep -rn 'file-history snapshot at\\|file-history delta\\|turn duration:\\|stop hooks:' src/",
          "observed": "Per-record probe over one project dir's top-level .jsonl: turn_duration 284 records, 0 whose raw line contains 'turn duration' (with the space); stop_hook_summary 410 records, 0 containing 'stop hooks'; file-history-delta 139 records, 0 containing 'file-history delta at'. A raw turn_duration record is fields only: {type, subtype, durationMs: 446815, messageCount: 105, timestamp, uuid, isMeta, userType, entrypoint, cwd, sessionId, version, gitBranch}. csift renders the fabricated excerpts: '[turn duration: 10m 29s . durationMs=629054 messageCount=266]', '[stop hooks: count=6 errors=0 prevented=false]', '[file-history delta at 2026-07-14T03:00:27.193Z: <path>@v1]', '[file-history snapshot at <ts>: <path>@v2, ...]'. `csift search 'turn duration' . -t harness.meta.turn-duration -c` = 270 (exchanges; several turn_duration records share a turn, so 270 exchanges cover 284 records). The by-file rg, however, returns 56 - the phrase occurs as ordinary prose in a corpus that documents the feature. away_summary renders VERBATIM record content (observed: the summary sentence itself, no bracket), consistent with it needing no marker.",
          "rule": "Restrict to the promoted records themselves, not the file: of the N records whose parsed (type, subtype) is the promoted kind, count those whose RAW line contains the rendered phrase - expect 0/N (measured 0/284, 0/410, 0/139). Then count csift hits for that phrase under the leaf's explicit selector - expect > 0 (measured 270 exchanges). A whole-file rg is not a valid denominator: prose about the feature also matches.",
          "note": "The behavior holds at record granularity, which is the granularity the synthesized-marker machinery actually needs: none of the three fabricated-text kinds can be found by matching the raw line, so without a registered marker the whole-file prefilter gate would drop those matches. Two corrections: the claim's negative instrument is stated at file granularity and fails there (56 prose lines match), and the snapshot render `[file-history snapshot at ...]` is a fourth fabricated form the claim omits, covered by the same `file-history-` marker."
        }
      ]
    },
    {
      "id": "CLS-025",
      "area": "classification",
      "behavior": "`file-history-snapshot` and `file-history-delta` lines carry no `message` object - the boundary instrument for LLM visibility - so they are machinery records rather than conversation records.",
      "depends": "csift maps BOTH line types to the single gated leaf `harness.meta.snapshot` with `Class::llm_visible` false, and its byte prefilter admits them only under an explicit `-t` selector reaching the leaf or a `show` address, so a bare scan or a `-T`-only filter never parses them and the performance contract holds.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "27",
          "snippet": "\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "327",
          "snippet": "/// v0.10.0: `file-history-snapshot` + `file-history-delta` lines."
        }
      ],
      "instrument": "`csift search '' <project-token> -t harness.meta.snapshot --count-by label` versus the same query without `-t`: the leaf must appear only under the explicit selector. Counting rule: one record per line admitted by the gate.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,30}\"file-history-snapshot\".{0,50}' | sort -u; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'file-history-(snapshot|delta)[^ ]{0,40}' | sort -u   ||   enc=$(pwd | sed 's/[^A-Za-z0-9]/-/g'); d=~/.claude/projects/$enc; python3 -c 'json-parse every line of $d/*.jsonl; for type in file-history-snapshot, file-history-delta count records and how many carry a message key'   ||   csift search '' . -t harness.meta.snapshot --count-by label   vs   csift search '' . --count-by label | rg -c 'harness.meta.snapshot'   ||   csift --claude-home $FX search '' @-fx -t harness.meta.snapshot --count-by label   vs   csift --claude-home $FX search '' @-fx --count-by label",
          "observed": "CC 2.1.258 writers, both message-less: 'ckWrite(async()=>{let d={type:\"file-history-snapshot\",messageId:e,snapshot:n,isSnapshotUpdate:r};await ' and 'file-history-delta\",messageId:e,snapshotMessageId:n,tracki...'. Corpus, 23 top-level .jsonl of one project dir: 433 file-history-snapshot + 139 file-history-delta = 572 records, 0 carrying a `message` key. Observed raw shapes: a snapshot line is {type, messageId, snapshot, isSnapshotUpdate} - it has no top-level `timestamp` and no `uuid` either; a delta line is {type, messageId, snapshotMessageId, trackingPath, backup, timestamp}. Single-leaf mapping and gate, live: `-t harness.meta.snapshot --count-by label` = 572 (== 433 + 139, the two types folded into one key), while a flagless `--count-by label` emits no harness.meta.snapshot key at all (grep count 0). Re-run minutes later on the still-growing corpus returned 575, same rule. Controlled fixture with exactly one snapshot line and one delta line: `-t harness.meta.snapshot --count-by label` = 2 under the one key; flagless `--count-by label` emits only agent.message 1 and user.message 1.",
          "rule": "message-less: of the lines in one project dir's top-level .jsonl whose parsed type is file-history-snapshot or file-history-delta, count those with a `message` key (expect 0 of 572). Single leaf: the count under `-t harness.meta.snapshot` must equal the sum of the two independent rg line counts. Gate: one record per line admitted; the leaf key must be absent from a flagless `--count-by label` and present under the explicit selector. Corpus counts drift upward on a live corpus - the rule, not the number, is the invariant.",
          "note": "Confirmed from both ends. CC 2.1.258's own writers for the two line types construct objects with no `message` field, and no such record in the sampled project dir carries one (0 of 572) - so the LLM-visibility boundary instrument the claim names is genuinely absent, not merely unobserved. The single-leaf mapping is exact: 433 snapshots plus 139 deltas fold to one key of 572, reproduced on a two-line synthetic fixture as a key of 2. The gate behaves as claimed - the leaf is unreachable without an explicit selector that names it."
        }
      ]
    },
    {
      "id": "CMP-001",
      "area": "compaction",
      "behavior": "A compaction SUMMARY is a `type:\"user\"` record carrying `isCompactSummary:true` co-set with `isVisibleInTranscriptOnly:true`, whose `message.content` is a bare STRING that commonly opens `This session is being continued from a previous conversation...`; Claude Code writes no `type:\"summary\"` record at all.",
      "depends": "csift keys compaction detection on the `isCompactSummary` FLAG (never on text), excludes the summary from `is_genuine_user` so it never opens a turn, labels it `harness.compaction.summary`, and counts it as the `compactions` figure in `stats`; `verbatim` uses it as the anchor for the turns it clipped. Keying on a `summary` record type finds nothing in current data, and treating the summary as a human turn injects a machine recap into the operator census.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "66-68",
          "snippet": "    /// Compaction summary marker - when true, this user record is NOT a human turn.\n    #[serde(default, rename = \"isCompactSummary\")]\n    pub is_compact_summary: Option<bool>,"
        },
        {
          "path": "src/model/record.rs",
          "lines": "77-79",
          "snippet": "    /// Co-set with `isCompactSummary` on compaction-summary records (§4.7).\n    #[serde(default, rename = \"isVisibleInTranscriptOnly\")]\n    pub is_visible_in_transcript_only: Option<bool>,"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "41-43",
          "snippet": "        if self.is_compact_summary.unwrap_or(false) {\n            return false;\n        }"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "238-241",
          "snippet": "        if self.is_compact_summary.unwrap_or(false) {\n            push_unique(out, Class::CompactionSummary);\n            return;\n        }"
        }
      ],
      "instrument": "`jq -r 'select(.isCompactSummary==true) | [.type, .isVisibleInTranscriptOnly] | @tsv' <transcript>` must print `user true` on every hit, and `rg -c '\"type\":\"summary\"' ~/.claude/projects -g '*.jsonl'` must return nothing; cross-check `csift search '' @<session> -t harness.compaction.summary -c` against `csift stats @<session> --format json | jq .compactions`. Counting rule: one summary record per compaction event, counted per raw line.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.1.0",
        "source": "SPEC.md sections 3.1 and 4.7; SPEC.md section 5; AGENTS.md section 3.5; src/model.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|[.type,(has(\"isVisibleInTranscriptOnly\")|tostring),(.isVisibleInTranscriptOnly|tostring),(.message.content|type)]|@tsv' | sort | uniq -c ; rg -l '\"type\":\"summary\"' -g '*.jsonl' . ; rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|.message.content[0:70]' | sort | uniq -c ; csift search '' @<session> -t harness.compaction.summary -c --no-subagents ; csift stats @<session> --no-subagents --format json | jq -c '{turns,compactions}' ; csift search '' @<session> -t harness.compaction.summary --format json --no-subagents --max-count 1 | jq -c 'select(.kind==\"exchange\")|{turn_index,labels:[.hits[].labels[]],line:[.hits[].line]}'",
          "observed": "232 of 232 parsed summary records: type=user, isVisibleInTranscriptOnly=true, message.content type=string. 232/232 open with the same 70 characters, 'This session is being continued from a previous conversation that ran'. rg -l '\"type\":\"summary\"' over the whole corpus exits 1 with no file listed. On one session csift reports 5 harness.compaction.summary records and stats reports compactions=5, turns=53; the summary at L4807 carries the single label harness.compaction.summary and sits INSIDE turn 15 (a member, not an opener).",
          "rule": "One row per raw jsonl line whose parsed isCompactSummary is boolean true, corpus-wide over ~/.claude/projects (5.1 GB, 7,753 jsonl files); csift counts one record per matched line under --no-subagents.",
          "note": "Verified verbatim in the current tree at the claimed line numbers: src/model/record.rs:65-67 and :76-78, src/model/predicates.rs:41-43, src/model/classify.rs:238-241 (inside classify_user). Nothing moved."
        }
      ]
    },
    {
      "id": "CMP-002",
      "area": "compaction",
      "behavior": "Beside the summary Claude Code writes a separate `type:\"system\"` record with `subtype:\"compact_boundary\"`, one per compaction event, carrying a top-level `content:\"Conversation compacted...\"` string plus `compactMetadata:{trigger:\"auto\"|\"manual\", preTokens, postTokens, durationMs, cumulativeDroppedTokens, preCompactDiscoveredTools[], preservedSegment:{headUuid,anchorUuid,tailUuid}, preservedMessages:{anchorUuid,uuids[],allUuids[]}}`. `cumulativeDroppedTokens` is the newest scalar (absent up to CC 2.1.191, present on every boundary from 2.1.199 through 2.1.258); `preCompactDiscoveredTools` and the `preservedMessages.allUuids` array are each absent on a handful of older boundaries; a short-lived `precomputed` key appeared on 6 boundaries around CC 2.1.191-2.1.202 and on none since.",
      "depends": "csift identifies the boundary purely by `type` + `subtype`, keeps `compactMetadata` UNPARSED, and renders the four scalar keys as `[compaction boundary: trigger=... preTokens=... postTokens=... durationMs=...]`, which is what makes `-t harness.compaction.boundary` both matchable and inspectable; the extra prefilter keep for this role-marker-less line is `-t`-gated behind `needs_compact_boundary`, so every other query pays zero.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "97-102",
          "snippet": "    /// `compact_boundary` metrics (§3.5 / D7): `{trigger, preTokens, postTokens, durationMs}` on a\n    /// `type:\"system\"`/`subtype:\"compact_boundary\"` record. Kept RAW; `search` renders it as a\n    /// readable excerpt (`record_raw_text`) so `-t harness.compaction.boundary` can enumerate\n    /// compaction points and inspect what each clipped. Absent on every other record. Tolerant.\n    #[serde(default, rename = \"compactMetadata\")]\n    pub compact_metadata: Option<serde_json::Value>,\n"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "7-12",
          "snippet": "    /// True when this is the `system`/`compact_boundary` metrics record (GOLD §5) -\n    /// `harness.compaction.boundary`.\n    #[must_use]\n    pub fn is_compact_boundary(&self) -> bool {\n        self.is_type(\"system\") && self.subtype.as_deref() == Some(\"compact_boundary\")\n    }"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "311-319",
          "snippet": "/// Render a `compact_boundary` record's `compactMetadata` object as a one-line readable excerpt -\n/// `[compaction boundary: trigger=auto preTokens=1000 postTokens=200 durationMs=50]` (only the\n/// present fields, stable order, scalars unquoted). `None` when it is not an object or carries none\n/// of the known fields.\npub(crate) fn compact_metadata_excerpt(meta: &serde_json::Value) -> Option<String> {\n    let obj = meta.as_object()?;\n    let mut fields: Vec<String> = Vec::new();\n    for key in [\"trigger\", \"preTokens\", \"postTokens\", \"durationMs\"] {\n        if let Some(v) = obj.get(key) {"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "386",
          "snippet": "        || (needs_compact_boundary && COMPACT_BOUNDARY_FINDER.find(line).is_some())"
        }
      ],
      "instrument": "`jq -r 'select(.subtype==\"compact_boundary\") | (.compactMetadata|keys|join(\",\"))' <transcript> | sort | uniq -c` enumerates the metadata keys actually present; `csift search '' @<session> -t harness.compaction.boundary` must print a `[compaction boundary: trigger=... preTokens=...]` excerpt per hit. Counting rule: one boundary record per compaction event; the boundary count must track the summary count on the same file.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.2.0",
        "source": "SPEC.md section 4.7; AGENTS.md section 3.5; CHANGELOG 0.8.1; src/model/record.rs compactMetadata comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|(.compactMetadata|keys|join(\",\"))' | sort | uniq -c ; same stream piped through jq for .compactMetadata.trigger, for the preservedSegment/preservedMessages key sets, for .content[0:40], and for [(.version),(.compactMetadata|has(\"cumulativeDroppedTokens\")),(.compactMetadata|has(\"precomputed\"))] ; per-file pairing script counting boundaries vs summaries ; csift search '' @<session> -t harness.compaction.boundary --no-subagents --max-count 2",
          "observed": "232 boundary records corpus-wide. Key sets: 130 carry cumulativeDroppedTokens,durationMs,postTokens,preCompactDiscoveredTools,preTokens,preservedMessages,preservedSegment,trigger; 91 the same minus cumulativeDroppedTokens; 4 minus preCompactDiscoveredTools; 6 also carry precomputed. trigger: auto 217 / manual 15. preservedSegment keys anchorUuid,headUuid,tailUuid on 232/232. preservedMessages keys allUuids,anchorUuid,uuids on 231/232 (one legacy record has only anchorUuid,uuids). content begins 'Conversation compacted' on 232/232. cumulativeDroppedTokens is absent on every boundary written by CC <= 2.1.191 and present on every boundary from 2.1.199 through 2.1.258; precomputed appears only on 6 boundaries at 2.1.191/2.1.199/2.1.202 and on none since. Boundary count equals summary count on 23 of 23 files that contain either (232 = 232). csift renders each hit as: 'Conversation compacted [compaction boundary: trigger=manual preTokens=787488 postTokens=22073 durationMs=168904] [logicalParent=<uuid>]'.",
          "rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; key sets compared as the sorted comma-joined key list of the compactMetadata object; the pairing rule counts, per file, lines whose parsed subtype is compact_boundary against lines whose parsed isCompactSummary is true.",
          "note": "The claim's key list is a subset of what current CC writes: it omits cumulativeDroppedTokens, which is on 100% of boundaries at 2.1.199 and later including the one written by 2.1.258. csift keeps compactMetadata unparsed and renders only the four scalars trigger/preTokens/postTokens/durationMs, so the extra keys change nothing in the render - but a reader treating the claim's list as exhaustive would be wrong. Code verified verbatim at the claimed lines: src/model/record.rs:90-95, src/model/classify.rs:7-12, src/search/record_text.rs:288-296, src/search/scan.rs:370."
        }
      ]
    },
    {
      "id": "CMP-003",
      "area": "compaction",
      "behavior": "A compaction boundary's own `parentUuid` is null; the true predecessor record the compaction re-links to is named by a TOP-LEVEL `logicalParentUuid` on the boundary - the harness's own ground truth for rejoining the conversation chain across a compaction.",
      "depends": "csift models `logical_parent_uuid` tolerantly and appends `[logicalParent=<uuid>]` to the boundary's rendered line, so a reader can rejoin the graph across a boundary; a walker that trusted `parentUuid` alone would see the post-compaction chain start from nothing.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "104-109",
          "snippet": "    /// `logicalParentUuid` (top-level on `compact_boundary` system records): the TRUE\n    /// predecessor record the compaction re-links to (`parentUuid` is null on a\n    /// boundary) - harness ground truth for the post-compaction chain. Additive +\n    /// tolerant.\n    #[serde(default, rename = \"logicalParentUuid\")]\n    pub logical_parent_uuid: Option<String>,"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "164-170",
          "snippet": "    // A boundary's `parentUuid` is null; `logicalParentUuid` names the record the\n    // compaction re-links to (harness ground truth) - surface it on the same line.\n    if rec.is_compact_boundary() {\n        if let Some(lp) = rec.logical_parent_uuid.as_deref() {\n            parts.push(format!(\"[logicalParent={lp}]\"));\n        }\n    }"
        }
      ],
      "instrument": "`csift search '' @<session> -t harness.compaction.boundary --max-count 1` then `csift show @<session> --line <n> --raw`: the raw line must carry a top-level `logicalParentUuid` and a null `parentUuid`. Bulk check: `rg -NI '\"subtype\":\"compact_boundary\"' <transcript> | rg -c '\"parentUuid\":null'` must equal the boundary count. Counting rule: one boundary record per compaction event.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.8.1",
        "source": "SPEC.md section 4.7; AGENTS.md section 3.5; src/model/record.rs logicalParentUuid comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|[(if has(\"parentUuid\") then (.parentUuid|if .==null then \"parentUuid:null\" else \"parentUuid:set\" end) else \"parentUuid:absent\" end),(if has(\"logicalParentUuid\") then (.logicalParentUuid|if .==null then \"lpu:null\" else \"lpu:set\" end) else \"lpu:absent\" end)]|@tsv' | sort | uniq -c ; csift search '' @<session> -t harness.compaction.boundary --no-subagents --max-count 2",
          "observed": "232 of 232 boundaries: parentUuid present and null, logicalParentUuid present and non-null. The split is clean across every CC version in the corpus, 2.1.150 through 2.1.258 (the single 2.1.258 boundary included). csift's rendered boundary line ends with '[logicalParent=<uuid>]' as designed.",
          "rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; parentUuid classified by key presence and null-ness, never by byte match.",
          "note": "Verified verbatim at the claimed lines: src/model/record.rs:97-102 and src/search/record_text.rs:163-169. The claim's own bulk check ('rg -c \"parentUuid\":null' on boundary lines) works but is a byte test; the parsed form above is the stricter counting rule and gives the same 232."
        }
      ]
    },
    {
      "id": "CMP-004",
      "area": "compaction",
      "behavior": "The compaction boundary is a record with NO `message{}` field at all - pure compaction metrics - so its only readable text is the top-level `content` string plus the `compactMetadata` object.",
      "depends": "csift's record text falls back to that message-less path so `-t harness.compaction.boundary` is search-reachable at all, and the absent `message{}` is the ONE LLM-visibility instrument (`Class::llm_visible` is false for the boundary, so a bare role selector skips it while an explicit path reaches it); because the excerpt is fabricated, `compact_boundary` is registered as a `-t`-gated VERIFIABLE synth marker so the whole-file gate can still prove a non-match.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "165-170",
          "snippet": "    ///   drew real replies before the retraction.)\n    /// - `harness.compaction.boundary`: a system record with NO message field at\n    ///   all - pure compaction metrics. (The compaction SUMMARY is visible: the\n    ///   DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n    ///   summaries, not a delivery flag, and `isMeta` is an authorship flag -\n    ///   neither is a visibility instrument.)"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "146-149",
          "snippet": "/// Combines the top-level `content` string (`\"Conversation compacted …\"`) with a readable\n/// `compactMetadata` excerpt so `-t harness.compaction.boundary` can both MATCH the boundary and SEE\n/// what each compaction clipped. `None` when neither is present (no fabricated text).\npub(crate) fn system_record_text(rec: &Record) -> Option<String> {"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "496-501",
          "snippet": "    if args\n        .label_filter()\n        .selected(Class::CompactionBoundary.path())\n    {\n        verifiable.push(b\"compact_boundary\");\n    }"
        }
      ],
      "instrument": "`rg -NI '\"subtype\":\"compact_boundary\"' <transcript> | jq 'has(\"message\")'` must be false on every line, while the same test on any `user`/`assistant` record is true; `csift search 'trigger' @<session> -t harness.compaction.boundary` must still return hits. Counting rule: one boundary record per compaction event.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.1",
        "source": "SPEC.md section 6 v0.9.4 ledger item 7; AGENTS.md section 3.5; src/model/taxonomy.rs llm_visible comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|(has(\"message\")|tostring)' | sort | uniq -c ; same stream | jq -r 'select(.subtype==\"compact_boundary\" and .version==\"2.1.258\")|keys|join(\",\")' ; csift search 'trigger' @<session> -t harness.compaction.boundary -c --no-subagents ; sed -n '175,186p' src/model/taxonomy.rs",
          "observed": "has(\"message\") is false on 232 of 232 boundaries. The 2.1.258 boundary's complete top-level key list is compactMetadata,content,cwd,entrypoint,gitBranch,isSidechain,level,logicalParentUuid,parentUuid,sessionId,slug,subtype,timestamp,type,userType,uuid,version - no message. Searching the literal 'trigger' under -t harness.compaction.boundary still returns all 5 boundaries of the test session, so the fabricated compactMetadata excerpt is matchable through the synth-marker path. Class::llm_visible returns false for Class::CompactionBoundary and true for Class::CompactionSummary (the invisible set is UserUnsent, UserQueued, CompactionBoundary, MetaTurnDuration, MetaAwaySummary, MetaStopHooks, MetaSnapshot).",
          "rule": "One row per raw jsonl line whose parsed subtype is compact_boundary, corpus-wide; message-presence tested as a parsed top-level key, not a byte match.",
          "note": "Verified verbatim at the claimed lines: src/model/taxonomy.rs:157-162, src/search/record_text.rs:145-148, src/search/matcher.rs:493-498. The 'trigger' search is the load-bearing check: it proves the -t-gated verifiable synth marker still lets the whole-file gate admit a boundary-only match."
        }
      ]
    },
    {
      "id": "CMP-005",
      "area": "compaction",
      "behavior": "Optional fields ride the boundary record: 43% of measured boundaries (101/232) carry an `isMeta` key whose value is always `false` - never `true` - and that key is a legacy/lane artifact (98 main-thread boundaries from CC <= 2.1.191 plus 3 subagent boundaries at 2.1.219; CC 2.1.199 onward, 2.1.258 included, writes no `isMeta` on a main-thread boundary at all); about 1% (3/232) carry an `agentId`, and those are exactly the `isSidechain:true` subagent boundaries (first seen CC 2.1.219); a boundary written by a background job carries `sessionKind` (1/232, value `bg`, CC 2.1.252).",
      "depends": "csift classifies the boundary on `type` + `subtype` alone, so an `isMeta`, `agentId` or `sessionKind` rider never reroutes it out of `harness.compaction.boundary`. Note the rationale the claim gave is wrong in one detail: because the rider's value is always `false`, a classifier gating on `isMeta==true` (as the genuine-user path does) would drop 0 boundaries, not 43% - the real hazard is a classifier gating on key PRESENCE.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "176-181",
          "snippet": "        match self.r#type.as_deref() {\n            Some(\"system\") => {\n                if self.is_compact_boundary() {\n                    push_unique(&mut out, Class::CompactionBoundary);\n                }\n            }"
        }
      ],
      "instrument": "`rg -NI '\"subtype\":\"compact_boundary\"' ~/.claude/projects -g '*.jsonl' | jq -r '[(.isMeta//false),(.agentId!=null),(.sessionKind//\"-\")] | @tsv' | sort | uniq -c`. Counting rule: one row per boundary record; denominator = all boundary records in scope.",
      "located": {
        "claude_code": "2.1.219",
        "csift": "0.6.1",
        "source": "AGENTS.md section 3.5; csift dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|[(has(\"isMeta\")|tostring),(.isMeta|tostring)]|@tsv' | sort | uniq -c ; same stream | jq -r '...|[(.version),(has(\"isMeta\")),(.isMeta)]|@tsv' | sort | uniq -c ; same stream | jq -r '...|[(.isSidechain),(has(\"isMeta\")),(has(\"agentId\"))]|@tsv' | sort | uniq -c ; same stream | jq -r 'select(has(\"sessionKind\"))|[.sessionKind,.version]|@tsv' | sort | uniq -c",
          "observed": "Of 232 boundaries: 101 (43.5%) carry an isMeta key and 131 do not - but the VALUE is false on 101 of 101 and true on 0 of 232. The 101 split by lane and version: 98 main-thread boundaries all written by CC 2.1.191 or earlier, plus 3 isSidechain:true subagent boundaries written by 2.1.219. Every main-thread boundary from 2.1.199 onward, including the 2.1.258 one, carries no isMeta key at all. agentId is present on 3 of 232 (1.3%), and those 3 are exactly the isSidechain:true boundaries at 2.1.219. sessionKind is present on 1 of 232, value 'bg', written by 2.1.252.",
          "rule": "One row per raw jsonl line whose parsed subtype is compact_boundary; denominator 232 = all boundary records in the corpus. isMeta counted twice: by key presence and by boolean value.",
          "note": "The 43% number reproduces exactly under a key-presence counting rule, so the claim is not refuted, but two things needed correcting: isMeta is never true on a boundary, and current CC no longer writes the key on main-thread boundaries. Code verified verbatim at src/model/classify.rs:176-181."
        }
      ]
    },
    {
      "id": "CMP-006",
      "area": "compaction",
      "behavior": "`compactMetadata.preservedMessages.uuids` is a TAIL WINDOW of what a compaction kept, not a visibility filter: measured over 173 boundaries, render-internal system subtypes appear in it at rates equal to conversation records (`stop_hook_summary` 46/3,848 = 1.20%, `turn_duration` 38/3,205 = 1.19%, `away_summary` 9/1,142 = 0.79%, versus `user` 279/39,583 = 0.70% and `assistant` 594/79,382 = 0.75%); later user records even name a `turn_duration` uuid as their `parentUuid`, so DAG threading is no instrument either.",
      "depends": "csift refuses `preservedMessages` membership and DAG threading as LLM-visibility instruments and uses `message{}` presence instead; keying `llm_visible` off `preservedMessages` would mark render-internal telemetry as delivered conversation.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "172-178",
          "snippet": "    /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n    /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n    /// over every non-record line type in the corpus; every user/assistant record\n    /// does), and Claude Code's\n    /// own source labels them REPL-render internals. DAG threading is NOT the\n    /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n    /// (chain continuity), and `preservedMessages` lists these uuids at the same rate"
        }
      ],
      "instrument": "For each `compact_boundary`, join `compactMetadata.preservedMessages.uuids` against the uuids of the lines preceding that boundary in the same file, bucketed by `type`/`subtype`. Counting rule: per line, per boundary window; denominator = uuid-bearing lines in that boundary's own pre-window. Expect rate parity across types.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger item 7; csift dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Python join over the 23 corpus files that contain a compact_boundary (jq per file emitting uuid/type/subtype/preservedMessages.uuids per line): for each boundary, bucket the uuid-bearing lines of its window by type/subtype and count how many of those uuids appear in that boundary's preservedMessages.uuids. Window = the lines since the previous boundary in the same file. Separately, a second Python pass building a parentUuid->child type histogram over the same files.",
          "observed": "232 boundaries over 23 files. Membership rate, kept/window: system/turn_duration 55/4,190 = 1.31%; system/stop_hook_summary 63/5,072 = 1.24%; system/away_summary 14/1,434 = 0.98%; assistant 1,007/122,723 = 0.82%; user 466/61,693 = 0.76%; attachment 1,361/396,386 = 0.34%. Render-internal system subtypes are preserved at a rate equal to or slightly above conversation records - the ordering the claim reports, reproduced with the same counting rule on a grown corpus (the claim's 1.20 / 1.19 / 0.79 / 0.70 / 0.75 percentages land within 0.2 points). Narrowing the window to the span the preserved set actually covers makes the parity starker: away_summary 14/16 = 87.5%, stop_hook_summary 64/80 = 80.0%, turn_duration 56/73 = 76.7%, assistant 1,012/1,403 = 72.1%, user 467/849 = 55.0%. The preserved set is also not a contiguous run: on 191 of 232 boundaries the preserved uuids have gaps (median 6 preserved uuids spread over a span of 11 lines, reaching back a median of 20 lines from the boundary). DAG threading: system/turn_duration is named as parentUuid by a later user record 3,088 times, by a system/away_summary record 1,357 times and by an assistant record 5 times; system/away_summary is named by a later user record 1,591 times.",
          "rule": "Numerator/denominator per line, per boundary window (windows overlap across boundaries in one file by design); denominator = uuid-bearing lines in that boundary's own window, bucketed by type or type/subtype. Parent histogram: one row per line carrying a parentUuid that resolves to a uuid in the same file.",
          "note": "Both halves of the claim reproduce: rate parity across types, and later user records naming a turn_duration uuid as parentUuid. Code verified verbatim at src/model/taxonomy.rs:164-170."
        }
      ]
    },
    {
      "id": "CMP-007",
      "area": "compaction",
      "behavior": "`compactMetadata.preservedMessages` EXCLUDES every superseded-draft uuid: 0 of 772 measured draft uuids appear in any preservedMessages array.",
      "depends": "That exclusion is the instrument behind csift labelling `user.unsent` LLM-invisible; read together with the tail-window rate parity it means absence from preservedMessages carries information while presence does not.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "160-164",
          "snippet": "    /// - `user.unsent`: a superseded draft is NOT in the surviving conversation -\n    ///   Claude Code's own `compactMetadata.preservedMessages` accounting excludes\n    ///   every draft uuid (0 of 772 measured), and the conversation DAG threads\n    ///   through the resend sibling, never the draft. (Wording law: \"not in the\n    ///   surviving conversation\", never \"the model never saw it\" - a few drafts"
        }
      ],
      "instrument": "Collect draft uuids with `csift search '' @<session> -t user.unsent --format json | jq -r .uuid`, then `rg -o 'preservedMessages\":\\[[^]]*' <transcript>` and test membership. Counting rule: draft uuids found in any preservedMessages array over all draft uuids measured.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger item 7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "For the 6 project directories that contain a compact_boundary: csift search '' @<project dir> -t user.unsent --format json --max-count 0 | jq -r '.hits[]?.uuid' | sort -u  (815 uuids). Then cd ~/.claude/projects && rg -NI --no-filename '\"subtype\":\"compact_boundary\"' -g '*.jsonl' . | jq -r 'select(.subtype==\"compact_boundary\")|((.compactMetadata.preservedMessages.uuids//[]) + (.compactMetadata.preservedMessages.allUuids//[]))[]' | sort -u  (3,486 uuids). Intersection via comm -12.",
          "observed": "815 distinct superseded-draft uuids, 3,486 distinct uuids across every preservedMessages array (uuids and allUuids unioned) on all 232 boundaries. Intersection: 0.",
          "rule": "Draft uuids = the uuid of every csift hit labelled user.unsent, deduplicated. Preserved uuids = the union of preservedMessages.uuids and preservedMessages.allUuids over every boundary, deduplicated. Membership counted as set intersection size.",
          "note": "The claim's 0-of-772 re-measures as 0 of 815 on a grown corpus - the denominator moved, the answer did not. Note the union with allUuids makes this test strictly harder than the claim's instrument and it still returns 0. Code verified verbatim at src/model/taxonomy.rs:152-156."
        }
      ]
    },
    {
      "id": "CMP-008",
      "area": "compaction",
      "behavior": "`isVisibleInTranscriptOnly` is a DISPLAY flag carried on compaction summaries only - 232 of 232 records with the top-level key are `isCompactSummary:true` user records, and no other record type carries it - and `isMeta` is an AUTHORSHIP flag a summary does not even carry (0 of 232 summaries have the key); neither reports whether the model received a record. The compaction summary IS delivered - the conversation DAG threads through it (231 of 232 measured).",
      "depends": "csift keeps the summary LLM-visible and reads neither flag as a delivery signal; misreading `isVisibleInTranscriptOnly` as a delivery flag would hide the one record carrying the compacted context from every role-level selector.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "165-170",
          "snippet": "    ///   drew real replies before the retraction.)\n    /// - `harness.compaction.boundary`: a system record with NO message field at\n    ///   all - pure compaction metrics. (The compaction SUMMARY is visible: the\n    ///   DAG threads through it; `isVisibleInTranscriptOnly` is a display flag on\n    ///   summaries, not a delivery flag, and `isMeta` is an authorship flag -\n    ///   neither is a visibility instrument.)"
        }
      ],
      "instrument": "Test the PARSED key, not the byte string: `rg -NI --no-filename 'isVisibleInTranscriptOnly' -g '*.jsonl' . | jq -r 'select(has(\"isVisibleInTranscriptOnly\"))|[.type,(.isCompactSummary|tostring)]|@tsv' | sort | uniq -c`. A bare `rg -c 'isVisibleInTranscriptOnly'` overcounts 2.75x on a corpus whose sessions discuss the format.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.9.4",
        "source": "SPEC.md section 5; SPEC.md section 6 v0.9.4 ledger item 7; src/model/taxonomy.rs llm_visible comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && rg -NI --no-filename 'isVisibleInTranscriptOnly' -g '*.jsonl' . | jq -r 'select(has(\"isVisibleInTranscriptOnly\"))|[.type,(.isCompactSummary|tostring),(.isVisibleInTranscriptOnly|tostring)]|@tsv' | sort | uniq -c ; same stream WITHOUT the has() filter, to expose the byte-grep overcount ; rg -NI --no-filename '\"isCompactSummary\":true' -g '*.jsonl' . | jq -r 'select(.isCompactSummary==true)|[(has(\"isMeta\")|tostring),(.userType)]|@tsv' | sort | uniq -c ; Python pass building a uuid->children index per file and testing whether each summary uuid is named as some record's parentUuid",
          "observed": "232 records carry a top-level isVisibleInTranscriptOnly key; all 232 are type=user with isCompactSummary=true and isVisibleInTranscriptOnly=true, and no other record type carries the key. The byte grep the claim proposes matches 638 lines, 2.75x the true 232, because csift's own dev sessions quote the field name in prose and tool results. Summaries carry no isMeta key at all (0 of 232 have it) and userType is 'external' on 232/232. DAG threading: 231 of 232 summary uuids are named as parentUuid by at least one later record in the same file (children: 211 attachment, 15 user, 7 assistant); 1 summary is unthreaded.",
          "rule": "One row per raw jsonl line, corpus-wide, keyed on the PARSED top-level key (never on the raw byte string). DAG denominator = summary records in scope; a summary counts as threaded when its uuid appears as some other record's parentUuid in the same file.",
          "note": "The claim holds; two corrections. First, its instrument as written is a byte grep that overcounts 638 to 232 on this corpus - the parsed form is the counting rule a stranger should rerun. Second, the DAG figure re-measures as 231/232 rather than 225/228, and the corpus shows summaries carry no isMeta key at all, so 'isMeta is an authorship flag' is a statement about other records, not about summaries. Code verified verbatim at src/model/taxonomy.rs:157-162; Class::llm_visible excludes CompactionBoundary but not CompactionSummary (src/model/taxonomy.rs:175-186)."
        }
      ]
    },
    {
      "id": "CMP-009",
      "area": "compaction",
      "behavior": "A compaction summary preserves task STATE in a nine-section synthesis whose section 6 lists the user messages, but it loses user-side TURN fidelity by VOLUME rather than by bullet count: the bullet count tracks the real turn count closely (measured 22 turns to 20 bullets, 27 to 26, 12 to 14 over three sessions), while the bullet text carries only 0.29x to 0.89x the characters of the turns it stands for. Explicit `...`-truncated bullets are the minority form: 67 of 2,089 measured bullets (3.2%), in 38 of 232 summaries. The nine numbered headers are the dominant but not the only rendering - all 232 summaries carry a user-messages section, 165 under the literal header `6. All user messages`.",
      "depends": "`verbatim` exists to supplement exactly that loss: it re-emits the clipped verbatim turns in order with their `Lnnnnn` line numbers instead of re-deriving task state, and its dedup fingerprints are extracted from those bullets, so a change to the summary template moves the prefix matching.",
      "code": [
        {
          "path": "src/turns/build.rs",
          "lines": "245-251",
          "snippet": "/// Extract dedup fingerprints from a summary body: the §6 \"All user messages\" bullets\n/// and the §9 verbatim last-assistant quote (the only verbatim turns a summary holds).\n/// Each fingerprint is `normalize_line(text).to_lowercase()` truncated to the first\n/// [`DEDUP_PREFIX`] chars. Conservative: when the structured sections are not found,\n/// every `- ` bullet line in the body is fingerprinted (a superset - still strict per\n/// line). Robust to summaries that omit the exact headers.\npub(crate) fn summary_fingerprints(body: &str) -> Vec<String> {"
        },
        {
          "path": "src/turns/config.rs",
          "lines": "43-47",
          "snippet": "// A genuine-user turn can own a LONG run of agent messages (a debugging/build chain the\n// model narrates step by step) that a compaction summary clips to its single §9 EOT\n// quote. The reconstruction's job is to restore the LOAD-BEARING members of that run\n// without flooding the budget with pure \"let me look into this\" declarations. The model\n// keeps EVERY agent message on the slice (`TurnSlice.agents`) but SELECTS a survivor set."
        }
      ],
      "instrument": "`csift search '' @<session> -t harness.compaction.summary --no-truncate`, count the bullets in the user-messages section, and compare with the genuine-user turn count preceding that boundary (`csift stats @<session> --format json | jq .turns` over the pre-boundary window). Counting rule: summary bullets versus genuine-user turns before the boundary.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.4.0",
        "source": "SPEC.md section 10.2; SPEC.md section 6.8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Python pass over the 23 boundary-bearing files extracting every summary body, locating its user-messages section and counting bullet lines and ellipsis-terminated bullet lines; plus, per session, a window join of csift search '' @<session> -t user.message --format json --max-count 0 --no-truncate --no-subagents against the boundary line numbers, comparing bullet count and total bullet characters with turn count and total turn characters; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}All user messages.{0,400}' and the same for '8. Current Work:' and '9. Optional Next Step:'.",
          "observed": "232 of 232 summaries carry a user-messages section, but the literal numbered headers are not universal: '6. All user messages' appears in 165/232 and '9. Optional Next Step' in 178/232 - the remaining bodies use markdown or unnumbered variants ('All user messages' 52, 'User Messages' 15), while 'Primary Request' and the closing 'Summary:' scaffold appear in 232/232. The 2.1.258 summary shows the full nine numbered sections, 1 Primary Request and Intent through 9 Optional Next Step. Truncation: of 2,089 user-message bullets corpus-wide only 67 (3.2%) end in an ellipsis, and only 38 of 232 summaries contain even one. What is actually lost is character volume, not bullet count: per compaction window, bullets vs genuine-user turns ran 11/8, 1/2, 1/2, 1/2, 6/8, 13/13, 13/14, 14/12, and total bullet characters vs total turn characters ran 0.89x, 0.38x and 0.29x on the three sessions measured. One session totalled 22 genuine-user turns to 20 bullets, the same shape as the claim's 22-to-17. The binary carries two template variants for section 6, both saying 'List ALL user messages that are not tool results' with no truncation instruction.",
          "rule": "Bullets = lines matching a leading '-' or '*' inside the user-messages section of a summary body. Truncated = the bullet's text, with a trailing quote stripped, ends in '...' or a single-character ellipsis. Turns = csift hits labelled user.message whose line number falls strictly between the previous boundary line and this boundary line; characters = length of the untruncated csift excerpt.",
          "note": "The claim's direction is right and its 22-to-17 figure is close to a re-measured 22-to-20, but the stated MECHANISM ('clips real prose turns to ...-truncated bullets') is the minority case at 3.2% of bullets. Since verbatim's dedup fingerprints key on those bullets, the correction matters: the bullets are mostly complete quoted messages, which is why prefix fingerprinting works at all. Code verified verbatim at src/turns/build.rs:245-251 and src/turns/config.rs:43-47."
        }
      ]
    },
    {
      "id": "CMP-010",
      "area": "compaction",
      "behavior": "The assistant side of a compaction summary collapses to AT MOST ONE verbatim quote and often to none: over 8 measured compaction windows holding 785 assistant messages, 3 windows carried exactly one 60-character-verbatim assistant quote and 5 carried zero, and no window carried two. When a quote is present it is not reliably the last pre-compaction assistant message (1 of 3 measured). Every summary ends with a trailer pointing at the full transcript path (232 of 232). CC 2.1.258 ships two section-9 templates and only one of them asks for direct quotes, which is consistent with the quote being optional.",
      "depends": "That single end-of-turn quote is why `verbatim`'s agent-message selection defaults to LONGEST rather than LAST and why its richness profiles exist: everything between the first and last assistant message of a compacted turn survives only in the raw transcript.",
      "code": [
        {
          "path": "src/turns/config.rs",
          "lines": "43-47",
          "snippet": "// A genuine-user turn can own a LONG run of agent messages (a debugging/build chain the\n// model narrates step by step) that a compaction summary clips to its single §9 EOT\n// quote. The reconstruction's job is to restore the LOAD-BEARING members of that run\n// without flooding the budget with pure \"let me look into this\" declarations. The model\n// keeps EVERY agent message on the slice (`TurnSlice.agents`) but SELECTS a survivor set."
        }
      ],
      "instrument": "Read one summary with `csift show @<session> --uuid <summary uuid>` and count its verbatim assistant quotes, then compare with `csift search '' @<session> -t agent.message -c` over the same pre-boundary window. Counting rule: verbatim assistant quotes in the summary versus assistant message records before the boundary.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.4.0",
        "source": "SPEC.md section 10.2; SPEC.md section 6.8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Python pass per session: for each compaction window (previous boundary to this boundary) collect every csift hit labelled agent.message with --no-truncate, whitespace-normalise both the summary body and each assistant text, build the set of all 60-character shingles of the summary body, and count the assistant messages having at least one 60-character run in common with the summary; also record the line of the last assistant message in the window. Plus rg -c 'read the full transcript at' over all 232 extracted summary bodies, and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Optional Next Step.{0,1400}' | sort -u.",
          "observed": "8 compaction windows across 3 sessions, holding 785 assistant messages (72, 12, 8, 6, 68, 245, 191, 183). Verbatim assistant quotes found in the summary: 1, 1, 1 in three windows and 0 in the other five - never more than one in any window. Of the 3 quotes, the quoted message was the LAST assistant message of its window in 1 case; in the other 2 the quote came from an earlier message (quoted line 3941 while the last was 3993; quoted 3200 while the last was 3945). The trailer 'read the full transcript at' is present in 232 of 232 summaries, as is 'Continue the conversation from where it left off'. The 2.1.258 binary carries exactly two section-9 templates: a long one that asks only for the next step, and a short one adding 'Include direct quotes from the most recent conversation.'",
          "rule": "One row per assistant-message record in the window. A record counts as quoted when its whitespace-normalised text shares at least one 60-character substring with the whitespace-normalised summary body; window = lines strictly between the previous boundary and this boundary.",
          "note": "The load-bearing consequence survives and is in fact strengthened: since the surviving quote is at most one and is not reliably the LAST assistant message, verbatim's LONGEST-not-LAST default and its richness profiles are the right design. The 'about 239 assistant turns' figure is in range - one measured window held 245. Code verified verbatim at src/turns/config.rs:43-47."
        }
      ]
    },
    {
      "id": "CMP-011",
      "area": "compaction",
      "behavior": "A compaction summary is a turn MEMBER, not a turn delimiter, so a backward walk from EOF passes through compaction boundaries transparently. Measured at the default 40,000-char budget on the five most compaction-heavy top-level transcripts in the corpus, the selected window crossed 1, 4, 5, 2 and 10 boundaries against session totals of 80, 33, 32, 15 and 11 - up to 10 of a session's 11 boundaries in one 40K reconstruction. How deep the window reaches is budget-relative, not a fixed number: a session whose turns are long spends the budget before the first boundary (1 of 80), a session of short turns crosses nearly all of them (10 of 11).",
      "depends": "`verbatim`'s recency-first selection reaches back across multiple compactions by default (`--max-compactions N` caps the crossing count) and banners every crossed summary; treating a summary as a delimiter would stop the reconstruction at the newest boundary.",
      "code": [
        {
          "path": "src/turns/build.rs",
          "lines": "5-9",
          "snippet": "/// Build the per-turn slices + summary dedup sets from a session's line-numbered\n/// records. Turn segmentation reuses the single shared engine\n/// [`group_turn_indices_deduped`], so an esc-cancel / edit-resend draft never surfaces as a\n/// phantom turn (§6.4.1); a compaction summary is a turn MEMBER (it is excluded from\n/// genuine-user), so the walk is transparent to it."
        },
        {
          "path": "src/turns/select.rs",
          "lines": "294-301",
          "snippet": "/// The EXACT compaction-boundary banner line a crossed summary renders to (no trailing\n/// newline). The renderer and the budget reservation both call this so the reserved\n/// banner length is byte-for-byte what is emitted.\npub(crate) fn boundary_banner_line(line_no: usize) -> String {\n    format!(\n        \"{0} compaction boundary · summary at L{1} · (turns below predate it) {0}\",\n        \"══\", line_no\n    )"
        }
      ],
      "instrument": "csift verbatim @<session> --budget 40000 --format json | jq -rc 'select(.kind==\"compaction_boundary\")|.line' | wc -l, cross-checked against the header row's boundaries_spanned and boundaries_total. Counting rule: boundaries the budget window crossed over the session's total. NOTE the boundary row's line key is `line`, not `line_no` (the row carries exactly kind/line/summary_chars), so the ledger's `.line_no` projection yields null on every row.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.4.0",
        "source": "SPEC.md section 6.8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for s in <five top-level sessions>; do csift verbatim @$s --budget 40000 --format json | python3 -c \"import sys,json; rows=[json.loads(l) for l in sys.stdin if l.strip()]; h=[r for r in rows if r['kind']=='header'][0]; print(h['boundaries_spanned'], h['boundaries_total'], h['chars_used'], sum(1 for r in rows if r['kind']=='compaction_boundary'))\"; done   (then: csift verbatim @<session> --budget 40000 --max-compactions 2 --format json)",
          "observed": "boundaries_spanned / boundaries_total at the default 40000-char budget, on the five most compaction-heavy top-level transcripts: 1/80, 4/33, 5/32, 2/15, 10/11. chars_used 39927-39999 in every run. The count of emitted kind==compaction_boundary rows equalled boundaries_spanned in all five runs (1,4,5,2,10). With --max-compactions 2 the 10/11 session fell to 1 spanned (turns 115 -> 119, chars_used 39983). Text banner emitted verbatim: '== compaction boundary . summary at L10077 . (turns below predate it) ==' (leading/trailing glyph is the double-bar character pair, four consecutive banners at output lines 10-13).",
          "rule": "One run per session at the default budget. Boundaries crossed = the header's boundaries_spanned, cross-checked against the number of kind==compaction_boundary rows in the same stream; session total = the header's boundaries_total (equals csift stats' compactions).",
          "note": "Mechanism confirmed and strong: transparency to boundaries is real and the cap flag demonstrably restricts it. The specific figure '26 boundaries on a 35-summary session' is not reproducible on the current corpus - no transcript has 35 compactions (the distribution is 80, 33, 32, 15, 11, 8, 7, 7, ...), and the deepest crossing measured at 40K was 10. Both code sites exist verbatim at the cited lines (src/turns/build.rs 5-9; src/turns/select.rs 294-301)."
        }
      ]
    },
    {
      "id": "CMP-012",
      "area": "compaction",
      "behavior": "A transcript whose FIRST TIMESTAMPED record is a `system`/`compact_boundary` was minted by COPYING another session at that compaction point: the copy preserves the origin's record uuids, timestamps, parentUuid and logicalParentUuid, rewrites sessionId, ADDS `sessionKind`, STRIPS the slug, and stamps the copying build's own `version` - so the copied records carry timestamps predating the file itself. Measured corpus-wide over 64 top-level transcripts, the first-timestamped-record type was `attachment` on 62, `compact_boundary` on exactly 1 (the single known fork, which csift is the only surface to flag) and `queue-operation` on 1 (correctly not flagged); zero false positives.",
      "depends": "`list`'s clone probe walks head lines to the first timestamped record and early-exits (near-free on a normal transcript, where a handful of bookkeeping lines lead the file), setting `is_clone` and `clone_boundary_uuid`; without the detection a clone DOUBLE-COUNTS its inherited records on every spanning surface until it is scoped away.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "152-159",
          "snippet": "/// The C-19 clone law: a transcript whose FIRST TIMESTAMPED record is a\n/// system/`compact_boundary` was minted by copying another session at a compaction\n/// point. Measured on a real 61-file project dir: exactly the one known fork\n/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file).\npub(crate) fn clone_head_boundary(path: &Path) -> Result<Option<String>> {"
        },
        {
          "path": "src/session/summarize.rs",
          "lines": "165-176",
          "snippet": "    for line in mmap.split(|&b| b == b'\\n') {\n        if TS.find(line).is_none() {\n            continue;\n        }\n        if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n            if rec.timestamp.is_some() {\n                let hit = rec.is_compact_boundary();\n                return Ok(hit\n                    .then(|| rec.uuid.clone().unwrap_or_default())\n                    .filter(|u| !u.is_empty()));\n            }\n        }"
        }
      ],
      "instrument": "Corpus-wide rather than one project dir: for each top-level transcript take the first line carrying a `timestamp` and record its type/subtype, then cross-check `csift list --max-count 0 --format json | jq -r 'select(.is_clone==true)|[.clone_of]|@tsv'`. Counting rule: one row per top-level transcript, first timestamp-bearing record only.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger item 3; AGENTS.md section 3.5; CHANGELOG 0.9.4; csift dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 census over every top-level transcript: for each ~/.claude/projects/<dir>/*.jsonl take the first JSON line carrying a non-null `timestamp` and print type+\"/\"+(subtype or \"-\"); then csift list --max-count 0 --format json | filter is_clone==true; then a python3 field-diff of the clone's head boundary line against the origin line carrying the same record uuid.",
          "observed": "64 top-level transcripts corpus-wide. First-timestamped-record type: attachment/- on 62, system/compact_boundary on 1, queue-operation/- on 1. csift list reported exactly 1 row with is_clone true, and it is the compact_boundary-headed file; the queue-operation-headed file was NOT flagged. Within the clone-bearing project dir alone (8 top-level transcripts): 7 attachment, 1 compact_boundary. Clone head boundary sits at line 28, behind 27 untimestamped bookkeeping lines (ai-title, agent-name, mode, permission-mode, atis-latch, file-history-snapshot). Copy evidence: 558 of the clone's uuid-bearing records carry a uuid that also occurs in the origin; 0 of those 558 carry a slug; the clone's first record timestamp precedes its own file birthtime by 15h19m. Head-boundary field diff vs the origin's native copy: 14 fields byte-identical (uuid, logicalParentUuid, parentUuid, timestamp, compactMetadata, content, cwd, entrypoint, gitBranch, isSidechain, level, subtype, type, userType), 4 rewritten (sessionId, slug present->absent, sessionKind absent->\"bg\", version 2.1.251->2.1.252).",
          "rule": "One row per top-level transcript, first timestamp-bearing record only. Copied-record count = clone records whose own uuid also appears as some origin record's own uuid.",
          "note": "The claim's '61-file project directory / attachment on 60' scope no longer exists - no project dir in the corpus holds 61 top-level transcripts (the largest holds 23), so the reproducible scope is corpus-wide (64) or the clone's own dir (8). A THIRD head shape has appeared since the measurement: one transcript's first timestamped record is a `queue-operation` line; the rule ignores it correctly, but a future ledger should stop describing the head census as two-valued. The same stale '61-file project dir' number is baked into the src/session/summarize.rs doc comment at line 154. Both code snippets exist verbatim at the cited lines (152-159 and 165-176). New untimestamped bookkeeping line types not in the ledger's enumerations were observed leading the file: `atis-latch`, `bridge-session`, `cost-state`, `file-history-delta`."
        }
      ]
    },
    {
      "id": "CMP-013",
      "area": "compaction",
      "behavior": "File birthtime is NOT a fork signal. Measured over 64 top-level transcripts, the birthtime is LATER than the first record on 64 of 64 (filesystem copies and migrations move birthtimes past the records; the largest ordinary gap is 45d17h, the median 4h13m). The true fork's gap is only 15h19m and ranks 27th of 64, so any 'first record much older than birthtime' threshold that catches the fork also fires on 26 ordinary transcripts. Comparing a UTC record timestamp against a LOCAL birthtime additionally inflates the delta by the zone offset - the same 15h19m gap reads as 25h19m.",
      "depends": "csift's clone law calls no `stat()`, reads no birthtime and trusts no clock, so it never reports an ordinary migrated transcript as a fork; the structural head-record rule is the whole instrument.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "155-158",
          "snippet": "/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file)."
        }
      ],
      "instrument": "For each top-level transcript compute first_record_ts_utc - birthtime_utc with BOTH operands in UTC, sort descending, and locate the known fork's rank. Counting rule: one delta per file; the refutation number is the count of non-forked files whose delta exceeds the fork's (26 of 63 here), which is threshold-free.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "AGENTS.md section 3.5; CHANGELOG 0.9.4; csift dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over all 64 top-level transcripts: delta = file birthtime (os.stat st_birthtime, converted to UTC) minus the first record timestamp; separately the mixed form, naive-local birthtime minus the naive UTC record timestamp; then rank the one true clone within the descending delta list.",
          "observed": "64 of 64 transcripts have a POSITIVE delta (birthtime later than the first record). Threshold sweep: delta > 1h on 40 files, > 6h on 31, > 24h on 26, > 72h on 21. Largest deltas: 45d17h, 27d11h, 20d23h, 20d7h, 19d12h. Median delta 4h13m. The one true clone's correct delta is 15h19m and it ranks 27th of 64 by descending delta - 26 ordinary non-forked transcripts have a LARGER delta than the fork. The same clone measured with a naive-local birthtime against the UTC record timestamp reads 1d1h19m (25h19m), an inflation of exactly 10h, the local zone offset.",
          "rule": "One delta per top-level transcript; both operands converted to UTC before subtracting. Discriminating power = how many non-clones outrank the clone at its own delta (a threshold low enough to catch the clone also fires on all of them).",
          "note": "The claim's '50 of 61' is threshold-dependent and no longer reproduces as stated (at >1h the current corpus gives 40 of 64; at >24h, 26 of 64). The rank-based rule above is threshold-free and stronger: the fork sits in the middle of the ordinary distribution, so birthtime cannot separate it at any threshold. The 15h19m -> 25h19m zone-offset inflation reproduced exactly. Code snippet exists verbatim at src/session/summarize.rs 155-158, and the function calls no stat(): it mmaps and reads head lines only (verified by reading the whole function body)."
        }
      ]
    },
    {
      "id": "CMP-014",
      "area": "compaction",
      "behavior": "A clone's head `compact_boundary` is a RE-SERIALIZED copy of the origin's, not a byte copy: the record's identity fields survive verbatim (the same boundary `uuid`, `logicalParentUuid`, `parentUuid`, `timestamp`, `compactMetadata` and `content`) while four fields are rewritten at copy time (`sessionId` to the clone's own, `slug` stripped, `sessionKind` added, `version` stamped with the COPYING build - 2.1.252 against the origin's 2.1.251). Because the uuid survives, sweeping the project directory for that uuid returns exactly two files - the clone and its origin - even when the sweep is recursive over 747 subagent transcripts.",
      "depends": "csift's origin join demands the uuid appear as a record's OWN `uuid` on a record that is itself a `compact_boundary`, so a prose mention of the uuid or a `logicalParentUuid` back-reference is refused and a co-clone (whose head probe returns the same uuid) is skipped; the one memmem sweep over the directory's siblings is paid only when a clone was detected.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "181-189",
          "snippet": "/// Join a detected clone to its ORIGIN: the sibling transcript where the boundary\n/// record NATIVELY lives. A prose mention of the uuid parses to a record whose own\n/// uuid differs; a co-clone's head probe returns the same boundary uuid and is\n/// skipped. Cost (one memmem sweep over the project dir's siblings) is paid ONLY\n/// when a clone was detected.\npub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n    let dir = path.parent()?;\n    let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());\n    let entries = std::fs::read_dir(dir).ok()?;"
        }
      ],
      "instrument": "`csift list <project dir> --format json | jq -r 'select(.is_clone==true)|[.session_id,.clone_of,.clone_boundary_uuid]|@tsv'`, then `grep -rl <that boundary uuid> <project dir>` must return exactly two `.jsonl` files. Counting rule: one file per `grep -l` line.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "CHANGELOG 0.9.4; src/session/summarize.rs clone_origin comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects/<project-dir> && rg -l --max-depth 1 '<boundary uuid>' . ; rg -l '<boundary uuid>' . | wc -l   (the second form is recursive over the whole project dir, subagent trees included); plus a python3 pass listing every origin record whose OWN uuid equals the clone's head boundary uuid, and a field-by-field diff of the two boundary lines.",
          "observed": "Non-recursive sweep returned exactly 2 .jsonl files (the clone and its origin). The recursive sweep over the same project dir - 755 .jsonl files including 747 subagent transcripts - returned the SAME 2 files, count 2. Exactly one origin record carries that uuid as its OWN uuid: a system/compact_boundary at origin line 90946 with an identical timestamp. The clone's logicalParentUuid is byte-identical to the origin boundary's, and it resolves natively to an origin attachment record at line 90931. Field diff of the two boundary lines: 14 fields identical, 4 rewritten (sessionId, slug present->absent, sessionKind absent->\"bg\", version 2.1.251->2.1.252).",
          "rule": "One file per `rg -l` line, run twice (top level only, then recursive) over the one project dir holding the clone.",
          "note": "The load-bearing part - the uuid-join to exactly one origin file - reproduced exactly, including the same logicalParentUuid. Only the phrase 'byte-copy' needed correcting; the 4 rewritten fields are what a stranger would trip on if they diffed the two lines expecting equality. The differing `version` field is independent evidence that the copy is written fresh by the forking build rather than memcpy'd. Code snippet exists verbatim at src/session/summarize.rs 181-189, and the join predicate in the function body does require the parsed record's OWN uuid to match AND to be a compact_boundary."
        }
      ]
    },
    {
      "id": "CMP-015",
      "area": "compaction",
      "behavior": "A session cloned at a compaction carries NO `plan_mode` attachment of its own - the attachment history predates the copy point and is not carried over - and the slug is STRIPPED from every copied record (0 of 558 copied records carry one; the clone's eventual slug differs from the origin's). The first slug-carrying record in such a fork is therefore a NATIVE record, and here it is the fork's own first `compact_boundary` (line 1471): a compaction mints a slug even when Plan Mode never ran.",
      "depends": "Claude Code's own plan binding rule takes the first slug-carrying record in the log and consults no attachment, so csift's `plan` falls back to that law with `binding_source:\"slug-only\"` plus a `minted_at_compaction` flag; binding by attachment alone answered `no Plan Mode` for forked sessions whose plan file Claude Code will still inject or rebuild in full.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "137-144",
          "snippet": "    // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n    // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n    // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n    // forked/background session reaches this state by construction (the clone strips\n    // attachment history), and its slug is often minted by the first own compaction -\n    // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file.\n    if latest.is_none() {\n        if let Some((line_no, rec)) = first_slug_record(bytes) {"
        }
      ],
      "instrument": "`csift plan @<a background-forked session>` must return a slug-bound plan file with `binding_source` `slug-only` and `minted_at_compaction` true. Counting rule: the first record carrying a `slug` key, scanned forward, one per file.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "AGENTS.md section 3.5; src/plan.rs binding comments"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift plan @<clone> --no-subagents --format json ; rg -c 'plan_mode' <clone>.jsonl <origin>.jsonl ; a python3 pass printing every clone line containing the token plan_mode with its attachment.type ; a python3 slug census of both files (distinct slug values, first slug-carrying record); strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'getSlugFromLog'.",
          "observed": "csift plan on the clone returns exactly one top-level row: binding_source \"slug-only\", minted_at_compaction true, line 1471, plan_exists true, plan_file ~/.claude/plans/<slug-A>.md. The clone matches the token plan_mode on 4 lines, but every one is a `plan_file_reference` attachment whose CONTENT quotes the words 'plan_mode attachment' in prose - there is no attachment of type plan_mode (the origin has 75 such lines). Slug census: clone has exactly 1 distinct slug on 2901 records, first carrier line 1471, a NATIVE system/compact_boundary (its uuid is absent from the origin); origin has exactly 1 distinct slug, a DIFFERENT value, on 69933 records, first carrier an assistant record at line 1575. Of the clone's 558 copied records (uuid also in the origin), 0 carry a slug. Binary: `function j(n){let e=n.messages.find((i)=>i.slug)?.slug;if(e===void 0)return;if(!ue.test(e)){t(\\`getSlugFromLog: rejecting malformed transcript slug (${e.length} chars)\\`);return}return e}` and the plan path builder `case\"plan\":return r(t,\"plans\",\\`${n.name}.md\\`)`.",
          "rule": "First record carrying a `slug` key, scanned forward, one per file; plan_mode presence counted as attachment records whose attachment.type equals plan_mode, not as raw token hits.",
          "note": "The clause 'slug-carrying records survive the copy' is REFUTED by the instrument: zero of the 558 copied records carry a slug, and the clone's single slug value is not the origin's. What survives the copy is the uuid, not the slug. Everything else holds, and the binding law is now confirmed at binary level: getSlugFromLog takes the FIRST message carrying a slug, validates it against a lowercase/dash/length pattern, consults no attachment, and the plan path is built as <plans-dir>/<slug>.md. Corroborating detail: the clone's copied `plan_file_reference` attachment still points at the ORIGIN's plan file while the fork's own binding names a different file, so a surface that bound on that attachment would answer with the wrong plan. Second code site (src/plan.rs 68-76) exists verbatim; the FIRST code site had drifted by one line (claim said 134-141, actual 135-142) - corrected above."
        }
      ]
    },
    {
      "id": "CMP-016",
      "area": "compaction",
      "behavior": "`sessionKind` is a PER-RECORD, write-time stamp of the writing process's kind. The harness's value domain is three-valued - `bg`, `daemon` and `daemon-worker` (the binary gates on exactly those three, and reads the field back out of a transcript line's head) - of which only `bg` occurs on disk here, on 909/909 stamped records. It rides `attachment`/`assistant`/`user`/`system` records while a background job is the writer and stops entirely once the same file is resumed interactively; the bookkeeping line types never carry it at all (measured never-stamped: last-prompt, ai-title, agent-name, mode, permission-mode, file-history-snapshot, file-history-delta, queue-operation, and the newer atis-latch, bridge-session and cost-state).",
      "depends": "csift's clone probe deliberately does NOT key on `sessionKind` - a file containing it is not necessarily a clone and a record lacking it is not necessarily native - the first-timestamped-record boundary shape is the discriminator; switching would make `list` report false clones.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "152-158",
          "snippet": "/// The C-19 clone law: a transcript whose FIRST TIMESTAMPED record is a\n/// system/`compact_boundary` was minted by copying another session at a compaction\n/// point. Measured on a real 61-file project dir: exactly the one known fork\n/// detected, zero false positives; file-birthtime rules were REFUTED (filesystem\n/// copies and migrations move birthtimes days past the records). Walks head lines\n/// until the first record carrying a timestamp and early-exits - near-free on a\n/// normal transcript (a handful of bookkeeping lines lead the file)."
        }
      ],
      "instrument": "Per project dir: `rg -l '\"sessionKind\"' ./<project-dir>/ | grep -c '\\.jsonl$'` (a corpus-wide rg over-reports by counting externalised tool-result .txt files that merely discuss the field). Then inside a stamped transcript tabulate presence of the top-level key by `type`, and split the records by whether their uuid also occurs in the origin. Counting rule: one row per jsonl line. Measured: 21 transcripts corpus-wide (one session plus its 20 subagent transcripts); within that file, 558/558 copied records stamped, then a mixed band, then 4467/4467 unstamped after the interactive-resume gap at line 1361.",
      "located": {
        "claude_code": "2.1.252",
        "csift": null,
        "source": "csift dev session 2026-09-01"
      },
      "first_seen_claude_code": "2.1.252",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && for d in */; do echo \"$(rg -l --no-messages '\\\"sessionKind\\\"' \"./$d\" | wc -l) / $(find \"./$d\" -name '*.jsonl' | wc -l)  $d\"; done   (scoped one project dir per invocation); then inside the one stamped top-level transcript a python3 census of sessionKind presence by record `type`, by copied-vs-native uuid, and by line band; then strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'sessionKind'.",
          "observed": "26 files corpus-wide contain the literal key, in 2 project dirs (22 + 4); of these, 4 are externalised tool-result .txt files and 1 is a .txt, leaving exactly 21 TRANSCRIPTS: 1 top-level (the fork) + 20 of its subagent transcripts, all in one project dir. Inside the fork: 909 stamped records, value `bg` on 909/909 (single value). Stamped by type: attachment 406, assistant 268, user 152, system 83 - and no other type ever stamped. Never stamped: ai-title 261, agent-name 261, mode 261, permission-mode 261, last-prompt 261, atis-latch 261, bridge-session 206, file-history-snapshot 75, queue-operation 84, file-history-delta 25, cost-state 4. Band: first stamped record at line 28 (the head boundary), last at line 1361 (2026-09-01T09:42:17.577Z); the next timestamped record is 13m15s later at line 1367 and every one of the 4467 records after line 1361 is unstamped. Copied-vs-native split: 558/558 copied records stamped, 0 unstamped; native records 351 stamped, 2959 unstamped. Binary: `let a=G1(t,\"sessionKind\");return a===\"daemon\"||a===\"daemon-worker\"}`, `B=G1(F,\"sessionKind\"),U=B===\"bg\"||B===\"daemon\"||B===\"daemon-worker\"?B:void 0`, `...t.sessionKind===\"bg\"?[\"bg\"]:[]`, and `filtered from /resume: sessionKind=`.",
          "rule": "File count: one row per file matched by a per-project-dir `rg -l`, then transcripts separated from tool-result .txt by extension. Record census: one row per JSON line, keyed on presence of the top-level `sessionKind` key.",
          "note": "The stated instrument does NOT work: `csift search '\"sessionKind\"' -l` matches rendered record TEXT, not raw JSON keys, so it returned 12 records in 12 transcripts that merely mention the field in prose - none of them stamped files. Use a scoped rg (or a jq/python key probe) for field-presence questions. The 21-transcript count and the 558/558 copied-record figure reproduced EXACTLY; the trailing unstamped band is now 4467 records (was 834) because the file kept growing after the interactive resume. New corpus fact: the binary's value domain is wider than the disk observation, so 'single observed value bg' should be stated as 'single observed value, of three the harness can write'. The independence claim also verified: 21 transcripts carry sessionKind while csift's list reports exactly 1 clone, so the field is not a clone discriminator - the clone probe reads only the first timestamped record's shape."
        }
      ]
    },
    {
      "id": "CMP-017",
      "area": "compaction",
      "behavior": "A compaction re-anchor can REPLAY duplicate records: the same `uuid` values and timestamps appear twice under different `parentUuid`s, and the replayed copy carries its top-level usage ZEROED (`input_tokens` 0, `output_tokens` 0, cache fields 0) while the real numbers survive only inside `usage.iterations` - so usage is not always identical within one `message.id`.",
      "depends": "csift's token dedupe takes a per-field MAX across an id's records rather than first-wins: identical on clean data, immune to the zeroed replay, and commutative so the rayon fold stays correct; first-wins would make the reported total depend on traversal order.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "250-256",
          "snippet": "                // CC repeats the IDENTICAL message.usage on every per-block record of\n                // one API message; summing per record over-reports 2.2-3.5x (measured).\n                // Dedupe per FILE by message.id, taking the per-field MAX across the\n                // id's admitted records: identical on clean data, and immune to the\n                // compaction-replay shape where a replayed copy carries ZEROED usage\n                // (first-wins would depend on traversal order). An id-less record\n                // counts on its own, as before."
        }
      ],
      "instrument": "Group a transcript's records by `message.id` and flag ids whose members disagree on any usage field; count duplicate uuids per file alongside. Counting rule: per id, per file. Measured over the 6 largest top-level transcripts: 4 had 0 mismatching ids, 1 had exactly 1 (a duplicate-uuid replay whose second copy is all-zero at the top level while usage.iterations keeps the real numbers), and 1 had exactly 1 of a different shape (a mid-stream partial, output_tokens 2 -> 1404, in a file with no duplicate uuids). Duplicate uuids themselves are common (474, 464, 250, 1175 in four of the six files), so the replay is frequent and the ZEROED replay is the rare variant.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.2",
        "source": "src/stats.rs usage-dedupe comment; csift dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over the 6 largest top-level transcripts (>5MB, sorted by size): group every record with a message.id by that id, collect the 4-tuple (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens), flag ids whose members disagree, and separately count uuids occurring more than once per file; then dump the disagreeing records' uuid, parentUuid, timestamp, top-level usage and usage.iterations.",
          "observed": "6 transcripts scanned (687MB/5385 ids, 396MB/7538, 284MB/12691, 113MB/6208, 94MB/2906, 94MB/2906). Mismatching ids: 0, 1, 0, 1, 0, 0. Duplicate uuids per file: 474, 464, 250, 0, 1175, 1175 - so replayed records are common while usage disagreement is rare. The one zeroed replay: the SAME three uuids appear at lines 27860-27862 and again at 29136-29138 with byte-identical timestamps; the head of the replayed triple has a DIFFERENT parentUuid; the first copy carries top-level usage (2, 708, 474834, 3997) on all three records and the replayed copy carries (0, 0, 0, 0) on all three, while `usage.iterations[0]` on the zeroed copies still holds {input_tokens 2, output_tokens 708, cache_read_input_tokens 474834, cache_creation_input_tokens 3997}. The other mismatching id is a DIFFERENT shape: three consecutive records at lines 5470-5472 in a file with 0 duplicate uuids, output_tokens 2 then 1404 then 1404 with the other three fields equal - a mid-stream partial, not a replay.",
          "rule": "Per message.id, per file: an id is 'mismatching' when its member records disagree on any of the four usage fields. Duplicate uuids counted per file as uuids occurring on more than one line.",
          "note": "The behavior sentence reproduced verbatim, down to the shape of the record triple; only the sample counts needed correcting (claim said 3 mismatching ids in one large session, measured 1). The second shape found here strengthens rather than weakens the design rationale: a mid-stream partial breaks first-wins in the OPPOSITE direction (first-wins would under-count 1402 output tokens), so per-field MAX is the only rule that survives both shapes. Code snippet exists verbatim at src/stats.rs 250-256."
        }
      ]
    },
    {
      "id": "CMP-018",
      "area": "compaction",
      "behavior": "A compaction does NOT rewrite the transcript: Claude Code APPENDS a `type:\"system\"` `subtype:\"compact_boundary\"` record and then, on the VERY NEXT line, a `type:\"user\"` `isCompactSummary:true` `isVisibleInTranscriptOnly:true` record, so every pre-compaction line number stays valid and the pre-compaction records stay in the same file below the boundary. The boundary is the append point and the summary is always boundary+1 (13 of 13 events measured); the transcript writer is an append call (`appendEntryToFileAsync`), never a rewrite of earlier lines.",
      "depends": "The whole `Lnnnn` addressing contract rests on it: `verbatim` reconstructs the clipped turns out of the SAME file (its crossed-summary banner names the summary's own line and says the turns below predate it), a `csift show --line` refetch printed before a compaction still resolves the identical record afterwards, and `image`'s stable id `L<line>i<n>` is stable for exactly this reason. If compaction rewrote or renumbered the file, every stored line address in a prior answer would go stale.",
      "code": [
        {
          "path": "src/image.rs",
          "lines": "8-11",
          "snippet": "//! Stable image id = `L<line>i<n>`: the 1-based JSONL line of the carrying record plus the\n//! 1-based ordinal of the image among that record's image blocks. It is stable because the\n//! transcript is append-only, and it is consistent with the `Lnnnnn` line references used\n//! across `recover` / `turns` / `search` (so an id surfaced there feeds straight back here)."
        },
        {
          "path": "src/turns/build.rs",
          "lines": "5-9",
          "snippet": "/// Build the per-turn slices + summary dedup sets from a session's line-numbered\n/// records. Turn segmentation reuses the single shared engine\n/// [`group_turn_indices_deduped`], so an esc-cancel / edit-resend draft never surfaces as a\n/// phantom turn (§6.4.1); a compaction summary is a turn MEMBER (it is excluded from\n/// genuine-user), so the walk is transparent to it."
        },
        {
          "path": "src/turns/select.rs",
          "lines": "294-301",
          "snippet": "/// The EXACT compaction-boundary banner line a crossed summary renders to (no trailing\n/// newline). The renderer and the budget reservation both call this so the reserved\n/// banner length is byte-for-byte what is emitted.\npub(crate) fn boundary_banner_line(line_no: usize) -> String {\n    format!(\n        \"{0} compaction boundary · summary at L{1} · (turns below predate it) {0}\",\n        \"══\", line_no\n    )"
        }
      ],
      "instrument": "Note a pre-compaction record's `Lnnnn` from any csift surface, let the session compact, then re-run `csift show @<id> --line <that same line> --format json`: it must return the identical record (same `uuid`). Then `csift search '' @<id> -t harness.compaction.boundary --format json | jq -r '.hits[].line'` must be greater than every pre-compaction line, and the summary's line must be exactly one greater than the boundary's. NOTE the jq path: search JSON nests matches inside `{\"hits\":[...]}` exchange rows, so a top-level `.line` selects nothing. Counting rule: one boundary record plus one immediately following summary record appended per compaction event; line numbers are 1-based and per file.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "AGENTS.md section 3.5; src/image.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt ; rg -o '.{0,90}subtype:\"compact_boundary\".{0,150}' cc258.strings.txt ; rg -o '.{0,140}isCompactSummary:!0.{0,120}' cc258.strings.txt ; rg -o 'appendEntryToFile[A-Za-z]*' cc258.strings.txt | sort | uniq -c  --  (2) structural scan of one transcript, run once per file: python3 -c \"import json,sys\\nb=[];s=[];t=[]\\nfor i,l in enumerate(open(sys.argv[1],errors='replace'),1):\\n    if not l.startswith('{'): continue\\n    try: o=json.loads(l)\\n    except Exception: continue\\n    ts=o.get('timestamp')\\n    if isinstance(ts,str): t.append((i,ts))\\n    if o.get('type')=='system' and o.get('subtype')=='compact_boundary': b.append((i,ts))\\n    if o.get('type')=='user' and o.get('isCompactSummary'): s.append(i)\\nprint('boundary_lines',[x for x,_ in b]); print('summary_lines',s)\\nprint('summary_minus_boundary',[y-x for (x,_),y in zip(b,s)])\\nprint('below_boundary_with_LATER_ts',[sum(1 for (l,ts) in t if l<bl and ts>bts) for bl,bts in b])\\nprint('timestamped_records',len(t))\" ~/.claude/projects/<encoded-project-dir>/<session>.jsonl  --  (3) the before/after line-address test: csift search 'csift show @[0-9a-f]{8}[^ ]* --line [0-9]+' . --no-subagents -t agent.tool.use --format json   (finds line addresses that were issued and answered INSIDE a transcript at a known timestamp), then re-run each recovered address today: csift show @<self> --line 37..38 --format json  and  csift show @<self> --line 23676  --  (4) csift search '' @<session> --no-subagents -t harness.compaction.boundary --format json | jq -r '.hits[].line' (and -t harness.compaction.summary)  --  (5) csift verbatim @<session> --budget 60000 | rg 'compaction boundary'  --  (6) csift search '' @<self> --no-subagents --count-by label | rg thinking",
          "observed": "BINARY 2.1.258 -- the boundary constructor is verbatim present: 'function Sce(e,n,r,o,d){return{type:\"system\",subtype:\"compact_boundary\",content:\"Conversation compacted\",isMeta:!1,timestamp:new Date().toISOString(),uuid:oT(),level:\"info\",compactMetadata:{trigger:e,preTokens:n,userContext:o,messagesSummarized:d'; the summary constructor is verbatim present: 'isCompactSummary:!0,isVisibleInTranscriptOnly:!0'; the post-compaction message array is assembled boundary-first, summary-second: 'nt=mh([xt,...Tn,...rt,...ht]); xt.compactMetadata.postTokens=nt' with xt the boundary and Tn=[{...isCompactSummary:!0...}]; the transcript writer is an append: 'appendEntryToFileAsync' occurs 4x, with the error string 'appendEntryToFileAsync: append to ${e} failed: '. ON DISK -- 13 compaction events scanned in full across the two largest compacted transcripts of one project dir: boundary lines [8351,14605,19195,25509,30873,35947,40702,44001] and [4806,6196,7022,8035,11968]; summary_minus_boundary = 1 for 13 of 13 events; below_boundary_with_LATER_ts = [0,0,0,0,0,0,0,0] and [0,1,0,1,0], and the two exceptions are each the single line at B-1 carrying a timestamp only +0.199 s / +0.132 s later than the boundary (ordinary async flush of the record written just before). BEFORE/AFTER LINE ADDRESS -- a command recorded in a transcript at 2026-08-31T13:11:20Z had asked that same transcript for '--line 37..38 --format json' and its recorded answer was '37 agent.thinking' / '38 agent.thinking.narration'; re-running the identical command today returns '37 agent.thinking 2026-08-12T00:58:27.789Z' / '38 agent.thinking.narration 2026-08-12T00:58:30.303Z' -- identical, with 3 compaction events (2026-09-01T12:37:33Z, 2026-09-01T15:33:45Z, 2026-09-02T03:37:54Z) landing in between. Second receipt: at 2026-08-27T12:09:09Z the same transcript answered 'csift show ... --line 23676' with 't103  2026-08-22 21:19:43.414 AEST(UTC+10) / agent.tool.use > agent.tool.result Bash  L23676  Bash {\"command\":\"SP=...'; re-running it today returns the same turn t103, the same turn timestamp 2026-08-22 21:19:43.414 AEST(UTC+10), the same label pair, the same L23676 and the same command head -- with 4 compaction events in between. The same transcript's label census over that window only grew: 1274 -> 2094 agent.thinking and 303 -> 493 agent.thinking.narration. CROSS-CHECK -- csift search -t harness.compaction.boundary returns lines [4806,6196,7022,8035,11968] and -t harness.compaction.summary returns [4807,6197,7023,8036,11969], matching the independent raw scan exactly; csift verbatim on that transcript prints five banners naming L4807, L6197, L7023, L8036, L11969 ('== compaction boundary . summary at L4807 . (turns below predate it) ==').",
          "rule": "One compaction event = exactly one line with type=\"system\" and subtype=\"compact_boundary\" plus the line immediately after it with type=\"user\" and isCompactSummary truthy; line numbers are 1-based and per file. 13 events counted = every event in the two transcripts that csift stats reports with the highest compaction counts in one project dir (8 and 5; the dir holds 17 events across 5 transcripts, the remaining 4 were not scanned). 'below_boundary_with_LATER_ts' counts, for each boundary at line B with timestamp T, how many timestamped records at a line < B carry a timestamp string greater than T -- a rewrite or renumber would put many post-compaction records below B, so this count is the append test; 0 is the pass. The before/after test counts compaction events whose boundary timestamp falls strictly between the timestamp of a line address recorded inside a transcript and the moment that same address is re-fetched; a pass requires the re-fetched record to be byte-identical in turn number, timestamp, label and text head.",
          "note": "Holds, with three wording/number refinements and three forward-looking cautions. Refinements: (a) the ordering is not merely 'summary greater still' -- the summary is exactly boundary+1 in 13 of 13 measured events, and the binary assembles the post-compaction array boundary-first; (b) the claim's instrument line uses `jq -r .line`, which selects nothing against the real envelope (matches live under `.hits[]`); (c) `compactMetadata` in 2.1.258 carries more than the four fields the ledger's prose names -- the constructor writes `{trigger, preTokens, userContext, messagesSummarized}` and `postTokens` is patched in afterwards, and other call sites add `preservedMessages` / `preservedSegment` (with `headUuid`/`anchorUuid`/`tailUuid`), so a consumer must not assume a closed field set. Cautions, none of which refute the claim: (1) 2.1.258 carries a second, distinct system subtype `microcompact_boundary`, which the transcript renderer explicitly returns null for; zero records with that subtype exist anywhere in the scanned project dir, so its on-disk shape is untested here and csift does not model it -- a session that micro-compacts would decide it. (2) 2.1.258 also has a `content-replacement` line type written through `insertContentReplacement` -> `appendEntry`, i.e. even a logical replacement of earlier content is expressed as a NEW appended line rather than an edit of an old one, which is consistent with the claim; zero such lines exist in the scanned corpus. (3) The append-only guarantee is per file: a session forked or cloned at a compaction is minted as a SEPARATE transcript, so a stored `Lnnnn` stays valid in the file it was read from but says nothing about the clone. Both before/after receipts come from the transcript this verification ran inside, which is precisely what makes them usable: the addresses were issued and answered by earlier work in that file, days and several compactions before the re-fetch. All three cited csift code sites were opened and match verbatim at the stated paths and lines, so corrections.code is empty: src/image.rs lines 8-11 (the `L<line>i<n>` stable-id doc naming the append-only transcript), src/turns/build.rs lines 5-9 (the compaction summary as a turn MEMBER), and src/turns/select.rs lines 294-301 (`boundary_banner_line`), whose format string emits exactly the banner observed live."
        }
      ]
    },
    {
      "id": "ELI-001",
      "area": "elicitation",
      "behavior": "An AskUserQuestion tool_use carries input.questions as an ORDERED array of {question, header, multiSelect, options[]}, and the answering carrier's toolUseResult echoes it; an option is {label, description, preview?} - the OPTIONAL third field `preview` is present on 158/416 ask-side and 155/378 echoed option objects (38-41%) and is operator-visible content (2.1.258: 'Optional preview content rendered when this option is focused. Use for mockups, code snippets, or visual comparisons'), not decoration.",
      "depends": "csift's auq_exchange reads only label + description, so the per-option `preview` is dropped from every reconstructed AUQ unit; the rendered header is `[AskUserQuestion . N question(s)]` (a middle-dot separator, singular for N==1), not `[AskUserQuestion - N questions]`.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "49-53",
          "snippet": "                    let header = q.get(\"header\").and_then(serde_json::Value::as_str);\n                    let question = q\n                        .get(\"question\")\n                        .and_then(serde_json::Value::as_str)\n                        .unwrap_or_default();"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "63-70",
          "snippet": "                                    let label =\n                                        o.get(\"label\").and_then(serde_json::Value::as_str)?;\n                                    let desc = o\n                                        .get(\"description\")\n                                        .and_then(serde_json::Value::as_str)\n                                        .filter(|s| !s.is_empty())\n                                        .map(str::to_string);\n                                    Some((label.to_string(), desc))"
        },
        {
          "path": "src/model/tests/boundaries.rs",
          "lines": "217",
          "snippet": "\"questions\":[{\"header\":\"Routing\",\"multiSelect\":false,\"options\":[{\"description\":\"the inbound path\",\"label\":\"Route A\"},{\"description\":\"the outbound path\",\"label\":\"Route B\"}],\"question\":\"which route for the queue?\"}]"
        }
      ],
      "instrument": "`csift search \"\" <target> -t user.answer --format json | jq -r .hits[].line`, then `csift show <target> --line <n> --raw | jq '.toolUseResult.questions[0] | keys'`: the keys must be `header`, `multiSelect`, `options`, `question`, and each option object must carry `label` + `description`. Counting rule: keys of ONE question object.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 4.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<count key sets of toolUseResult.questions[] and of every questions[].options[] object>'  AND  csift search 'AskUserQuestion' -t agent.tool.use --raw --max-count 0 | python3 -c '<same census on the ask side input.questions>'  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Optional preview content rendered when this option is focused'",
          "observed": "Answer side: 95 answered carriers, 118 question objects, key set ('header','multiSelect','options','question') on 118/118; 378 option objects, of which ('description','label') 223 and ('description','label','preview') 155. Ask side: 111 distinct AskUserQuestion tool_use blocks, 106 with a parseable input (5 carry only '__unparsedToolInput'), 129 question objects with the same 4 keys, 416 option objects, ('description','label') 258 and ('description','label','preview') 158. Binary 2.1.258 carries the option-field descriptions 'The display text for this option that the user will see and select.', 'Explanation of what this option means or what will happen if chosen.' and 'Optional preview content rendered when this option is focused.'",
          "rule": "one key set per question object and per option object, over every record csift labels user.answer corpus-wide (deduped by record uuid; --raw already deduped 116 hits to 95 distinct records, the 21 duplicates being clone-copied transcripts)",
          "note": "Code sites verify verbatim: src/model/exchange.rs:49-53 and 63-70 are unchanged, and the fixture line src/model/tests/boundaries.rs:217 still carries the quoted questions[] shape. The structural claim is exactly right; only the option-object field list needed widening. csift also ignores annotations[q].preview (16 of 46 annotation objects in the corpus are {preview} only, 1 is {notes,preview}), so an operator's preview-based choice is invisible in the reconstructed unit."
        }
      ]
    },
    {
      "id": "ELI-002",
      "area": "elicitation",
      "behavior": "The answered-AskUserQuestion carrier's toolUseResult has TWO always-present keys - questions[] (the echoed ordered array) and answers{} (keyed by the VERBATIM question string) - plus THREE conditional ones: annotations{} (per-question {notes?, preview?}; absent on 1 of 95 corpus carriers), response (the operator's freeform text when they typed instead of selecting; new in the 2.1.258 schema, not yet seen on disk here) and afkTimeoutMs (set only when the dialog auto-resolved on idle; seen on 3 corpus carriers). chosenOption / answer / selected still do not exist.",
      "depends": "The inline synthesized tool_result string is not TRUNCATED - it is lossy by omission: it drops every option description (0 of 378 retained) and every non-chosen label (66 of 378 retained), and it skips any question that has neither an answer nor notes.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "26-31",
          "snippet": "        let tur = self.tool_use_result_value();\n        let answers = tur\n            .as_ref()\n            .and_then(|t| t.get(\"answers\"))\n            .and_then(serde_json::Value::as_object)\n            .filter(|m| !m.is_empty());"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "37-43",
          "snippet": "            // `annotations` map (§4.4) - per-question `{notes?, preview?}`; when the answer\n            // is the `\"(notes only)\"` placeholder the user's ENTIRE real message lives here.\n            let annotations = tur\n                .as_ref()\n                .and_then(|t| t.get(\"annotations\"))\n                .and_then(serde_json::Value::as_object)\n                .filter(|m| !m.is_empty());"
        },
        {
          "path": "src/model/tests/boundaries.rs",
          "lines": "217",
          "snippet": "\"answers\":{\"which route for the queue?\":\"(notes only)\"},\"annotations\":{\"which route for the queue?\":{\"notes\":"
        }
      ],
      "instrument": "`csift search \"\" <target> -t user.answer --format json | jq -r .hits[].line` then `csift show <target> --line <n> --raw | jq '.toolUseResult | keys'`: the keys must be `answers` / `questions` / `annotations`. Counting rule: keys of ONE record's toolUseResult object.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "SKILL.md wrong-assumption table (AUQ field-name rows); src/model/exchange.rs auq_exchange doc; SPEC.md section 4.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<count sorted(toolUseResult.keys()) per record; count option labels/descriptions that also occur in the inline tool_result string>'  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Freeform text the user typed instead of selecting a structured option'",
          "observed": "toolUseResult key sets over 95 answered carriers: ('annotations','answers','questions') 94, ('answers','questions') 1. Zero records carry chosenOption, answer or selected as a key. The 2.1.258 result schema is questions + answers + response:i().optional().describe(\"Freeform text the user typed instead of selecting a structured option\") + annotations + afkTimeoutMs:A().int().positive().optional().describe(\"Set when the dialog auto-resolved after this many milliseconds of idle (user away from keyboard). Absent on every human-resolved path.\"), and the builder emits `{questions:r,answers:o,...f?.trim()&&{response:f},...d&&{annotations:d},..._&&{afkTimeoutMs:_}}`. Three on-disk carriers already show the key set ('afkTimeoutMs','annotations','answers','questions'). Of 378 echoed option objects, 66 labels and 0 descriptions occur anywhere in the inline synthesized string.",
          "rule": "one key set per record's toolUseResult object; label/description retention counted as substring presence in the same record's tool_result content string, one count per option object",
          "note": "Code sites verify verbatim at src/model/exchange.rs:26-31 and 37-43. The new `response` key matters for csift: auq_exchange reads answers + annotations.notes only, so on a 2.1.258 carrier where the operator typed freeform text the prose would live in toolUseResult.response and be dropped (the synthesized string would read 'The user responded: ...'); no such record exists in this corpus yet, so the gap is prospective, not observed."
        }
      ]
    },
    {
      "id": "ELI-003",
      "area": "elicitation",
      "behavior": "An ANSWERED AskUserQuestion carrier is a type:\"user\" record with a NON-errored tool_result block AND a non-empty structured toolUseResult.answers object. Those two signals co-occur on 95/95 corpus carriers, but the THIRD signal (a marker string csift knows) now fails on 16/95 - Claude Code writes a third prefix csift does not list. A fourth carrier shape exists that is neither answer nor error: the idle auto-resolve, non-errored with answers:{} - excluded only because csift requires answers to be NON-EMPTY.",
      "depends": "`is_auq_answer_boundary` opens a turn on the structured signal and keeps the marker only as a fallback for older records without `toolUseResult`, so an AUQ answer classifies `user.answer`; keying on the marker alone loses answers on pre-`toolUseResult` records, and keying on the carrier shape alone mints turns out of cancellations.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "248-252",
          "snippet": "    /// A CANCELLED / rejected / validation-errored AUQ (no `answers`, `is_error:true`,\n    /// or a `Cancelled…` / `<tool_use_error>…` body) is NOT a boundary - those carry no\n    /// typed user message. Verified on real data: all 81 answered carriers have\n    /// non-empty `toolUseResult.answers`, the marker string, and `is_error` false; the\n    /// rejection/cancel carriers have none of the three."
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "271-279",
          "snippet": "        if !has_non_errored_tool_result {\n            return false;\n        }\n        // Primary signal: structured, non-empty `toolUseResult.answers`.\n        if self.has_auq_answers() {\n            return true;\n        }\n        // Fallback (older records without `toolUseResult`): the synthesized marker.\n        self.is_auq_answer()"
        }
      ],
      "instrument": "`csift search \"\" @<session> -t user.answer --format json | jq -r .hits[].line`, fetch each with `csift show @<session> --line <n> --raw`, and check every one has `\"answers\":{` with at least one key and no `\"is_error\":true`. Counting rule: AUQ tool_result carriers, split by the three signals.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/model/predicates.rs is_auq_answer_boundary doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<per record: is_error of every tool_result block, emptiness of toolUseResult.answers, and which marker prefix the content starts with>'  plus a per-ask outcome census joining every AskUserQuestion tool_use id to its tool_result block inside the same file",
          "observed": "95 answered carriers: is_error false on 95/95 tool_result blocks, non-empty toolUseResult.answers on 95/95, but only 79/95 start with one of csift's two known markers ('Your questions have been answered'); the other 16 start with a third phrasing, 'The user answered: '. Per-ask census over 111 distinct AskUserQuestion tool_use ids: 95 answered, 13 errored (is_error true, toolUseResult a plain STRING with no answers map), 3 auto-resolved after idle (is_error absent, toolUseResult {afkTimeoutMs:60000, annotations:{}, answers:{}, questions:[1]}, content starting 'No response after 60s - the user may be away from keyboard').",
          "rule": "one row per distinct AskUserQuestion tool_use block id found corpus-wide, joined to the tool_result block carrying the same id in the same transcript",
          "note": "Code sites verify verbatim at src/model/predicates.rs:248-252 and 262-279. The primary/fallback ordering is what saves csift here: the 16 third-phrasing records are recognised through toolUseResult.answers, and the marker fallback only runs on records with no toolUseResult (none in this corpus). The doc comment's 'all 81 answered carriers have ... the marker string' is now false and should be reworded."
        }
      ]
    },
    {
      "id": "ELI-004",
      "area": "elicitation",
      "behavior": "Claude Code 2.1.258 synthesizes the AskUserQuestion tool_result from a five-way branch. 'User has answered your questions' is gone from the binary and never appears as a real marker in this corpus. The two live prefixes are 'Your questions have been answered: ' (every question answered by clicking a listed option, no notes) and 'The user answered: ' (anything else - notes attached, or an answer that is not one of the offered labels); the other three are 'The user responded: ' (freeform text), 'The user did not answer the questions.' and the idle path 'No response after Ns ...' optionally followed by 'Before going idle the user had selected: ...'.",
      "depends": "csift recognises an AUQ answer by START-anchored prefix (`trim_start` + `starts_with`) against a LIST of markers, three since v0.10.1: the retired `User has answered your questions` (kept for historical transcripts), `Your questions have been answered`, and 2.1.258's `The user answered:`. A single hardcoded marker missed the dominant form entirely and silently dropped real `user.answer` turns, while a `contains` check false-positived on any tool result that merely QUOTES a marker (which relabeled whole documentation files as `user.answer`). The list is the FALLBACK for records without `toolUseResult` (the primary boundary signal is a non-empty `toolUseResult.answers`); the `The user did not answer the questions.` branch is deliberately excluded because it opens no turn. All three phrasings are also verifiable synth markers in the search prefilter.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "3-9",
          "snippet": "/// The synthesized prefixes Claude Code writes into the `tool_result` answering an\n/// `AskUserQuestion` (§4.4). CC has shipped THREE phrasings for the same synthesized\n/// answer - verified across real `~/.claude/projects` data and the 2.1.258 binary:\n///\n/// - `\"User has answered your questions: \\\"<q>\\\"=\\\"<a>\\\". …\"`\n/// - `\"Your questions have been answered: \\\"<q>\\\"=\\\"<a>\\\". …\"`  (the dominant form\n///   in older data; a single hardcoded marker missed it entirely)"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "15",
          "snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &["
        },
        {
          "path": "src/model/markers.rs",
          "lines": "34-38",
          "snippet": "#[must_use]\npub fn is_auq_answer_text(text: &str) -> bool {\n    let head = text.trim_start();\n    AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "493",
          "snippet": "// The agents-stopped kill notice renders a fabricated `[subagent stopped]` head."
        },
        {
          "path": "src/model/markers.rs",
          "lines": "15-22",
          "snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &[\n    \"User has answered your questions\",\n    \"Your questions have been answered\",\n    // CC 2.1.258's third branch (`The user answered: \"<q>\"=\"<a>\". Read the answers\n    // carefully ...`); the fourth branch `The user did not answer the questions.` is\n    // the UNANSWERED case and must never open a turn, so it stays out.\n    \"The user answered:\",\n];"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "487-495",
          "snippet": "    let mut verifiable: Vec<&[u8]> = vec![\n        b\"<task-notification>\",\n        br#\"\"answers\"\"#,\n        b\"User has answered your questions\",\n        b\"Your questions have been answered\",\n        b\"The user answered:\",\n        // The agents-stopped kill notice renders a fabricated `[subagent stopped]` head.\n        b\"stopped by the user\",\n    ];"
        }
      ],
      "instrument": "`rg -c 'User has answered your questions' ~/.claude/projects -g '*.jsonl'` versus `rg -c 'Your questions have been answered'`; both must be non-zero over a corpus spanning versions, and a file carrying both spans the transition. Counting rule: one per tool_result content STARTING with the marker, which `csift search \"\" -t user.answer -c` reports directly.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "src/model/markers.rs AUQ_ANSWER_MARKERS doc comment; SPEC.md section 4.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'answered your questions|questions have been answered|The user answered|The user responded|did not answer the questions|Before going idle the user had selected'  AND  csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<bucket each carrier by the prefix its tool_result content starts with, cross-tabbed with the record version field>'",
          "observed": "'User has answered your questions' has ZERO occurrences in the 2.1.258 binary. The binary emits five distinct synthesized results from one branch: `Before going idle the user had selected: ${_}.`, `The user responded: ${r}`, `Your questions have been answered: ${_}. You can now continue with these answers in mind.`, `The user answered: ${_}. Read the answers carefully - they may request clarification, changes, or that you not proceed - and follow what they actually say.`, and `The user did not answer the questions.` The branch chooses 'Your questions have been answered' only when every question was answered by a listed option with no notes. On disk: 79 carriers start with 'Your questions have been answered' (versions 2.1.156 through 2.1.258), 16 start with 'The user answered: ' (2.1.217 through 2.1.251), 0 start with 'User has answered your questions'. The 112 corpus hits for that dead phrase are all quotes inside prose (labels: agent.tool.result 198, agent.tool.use 69, agent.message 24 - zero user.answer).",
          "rule": "one count per answered carrier, bucketed by the prefix the tool_result content STARTS with after trim_start; binary counts are literal string occurrences",
          "note": "The START-anchored matching design is vindicated (every one of the 112 'User has answered your questions' occurrences in this corpus is a mid-prose quote, and a contains-check would relabel them). Impact today is bounded: is_auq_answer_text is only reached as the fallback in is_auq_answer_boundary (src/model/predicates.rs:279) for records with no toolUseResult, and the search prefilter's `\"answers\"` needle still admits the third-phrasing lines - but the marker list and its doc comment are stale as of 2.1.258. code-site note: src/model/markers.rs:14-17 AUQ_ANSWER_MARKERS lists a phrase 2.1.258 no longer emits and omits the live 'The user answered' / 'The user responded' forms; src/search/matcher.rs:485-492 repeats the same two byte needles in the verifiable synth-marker list."
        }
      ]
    },
    {
      "id": "ELI-005",
      "area": "elicitation",
      "behavior": "When the operator answers by typing prose instead of clicking, the value in `answers` is the literal placeholder `(notes only)` and their whole message lives in annotations[<verbatim question>].notes. `(no option selected)` is NOT a sibling value in `answers`: it is the string the inline synthesized tool_result substitutes for that same state (2.1.258 renders `\"<q>\"=(no option selected) notes: <prose>`).",
      "depends": "csift renders the notes BESIDE the placeholder (`A3: (notes only) note: ...`), it does not replace the placeholder with them - so the placeholder still appears in output, and a reader must not treat its presence as a lost answer.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "37-43",
          "snippet": "            // `annotations` map (§4.4) - per-question `{notes?, preview?}`; when the answer\n            // is the `\"(notes only)\"` placeholder the user's ENTIRE real message lives here.\n            let annotations = tur\n                .as_ref()\n                .and_then(|t| t.get(\"annotations\"))\n                .and_then(serde_json::Value::as_object)\n                .filter(|m| !m.is_empty());"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "80-88",
          "snippet": "                    // Free-text notes the user attached to THIS answer. When the answer is\n                    // the `\"(notes only)\"` placeholder, the notes ARE the user's message -\n                    // dropping them silently swallowed the whole turn (the common path,\n                    // since the user routinely answers AUQs with typed prose, not a click).\n                    let note = annotations\n                        .and_then(|a| a.get(question))\n                        .and_then(|v| v.get(\"notes\"))\n                        .and_then(serde_json::Value::as_str)\n                        .filter(|s| !s.is_empty());"
        },
        {
          "path": "src/model/tests/boundaries.rs",
          "lines": "217",
          "snippet": "\"answers\":{\"which route for the queue?\":\"(notes only)\"},\"annotations\":{\"which route for the queue?\":{\"notes\":\"never conflate the two — Route A is inbound only, Route B is outbound only\"}}},\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"q1\",\"content\":\"User has answered your questions: \\\"which route for the queue?\\\"=\\\"(notes only)\\\". You can now continue.\"}]}}\"#,"
        }
      ],
      "instrument": "`csift search '(notes only)' -t user.answer --no-truncate`: every hit must render the notes prose as the answer, not the placeholder. Counting rule: one unit per answered AUQ carrier. The unit test `auq_exchange_surfaces_notes_when_answer_is_notes_only` in src/model/tests/boundaries.rs pins it on a real-captured shape.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.1.0",
        "source": "SKILL.md notes-only row; src/model/exchange.rs:37-43 comment; SPEC.md section 4.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '\\(notes only\\)' -t user.answer -c ; csift show @<session> --line <n> (on the first such hit's own refetch line) ; csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<for every answers value equal to the placeholder, check which placeholder string the inline tool_result text uses>' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'no option selected|notes only'",
          "observed": "31 exchanges match the placeholder under -t user.answer. Every one of the 29 placeholder answer values in the corpus is the literal '(notes only)'; in all 29 the inline synthesized string writes '(no option selected)' for the same question, never '(notes only)'. The 2.1.258 binary holds both strings twice each, with the constant `var h6t=\"(notes only)\"` used as the answers value and the renderer `B?`\"${C}\"=\"${I}\"`:`\"${C}\"=(no option selected)`` used for the string. A fetched record renders `A3: (notes only) note: <the operator's prose>` and `A4: (notes only) note: <prose>` - placeholder and notes both present.",
          "rule": "one count per (question, answer) pair whose answers value is a placeholder; the render check is one fetched record unit read in full",
          "note": "Code sites verify verbatim at src/model/exchange.rs:37-43 and 80-88. In 2.1.258 the same branch also honours annotations[q].preview (`selected preview:\\n<preview>`), which csift does not render."
        }
      ]
    },
    {
      "id": "ELI-006",
      "area": "elicitation",
      "behavior": "The rendered header is `[AskUserQuestion . N question(s)]` (middle dot, singular for N==1).",
      "depends": "csift zips `questions[]` with `answers{}` and renders every pair under one `[AskUserQuestion - N questions]` header, so no intervention is undercounted; counting one intervention per record shrinks every human-turn denominator by roughly 5.5%.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "44-48",
          "snippet": "            let mut out = String::new();\n            let n = questions.map_or(answers.len(), Vec::len);\n            out.push_str(&format!(\"[AskUserQuestion · {n} question{}]\", plural(n)));\n            if let Some(qs) = questions {\n                for (i, q) in qs.iter().enumerate() {"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "89-93",
          "snippet": "                    out.push_str(&format!(\"\\nQ{} \", i + 1));\n                    if let Some(h) = header {\n                        out.push_str(&format!(\"({}): \", normalize_line(h)));\n                    }\n                    out.push_str(&normalize_line(question));"
        }
      ],
      "instrument": "`csift search \"\" @<session> -t user.answer --no-truncate` and read the rendered `[AskUserQuestion - N questions]` header on each unit. Counting rule: one exchange per answered AUQ record; pairs counted from N in that header, so records with N>1 are the multi-question set.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "SKILL.md wrong-assumption table (121 records / 166 pairs)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search \"\" -t user.answer --format json --max-count 0 | python3 -c '<count exchange rows and distinct record uuids>'  AND  csift search \"\" -t user.answer --raw --max-count 0 | python3 -c '<sum len(toolUseResult.questions) and count records with len>1>'",
          "observed": "116 exchange rows collapse to 95 distinct record uuids (the 21 duplicates are clone-copied transcripts double-counting the same records). Those 95 carry 118 question-answer pairs; 17 records (17.9%) carry more than one question (13 with 2, 2 with 3, 2 with 4), hiding 23 pairs behind the first question. On the ask side, 106 parseable AskUserQuestion inputs carry 129 questions, 17 of them multi-question (16.0%). Every pair renders under one `[AskUserQuestion . N question(s)]` header with its own `Q<i>` / `A<i>` lines.",
          "rule": "one record per distinct uuid csift labels user.answer corpus-wide; pairs = sum of len(toolUseResult.questions) over those records; 'multi' = records with len > 1",
          "note": "Code sites verify verbatim at src/model/exchange.rs:44-48 and 89-93. The phenomenon holds and is worth stating; only the absolute numbers were corpus-bound. Any restatement should name the counting rule, since raw hit counts overstate by the clone-duplication factor (116 vs 95 here)."
        }
      ]
    },
    {
      "id": "ELI-007",
      "area": "elicitation",
      "behavior": "Add the non-errored non-answer shape: an AskUserQuestion that auto-resolves after idle lands with is_error ABSENT, content 'No response after 60s - the user may be away from keyboard...', and toolUseResult {questions, answers:{}, annotations:{}, afkTimeoutMs}. Only the non-EMPTY answers requirement keeps it out of user.answer.",
      "depends": "is_auq_answer_boundary refuses any errored carrier before it looks for answers, so a cancelled question never classifies user.answer - but it is NOT true that it never opens a turn: 6 of the 13 errored AUQ carriers here carry the typed 'To tell you how to proceed, the user said:' tail and open a turn through is_plan_rejection_boundary, classified user.rejection (that is correct - the operator did type a message).",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "262-270",
          "snippet": "        let mut has_non_errored_tool_result = false;\n        for b in blocks {\n            if let Block::ToolResult { is_error, .. } = b {\n                if is_error.unwrap_or(false) {\n                    return false; // an errored AUQ result (cancel/reject) is never a boundary\n                }\n                has_non_errored_tool_result = true;\n            }\n        }"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "20-22",
          "snippet": "        if !self.is_auq_answer_boundary() {\n            return None;\n        }"
        }
      ],
      "instrument": "`csift search \"\" <target> --count-by result` (buckets `ok` | `error`), then fetch an `error` AUQ carrier with `csift show <target> --line <n>`: it must not be labeled `user.answer`. Counting rule: one record per bucket.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "src/model/predicates.rs is_auq_answer_boundary body; AGENTS.md section 3.3 case 2; SPEC.md section 4.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "<join every distinct AskUserQuestion tool_use id to the tool_result block carrying it in the same transcript, then bucket by is_error and content shape>  followed by  csift show @<session> --line <n> on one carrier of each bucket",
          "observed": "Of 111 distinct AskUserQuestion asks on disk, 16 have no answer carrier: 6 rejections whose body carries the typed tail 'To tell you how to proceed, the user said:', 2 bare rejections, 5 '<tool_use_error>InputValidationError: AskUserQuestion was called with input that could not be parsed as JSON.', and 3 idle auto-resolves. All 13 rejection/validation carriers have is_error true and a toolUseResult that is a plain STRING ('Error: The user doesn't want to proceed with this tool use. ...'), with no answers map. csift renders a bare/validation one as an agent.tool.use paired with an agent.tool.result marked [error] for AskUserQuestion, and a typed-tail one as user.rejection - never user.answer.",
          "rule": "one row per distinct AskUserQuestion tool_use id, bucketed by its tool_result block's is_error flag and content prefix",
          "note": "Code sites verify verbatim at src/model/predicates.rs:262-270 and src/model/exchange.rs:20-22. On a rejected AskUserQuestion, Claude Code injects its own prose into the typed tail ('The user wants to clarify these questions. This means they may have additional information, context or questions for you...'), so the user.rejection body on that path is partly harness text rather than purely operator text."
        }
      ]
    },
    {
      "id": "ELI-008",
      "area": "elicitation",
      "behavior": "A PENDING AskUserQuestion or ExitPlanMode is never flushed to the transcript while it waits: Claude Code buffers the WHOLE assistant turn and commits the `tool_use` to the persisted message array only after the interactive tool resolves (verified across 51 sessions with zero counterexamples; on one measured session the ask's `tool_use` was still absent from disk 37 minutes after its own emit timestamp, and the answering `tool_result` landed about 89 minutes after that timestamp). If such a `tool_use` is on disk, its answer is too - so an option-picker freeze is byte-identical on disk to a stall.",
      "depends": "csift cannot render a pending-question state from the native transcript at all; the unanswered window is recoverable only from the hook-written sidecar, whose unresolved pendings `search`/`show`/`verbatim`/`list` merge as if native (a pending marker classifies `agent.tool.use`, renders `(elicitation sidecar)` in place of a line number, and every surface that merged one prints `with elicitation sidecar`). A `status` that inferred 'blocked on a question' from the transcript alone would be fabricating.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "4-7",
          "snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
        },
        {
          "path": "src/elicitation.rs",
          "lines": "85-89",
          "snippet": "/// only to carry what the native jsonl does NOT yet hold. An AUQ/ExitPlanMode turn is\n/// buffered in memory and flushed ONLY when the elicitation closes, so its tool id\n/// appearing on a native record - as a `tool_use` block `id` OR a `tool_result`\n/// `tool_use_id` - proves closure (answered or rejected alike). Such a stale pending is\n/// dropped exactly like a sidecar-resolved pair (the same dedup class, silent by design)."
        },
        {
          "path": "src/model/classify.rs",
          "lines": "140-148",
          "snippet": "        // Elicitation sidecar markers (§3.10): a PENDING marker stands in for the native\n        // tool_use CC has not yet written → agent.tool.use (covers the MCP `system` form too,\n        // which carries no tool_use block). A RESOLVED marker is a pairing artifact → no label.\n        if self.is_elicitation_marker() {\n            if self.csift_phase.as_deref() == Some(\"pending\") {\n                push_unique(&mut out, Class::AgentToolUse);\n            }\n            return out;\n        }"
        }
      ],
      "instrument": "With an AskUserQuestion open on screen, run `csift search AskUserQuestion @<that session> -t agent.tool.use` from another terminal: zero native hits while pending. Re-run after answering and the tool_use appears, timestamped at its EMIT instant rather than the answer instant. Counting rule: one record per tool_use block. Requires a live session - it cannot be observed from a static corpus.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.4; SPEC.md sections 4.4 and 6.10; src/elicitation.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "unverifiable-here",
          "instrument": "ls ~/.claude/sessions/*.json | <read each row's status>  AND  <join every AskUserQuestion ask id to its tool_result and check for unreturned asks>  AND  <for each ask/answer pair, compare file order against timestamps: count records physically before the ask whose timestamp falls inside the wait window, and records physically between ask and answer whose timestamp is >5s after the ask>",
          "observed": "No live pending dialog exists to observe: the 7 registry rows read idle x5, busy x1 (2.1.258), shell x1 - none 'waiting'. On the static corpus 111 of 111 AskUserQuestion asks that are on disk also have their tool_result on disk (0 unreturned), which is consistent with the claim's corollary but cannot see the pending window. The file-order probe is not decisive and points both ways: 7 ask/answer pairs (2.1.156, 2.1.199 x2, 2.1.220 x4) have a queue-operation line physically BEFORE the ask whose timestamp falls inside the wait window (consistent with the ask being flushed late), while 3 pairs (2.1.199 multi-question, 2.1.228 multi-question, 2.1.228 single-question) have queue-operation lines physically AFTER the ask spread across the wait window (consistent with the ask already being on disk). Both readings depend on an unverified assumption about when a queue-operation line is written.",
          "rule": "one row per registry file; one row per distinct AskUserQuestion ask id; for the order probe, one classification per ask/answer pair with a wait longer than 60s",
          "note": "What would decide it: with an AskUserQuestion picker open on screen, run `csift search AskUserQuestion @<that session> -t agent.tool.use` (or `csift show @<session> --turn -1 --raw`) from a second terminal and record whether the tool_use is on disk while pending, then re-run after answering. That needs a live session at 2.1.258 and cannot be produced from a static corpus. Code sites verify verbatim at src/elicitation.rs:4-7 and 85-89 and src/model/classify.rs:140-148. Caution for the ledger: the claim's absolute 'never flushed' now sits in tension with ELI-009 in the same ledger, and the file-order evidence above is mixed - the wording should name the versions and shapes it was measured on."
        }
      ]
    },
    {
      "id": "ELI-009",
      "area": "elicitation",
      "behavior": "Since Claude Code 2.1.258 a MULTI-question AskUserQuestion ask IS written to the transcript at QUESTION time rather than held to the end of the turn, while a single-question ask still stays buffered until answered - so an unreturned `AskUserQuestion` / `ExitPlanMode` `tool_use` at the tail is itself a live blocked-on-a-human signal for the multi-question shape only.",
      "depends": "`status`/`wait` carry the tail dialog as its own HITL leg beside the registry `waiting` row and the sidecar, and the evidence line says which leg fired, so a multi-question ask no longer needs the sidecar to be seen and a single-question ask is still honestly attributed to it.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "170-227",
          "snippet": "            ),\n        };\n        evidence.push(Evidence {\n            surface: \"pid\",\n            value: v,\n            age_secs: None,\n        });\n        if let Some(n) = note {\n            notes.push(n);\n        }\n    }\n    match &main_tail.unreturned_use {"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "255-262",
          "snippet": "                    String::new()\n                }\n            ),\n            age_secs: None,\n        });\n    }\n    for p in pending_elicitations {\n        evidence.push(Evidence {"
        }
      ],
      "instrument": "Open a MULTI-question AskUserQuestion in a live session and, while the picker is up, run `csift status @<uuid>` from another terminal: the verdict is `waiting-hitl` with the tail-dialog evidence line, and `csift show @<uuid> --turn -1 --raw` shows the `tool_use` with no paired `tool_result`. Repeat with a SINGLE-question ask: nothing is on disk and only a sidecar pending (or a current registry `waiting` row) can show it. Counting rule: one tool_use id, checked for a matching tool_result id in the same file.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "src/live/verdict.rs:216-220 comment"
      },
      "first_seen_claude_code": "2.1.258",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "unverifiable-here",
          "instrument": "csift search 'AskUserQuestion' -t agent.tool.use --raw --max-count 0 | python3 -c '<group asks by record version and question count>'  AND  <per ask/answer pair at 2.1.251+, measure line gap, wait seconds and records landing inside the wait window>  AND  grep -n 'MULTI-question ask is written at question time' src/live/verdict.rs",
          "observed": "The corpus holds only two AskUserQuestion pairs at 2.1.258 - one single-question (wait 90s) and one multi-question (wait 244s) - and in BOTH the answer record is on the immediately next line (gap 1) with no record landing inside the wait window, so neither can discriminate question-time from resolve-time writing. Across all versions the earlier per-pair order probe found one 2.1.220 MULTI-question ask that looks flushed LATE and one 2.1.228 multi-question ask that looks written EARLY, so the corpus does not support a clean 'since 2.1.258' boundary either. The claim's cited code site does not hold the claim: src/live/verdict.rs:216-227 and 255-262 are the pid-evidence and sidecar-evidence blocks.",
          "rule": "one row per ask/answer pair, keyed by the ask record's version field and its input.questions length",
          "note": "What would decide it: open a MULTI-question AskUserQuestion in a live 2.1.258 session and, while the picker is up, run `csift status @<uuid>` and `csift show @<uuid> --turn -1 --raw` from another terminal; repeat with a single-question ask. The registry currently has no 'waiting' row, so no such window exists on this machine right now. Until that is run the claim rests on a code comment, not on an instrument, and the version boundary in particular is unsupported by the corpus. code-site note: The load-bearing site is src/live/verdict.rs:291-295, the comment '// Two HITL legs beside the sidecar: the registry's `waiting` status (the binary sets / // it whenever a dialog blocks the session: a question, a permission prompt, a plan / // approval, a sandbox/worker request), and an unreturned AskUserQuestion/ExitPlanMode / // at the tail (a MULTI-question ask is written at question time since CC 2.1.258; / // a single-question ask stays buffered until answered - the sidecar's shape).', plus the emitted note at src/live/verdict.rs:345-352 ('the tail holds an unreturned AskUserQuestion/ExitPlanMode call: a multi-question ask is written at question time, a single-question ask stays buffered until answered (the sidecar covers that shape)')."
        }
      ]
    },
    {
      "id": "ELI-010",
      "area": "elicitation",
      "behavior": "An MCP elicitation's inner `elicitation/create` request is an in-memory MCP-client callback: it never becomes a transcript record in any form, so the session gains no record at all between the outer `mcp__*` tool_use and its tool_result while the server waits on the human.",
      "depends": "The sidecar's MCP marker is shaped as a `type:\"system\"` record with NO tool_use block; csift's classify gives any pending marker `agent.tool.use` through a guarded arm (and `search` matches the MCP form on its top-level `content`), so `-t agent.tool.use` reaches all three elicitation kinds uniformly.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "4-7",
          "snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "140-148",
          "snippet": "        // Elicitation sidecar markers (§3.10): a PENDING marker stands in for the native\n        // tool_use CC has not yet written → agent.tool.use (covers the MCP `system` form too,\n        // which carries no tool_use block). A RESOLVED marker is a pairing artifact → no label.\n        if self.is_elicitation_marker() {\n            if self.csift_phase.as_deref() == Some(\"pending\") {\n                push_unique(&mut out, Class::AgentToolUse);\n            }\n            return out;\n        }"
        }
      ],
      "instrument": "Trigger an MCP elicitation and confirm the session transcript gains no record between the outer mcp tool_use and its tool_result (`csift show @<session> --turn -1 --raw` before and after answering); `csift search \"\" @<session>` prints the `with elicitation sidecar` note only when the hook wrote the sidecar. Counting rule: one record per jsonl line added during the pending window (expected zero). Requires a live MCP server.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.10; SPEC.md section 6.10"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "unverifiable-here",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'elicitation/create|notifications/elicitation/complete'  AND  find ~/.claude/projects -name elicitations.jsonl | python3 -c '<census csiftKind / csiftPhase / type over every sidecar line>'  AND  csift search 'elicitation/create' -c",
          "observed": "2.1.258 registers the inner request as an in-process client handler: `e.setRequestHandler(\"elicitation/create\",(o,i)=>n.handle(o,{signal:i.mcpReq.signal}))`, paired with `e.setNotificationHandler(\"notifications/elicitation/complete\", ...)` whose only side effect in that block is a user notification ('MCP server \"...\" confirmed elicitation ... complete', notificationType 'elicitation_complete'); the surrounding constructor takes runElicitationHooks / runElicitationResultHooks. No MCP elicitation has ever occurred on this machine: the 20 sidecar files hold 260 marker lines whose csiftKind is AskUserQuestion 239 and ExitPlanMode 21 - zero MCP - and the 34 corpus hits for 'elicitation/create' are all prose quotes in development sessions, none an MCP record.",
          "rule": "one count per sidecar line grouped by csiftKind; binary evidence is the literal handler registration string",
          "note": "The mechanism half is corroborated (an in-process MCP-client request handler, not a transcript writer), but the load-bearing half - that the session gains NO record between the outer mcp__* tool_use and its tool_result - cannot be decided here because no MCP server on this machine has ever issued an elicitation. What would decide it: connect an MCP server that calls elicitation/create, snapshot the transcript line count and `csift show @<session> --turn -1 --raw` during the pending window, and diff after answering. Code sites verify verbatim at src/elicitation.rs:4-7 and src/model/classify.rs:140-148."
        }
      ]
    },
    {
      "id": "ELI-011",
      "area": "elicitation",
      "behavior": "An MCP elicitation hook DOES receive an elicitation_id at 2.1.258, but the field is declared optional on both the Elicitation and the ElicitationResult schema, while mcp_server_name is required on both. So the server name is the only key guaranteed to be present, which is what makes it unsafe to substring-scan - the claim's conclusion stands but its premise should say 'the only guaranteed key is a non-unique server name', not 'the id may be a server name'. csift's own hook recipe already prefers the unique id: key=$(jq -r '.tool_use_id // .elicitation_id // .mcp_server_name // \"unknown\"').",
      "depends": "csift exempts MCP markers from the native-closure cross-check and keeps them sidecar-paired only (the guard runs only for the `AskUserQuestion` / `ExitPlanMode` kinds); substring- scanning a server name against the transcript would falsely close a genuinely pending MCP elicitation.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "91-93",
          "snippet": "/// MCP markers are exempt: an MCP elicitation never has a native form, so sidecar pairing\n/// stays its only signal - and its `csiftKey` may be a non-unique server name, unsafe to\n/// substring-scan."
        },
        {
          "path": "src/elicitation.rs",
          "lines": "118-125",
          "snippet": "/// A pending marker whose kind eventually gets a NATIVE record (keyed by tool_use_id) -\n/// the only kinds the ghost guard may cross-check.\nfn is_native_tool_kind(rec: &Record) -> bool {\n    matches!(\n        rec.csift_kind.as_deref(),\n        Some(\"AskUserQuestion\" | \"ExitPlanMode\")\n    )\n}"
        },
        {
          "path": "src/model/record.rs",
          "lines": "221-224",
          "snippet": "    /// The sidecar pairing KEY (tool_use_id / elicitation_id / MCP server) - groups a\n    /// `pending` with its later `resolved`. `None` on a native record.\n    #[serde(default, rename = \"csiftKey\")]\n    pub csift_key: Option<String>,"
        }
      ],
      "instrument": "Read the sidecar at `<session sidecar dir>/elicitations.jsonl` and compare each `csiftKind:\"mcp-elicitation\"` key against the native transcript with `csift search '<key>' @<session> --raw`: no record carries it as a tool id. Counting rule: MCP keys found natively over MCP keys total (expected 0).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.3",
        "source": "AGENTS.md section 3.10; SPEC.md section 6.10; src/elicitation.rs drop_natively_closed doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,320}elicitation_id:i\\(\\)\\.optional\\(\\).{0,220}'   AND   a census of every ~/.claude/projects/*/*/elicitations.jsonl grouped by (csiftKind, csiftKey)   AND   csift search 'mcp_elicitation|elicitation/create|\"subtype\":\"elicitation\"' --count-by label over the csift project directory under ~/.claude/projects",
          "observed": "Both elicitation hook schemas came back verbatim. Elicitation: hook_event_name:x(\"Elicitation\"),mcp_server_name:i(),message:i(),mode:ee([\"form\",\"url\"]).optional(),url:i().optional(),elicitation_id:i().optional(),requested_schema:ge(i(),de()).optional(). ElicitationResult: hook_event_name:x(\"ElicitationResult\"),mcp_server_name:i(),elicitation_id:i().optional(),mode:...,action:ee([\"accept\",\"decline\",\"cancel\"]),content:...optional(). So mcp_server_name is REQUIRED on both events and elicitation_id is OPTIONAL on both. Corpus: 20 sidecar files, 260 lines, 135 (csiftKind,csiftKey) groups; csiftKind counts AskUserQuestion 235, ExitPlanMode 21, absent 4; mcp-elicitation 0. The label census of elicitation-word matches returned 129 records under agent.tool.result 79, agent.tool.use 35, agent.thinking 7, agent.message 5, agent.communication.inbox 2, agent.communication.sent 2, harness.compaction.summary 1 - i.e. only prose about elicitations, no harness elicitation record.",
          "rule": "One schema literal per hook event name in the binary. Sidecar: one group per (csiftKind, csiftKey) pair, counted across every elicitations.jsonl under ~/.claude/projects. Native search: one record per matched jsonl record, bucketed by the label csift assigns it.",
          "note": "The other half of the claim - that an MCP elicitation has no native transcript form - could not be exercised on this machine. No MCP elicitation has ever fired here: 0 of 260 sidecar lines carry csiftKind mcp-elicitation, and a corpus search over 7579 sessions surfaced no harness-labelled elicitation record, only assistant prose discussing them. What would decide it: register an MCP server that issues elicitation/create, answer one elicitation, then read the owning session jsonl for any record carrying that elicitation. All three cited csift code sites are verbatim at the claimed lines (src/elicitation.rs 91-93 and 118-125, src/model/record.rs 214-217)."
        }
      ]
    },
    {
      "id": "ELI-012",
      "area": "elicitation",
      "behavior": "The contrast clause needs qualifying. csift's own current code (src/live/verdict.rs 294-295) states that since Claude Code 2.1.258 a MULTI-question AskUserQuestion is written at question time while a single-question ask stays buffered until answered, so 'the whole turn is buffered' is no longer unconditional for AskUserQuestion.",
      "depends": "`lifecycle`'s frozen-lane detection and `status`'s in-flight verdict both read an unreturned `tool_use` at the tail as a live block rather than a clean finish; if the assistant commit were deferred like an elicitation, a blocked lane would look like an end-of-turn idle.",
      "code": [
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "35-40",
          "snippet": "    // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n    // terminates with a visible assistant message (a clean finish), AND whether the lane is\n    // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n    // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n    // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n    // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
        }
      ],
      "instrument": "Run Claude Code without bypassed permissions, trigger a Bash command that prompts, and while the approval dialog is up run `csift show @main --turn -1`: the tool_use record must be present with no paired tool_result. Counting rule: one tool_use id, checked for a matching tool_result id in the same file.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift status @trap:SilverPineHollow5821   (a self-probe run from inside the very Bash call it describes)   AND   a scan of all 64 top-level transcripts under ~/.claude/projects for a transcript whose last user/assistant record is an assistant tool_use with no tool_result for that id anywhere in the file   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,150}hook_event_name:\"PermissionRequest\".{0,260}'",
          "observed": "The self-probe printed: verdict running / registry status busy / pid alive (start-time guard matched) / 'tail  unreturned Bash call  (1s ago)'. The tool_use for the still-executing Bash call was already on disk while its tool_result did not exist. Corpus scan: 1 of 64 top-level transcripts ends at an unreturned tool_use; its tool is Bash, its record carries stop_reason tool_use, its record timestamp is 2026-07-22T06:25:33Z and the file has not been written for ~42 days - a permanently frozen lane whose tool_use was persisted and whose result never was. Binary: async function*Zte(...){t(`executePermissionRequestHooks called for tool: ${e}`);let C={...Sa(o.session,ne(),d,o),hook_event_name:\"PermissionRequest\",tool_name:e,tool_input:r,permission_suggestions:f};yield*Qy({session:o.session,hookInput:C,toolUseID:n,...})} - the permission prompt is dispatched with the tool_use id already in hand.",
          "rule": "One transcript per top-level session file. 'Frozen tail' = the last record of type user or assistant is an assistant record containing a tool_use block whose id appears in no tool_result block anywhere in the same file. One hook-input literal per event name in the binary.",
          "note": "No live approval dialog was held open during this verification, so the ordering was observed for a running tool and for a permanently frozen one, not for a tool sitting behind an approval prompt. The permission-specific case rests on the PermissionRequest hook being dispatched with an existing toolUseID. What would decide it outright: trigger a permission prompt in one session, leave it up, and from a second session run csift show on that session's last turn - the tool_use must be present with no paired tool_result. The multi-question AskUserQuestion split named in the correction was also not instrumented here; 0 of 64 top-level transcripts ended at an unreturned AskUserQuestion, so the corpus neither confirms nor refutes it. What would decide that: open a two-question ask, leave it unanswered, and read the transcript tail from another session. The cited code site src/subagent/lifecycle.rs 35-40 is verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "ELI-013",
      "area": "elicitation",
      "behavior": "The registry status closed set is exactly busy | shell | idle | waiting, as claimed. But 'without distinguishing which' is wrong: a waiting row carries a companion waitingFor reason, computed by o_o and persisted only while status is waiting. Six values were read out of the binary - input needed (a queued elicitation), dialog open, sandbox request, worker request, goal proposal, and the dialog-kind default permission prompt. So a pending permission prompt IS distinguishable from a question or a plan approval in the registry row, even though it still leaves no transcript trace.",
      "depends": "csift never claims to see one: `status` attaches an unconditional honesty note to `idle-eot`, `waiting-children` and `idle-background-open` saying a pending permission prompt would masquerade as idle, and names the registry row as the only witness - transition- written, never a heartbeat, so a stale row from a dead session still needs the pid probe to be refuted.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "305-308",
          "snippet": "    // never a running shape. `busy` is the only registry running signal.\n    let registry_status = registry.and_then(|r| r.status.as_deref());\n    let registry_shell = registry_status == Some(\"shell\");\n    let running_shape = main_tail.unreturned_use.is_some() || registry_status == Some(\"busy\");"
        }
      ],
      "instrument": "Trigger a permission prompt in one session and leave it unanswered, then from another session run `csift status @<uuid>` and `csift search 'permission' @<uuid>`: the transcript carries no marker for the prompt, and the verdict reaches `waiting-hitl` only via a current registry row reading `waiting`. Counting rule: one blocked session, one status read; the absence in the transcript is the observation.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "AGENTS.md section 3.9; src/live/verdict.rs F7 honesty note"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"busy\",\"shell\",\"idle\",\"waiting\"\\].{0,200}'   AND   rg -o 'function o_o\\(T\\)\\{.{0,700}'   AND   rg -o '.{0,140}waitingFor\\?\\?\"permission prompt\".{0,140}'   AND   a read of every ~/.claude/sessions/<pid>.json   AND   a scan of the csift project directory under ~/.claude/projects for every rejected tool_use / rejection tool_result pair, listing the record types between them",
          "observed": "Status validator, verbatim: var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0} - and the row parser uses it as status:Ke(o.status). Status computer, verbatim: function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"idle\",waitingFor:void 0,working:T.isQueryActive}}. Reason computer, verbatim: function o_o(T){if(T.queuedElicitation)return\"input needed\";if(T.topDialogWaitingFor!==void 0)return T.topDialogWaitingFor;if(T.pendingWorkerRequest)return\"worker request\";if(T.pendingSandboxRequest)return\"sandbox request\";if(T.isShowingLocalJSXCommand&&!T.isResponseStreaming&&!T.delegatedActive)return\"dialog open\";return}. Dialog-kind default, verbatim: function fvt(T){return T===void 0?void 0:zS[T]?.waitingFor??\"permission prompt\"}. Shell relabel, verbatim: Nne=VA===\"idle\"&&PIr?\"shell\":VA. Serializer gate, verbatim: ...t.status===\"waiting\"&&t.waitingFor&&{waitingFor:t.waitingFor}. Live registry: 7 rows, statuses idle 5, busy 1, shell 1, none waiting, and every row carried a status key while none carried waitingFor. Transcript trace: 6 rejected tool_use/tool_result pairs; record types appearing between the tool_use and its rejection result were attachment (4 pairs), nothing at all (1 pair), and two queue-operation lines (1 pair) - zero user, assistant or system records; the gaps ran 1.3s to 1270.6s.",
          "rule": "One registry row per ~/.claude/sessions/<pid>.json file. One rejection pair per tool_use id whose tool_result is is_error true and whose text begins 'The user doesn't want to proceed'. 'Between' = every line strictly between the two records in file order, counted by its top-level type field.",
          "note": "waitingFor is not new in 2.1.258 - the same status:\"waiting\",waitingFor construction is present in the 2.1.229 binary, which predates the version this claim was located at, so this is a gap in the claim's wording rather than a change in Claude Code. The field itself was not observed on disk because no session was in the waiting state during the read; the serializer gates it on status===\"waiting\". What would decide that: open any blocking dialog and read that session's ~/.claude/sessions/<pid>.json while it is up. The cited src/live/registry.rs 5-11 is verbatim at the claimed lines; the src/live/verdict.rs snippet has moved from 292-310 to 305-323 (the block now opens with two new lines about multi-question asks)."
        }
      ]
    },
    {
      "id": "ELI-014",
      "area": "elicitation",
      "behavior": "Two wording corrections. (1) session_id and transcript_path are not cached at bootstrap - they are recomputed on every hook dispatch from the session object (session_id:e.id, transcript_path:lm(e.id)); the net effect is the claimed one, that they always name the top-level session. (2) Claude Code 2.1.258 adds a sibling field agent_transcript_path that DOES name the subagent's own transcript; it is added alongside agent_id on the SubagentStop input (agent_id:d,agent_transcript_path:Ud(d)), never on the elicitation hooks, and transcript_path itself still names the top-level session.",
      "depends": "The elicitation sidecar path is derived from the hook's `session_id`, so it always lands in the TOP-LEVEL session's sidecar dir (the `<uuid>/` dir beside `subagents/`); csift merges it only when reading a top-level transcript, a subagent lane skips the read entirely, and a subagent's MCP elicitation surfaces under its parent - agent-blind by construction, which csift does not pretend otherwise.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "31-35",
          "snippet": "//! ## Keyed by the TOP-LEVEL session\n//!\n//! The sidecar always lives beside the TOP-LEVEL session jsonl (the hook's `session_id` is\n//! the top-level/leader uuid, never a subagent's). Callers therefore merge the sidecar only\n//! when reading a top-level session file - a subagent transcript has none."
        },
        {
          "path": "src/elicitation.rs",
          "lines": "47-53",
          "snippet": "/// The `elicitations.jsonl` path for a session jsonl, or `None` when the session has no\n/// sidecar dir / the path has no stem. The sidecar dir is `<ENC>/<uuid>/` (the same dir that\n/// holds `subagents/`); the marker file sits inside it.\n#[must_use]\npub fn sidecar_path(session_jsonl: &Path) -> Option<PathBuf> {\n    Some(crate::subagent::sidecar_dir_for_session(session_jsonl)?.join(SIDECAR_FILE))\n}"
        }
      ],
      "instrument": "Install a trivial PreToolUse hook that appends its stdin JSON to a file, run a tool from inside a subagent, and compare the recorded `session_id` with the subagent transcript's own basename: they must differ, `session_id` must equal the top-level uuid, and `agent_id` must name the subagent. Counting rule: one hook invocation per tool call.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.2.0",
        "source": "AGENTS.md section 3.10; SPEC.md section 6.10; src/elicitation.rs:31-35 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Sa\\(e,n,r,o\\)\\{.{0,900}'   AND   rg -o '.{0,180}hook_event_name:\"Elicitation\".{0,300}'   AND   rg -o '.{0,200}hook_event_name:\"PreToolUse\".{0,320}'   AND   rg -o 'agent_id:i\\(\\)\\.optional\\(\\)\\.describe\\(\"[^\"]{0,400}'   AND   a check that every elicitations.jsonl under ~/.claude/projects sits beside a top-level transcript, plus a comparison of each sidecar line's recorded sessionId against its owning directory name",
          "observed": "Base builder, verbatim: function Sa(e,n,r,o){...return{session_id:e.id,transcript_path:lm(e.id),cwd:n,scratchpad_dir:...,prompt_id:...,permission_mode:r,agent_id:o?.agentId,agent_type:d,effort:v}} - session_id and transcript_path both come from the session object, agent_id from the fourth options argument. Transcript path builder, verbatim: function lm(e){if(e===Q())return cl()??El();let n=fl(ye());return Uf(n,`${e}.jsonl`)} - a flat <session-id>.jsonl, never a subagents/ path. Call sites: PreToolUse uses Sa(o.session,ne(),d,o), PostToolUse uses Sa(d.session,ne(),f,d), PostToolUseFailure uses Sa(d.session,ne(),_,d) - four arguments, so agent_id is populated; Elicitation uses Sa(e,ne(),d) and ElicitationResult uses Sa(e,ne(),d) - THREE arguments, so agent_id is undefined on both. Schema describe, verbatim: 'Subagent identifier. Present only when the hook fires from within a subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in --agent sessions. Use this field (not agent_type) to distinguish subagent calls from main-thread calls.' On-disk: all 20 elicitations.jsonl files sit beside a top-level <uuid>.jsonl, 0 under any subagents/ path, and 260 of 260 sidecar lines carry a sessionId equal to the owning top-level directory name.",
          "rule": "One hook-input construction per event name in the binary; argument count of the shared base builder decides whether agent_id is populated. On disk: one sidecar file per session directory; one comparison per sidecar line that carries a sessionId.",
          "note": "The on-disk half only exercises main-thread firings, because AskUserQuestion and ExitPlanMode cannot fire in a subagent lane at all (see ELI-017), so no subagent-context elicitation exists in this corpus. The subagent case rests on the binary: the base builder reads the session object for session_id/transcript_path and takes agent_id from a separate options argument, and the schema text says agent_id, not session_id, is the field that distinguishes a subagent call. Both cited code sites (src/elicitation.rs 31-35 and 47-53) are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "ELI-015",
      "area": "elicitation",
      "behavior": "Claude Code exposes distinct hook events `Elicitation` and `ElicitationResult` for an MCP elicitation's open and close, alongside `PreToolUse`, `PostToolUse` and `PostToolUseFailure` for the tool-shaped elicitations - five events in total cover the three blocking kinds.",
      "depends": "csift's sidecar recipe subscribes all five with one script: `PreToolUse` writes the `pending` line, `PostToolUse`/`PostToolUseFailure` and `ElicitationResult` write the `resolved` close marker, so every kind is paired by `csiftKey` without touching the native transcript.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "568-573",
          "snippet": "case \"$ev\" in\n  PreToolUse)  case \"$tool\" in AskUserQuestion|ExitPlanMode) kind=\"$tool\"; phase=\"pending\";; esac ;;\n  PostToolUse|PostToolUseFailure) case \"$tool\" in AskUserQuestion|ExitPlanMode) kind=\"$tool\"; phase=\"resolved\";; esac ;;\n  Elicitation)       kind=\"mcp-elicitation\"; phase=\"pending\" ;;\n  ElicitationResult) kind=\"mcp-elicitation\"; phase=\"resolved\" ;;\nesac"
        }
      ],
      "instrument": "Register a hook under an invalid event name and read the harness error, which enumerates the recognized events; or trigger an MCP elicitation with the recipe installed and confirm the sidecar gains one `csiftPhase:\"pending\"` line on open and one `csiftPhase:\"resolved\"` line on close. Counting rule: one sidecar line per event fired.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.10; SKILL.md elicitation-marker hook recipe"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n '^(PreToolUse|PostToolUse|PostToolUseFailure|Elicitation|ElicitationResult)$'   AND   rg -o '\\[\"PreToolUse\",\"PostToolUse\",\"PostToolUseFailure\",\"PostToolBatch\",\"Notification\"[A-Za-z\",]{0,2000}'",
          "observed": "All five bare event-name strings are present, one occurrence each. The canonical event array came back as a single literal with 33 entries: [\"PreToolUse\",\"PostToolUse\",\"PostToolUseFailure\",\"PostToolBatch\",\"Notification\",\"UserPromptSubmit\",\"UserPromptExpansion\",\"SessionStart\",\"SessionEnd\",\"Stop\",\"StopFailure\",\"SubagentStart\",\"SubagentStop\",\"PreCompact\",\"PostCompact\",\"PreModelSwitch\",\"PostModelSwitch\",\"PermissionRequest\",\"PermissionDenied\",\"Setup\",\"TeammateIdle\",\"TaskCreated\",\"TaskCompleted\",\"Elicitation\",\"ElicitationResult\",\"ConfigChange\",\"WorktreeCreate\",\"WorktreeRemove\",\"InstructionsLoaded\",\"CwdChanged\",\"FileChanged\",\"DirectoryAdded\",\"MessageDisplay\"] - all five named by the claim appear in it. The two elicitation events also carry their own schema descriptions: 'Hook input for the Elicitation event. Fired when an MCP server requests user input. Hooks can auto-respond (accept/decline) instead of showing the dialog.' and 'Hook input for the ElicitationResult event. Fired after the user responds to an MCP elicitation. Hooks can observe or override the response before it is sent to the server.'",
          "rule": "One event name per entry of the canonical hook-event array literal; membership checked by exact string equality against the five names the claim asserts.",
          "note": "All five events exist and the two elicitation events are documented as the open and close of an MCP elicitation, exactly as claimed. Worth recording alongside: the array holds 33 events in total, among them PermissionRequest and PermissionDenied, which the csift sidecar recipe does not subscribe - relevant to ELI-013, since a hook could record permission prompts the same way. The cited SKILL.md 562-567 recipe fragment is verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "ELI-016",
      "area": "elicitation",
      "behavior": "The measurement should now read: 12 of 12 real unpaired sidecar keys across 7 sessions were rejections (9 AskUserQuestion, 3 ExitPlanMode), not 6 of 6 across 4 sessions. Every one of the 12 was already on its native transcript as a tool_use id with a rejection tool_result, and csift reported none of them as pending.",
      "depends": "csift's ghost-pending guard drops a sidecar-unresolved pending whose `csiftKey` appears STRUCTURALLY on a native record (as a `tool_use` block `id` or a `tool_result` `tool_use_id`; a key merely quoted in prose fails the block-id check), because the native transcript outranks the sidecar. Without it a rejected elicitation is reported pending forever by `list`, `search` and `status`, duplicated beside its own flushed native record. Cost is paid only when at least one AUQ/ExitPlanMode key is sidecar-unresolved: one mmap plus a per-key byte scan, parsing only the lines that contain the key bytes.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "79-84",
          "snippet": "/// Claude Code fires NO `PostToolUse` hook for a REJECTED AskUserQuestion / ExitPlanMode\n/// (a rejection is not a tool completion), so the sidecar's `resolved` marker is never\n/// written on that path - sidecar-internal pairing alone would then report the elicitation\n/// as pending FOREVER, while the native transcript long since holds the flushed `tool_use`\n/// plus its rejection `tool_result` (verified on real data: every observed ghost was a\n/// rejection). The native record is the higher-authority truth source - the sidecar exists"
        },
        {
          "path": "src/elicitation.rs",
          "lines": "95-98",
          "snippet": "/// Cost: paid ONLY when ≥1 AUQ/ExitPlanMode key is sidecar-unresolved (rare - typically\n/// zero). One mmap + a per-key `memmem` byte scan; only the few lines containing the key\n/// bytes are parsed, and a key merely QUOTED in prose (e.g. a Bash command grepping for it)\n/// fails the structural block-id check and does not count as closure."
        },
        {
          "path": "src/elicitation.rs",
          "lines": "127-131",
          "snippet": "/// True when the native transcript bytes hold a record whose `tool_use` block `id` or\n/// `tool_result` `tool_use_id` equals `key` - STRUCTURAL, not substring: each `memmem` hit\n/// expands to its enclosing line and only that line is parsed, so a key quoted inside some\n/// other record's text never counts as closure.\nfn native_closes(bytes: &[u8], key: &str) -> bool {"
        }
      ],
      "instrument": "With the elicitation hook installed, open a plan and REJECT it: `<uuid>/elicitations.jsonl` holds a `csiftPhase:\"pending\"` line with no matching `resolved` line, while the native transcript already carries that same key as a `tool_use` id (`csift show @<session> --uuid <the tool_use id>`), and `csift list @<session>` reports no pending elicitation. Counting rule: group sidecar lines by `csiftKey`, one group per elicitation. The unit test `native_closes_on_a_structural_tool_use_id` in src/elicitation.rs pins the structural check.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.6.3",
        "source": "AGENTS.md section 3.10; SPEC.md section 4.4 and the v0.6.3 ledger; CHANGELOG 0.6.3; src/elicitation.rs drop_natively_closed doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,1100}for await\\(let ys of \\$Pe\\(o,e,n,_,Ie,nr,Cn\\|\\|Rn.{0,60}'   AND   a pairing of every sidecar group with no resolved line against its own session transcript, checking whether the csiftKey appears as a structural tool_use block id and what its tool_result says   AND   csift list @<session> --no-subagents for each such session, grepped for a pending/elicitation annotation",
          "observed": "Binary: the tool-execution catch block fires the PostToolUseFailure runner and returns - let nr=$Lo(xn,o.abortController.signal),...,Rr=[];for await(let ys of $Pe(o,e,n,_,Ie,nr,Cn||Rn,v,C,I,An))Rr.push(ys);if(Rn)return... - while the PostToolUse runner (v9) is only reached on the success path after a tool result exists, so a rejection reaches PostToolUseFailure and never PostToolUse. Corpus: 20 sidecar files, 135 groups, 13 groups with no resolved line, of which 1 is a schema-skew group with no csiftKind (4 lines) and 12 are real. All 12 real unpaired keys - 9 AskUserQuestion and 3 ExitPlanMode, spread over 7 sessions - were found on their native transcript as a structural tool_use block id, and all 12 tool_results were is_error true beginning 'The user doesn't want to proceed with this tool use. The tool use was rejected'. 12 of 12 were rejections. csift list on all 7 of those sessions printed no pending or elicitation annotation, so the ghost guard dropped every one.",
          "rule": "Group sidecar lines by (csiftKind, csiftKey); one group per elicitation. A group is unpaired when no line in it has csiftPhase resolved. A key is natively closed when some record in the owning transcript carries it as a tool_use block id or a tool_result tool_use_id. A rejection is a tool_result with is_error true whose text starts 'The user doesn't want to proceed'.",
          "note": "The census also turned up one 13th unpaired group that is not a rejection and not a ghost: 4 lines carrying the csift sentinel but no readable csiftKind or csiftPhase - the pre-release field-name fossil that csift counts and refuses to merge. Worth keeping in the claim's arithmetic so a future re-count of '13 unpaired groups' is not read as 13 ghosts. All three cited code sites (src/elicitation.rs 79-84, 95-98, 127-131) are verbatim at the claimed lines, and the unit test native_closes_on_a_structural_tool_use_id exists at src/elicitation.rs:508."
        }
      ]
    },
    {
      "id": "ELI-017",
      "area": "elicitation",
      "behavior": "Three corrections. (1) ExitPlanMode is NOT unconditionally main-thread-only: the agent pool filter re-admits it whenever the agent's own permissionMode is plan (if(an(C,kc)&&f===\"plan\")return!0) and force-appends it if the pool lacks it, so only AskUserQuestion is excluded from every agent context without exception. (2) The measured absence is now 0 occurrences of either tool across 7515 subagent transcripts, not 5806. (3) The MCP figure is 8 subagent transcripts carrying an mcp__ tool_use, not 193 - and the mechanism is stronger than 'allowed': the mcp__ test is the filter's first branch and returns true before any exclusion is consulted.",
      "depends": "Keying the elicitation sidecar on the top-level session alone is sound only because the two invisible tool-shaped blockers cannot occur in a subagent lane; a subagent's MCP elicitation is the one kind that can, and it surfaces under the parent session that owns the sidecar.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "31-35",
          "snippet": "//! ## Keyed by the TOP-LEVEL session\n//!\n//! The sidecar always lives beside the TOP-LEVEL session jsonl (the hook's `session_id` is\n//! the top-level/leader uuid, never a subagent's). Callers therefore merge the sidecar only\n//! when reading a top-level session file - a subagent transcript has none."
        },
        {
          "path": "SPEC.md",
          "lines": "1851",
          "snippet": "AskUserQuestion/ExitPlanMode are main-thread-only, and a subagent's MCP elicitation surfaces under its top-level session (agent-blind — the Elicitation hook carries no `agent_id`)"
        }
      ],
      "instrument": "`csift search 'AskUserQuestion|ExitPlanMode' . -t agent.tool.use --count-by session`, then compare the keys against `csift list . --format json | jq -r 'select(.is_subagent==true).session_id'`: no subagent id may appear. Counting rule: one record per matched tool_use record.",
      "located": {
        "claude_code": "2.1.193",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.10; SPEC.md section 6.10"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function dAo\\(.{0,900}'   AND   rg -o '.{0,120}XNe=fe\\(\"external\"\\),lZn=new Set\\(\\[\\.\\.\\.XNe\\]\\).{0,300}'   AND   rg -o 'function zh\\(.{0,160}'   AND   csift search 'AskUserQuestion|ExitPlanMode' -t agent.tool.use --format json --max-count 0 (no target, so all projects) filtered to hits whose tool_name is one of the two   AND   csift search 'mcp__' -t agent.tool.use --format json --max-count 0 filtered to hits whose tool_name starts with mcp__   AND   find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl | wc -l, cross-checked with rg -l on the same file list",
          "observed": "Exclusion set, verbatim: function fe(e){return new Set([hP,kc,_A,...Vtr,Ki,iG,KNe,uJ,nG,rG,...e!==\"ant\"?[Yc]:[],wa,toe,fbt,_b])}var XNe=fe(\"external\"),lZn=new Set([...XNe]); with var r_=\"ExitPlanMode\",kc=\"ExitPlanMode\" and Ki=\"AskUserQuestion\" - both tool names are members. Agent pool filter, verbatim: function dAo({tools:e,isBuiltIn:n,agentType:r,isAsync:o=!1,isTeammate:d=!1,permissionMode:f,agentDepth:_=0}){let v=e.filter((C)=>{if(zh(C))return!0;if(an(C,kc)&&f===\"plan\")return!0;if(ax(C,XNe))return!1;...});if(f===\"plan\"&&!v.some((C)=>an(C,kc)))v.push(_6);return v}. MCP predicate, verbatim: function zh(e){return e.name?.startsWith(\"mcp__\")||e.isMcp===!0} - the first test in the filter, returning true unconditionally. Corpus (scope line: 7579 sessions, 64 top-level, 7515 subagent): 134 AskUserQuestion and 16 ExitPlanMode tool_use records, every one with is_subagent false; 0 in any subagent transcript. On disk there are 7515 subagent transcripts (160 journal.jsonl files excluded). mcp__ tool_use records: 55 across 4 top-level transcripts and 12 across 8 subagent transcripts; an independent rg -l '\"name\":\"mcp__' over the same 7515 files also returned 8.",
          "rule": "One record per matched jsonl record; a record counts for a tool only when csift's structural tool_name field equals it, not when the name merely appears in the rendered text. Subagent transcript = a .jsonl under a subagents/ path directory, excluding journal.jsonl. 'Transcripts carrying an mcp__ tool_use' = distinct session ids among hits whose tool_name starts with mcp__.",
          "note": "The plan-mode exception is not new. The 2.1.229 binary carries the same branch - gS({tools:e,isBuiltIn:t,isAsync:r=!1,isTeammate:n=!1,permissionMode:o,agentDepth:i=0}){let s=e.filter((a)=>{if(IL(a))return!0;if($a(a,$2)&&o===\"plan\")return!0;...}) - which predates the version this claim was located at, so the gap is in the claim's wording, not in Claude Code. The 193 figure could not be reproduced under any counting rule I could construct: 8 subagent transcripts contain an mcp__ tool_use, while 7482 of 7515 contain the bytes mcp__ somewhere (the injected tool list), so neither rule yields 193. What would decide the ExitPlanMode exception in practice: run a subagent in plan permission mode and check whether it can emit an ExitPlanMode tool_use. The SPEC.md sentence cited by the claim has moved from line 1851 to line 1861."
        }
      ]
    },
    {
      "id": "ELI-018",
      "area": "elicitation",
      "behavior": "A session's sidecar directory (the `<uuid>/` dir beside the transcript, the same dir that holds `subagents/`) is created LAZILY: it does not exist at session start and appears only when something first writes into it, so an elicitation sidecar file is typically born mid-session, well after any watcher started.",
      "depends": "`csift wait` re-attempts sidecar discovery on EVERY poll instead of only at start, and seeds a newly discovered file at baseline offset 0 because a file born after start is wholly post-start; the startup-only lookup meant a sidecar born mid-wait was never watched, so `--until auq` timed out while the final assessment said waiting-hitl.",
      "code": [
        {
          "path": "src/subagent/discover.rs",
          "lines": "5-14",
          "snippet": "/// The sidecar directory `<ENCODED>/<session-uuid>/` for a top-level session jsonl,\n/// or `None` if the session has no sidecar. The sidecar is named after the session\n/// uuid (the jsonl basename without `.jsonl`).\n#[must_use]\npub fn sidecar_dir_for_session(session_jsonl: &Path) -> Option<PathBuf> {\n    let stem = session_jsonl.file_stem()?.to_str()?;\n    let parent = session_jsonl.parent()?;\n    let dir = parent.join(stem);\n    dir.is_dir().then_some(dir)\n}"
        },
        {
          "path": "src/live/wait.rs",
          "lines": "113-129",
          "snippet": "        // ── Sidecar discovery: the sidecar DIR is typically born mid-wait (the first\n        //    pending ask creates it), and its path resolves only once the dir exists -\n        //    so re-attempt each poll. A file born after start is wholly post-start, so\n        //    baseline 0 is exact. ──\n        if !is_subagent_target {\n            if let Some(sc) = crate::elicitation::sidecar_path(&main) {\n                if !cursors.iter().any(|c| c.path == sc) {\n                    let lane = crate::subagent::session_id_from_path(&main);\n                    cursors.push(Cursor {\n                        path: sc,\n                        offset: 0,\n                        is_main: true,\n                        lane,\n                    });\n                }\n            }\n        }"
        }
      ],
      "instrument": "Run `csift wait @<uuid> --until auq --timeout 60 --interval 50` against a session that has no sidecar directory yet, then trigger the session's first AskUserQuestion: the condition must fire rather than time out. Counting rule: one discovery attempt per poll, one wait per ask. The e2e test `p10_wait_sees_a_sidecar_ask_that_lands_mid_wait` in tests/cli/live/wait.rs pins the mid-wait discovery.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "CHANGELOG 0.9.0; src/live/wait.rs:110-113 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "For every top-level transcript under ~/.claude/projects, compare stat -f %B on its sibling <uuid>/ directory and on that directory's elicitations.jsonl against the transcript's FIRST record timestamp (parsed from the jsonl, not from file metadata)",
          "observed": "64 top-level transcripts; 42 have a <uuid>/ sidecar directory; 20 have an elicitations.jsonl. Directory birth minus first-record timestamp: n=42, min +1s, median +3262s, max +1101283s - 42 of 42 strictly after the session's first record. Sidecar-file birth minus first-record timestamp: n=20, min +24s, median +98230s, max +1258058s - 20 of 20 strictly after. Not one directory and not one sidecar file existed at session start.",
          "rule": "One measurement per top-level transcript that has a sibling directory named after its uuid. Session start = the timestamp of the first record in the transcript that carries one. Birth time = the APFS creation time from stat -f %B. 'Lazy' = birth strictly greater than session start.",
          "note": "File birth times alone are not a safe instrument here - measured against the transcript FILE's birth time the same directories appear to predate it (median -148026s), because forked and copied transcripts carry record timestamps predating their own file. Anchoring on the first record's timestamp is what makes the rule sound, and under that rule the result is unanimous: 42/42 directories and 20/20 sidecar files were born after their session started, the median sidecar arriving more than a day in. That is exactly the condition the wait loop's per-poll rediscovery exists for. Both cited code sites are verbatim at the claimed lines (src/subagent/discover.rs 5-14, src/live/wait.rs 110-126), and the e2e test p10_wait_sees_a_sidecar_ask_that_lands_mid_wait exists at tests/cli/live/wait.rs:175."
        }
      ]
    },
    {
      "id": "ELI-019",
      "area": "elicitation",
      "behavior": "Claude Code hands a hook its payload as ONE JSON object on the hook process's STDIN. The keys a tool-shaped hook reads are `hook_event_name`, `tool_name`, `tool_input`, `session_id` and `transcript_path`; a tool elicitation additionally carries `tool_use_id`, and the two MCP elicitation events carry `elicitation_id` and `mcp_server_name` instead.",
      "depends": "csift's elicitation-sidecar hook recipe parses exactly those keys: `hook_event_name` + `tool_name` pick the kind and phase, `tool_use_id` / `elicitation_id` / `mcp_server_name` build the `csiftKey` the merge pairs `pending` against `resolved` on, and `transcript_path` derives the sidecar path (with a `session_id` glob as fallback). A renamed key never errors - the script exits 0, the sidecar is never written, and `list`'s `sidecar_present` reads false, which is the honest \"hook unknown, cannot conclude\" arm rather than a wrong \"nothing pending\".",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "566",
          "snippet": "ev=$(jq -r '.hook_event_name//empty' <<<\"$in\" 2>/dev/null); tool=$(jq -r '.tool_name//empty' <<<\"$in\" 2>/dev/null)"
        },
        {
          "path": "SKILL.md",
          "lines": "575-576",
          "snippet": "tp=$(jq -r '.transcript_path//empty' <<<\"$in\" 2>/dev/null); sid=$(jq -r '.session_id//empty' <<<\"$in\" 2>/dev/null)\nkey=$(jq -r '.tool_use_id // .elicitation_id // .mcp_server_name // \"unknown\"' <<<\"$in\" 2>/dev/null)"
        },
        {
          "path": "src/model/record.rs",
          "lines": "210-214",
          "snippet": "    /// The sidecar record's pairing PHASE - `\"pending\"` (an unanswered elicitation,\n    /// missing from the native transcript) or `\"resolved\"` (a lightweight close marker\n    /// used only for pairing). Read by [`crate::elicitation`]. `None` on a native record.\n    #[serde(default, rename = \"csiftPhase\")]\n    pub csift_phase: Option<String>,"
        },
        {
          "path": "src/session/rows.rs",
          "lines": "68-73",
          "snippet": "    /// True when the session's elicitation SIDECAR FILE exists at all (= the csift hook\n    /// is installed for this session - resolved pairs stay in the file). The tri-state a\n    /// consumer needs: present+pending / present+none (safe to conclude \"not blocked on\n    /// an elicitation\") / absent (hook unknown - CANNOT conclude anything). Always false\n    /// for a subagent row (the sidecar is keyed by the top-level session).\n    pub sidecar_present: bool,"
        }
      ],
      "instrument": "Do NOT tally the QUOTED key (`\"hook_event_name\"`): the harness builds its hook payload as a JS object literal with bare keys, so the quoted form is 0 for hook_event_name and elicitation_id. Tally the bare token (`grep -oF hook_event_name`) or, better, read the four payload constructors directly with `strings -n 40 \"$bin\" | grep -oE 'hook_event_name:\"(PreToolUse|PostToolUse|Elicitation|ElicitationResult)\"'` and the shared base `grep -oF 'return{session_id:e.id,transcript_path:lm(e.id),cwd:n'`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.3",
        "source": "measured now; SKILL.md elicitation-marker hook recipe"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oE 'hook_event_name:\"(PreToolUse|PostToolUse|Elicitation|ElicitationResult)\"' ; strings -n 40 \"$bin\" | grep -oF 'return{session_id:e.id,transcript_path:lm(e.id),cwd:n' ; for k in hook_event_name tool_name tool_input session_id transcript_path tool_use_id elicitation_id mcp_server_name; do echo \"$k bare=$(strings -n 6 \"$bin\" | grep -oF \"$k\" | wc -l) quoted=$(strings -n 6 \"$bin\" | grep -oF \"\\\"$k\\\"\" | wc -l)\"; done ; python3 -c 'import json,glob;rows=[json.loads(l) for f in glob.glob(\"*/*/elicitations.jsonl\") for l in open(f) if l.strip()];print(len(rows))' (run in ~/.claude/projects)",
          "observed": "Four payload constructors found verbatim: `{...Sa(o.session,ne(),d,o),hook_event_name:\"PreToolUse\",tool_name:e,tool_input:r,tool_use_id:n}`; `{...Sa(d.session,ne(),f,d),hook_event_name:\"PostToolUse\",tool_name:e,tool_input:r,tool_response:o,tool_use_id:n,duration_ms:C}`; `{...Sa(e,ne(),d),hook_event_name:\"Elicitation\",mcp_server_name:n,message:r,mode:v,url:C,elicitation_id:I,requested_schema:o}`; `{...Sa(e,ne(),d),hook_event_name:\"ElicitationResult\",mcp_server_name:n,elicitation_id:C,mode:v,action:r,content:o}`. The shared base returns `{session_id:e.id,transcript_path:lm(e.id),cwd:n,scratchpad_dir:...,prompt_id:...,permission_mode:r,agent_id:...,agent_type:d,effort:v}`. String-table tallies at 2.1.258, bare vs quoted: hook_event_name 91/0, tool_name 255/20, tool_input 107/10, session_id 507/12, transcript_path 23/1, tool_use_id 836/18, elicitation_id 12/0, mcp_server_name 36/3. Live end-to-end: 260 sidecar lines across the corpus, 256 carrying a csiftKey that starts `toolu_`, kinds AskUserQuestion 235 / ExitPlanMode 21 / unreadable-fossil 4; the newest pair (pending + resolved, 2026-09-02T08:20:36Z and T08:24:40Z) sits in a session whose records at that instant are stamped version 2.1.258.",
          "rule": "Binary: one payload constructor per distinct `hook_event_name:\"<Event>\"` literal in the minified source. String tallies: `grep -oF` counts OCCURRENCES, and the bare and quoted forms are DIFFERENT counts - two of the eight keys (hook_event_name, elicitation_id) never appear in the quoted form at all, so a quoted-only rule reads them as absent. Corpus: one sidecar line per `elicitations.jsonl` line that parses as JSON; a csiftKey beginning `toolu_` proves the hook read a real `tool_use_id` off its stdin, and the file's existence at the transcript-derived path proves it read `transcript_path`/`session_id`.",
          "note": "The behavior is confirmed twice over - statically in the four payload constructors, and live in sidecar lines written under 2.1.258 today. Only the instrument's counting rule and the tallies it produced needed fixing. csift's four code sites are verbatim at the claimed lines: SKILL.md:566, SKILL.md:575-576, src/model/record.rs:209-213, src/session/rows.rs:68-73."
        }
      ]
    },
    {
      "id": "ELI-020",
      "area": "elicitation",
      "behavior": "Claude Code 2.1.258 constructs a `type:\"system\"` / `subtype:\"hook_response\"` message with exactly the claimed fields, but (a) it is emitted only for hook events in `[\"SessionStart\",\"Setup\"]` unless a session-wide `allHookEventsEnabled` flag is set, and (b) it is NOT a transcript record: zero such lines exist in any transcript jsonl. It belongs to the same family as its siblings `hook_started` and `hook_progress`, which are likewise absent from disk - a live message stream, not persisted state.",
      "depends": "csift's gated `harness.meta.system` leaf DOES classify such a record (verified against a synthetic fixture), but the record has no top-level `content`, so the leaf's `[<subtype> <level>] <content>` render produces `[hook_response]` with empty text: the hook's stdout/stderr are not matchable through it, and `show --line` shows the same empty excerpt. Only `--raw` would reach the bytes. And since no such record is ever written, csift cannot use it as evidence a hook ran; the on-disk trace of a hook that ran is the `hook_additional_context` attachment (leaf `harness.meta.hook`) when the hook injected context, and nothing at all otherwise.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "16-25",
          "snippet": "            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n                // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n                // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n                // informational, api_error, model_refusal_*, agents_killed, local_command,\n                // scheduled_task_fire, and whatever a later build adds).\n                \"compact_boundary\" => None,\n                _ => Some(Class::MetaSystem),"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "145-152",
          "snippet": "    /// `harness.meta.system` (v0.10.1) - the catch-all for every OTHER `type:\"system\"`\n    /// subtype the harness writes for its own UI and never sends to the model:\n    /// `informational` (e.g. the Remote Control disconnect warning, `level:\"warning\"`),\n    /// `api_error`, `model_refusal_fallback` / `model_refusal_no_fallback`,\n    /// `agents_killed`, `local_command`, `scheduled_task_fire`, and any subtype a\n    /// future build adds. Renders `[<subtype> <level>] <content>`. No `message{}`, so\n    /// invisible by the same instrument as the rest of this family; gated like them.\n    MetaSystem,"
        }
      ],
      "instrument": "`csift search \"\" <target> -t harness.meta.system --count-by label` (the leaf is gated: without an explicit `-t` reaching it those lines are never parsed), then address one hit with `csift show <target> --line <n> --raw | jq 'keys, .subtype'`. Counting rule: one record per jsonl line whose `type` is `system` and `subtype` is `hook_response`. Binary cross-check: resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -F 'subtype:\"hook_response\"' | head -1` prints the single minified record constructor listing the whole field set.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -cF 'subtype:\"hook_response\"' ; strings -n 40 \"$bin\" | grep -oF 'var J2r=[\"SessionStart\",\"Setup\"]' ; cd ~/.claude/projects && rg -g '*.jsonl' -o '\"subtype\":\"hook_response\"' . | wc -l ; rg -g '*.jsonl' -o '\"subtype\":\"[a-z_]+\"' . | sed 's/.*subtype...//;s/\"$//' | sort | uniq -c | sort -rn ; plus a synthetic fixture under --claude-home carrying one `type:\"system\"`/`subtype:\"hook_response\"` record with the exact field set, scanned by the in-tree csift build.",
          "observed": "Binary: exactly 1 constructor, `_u({type:\"system\",subtype:\"hook_response\",hook_id:e.hookId,hook_name:e.hookName,hook_event:e.hookEvent,output:e.output,stdout:e.stdout,stderr:e.stderr,...e.exitCode!==void 0&&{exit_code:e.exitCode},outcome:e.outcome})` - the claimed field set exactly. But it sits behind `if(!xSe(e.hookEvent))return;` where `function xSe(e){if(J2r.includes(e))return!0;return $5t().allHookEventsEnabled&&Hh.includes(e)}` and `var J2r=[\"SessionStart\",\"Setup\"]`. Corpus: `\"subtype\":\"hook_response\"` occurs 0 times across every *.jsonl under ~/.claude/projects. The system subtypes that DO occur, with counts: stop_hook_summary 5656, turn_duration 4644, away_summary 1636, scheduled_task_fire 459, compact_boundary 232, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, informational 5, model_refusal_no_fallback 2 - eleven subtypes, none of them hook_response, hook_started or hook_progress. Fixture: with the exact harness field set (no top-level `content`), csift's gated leaf classifies and censuses the record (`-t harness.meta.system --count-by label` -> 1) and renders `harness.meta.system L2 [hook_response]` with an EMPTY excerpt, so `search \"OUTPUTMARKER\" -t harness.meta.system` returns 0 while a bare scan returns 0 as designed; adding a top-level `content` field makes it matchable.",
          "rule": "Binary: one tally per exact `subtype:\"hook_response\"` byte sequence in the string table. Corpus: one record per jsonl line whose `subtype` value is the named string, counted over every *.jsonl under ~/.claude/projects (the `-g '*.jsonl'` glob matters - the same token appears 101 times inside message text and inside externalised tool-result .txt files, which are not records). Fixture: one record per line of a two-line synthetic transcript under `--claude-home`.",
          "note": "The claim's strongest consequence - 'the only on-disk evidence that a hook recipe ran at all and what it printed' - is refuted: there is no such on-disk evidence, at 2.1.258 or in any version present in this corpus. A future build that flips allHookEventsEnabled and persists the stream would be caught by re-running the corpus census above. csift's two code sites are verbatim at the claimed lines: src/model/classify_promoted.rs:16-25 and src/model/taxonomy.rs:145-152."
        }
      ]
    },
    {
      "id": "ELI-021",
      "area": "elicitation",
      "behavior": "A hook's stdout `additionalContext` is injected ONLY when the returned JSON nests it under `hookSpecificOutput` together with a `hookEventName` matching the firing event; a bare top-level `additionalContext` is discarded, and Claude Code's own diagnostic for that shape is the literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?`.",
      "depends": "Dropping the `hookEventName` echo is not a fully silent no-op, and the two ways to get it wrong differ. Omitting the nesting entirely (a bare top-level `additionalContext`) is ignored WITH a logged diagnostic naming the fix. Nesting it but echoing the WRONG event name THROWS: `Hook returned incorrect event name: expected '<Event>' but got '<Event>'. Full stdout: ...`. Also, neither path records a `hook_response` transcript record - see ELI-020 - so the phrase 'still records its hook_response' should be struck.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "532",
          "snippet": "$chunk\" '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:$c}}'"
        },
        {
          "path": "SKILL.md",
          "lines": "554",
          "snippet": "jq -n --arg c \"$ctx\" '{hookSpecificOutput:{hookEventName:\"PostToolUseFailure\",additionalContext:$c}}'"
        }
      ],
      "instrument": "resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -F 'with a hookEventNa'`: the literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?` must be present. Counting rule: exact literal presence in the string table (one match). Live check: return the bare top-level form from a `SessionStart` hook and confirm `csift search \"\" @<session> -t harness.meta.hook --additional-context -c` stays 0, then re-run with the nested form and confirm it becomes 1.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now; SKILL.md hook recipes 1-2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -cF 'Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?' ; strings -n 40 \"$bin\" | grep -oF 'Hook JSON output had unrecognized keys (ignored): ' ; strings -n 40 \"$bin\" | grep -oF 'if(f&&e.hookSpecificOutput.hookEventName!==f)throw Error' ; csift search \"\" . -t harness.meta.hook --additional-context --count-by session",
          "observed": "The literal `Did you mean hookSpecificOutput.additionalContext (with a hookEventName)?` is present (2 occurrences: the string-table blob and the code site). Its function walks the returned object's keys against the accepted schema, collects the unrecognized ones, and logs `Hook JSON output had unrecognized keys (ignored): <keys>.` appending that hint only when `additionalContext` is among them - so a bare top-level `additionalContext` is dropped as an unrecognized key. Separately, the accepted path is gated: `if(e.hookSpecificOutput){if(f&&e.hookSpecificOutput.hookEventName!==f)throw Error(\\`Hook returned incorrect event name: expected '${f}' but got '${e.hookSpecificOutput.hookEventName}'...\\`);switch(e.hookSpecificOutput.hookEventName){...}}`. Live: with the nested form in use, `csift search \"\" . -t harness.meta.hook --additional-context --count-by session` reports 9961 matched records across 353 session keys.",
          "rule": "Binary: exact literal presence in the string table (grep -cF), one match each for the diagnostic, the unrecognized-keys prefix, and the event-name equality throw. Corpus: one census record per `type:\"attachment\"` record whose payload type is `hook_additional_context`, counted per owning session by csift's `--count-by session` axis.",
          "note": "The behavior statement is corroborated verbatim; only the depends narrative needed tightening. csift's two recipe sites are verbatim at the claimed lines: SKILL.md:532 and SKILL.md:554, both emitting `{hookSpecificOutput:{hookEventName:\"<Event>\",additionalContext:$c}}` with the event echoed."
        }
      ]
    },
    {
      "id": "ELI-022",
      "area": "elicitation",
      "behavior": "TWELVE hook events read `hookSpecificOutput.additionalContext` and inject it: PostModelSwitch, PostToolBatch, PostToolUse, PostToolUseFailure, PreToolUse, SessionStart, Setup, Stop, SubagentStart, SubagentStop, UserPromptExpansion, UserPromptSubmit. Every other event runs its hook and discards the field.",
      "depends": "csift's shipped recipes are registered under events from this list (`SessionStart(compact)` for the verbatim re-injection, `PostToolUseFailure(TaskStop)` for the teammate-kill redirect); one registered under any OTHER event runs, exits 0, and is silently ignored. What an injection leaves on disk is a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}` - csift labels it `harness.meta.hook` and parses it only under `search --additional-context` or an explicit `show` address, so the trace exists but never pollutes a default scan.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "288-341",
          "snippet": "    #[must_use]\n    pub fn hook_additional_context_text(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }\n        let v = self.attachment_value()?;\n        let att = v.as_object()?;"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "150-155",
          "snippet": "        // Hook-injected additionalContext (a `type:\"attachment\"` record): harness machinery,\n        // not a message - labeled `harness.meta.hook`. Only `search --additional-context`\n        // (or an explicit `show --line`/`--uuid` address) ever parses these lines, so the\n        // label is unreachable elsewhere; the record never opens a turn.\n        if self.hook_additional_context_text().is_some() {\n            push_unique(&mut out, Class::MetaHook);"
        },
        {
          "path": "SKILL.md",
          "lines": "532",
          "snippet": "$chunk\" '{hookSpecificOutput:{hookEventName:\"SessionStart\",additionalContext:$c}}'"
        },
        {
          "path": "SKILL.md",
          "lines": "554",
          "snippet": "jq -n --arg c \"$ctx\" '{hookSpecificOutput:{hookEventName:\"PostToolUseFailure\",additionalContext:$c}}'"
        }
      ],
      "instrument": "The claim's regex `case\"[A-Za-z]+\":(if\\()?F\\.additionalContext` structurally cannot see two of the twelve: PreToolUse assigns after a nested permissionDecision switch, and Stop is a bare fallthrough label sharing SubagentStop's assignment. Use the brace-depth arm scan above, or cross-check against the on-disk `attachment.hookEvent` census, which surfaces PreToolUse immediately as the largest bucket.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" into a scratch file, then a python brace-depth scan that locates `switch(e.hookSpecificOutput.hookEventName){`, walks braces to the matching close, splits the body on `case\"X\":` labels found at brace depth 0, and reports every label whose arm (fallthrough runs inherited) mentions `additionalContext`. Corpus side: python over ~/.claude/projects/*/*.jsonl tallying `attachment.hookEvent` for every record whose `attachment.type` is `hook_additional_context`.",
          "observed": "The switch has 18 top-level case arms: PreToolUse, UserPromptSubmit, UserPromptExpansion, SessionStart, Setup, PreModelSwitch, PostModelSwitch, SubagentStart, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, PermissionDenied, PermissionRequest, Elicitation, ElicitationResult, MessageDisplay. TWELVE of them assign additionalContext: PostModelSwitch, PostToolBatch, PostToolUse, PostToolUseFailure, PreToolUse, SessionStart, Setup, Stop, SubagentStart, SubagentStop, UserPromptExpansion, UserPromptSubmit. The two the claim missed are visible verbatim: PreToolUse's arm ends `...if(F.hookPermissionDecisionReason=...,e.hookSpecificOutput.updatedInput)F.updatedInput=...;F.additionalContext=e.hookSpecificOutput.additionalContext;break;`, and Stop is a fallthrough sharing the assignment: `case\"Stop\":case\"SubagentStop\":F.additionalContext=e.hookSpecificOutput.additionalContext;break;`. Corpus: 80756 `hook_additional_context` attachment records, by hookEvent - PreToolUse 50074, PostToolUse 26900, UserPromptSubmit 2783, PostToolUseFailure 688, SessionStart 311. PreToolUse is thus the dominant on-disk carrier, 62% of all injections.",
          "rule": "Binary: one event per case label in the additionalContext switch whose arm assigns `additionalContext`, with fallthrough label runs credited to the arm they fall into and nested switches skipped by brace depth. Corpus: one record per jsonl line whose `attachment.type` is `hook_additional_context`, bucketed by its `attachment.hookEvent` field.",
          "note": "The mechanism, the attachment trace and csift's gating all hold; only the enumeration was wrong, and wrong in a way that omitted the majority carrier. csift's four code sites are verbatim at the claimed lines: src/model/predicates.rs:335-341, src/model/classify.rs:150-155, SKILL.md:532, SKILL.md:554."
        }
      ]
    },
    {
      "id": "ELI-023",
      "area": "elicitation",
      "behavior": "A hook matcher is consulted for 21 hook events, not two, and what it is tested against varies by event: the tool name for the five tool/permission events, `source` for SessionStart, `trigger` for Setup and the compaction events, `agent_type` for the subagent events, and - for the two MCP elicitation events - `mcp_server_name`. So Elicitation and ElicitationResult DO take a matcher; it selects which MCP server the hook fires for. The grammar is `/^[a-zA-Z0-9_|, -]+$/` split on `/[|,]/` with each part trimmed for events in the lenient set (which includes both elicitation events); events outside it use the stricter `/^[a-zA-Z0-9_|]+$/` and split on `|` only, with no comma and no space. An absent or empty matcher matches everything, and so do `*` AND `.*`; an event whose matched field is undefined also always matches.",
      "depends": "csift's registration remains correct in practice for a different reason than stated: registering Elicitation and ElicitationResult with NO matcher matches every MCP server, which is what the recipe wants. The claim that those events 'take no matcher at all' is what is wrong.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "598",
          "snippet": "Register 5 events (one script, absolute path): `PreToolUse`+`PostToolUse`+`PostToolUseFailure` matcher `\"AskUserQuestion|ExitPlanMode\"`; `Elicitation`+`ElicitationResult` (no matcher)."
        },
        {
          "path": "src/session/rows.rs",
          "lines": "68-73",
          "snippet": "    /// True when the session's elicitation SIDECAR FILE exists at all (= the csift hook\n    /// is installed for this session - resolved pairs stay in the file). The tri-state a\n    /// consumer needs: present+pending / present+none (safe to conclude \"not blocked on\n    /// an elicitation\") / absent (hook unknown - CANNOT conclude anything). Always false\n    /// for a subagent row (the sidecar is keyed by the top-level session).\n    pub sidecar_present: bool,"
        },
        {
          "path": "src/elicitation.rs",
          "lines": "91-93",
          "snippet": "/// MCP markers are exempt: an MCP elicitation never has a native form, so sidecar pairing\n/// stays its only signal - and its `csiftKey` may be a non-unique server name, unsafe to\n/// substring-scan."
        }
      ],
      "instrument": "resolve the Claude Code binary (`bin=$(command -v claude)`; follow symlinks with `readlink -f`) and `strings -n 40 \"$bin\" | grep -oE 'if\\(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\"\\)return!1;.{0,200}'`: the early return is followed by the `/^[a-zA-Z0-9_|, -]+$/` test and the `.split(/[|,]/)` with a trim per part. Counting rule: exact literal presence of the regex and of the split expression (one match each). Live check: register the elicitation recipe with a matcher containing a colon and confirm the sidecar is never created.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now; SKILL.md elicitation-marker hook registration"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oE 'if\\(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\"\\)return!1;.{0,200}' ; strings -n 40 \"$bin\" | grep -oF 'case\"Elicitation\":return e.mcp_server_name' ; strings -n 40 \"$bin\" | grep -oE 'matcherMetadata:\\{fieldToMatch:\"[a-z_]+\"' | sort | uniq -c ; strings -n 40 \"$bin\" | grep -oF '.split(/[|,]/).map((d)=>d.trim()).filter(Boolean)'",
          "observed": "The runtime matcher is `function hXn(e,n,r){let o=_Xn(e);return!o||!n||Kwt(o,xnr(e.hook_event_name,n),Wje.has(e.hook_event_name),r,void 0,\"tool_input\"in e?e.tool_input:void 0)}` and `_Xn` is a switch that returns the value the matcher tests against, PER EVENT: `tool_name` for PreToolUse/PostToolUse/PostToolUseFailure/PermissionRequest/PermissionDenied; `command_name` for UserPromptExpansion; `source` for SessionStart/ConfigChange/DirectoryAdded; `trigger` for Setup/PreCompact/PostCompact; `notification_type` for Notification; `reason` for SessionEnd; `error` for StopFailure; `agent_type` for SubagentStart/SubagentStop; `load_reason` for InstructionsLoaded; a path derivation for FileChanged; and, verbatim, `case\"Elicitation\":return e.mcp_server_name;case\"ElicitationResult\":return e.mcp_server_name;`. The declared registry agrees: 22 `matcherMetadata:{fieldToMatch:...}` entries, of which `tool_name` 5, `source` 3, `trigger` 3, `agent_type` 2, `to_model` 2, `mcp_server_name` 2, and one each for `command_name`, `error`, `load_reason`, `notification_type`, `reason`. `Wje` - the set that selects the LENIENT grammar - holds 21 events including Elicitation and ElicitationResult. The grammar itself is real but two-valued: `function Cnr(e,n,r){if(!(n?/^[a-zA-Z0-9_|, -]+$/:/^[a-zA-Z0-9_|]+$/).test(e))return;return e.split(n?/[|,]/:\"|\").map((d)=>d.trim()).filter(Boolean)...}`. Match-all is `function mEe(e){return!e||e===\"*\"||e===\".*\"}` - three forms, not two. The function the claim cited, `if(e!==\"PostToolUse\"&&e!==\"PostToolUseFailure\")return!1;`, is a NARROW helper: its body then tests whether every tool the matcher names is in `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`, i.e. 'does this matcher select only file-writing tools', not 'does this matcher fire'.",
          "rule": "One matched-field mapping per `case\"<Event>\":return e.<field>` arm of the runtime field switch; one declared matcher per `matcherMetadata:{fieldToMatch:\"<field>\"` entry in the event registry; exact literal presence for the two grammar regexes and the match-all predicate.",
          "note": "Live corroboration that the grammar half is real: the elicitation recipe is registered with matcher \"AskUserQuestion|ExitPlanMode\" on the three tool events and no matcher on the two MCP events, and it has written 260 sidecar lines across the corpus (235 AskUserQuestion, 21 ExitPlanMode, 4 unreadable fossils), the newest pair under 2.1.258. csift's two code sites are verbatim at the claimed lines: SKILL.md:598 and src/session/rows.rs:68-73. code-site note: The claim's cited function is the wrong one. The matcher dispatch to cite is the pair `hXn` (the test) and `_Xn` (the per-event matched-field switch), reachable with `strings -n 40 \"$bin\" | grep -oF 'case\"Elicitation\":return e.mcp_server_name'`."
        }
      ]
    },
    {
      "id": "ELI-024",
      "area": "elicitation",
      "behavior": "`CLAUDE_PROJECT_DIR` is set in the environment of EVERY hook child process at 2.1.258, with no distinction between a user-level and a project-level registration, and `${CLAUDE_PROJECT_DIR}` is substituted literally into exec-form command and args regardless of scope. A shell-form user-level hook can therefore expand it normally. The variables that genuinely are scope-gated are `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}`, which throw a named error when used outside a plugin registration. A relative `script`-type hook path is anchored to `${CLAUDE_PROJECT_DIR}/<path>` rather than failing.",
      "depends": "Absolute script paths in csift's recipes remain good practice - they make the registration independent of which project the session opened in, which is a real hazard when one global hook serves many repositories - but the stated REASON (no CLAUDE_PROJECT_DIR for global installs) does not hold at 2.1.258, so a relative path in a global registration does not fail for that reason.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "534",
          "snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
        },
        {
          "path": "SKILL.md",
          "lines": "598",
          "snippet": "Register 5 events (one script, absolute path): `PreToolUse`+`PostToolUse`+`PostToolUseFailure` matcher `\"AskUserQuestion|ExitPlanMode\"`; `Elicitation`+`ElicitationResult` (no matcher)."
        }
      ],
      "instrument": "Quote the counting rule: `strings -n 10 \"$bin\" | grep -c 'CLAUDE_PROJECT_DIR'` = 29 LINES at 2.1.258; occurrences are 46. Neither number tests the claim - the deciding instrument is `grep -oF 'CLAUDE_PROJECT_DIR:Ue(je)'` and `grep -oF 'CLAUDE_PROJECT_DIR:n.projectDir'`, showing the ungated env injection.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SKILL.md hook recipe 1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 10 \"$bin\" | grep -c 'CLAUDE_PROJECT_DIR' ; strings -n 10 \"$bin\" | grep -o 'CLAUDE_PROJECT_DIR' | wc -l ; strings -n 40 \"$bin\" | grep -oF 'CLAUDE_PROJECT_DIR:Ue(je)' ; strings -n 40 \"$bin\" | grep -oF 'CLAUDE_PROJECT_DIR:n.projectDir' ; strings -n 40 \"$bin\" | grep -oF 'but the hook is not associated with a plugin. This variable is only available in hooks defined in a plugin'",
          "observed": "grep -c reports 29 and grep -o | wc -l reports 46 - the claim's 29 is a LINE count, not an occurrence count. In the single hook executor, `je=f.projectRoot` and the environment handed to every hook child is built as `gn={...,CLAUDE_PROJECT_DIR:Ue(je)}`; the fallback spawn path likewise passes `env:{...,CLAUDE_PROJECT_DIR:n.projectDir}`. Neither site branches on where the hook was registered. Exec-form commands additionally get literal substitution: `Ps=Ps.replaceAll(\"${CLAUDE_PROJECT_DIR}\",()=>je)` applied to `e.command` and every arg, again ungated. The ONLY scope-gated variables are the plugin ones, and the harness says so explicitly: `Hook command references ${CLAUDE_PLUGIN_ROOT} but the hook is not associated with a plugin. This variable is only available in hooks defined in a plugin's hooks/hooks.json file, not in settings.json.` - there is no counterpart message for CLAUDE_PROJECT_DIR. A `script`-type hook with a relative path is not rejected but ANCHORED: `if(isAbsolute(e)||[\"${CLAUDE_PROJECT_DIR}\",\"${CLAUDE_PLUGIN_ROOT}\",\"${CLAUDE_PLUGIN_DATA}\"].some(r=>e.startsWith(r)))return e; if(e===\"~\"||/^~[\\\\/]/.test(e))return join(homedir(),e.slice(2)); return `${CLAUDE_PROJECT_DIR}/${e}``.",
          "rule": "grep -c counts matching LINES of the strings output; grep -o | wc -l counts occurrences - the two differ 29 vs 46 here, so a claim quoting one must name which. For the mechanism: one hook-executor env construction site and one fallback spawn site, both inspected for a registration-scope branch; absence of any such branch, plus the presence of an explicit scope error for the plugin variables and none for CLAUDE_PROJECT_DIR, is the finding.",
          "note": "What is still open: whether a shell-form relative command path resolves, which depends on the hook child's cwd rather than on CLAUDE_PROJECT_DIR. Deciding it needs the live re-registration test the claim describes (register a global hook with a relative command, fire its event, look for a `harness.meta.hook` attachment); it was not run here because it would mutate the operator's global hook registrations. csift's two code sites are verbatim at the claimed lines: SKILL.md:534 and SKILL.md:598."
        }
      ]
    },
    {
      "id": "ELI-025",
      "area": "elicitation",
      "behavior": "10000 characters is a THRESHOLD, not a hard cap, and crossing it does not silently truncate. Above it the harness persists the payload to a `tool-results/hook-<id>-<n>-additionalContext.txt` file beside the transcript and injects a `<persisted-output>` pointer carrying the true size and a preview; only if persisting fails does it truncate, and then it appends `[Hook additionalContext truncated at 10000 chars - persist-to-disk failed: <error>]`. Both outcomes are self-disclosing. Separately, the SessionStart carrier is observed delivering payloads far above the threshold intact - 32127 characters at 2.1.258.",
      "depends": "The failure mode the claim names - 'exceeding the cap re-injects clipped turns as if whole' - does not occur at 2.1.258: neither over-threshold path is silent. `verbatim --window 9000` remains a reasonable default (it keeps each slice inside the threshold and so avoids the pointer indirection entirely), but it is a tidiness choice, not a fidelity guard.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "372",
          "snippet": "- Slicing (hook injection, ≤10000-char cap): `--slices N --slice i --window W` = fixed-fleet chunks, whole turns; out-of-range ⇒ nothing, exit 0 (hook affordance). Text-only."
        },
        {
          "path": "SKILL.md",
          "lines": "534",
          "snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
        },
        {
          "path": "src/turns/run.rs",
          "lines": "49-53",
          "snippet": "    // Normalize the budget to characters. `--slices N` pins the FLEET size, so the budget is\n    // derived as N windows (the slice COUNT is the hard constraint - a fixed set of registered\n    // hooks must never need to grow); otherwise it is the requested char/token amount.\n    let budget_chars = if let Some(n) = args.slices {\n        n.saturating_mul(args.window)"
        },
        {
          "path": "src/cli/verbatim_whoami_args.rs",
          "lines": "285-288",
          "snippet": "    /// fanning a >10K reconstruction across several SessionStart hooks: Claude Code caps EACH\n    /// hook's `additionalContext` at 10,000 CHARACTERS (over-cap is replaced by a file-path +\n    /// short preview, i.e. the body is effectively LOST to the model), so one hook per slice\n    /// keeps every injected chunk under the wall. Slicing is DETERMINISTIC (same session +"
        },
        {
          "path": "src/turns/render.rs",
          "lines": "49-51",
          "snippet": "        let cap_override = slices.map(|_| window.saturating_sub(SLICE_BODY_HEADROOM).max(1));\n        let doc = build_document_body(sessions, plans, &ctx.cfg, cap_override);\n        let chunks = slice_into_windows(&doc, window);"
        }
      ],
      "instrument": "Return an `additionalContext` of exactly 10,001 characters from a `SessionStart` hook, then read what landed: `csift search \"\" @<session> -t harness.meta.hook --additional-context --format json --no-truncate | jq '.text|length'`. Counting rule: characters of the injected attachment payload compared against the characters the hook printed; a shortfall at 10,000 is the cap. A static corpus cannot settle this - it needs a live hook.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "SKILL.md hook recipe 1; SKILL.md verbatim slicing"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "bin=$(readlink -f \"$(command -v claude)\"); strings -n 40 \"$bin\" | grep -oF 'NQn=1e4' ; strings -n 40 \"$bin\" | grep -oF 'async function ipe(e,n,r,{threshold:o=NQn,storageV5:d}={}){if(e.length<=o)return e;' ; strings -n 40 \"$bin\" | grep -oF 'yield{additionalContexts:[await ipe(Rn.additionalContext,`${o}-${Bn}`,\"additionalContext\",{storageV5:we})]}' ; plus a python pass over one project directory's *.jsonl measuring `len(attachment.content)` for every `hook_additional_context` record, bucketed by the record's `version` field.",
          "observed": "The constant is real: `NQn=1e4`, and it is the default `threshold` of the handler every injected additionalContext passes through. But the over-threshold branch is NOT truncation: `if(e.length<=o)return e; let f=await $3(e,\\`hook-${n}-${r}\\`,kS(),d); if(U3(f))return ...,\\`${e.slice(0,o)}\\n[Hook ${r} truncated at ${o} chars - persist-to-disk failed: ${f.error}]\\`; let _=Npe(f); return ...,_;` - the payload is persisted to a sibling file and replaced by a pointer, and truncation is only the persist-FAILURE fallback, carrying an explicit marker naming the char count and the reason. On disk both halves are visible. Ten records carry the pointer form, e.g. `<persisted-output>\\nOutput too large (12.3KB). Full output saved to: <project>/<session>/tool-results/hook-<tool_use_id>-<n>-additionalContext.txt` followed by a preview - and the pointer record itself is ~2.2KB, i.e. the model saw the pointer, not a silent stub. And the cap is not universal: of 9459 hook_additional_context records in one project directory, 17 exceed 10000 chars (max 41398), and the single record stamped version 2.1.258 among them is a SessionStart injection of 32127 chars that landed WHOLE - no persisted pointer, no truncation marker.",
          "rule": "Binary: exact literal presence of the constant, the handler signature and the injection call site. Corpus: one measurement per `type:\"attachment\"` record whose payload type is `hook_additional_context`, length = characters of the payload content (list blocks joined with a newline), attributed to the CC build by the record's own `version` field; 'landed whole' = length above 10000 with neither the substring `persisted-output` nor `truncated at` present.",
          "note": "The claim's own instrument (return exactly 10001 characters and measure what landed) would still be the cleanest confirmation, but it is no longer needed: the corpus already contains the over-threshold cases in both forms, including one under the current build. csift's three code sites are verbatim at the claimed lines: SKILL.md:372, SKILL.md:534, src/turns/run.rs:49-53 - note that SKILL.md:372 and :534 both describe the threshold as a hard 10K cap and would need rewording to match the persist-then-pointer behavior."
        }
      ]
    },
    {
      "id": "FH-001",
      "area": "freshness-file-history",
      "behavior": "Claude Code writes a `type:\"file-history-snapshot\"` line carrying exactly the top-level keys `type`, `messageId`, `snapshot` and `isSnapshotUpdate` - no top-level `timestamp`, `uuid` or `parentUuid` - and nests `{messageId, timestamp, trackedFileBackups}` inside `snapshot`, so the line's instant must be read from `snapshot.timestamp` (or a per-entry `backupTime`).",
      "depends": "csift models it as a timestamp-less metadata line, promotes it to the gated leaf `harness.meta.snapshot`, and reads the instant out of `snapshot.timestamp` (falling back to the record timestamp): reading a top-level timestamp yields nothing, which would place every `recover` snapshot event at an unknown time and drop the record from any time window.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "265-268",
          "snippet": "/// The `harness.meta.snapshot` excerpt for a `file-history-snapshot` (every tracked\n/// path with its version, sorted) or a `file-history-delta` (the one path). The\n/// snapshot line has no top-level timestamp; the nested one rides the excerpt.\nfn snapshot_record_text(rec: &Record) -> Option<String> {"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "234-290",
          "snippet": "    }\n    let snap = rec.snapshot.as_ref()?;\n    let at = snap\n        .get(\"timestamp\")\n        .and_then(serde_json::Value::as_str)\n        .unwrap_or(\"?\");"
        }
      ],
      "instrument": "`rg -m1 '\"type\":\"file-history-snapshot\"' <transcript> | jq 'has(\"timestamp\"), .snapshot.timestamp'` must print `false` then an ISO instant; then union the top-level keys of every such line across `~/.claude/projects` and expect exactly those four. Counting rule: one key set per snapshot line, unioned per file.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md sections 4.8 and 5.1; CHANGELOG 0.10.0; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' -t harness.meta.snapshot --raw --max-count 0 > snap_raw.jsonl; rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c 'keys' | sort | uniq -c; rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c '.snapshot|keys' | sort | uniq -c; rg -N -m1 '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl | jq -c 'has(\"timestamp\"), has(\"uuid\"), has(\"parentUuid\"), .snapshot.timestamp'; csift search '' @<SESSION> -t harness.meta.snapshot --max-count 2 --format json | jq -c 'select(.kind==\"exchange\")|.hits[0]|{line,label,ts_utc,excerpt}'",
          "observed": "3947 of 3947 snapshot lines carry the key set [\"isSnapshotUpdate\",\"messageId\",\"snapshot\",\"type\"] and nothing else; 3947 of 3947 nest exactly [\"messageId\",\"timestamp\",\"trackedFileBackups\"]; the single-line probe printed false, false, false then \"2026-08-12T00:57:24.517Z\"; snapshot.timestamp matched ^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*Z$ on 3947 of 3947. csift's own render agrees: the hit JSON carries \"ts_utc\":null with excerpt \"[file-history snapshot at 2026-08-12T00:57:24.517Z: ]\".",
          "rule": "One key set per file-history-snapshot line, unioned over every such line csift emitted with --raw across all projects; presence tested with jq has() so a present-but-null key would still count as present.",
          "note": "Confirmed unchanged at CC 2.1.258. The consequence the claim predicts is directly visible in csift's output: a snapshot hit's ts_utc/ts_local are null because there is no top-level timestamp, and the instant that reaches the reader comes from snapshot.timestamp inside the excerpt. src/recover/events.rs:196-200 still matches the pinned snippet verbatim; the two record_text.rs sites moved down 22 lines with byte-identical text."
        }
      ]
    },
    {
      "id": "FH-002",
      "area": "freshness-file-history",
      "behavior": "A snapshot's snapshot.trackedFileBackups maps each tracked path to exactly {backupFileName, version, backupTime} plus realParentDir, and the tracked path is spelled either absolute or relative. realParentDir is declared optional and IS absent from every entry written by older builds, but current Claude Code emits it on every entry: measured 0 of 1014 entries in a 2.1.207-only session against 1214 of 1214 in a 2.1.234-only session, 37 of 37 at 2.1.251 and 667 of 667 at 2.1.258. Corpus-wide across all builds present in the store: 90047 of 293298 entries (30.7 percent), paths 188048 absolute to 105250 relative.",
      "depends": "`recover` reads `version`/`backupFileName`/`backupTime` off each entry and never assumes `realParentDir`; a relative tracked path with no `realParentDir` cannot be resolved from the record alone, which is why csift's settings-family scope law matches on path SHAPE rather than on a reconstructed absolute path.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "135-143",
          "snippet": "    /// `file-history-snapshot` payload (a top-level sibling). Carries\n    /// `{messageId, trackedFileBackups: {<path>: {backupFileName, version, backupTime}}}`.\n    /// Read only by `recover` to know a disk backup EXISTED for a path at a time\n    /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n    /// across real corpora), but the store it names is PRUNED and its content has no\n    /// transcript anchor, so it is never used to fabricate content; `recover\n    /// --list-backups` lists the store itself. Additive + tolerant.\n    #[serde(default)]\n    pub snapshot: Option<serde_json::Value>,"
        }
      ],
      "instrument": "Iterate every `trackedFileBackups` entry of every snapshot line in a set of large transcripts and count presence of `realParentDir`, null `backupFileName`, and absolute-vs-relative path spelling. Counting rule: one entry per (snapshot line, tracked path) pair, not per line.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.1",
        "source": "src/model/record.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -N '\"type\":\"file-history-snapshot\"' snap_raw.jsonl | jq -c '.snapshot.trackedFileBackups | to_entries[] | {p:.key, k:(.value|keys), rp:(.value|has(\"realParentDir\")), bfn:.value.backupFileName}' > entries.jsonl; jq -c '.k' entries.jsonl | sort | uniq -c; jq -r '.rp' entries.jsonl | sort | uniq -c; jq -r '.p | if startswith(\"/\") then \"absolute\" else \"relative\" end' entries.jsonl | sort | uniq -c; then per session: for f in $(rg -lN '\"type\":\"file-history-snapshot\"' <CORPUS>/*.jsonl); do rg -N '\"type\":\"file-history-snapshot\"' $f | jq -r '[.snapshot.trackedFileBackups|to_entries[]|(.value|has(\"realParentDir\"))]|map(select(.))|length'; rg -oN '\"version\":\"2\\.1\\.[0-9]+\"' $f | sort -u; done",
          "observed": "293298 entries. Entry key sets are exactly two: [\"backupFileName\",\"backupTime\",\"version\"] on 203251 and [\"backupFileName\",\"backupTime\",\"realParentDir\",\"version\"] on 90047 (30.7 percent), so no third shape exists. Paths: 188048 absolute, 105250 relative. Split by the CC version stamped on the session's records: 0 of 1014 entries carry realParentDir in a session whose records are all 2.1.207, 0 of 305 at 2.1.206, 0 of 627 at 2.1.191; 1214 of 1214 in a session whose records are all 2.1.234, 37 of 37 at 2.1.251, and 667 of 667 over the five newest snapshot lines of a live session whose tail records are 2.1.258.",
          "rule": "One entry per (snapshot line, tracked path) pair, not per line. A session is assigned a version set by the distinct \"version\":\"2.1.N\" values appearing anywhere in its records; only single-version sessions were used for the boundary.",
          "note": "The schema half holds verbatim: exactly two key shapes, never a third, and csift's Option handling is unaffected. What needs correcting is the 38 percent figure, which is a mixed-build aggregate and is not current-CC behavior. The version boundary lies between 2.1.208 and 2.1.234 and could not be narrowed further here because snapshot lines carry no version field of their own and the only sessions spanning that gap are multi-version. The practical consequence for the claim's depends clause: under current CC a relative tracked path always arrives with a realParentDir, so the unresolvable-relative-path case exists only in older transcripts, which csift still reads. src/recover/events.rs:209-220 matches verbatim."
        }
      ]
    },
    {
      "id": "FH-003",
      "area": "freshness-file-history",
      "behavior": "backupFileName is a key that is always emitted; its VALUE is null on 63492 of 293298 entries corpus-wide (21.6 percent), so 78.4 percent of entries name a store file. Per session the rate is 79-99 percent among sessions with at least 250 entries, and 91.8 percent (612 of 667) in the newest snapshot lines of a live 2.1.258 session. The doc's \"83-98% present\" band is slightly too narrow at the low end and the corpus figure sits just below it.",
      "depends": "csift reads it only to know a disk backup EXISTED for a path at a time (a coverage annotation) and to key the store lookup; the store it names is pruned and its content has no transcript anchor, so it is never used to fabricate content.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "138-141",
          "snippet": "    /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n    /// across real corpora), but the store it names is PRUNED and its content has no\n    /// transcript anchor, so it is never used to fabricate content; `recover\n    /// --list-backups` lists the store itself. Additive + tolerant."
        }
      ],
      "instrument": "Count `trackedFileBackups` entries corpus-wide and, among them, how many carry a non-null `backupFileName`. Counting rule: one probe per tracked-path entry, presence reported as a percentage of entries.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.1",
        "source": "CHANGELOG 0.8.1 (four doc sites corrected); src/model/record.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "jq -r 'if .bfn == null then \"null\" else \"present\" end' entries.jsonl | sort | uniq -c; per session: for f in $(rg -lN '\"type\":\"file-history-snapshot\"' <CORPUS>/*.jsonl); do rg -N '\"type\":\"file-history-snapshot\"' $f | jq -c --arg f $f '.snapshot.trackedFileBackups|to_entries[]|{f:$f,bfn:.value.backupFileName}'; done | jq -s 'group_by(.f)|map({n:length,null_bfn:(map(select(.bfn==null))|length)})'; and on the live 2.1.258 session: rg -N '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl | tail -5 | jq -c '.snapshot.trackedFileBackups|to_entries[]|{bfn:.value.backupFileName}'",
          "observed": "Corpus-wide 229806 of 293298 entries carry a non-null backupFileName (78.4 percent); 63492 are null (21.6 percent). Per session in one project directory, the seven sessions with at least 250 entries read 79.3, 89.6, 89.8, 98.5, 98.6, 98.7 and 98.8 percent present. Over the five newest snapshot lines of the live 2.1.258 session, 612 of 667 present (91.8 percent). The key \"backupFileName\" itself is present in 293298 of 293298 entries; only its value is ever null.",
          "rule": "One probe per (snapshot line, tracked path) entry; \"present\" means the JSON value is not null, so a present-but-null key counts as absent. Per-session rates computed only over sessions with at least 250 entries, to keep small-n sessions (which range 0-100 percent on a handful of entries) out of the band.",
          "note": "The direction of the correction the claim records (usually present, not frequently null) is confirmed and is unchanged at 2.1.258. Only the band moves: 79-99 percent per session rather than 83-98, and 78.4 percent when every entry in the store is pooled, because the pooled figure is dominated by a few large low-rate sessions. The clause about correcting four earlier doc sites is a repo-history statement and no instrument on this machine can check it; it is not part of this verdict."
        }
      ]
    },
    {
      "id": "FH-004",
      "area": "freshness-file-history",
      "behavior": "Claude Code also writes `type:\"file-history-delta\"` lines - one per tracked file per write - carrying `messageId`, `snapshotMessageId`, a top-level `trackingPath` naming exactly ONE path, a `backup` object `{backupFileName, version, backupTime, realParentDir?}`, and, unlike the snapshot line, a top-level `timestamp`.",
      "depends": "csift models `trackingPath` and `backup` as tolerant optional fields and promotes BOTH file-history line types to the single gated leaf `harness.meta.snapshot`; a delta parsed as a snapshot would report the wrong tracked-path set, and omitting the delta arm loses roughly 1,934 corpus lines from the searchable surface.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "145-153",
          "snippet": "    /// `file-history-delta` (v0.10.0): the ONE path this delta line tracks, beside the\n    /// `backup` object below. Absent everywhere else. Additive + tolerant.\n    #[serde(default, rename = \"trackingPath\")]\n    pub tracking_path: Option<String>,\n\n    /// `file-history-delta` `backup` object: `{backupFileName, version, backupTime,\n    /// realParentDir?}`. Kept as a `Value` (tiny); read by the promoted-leaf render.\n    #[serde(default)]\n    pub backup: Option<serde_json::Value>,"
        },
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "27",
          "snippet": "            \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "272-276",
          "snippet": "    if rec.is_type(\"file-history-delta\") {\n        let path = rec.tracking_path.as_deref()?;\n        let backup = rec.backup.as_ref();\n        let version = ver(backup.and_then(|b| b.get(\"version\")));\n        let at = rec.timestamp.as_deref().unwrap_or(\"?\");"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "358-362",
          "snippet": "    // v0.10.0 promoted lines. Quoted-value / bare-value needles per the R13 law: the\n    // `type` values `\"queue-operation\"` and `\"file-history-` (a prefix covering both\n    // `-snapshot` and `-delta`), and the bare `subtype` values for the three system\n    // records (a value substring survives a reserialize; prose quoting the word lands\n    // on a role line and is harmless - it already parses)."
        }
      ],
      "instrument": "The census has grown since the ledger was written: 4059 snapshot lines against 1971 delta lines (was 4013 and 1934). csift stats --format json is a cheaper and exact instrument for this than a corpus-wide rg, because its line-type census validates every line rather than only prefilter candidates.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 5.1 and the v0.10.0 ledger; CHANGELOG 0.10.0; src/model/record.rs comment; dev sessions 2026-09-01 and 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -N '\"type\":\"file-history-delta\"' snap_raw.jsonl | jq -c 'keys' | sort | uniq -c; rg -N '\"type\":\"file-history-delta\"' snap_raw.jsonl | jq -c '.backup|keys' | sort | uniq -c; csift stats --max-count 0 --format json | jq -s '[.[]|select(.kind==\"session\")] | {sessions: length, snap: (map(.line_types[\"file-history-snapshot\"]//0)|add), delta: (map(.line_types[\"file-history-delta\"]//0)|add)}'; csift search '' @<SESSION> -t harness.meta.snapshot --count-by label --max-count 0; rg -cN '\"type\":\"file-history-snapshot\"' <CORPUS>/<SESSION>.jsonl; rg -cN '\"type\":\"file-history-delta\"' <CORPUS>/<SESSION>.jsonl",
          "observed": "1539 of 1539 delta lines carry the key set [\"backup\",\"messageId\",\"snapshotMessageId\",\"timestamp\",\"trackingPath\",\"type\"] and nothing else, so the top-level timestamp the snapshot line lacks is present on every delta line. The nested backup object is [\"backupFileName\",\"backupTime\",\"realParentDir\",\"version\"] on 1037 and [\"backupFileName\",\"backupTime\",\"version\"] on 502. trackingPath is one path per line: 417 absolute, 1122 relative. Corpus line-type census over 7586 transcripts: 4059 file-history-snapshot lines in 62 sessions, 1971 file-history-delta lines in 19 sessions. csift folds both into one leaf exactly: on one session --count-by label reported 235 harness.meta.snapshot records against rg counts of 93 snapshot plus 142 delta lines.",
          "rule": "One key set per raw jsonl line. The corpus counts come from csift stats' whole-file line-type census (which fully validates every line), one count per raw line, summed over every session row; the csift-versus-rg agreement check is one record per matching line in a single transcript.",
          "note": "Holds at 2.1.258, and the render confirms the consumer-visible difference between the two line types: csift prints \"[file-history snapshot at <snapshot.timestamp>: ...]\" for one and \"[file-history delta at <top-level timestamp>: <trackingPath>@v<N> backup=<name>]\" for the other. One code-site caveat: the pinned classify_promoted.rs snippet matches the committed file at 13-25 byte for byte, but the working tree carries uncommitted 0.10.1 work that adds a compact_boundary arm and a MetaSystem catch-all, moving the file-history arm to line 27. The correction above anchors on the arm itself, which is stable under that edit."
        }
      ]
    },
    {
      "id": "FH-005",
      "area": "freshness-file-history",
      "behavior": "The hint is emitted only when the Bash command matches a 19-alternative FORMATTER-IDIOM regex (--write, --fix, --in-place, --auto-correct, run format, run fix, (yarn|pnpm) format, lint:file, lint:fix, black, isort, ruff format, cargo (fmt|fix), rustfmt, go fmt, terraform fmt, dprint fmt, swiftformat, phpcbf), not after every write-shaped command; a command that fails that regex is never stat-checked at all, and a backgrounded Bash call is skipped outright. When it does fire, Claude Code stats every read-set entry and keeps those whose mtime moved past both the command start and the recorded read instant. The sentence uses a real pluralizer, so the literal is \"N file\" or \"N files\", never the literal \"file(s)\"; the named list caps at 5 comma-space separated paths with \" and M more\" for the rest; each path is rendered relative to the cwd with a fallback to the raw (absolute) path when relativization fails; and the closing clause interpolates the Read tool's registered name, so the tail is \"Call <tool name> before editing.]\" rather than a fixed string.",
      "depends": "`recover` parses the hint into named paths plus the remainder count, resolves each against the CARRYING record's own `cwd`, and raises a hard `hint_modified` boundary - the one signal that attributes a formatter-class rewrite to concrete files. A target hidden in the truncated remainder is undetectable: a documented limit rather than a silent gap, and a change to the sentence head or the relative-path convention would remove csift's only authoritative formatter attribution.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "22-33",
          "snippet": "    // ── (0) Bash result hint: `staleReadFileStateHint` names files THIS command\n    //    modified out of the read set. CC computed the list from its own readFileState\n    //    (an mtime stat over every read file), so a named match is authoritative. The\n    //    paths are rendered relative to the shell's cwd; resolve before matching. A\n    //    truncated tail (\"… and N more\") names no paths, so only the named first-5 can\n    //    ever match: a target hidden in the remainder is NOT detected here (documented\n    //    limit; the window's bash disclosure still covers the command itself).\n    if let Some(hint) = tur\n        .get(\"staleReadFileStateHint\")\n        .and_then(serde_json::Value::as_str)\n    {\n        if let Some((paths, _more)) = parse_stale_read_hint(hint) {"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "299-310",
          "snippet": "/// Parse a `toolUseResult.staleReadFileStateHint` string into its named paths plus\n/// the truncated-remainder count. Wire shape (Claude Code 2.1.237, corpus-verified):\n/// `[This command modified N file(s) you've previously read: p1, p2 and M more. Call\n/// Read before editing.]` - the list caps at five names; `M` counts the unnamed rest\n/// (0 when the list is complete). Paths are comma-space separated and rendered\n/// relative to the recording shell's cwd.\npub(crate) fn parse_stale_read_hint(hint: &str) -> Option<(Vec<String>, usize)> {\n    let rest = hint.strip_prefix(\"[This command modified \")?;\n    let colon = rest.find(\": \")?;\n    let mut tail = &rest[colon + 2..];\n    if let Some(end) = tail.rfind(\". Call Read before editing.]\") {\n        tail = &tail[..end];"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "55-61",
          "snippet": "    /// Claude Code's own modified-file attribution: a Bash result's\n    /// `staleReadFileStateHint` named this file as modified by the command (\"[This\n    /// command modified N file(s) you've previously read: …]\"). The hint's paths are\n    /// rendered RELATIVE to the recording shell's cwd and are resolved against the\n    /// carrying record's own `cwd` before matching. AUTHORITATIVE (CC stat'd the\n    /// read-set itself), content-less: a hard boundary at replay.\n    StaleReadHint { path: String },"
        }
      ],
      "instrument": "`rg -c 'This command modified' ~/.claude/projects --glob '*.jsonl'` (577 attributions measured), then dump one value and confirm the exact prefix, the five-name cap and the ` and N more` tail; `csift recover <target> --file <a named path> --coverage --format json | jq 'select(.cause==\"hint_modified\")'` must show the boundary. Counting rule: one hint per Bash result object carrying the key, at most five named paths per hint plus a separately counted remainder.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9 as consumed by section 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0; src/recover/carriers.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,420}This command modified.{0,200}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function Wqo\\(.{0,300}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'jqo=new RegExp\\(\\[[^\\]]{0,900}\\]'; rg -NI 'staleReadFileStateHint' <CORPUS>/*.jsonl | jq -r '.toolUseResult.staleReadFileStateHint' > hints.txt; sed -E 's/^\\[This command modified [0-9]+ files? you.ve previously read: //; s/\\. Call Read before editing\\.\\]$//' hints.txt | awk -F', ' '{print NF}' | sort -n | uniq -c; csift recover @<SESSION> --file <REPO>/src/search.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
          "observed": "Binary, verbatim: \"let Ht=ne(),bn=5,Rn=Cn.slice(0,5).map((Tr)=>Rqo(Ht,Tr)||Tr).join(\\\", \\\"),Dn=Cn.length>5?` and ${Cn.length-5} more`:\\\"\\\",nr=t3t(dt,n.options.tools);An=`[This command modified ${Cn.length} ${H(Cn.length,\\\"file\\\")} you've previously read: ${Rn}${Dn}. Call ${nr} before editing.]`\" and the producer \"function Wqo(e,n,r){if(!jqo.test(e))return[];let o=[];return await Promise.all(Array.from(n.entries(),([d,f])=>Bx(d).then((_)=>{if(_>r&&_>f.timestamp)o.push(d)}).catch(()=>{}))),o}\", gated by jqo=new RegExp([\"--write\",\"--fix\",\"--in-place\",\"--auto-correct\",\"\\\\brun\\\\s+format\\\\b\",\"\\\\brun\\\\s+fix\\\\b\",\"\\\\b(yarn|pnpm)\\\\s+format\\\\b\",\"\\\\blint:file\\\\b\",\"\\\\blint:fix\\\\b\",\"\\\\bblack\\\\b\",\"\\\\bisort\\\\b\",\"\\\\bruff\\\\s+format\\\\b\",\"\\\\bcargo\\\\s+(fmt|fix)\\\\b\",\"\\\\brustfmt\\\\b\",\"\\\\bgo\\\\s+fmt\\\\b\",\"\\\\bterraform\\\\s+fmt\\\\b\",\"\\\\bdprint\\\\s+fmt\\\\b\",\"\\\\bswiftformat\\\\b\",\"\\\\bphpcbf\\\\b\"]). Corpus: 145 hints in one project directory over 6 transcripts; named-path histogram 73 with 1, 20 with 2, 11 with 3, 3 with 4, 3 with 5, never more; both hints carrying a remainder tail named exactly 5 and read \" and 1 more\" with a stated total of 6. Sample value: \"[This command modified 1 file you've previously read: tests/cli_integration.rs. Call Read before editing.]\". csift recover reported cause \"hint_modified\", confidence \"authoritative\", events.stale_hint 2.",
          "rule": "One hint per Bash tool_result object carrying the key; named paths counted by splitting the colon-tail on \", \" after stripping the fixed head and the fixed tail, so a remainder tail glues onto the fifth name and still yields five fields.",
          "note": "The cap, the remainder tail, the comma-space join and the cwd-relative rendering all hold exactly as claimed, and csift's hint_modified boundary fires on real data. Two refinements matter for csift. First, the gate is narrower than \"write-shaped\": every hint-producing command sampled from the live 2.1.258 session began with a formatter invocation, and a shell rewrite that does not match the regex produces no hint at all, which strengthens rather than weakens the documented limit. Second, parse_stale_read_hint strips a hardcoded \". Call Read before editing.]\" suffix while the binary interpolates the tool name there; the gate regex is byte-identical in 2.1.241 and 2.1.251, so this is a long-standing imprecision in the claim rather than drift. All three pinned code sites (src/recover/carriers.rs 22-33 and 299-310, src/recover/types.rs 55-61) match verbatim at the stated lines."
        }
      ]
    },
    {
      "id": "FH-006",
      "area": "freshness-file-history",
      "behavior": "A SUCCESSFUL Edit whose target had drifted on disk since the last Read, but whose old_string stayed unique so the edit applied anyway, carries toolUseResult.staleRecovered:true. Claude Code computes it as (the edit still applies) AND (a guard predicate), and reports it to its own telemetry as the \"recovered\" field of tengu_edit_tool_stale_read. The relative frequency of these successes against hard rejections is corpus-specific and does not generalize: measured 13 successes to 14 rejections across the two largest project directories in this store (13 to 8 in one, 0 to 6 in the other), against the 75 to 38 recorded earlier.",
      "depends": "csift records a NON-invalidating `StaleRecovered` annotation on that window: the edited span stays trusted while the disk is disclosed as carrying unseen changes, so `recover` never presents a reconstruction as complete when it provably is not.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "131-138",
          "snippet": "        // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n        // disk since the last read, but old_string stayed unique so the edit applied.\n        // The disk holds changes this stream never saw - an authoritative annotation.\n        if tur\n            .get(\"staleRecovered\")\n            .and_then(serde_json::Value::as_bool)\n            .unwrap_or(false)\n        {"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "62-67",
          "snippet": "    /// A SUCCESSFUL Edit whose `toolUseResult.staleRecovered:true` reports the file\n    /// had been modified on disk since the last read; the edit still applied cleanly\n    /// (old_string stayed unique). The buffer's edited span is right, but the disk\n    /// holds other changes this stream never saw: an authoritative, NON-invalidating\n    /// annotation boundary.\n    StaleRecovered,"
        }
      ],
      "instrument": "`rg -c '\"staleRecovered\":true' ~/.claude/projects --glob '*.jsonl'` against `rg -c 'File has been modified since read'` over the same scope (expect roughly 2:1 successes to rejections), confirming each carrier is a non-error Edit result; then `csift recover <target> --file <path> --coverage --format json | jq '.events'` counts it under `stale_recovered`. Counting rule: one per tool_result object carrying the flag.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md sections 4.9 and 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,260}File has been modified since read.{0,120}'; rg -N '\"staleRecovered\":true' <CORPUS>/<SESSION>.jsonl | jq -c '{type, is_err:([.message.content[]?|select(.type==\"tool_result\")|.is_error]|first), tur_keys:(.toolUseResult|keys)}'; rg -cNI '\"staleRecovered\":true' <CORPUS>/*.jsonl; rg -NI 'has been modified since read' <CORPUS>/*.jsonl | jq -r 'select(.type==\"user\")|.message.content[]?|select(.type==\"tool_result\")|select((.content|tostring)|test(\"has been modified since read\"))|(.is_error // false)|tostring' | sort | uniq -c; csift recover @<SESSION> --no-subagents --file <REPO>/tests/cli_integration.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|.events'",
          "observed": "Binary Edit gate, verbatim: \"if(U){if(cB(_)>U.timestamp)if(Soe(U)&&z$(U,B));else{let ke=OQe(B,d,o),we=ke===\\\"applies\\\"&&CJ(zt,_,n,pe(n));if(s(\\\"tengu_edit_tool_stale_read\\\",{wouldHaveResult:CJt(ke),recovered:we}),!we)return{result:!1,behavior:\\\"ask\\\",message:\\\"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\\\",errorCode:7}}}\". Every carrier sampled is a type:\"user\" record whose tool_result has no is_error and whose toolUseResult keys are [filePath,newString,oldString,originalFile,replaceAll,staleRecovered,structuredPatch,userModified]. Counts: 13 staleRecovered:true lines in the largest project directory against 8 genuine rejections there, and 0 against 6 in the second-largest, so 13 successes to 14 rejections across the two. csift recover reported events.stale_recovered 4 on a file with 4 such carriers.",
          "rule": "One success per tool_result object carrying toolUseResult.staleRecovered == true. One rejection per tool_result block whose content matches \"has been modified since read\" AND whose is_error is true; the is_error filter is load-bearing, because in these two directories 45 further blocks carry the same string only as echoed file content or command output and would inflate a bare line count more than threefold.",
          "note": "The mechanism and the on-disk shape hold exactly, and the binary now supplies the missing half of the story: the flag is set when the edit-applies check succeeds on a drifted file, which is precisely the condition the claim describes. Only the ratio needs a scope caveat; it inverts between the two directories measured here, so it should not be quoted as a property of the signal. Both pinned code sites (src/recover/carriers.rs 131-138, src/recover/types.rs 62-67) match verbatim at the stated lines."
        }
      ]
    },
    {
      "id": "FH-007",
      "area": "freshness-file-history",
      "behavior": "The attachment is built by walking the whole read set, skipping any entry recorded with an offset or limit (full-view reads only), requiring the file's mtime to have moved past the recorded read instant, requiring the content hash to differ, and requiring a non-empty rendered diff. It names the file in `filename` and carries the excerpt in `snippet`; `content` is the sibling edited_image_file payload's key, not an alternative spelling on the text form, and `filePath` does not appear at all in current data (0 of 1047 records). The 16 KB budget is the literal constant 16384 and is CUMULATIVE across one batch of attachments: attachments are filled in order until the running total of snippet lengths reaches 16384, after which every remaining snippet is replaced with the empty string. So an empty snippet is not a per-file overflow but a position-in-batch effect, and it is the degraded form of the same record rather than an empty edit.",
      "depends": "`recover` reads it as an `ExternalEdit` HARD boundary and names the empty-snippet degraded form explicitly; reading an empty snippet as \"no change\" would silently continue a replay across an unseen external edit, and a payload key rename would drop the only content-bearing external-edit signal. The attachment cannot distinguish a human edit from a formatter run, which csift discloses.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "218-230",
          "snippet": "    // (7a) edited_text_file → an external edit (hard boundary).\n    if atype == Some(\"edited_text_file\") {\n        let path = att\n            .get(\"filename\")\n            .or_else(|| att.get(\"filePath\"))\n            .and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {\n            let snippet_text = att\n                .get(\"snippet\")\n                .or_else(|| att.get(\"content\"))\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default();\n            let snippet = strip_gutter(snippet_text);"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "50-54",
          "snippet": "    /// An external/user edit captured as an `edited_text_file` attachment snippet.\n    /// An EMPTY snippet is the attachment's over-budget degraded form (the change\n    /// exceeded the 16KB budget): the signal is still authoritative, the content is\n    /// not carried.\n    ExternalEdit { snippet: Vec<(usize, String)> },"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "207-213",
          "snippet": "                // An EMPTY snippet is the attachment's over-budget degraded form: the\n                // change exceeded the 16KB budget, so the signal arrives content-less.\n                let detail = if snippet.is_empty() {\n                    \"edited_text_file attachment (file changed outside the tool stream; \\\n                     no snippet: the change exceeded the attachment budget)\"\n                } else {\n                    \"edited_text_file attachment (file changed outside the tool stream)\""
        }
      ],
      "instrument": "`rg -c 'edited_text_file' ~/.claude/projects --glob '*.jsonl'` (1,898 occurrences across 133 files measured; a separate per-record census counted 1,322-1,328 attachment records of that payload type), then `jq -r 'select(.attachment.type==\"edited_text_file\") | [(.attachment.filename // .attachment.filePath), ((.attachment.snippet // \"\")|length)] | @tsv' <file>` to see names and snippet sizes including zeros. Counting rule: one attachment record per line whose attachment payload `type` is `edited_text_file`.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md sections 4.9 and 6.7; AGENTS.md section 3.11; CHANGELOG 0.8.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,700}\"edited_text_file\".{0,300}'; csift search '' --count-by attachment --max-count 0; csift search '\"type\":\"edited_text_file\"' --attachments -t harness.meta.attachment --raw --max-count 0 > etf_raw.jsonl; jq -c 'select(.type==\"attachment\")|.attachment|keys' etf_raw.jsonl | sort | uniq -c; jq -r 'select(.type==\"attachment\")|.attachment|if ((.snippet // .content // \"\")|length)==0 then \"empty\" else \"nonempty\" end' etf_raw.jsonl | sort | uniq -c; csift recover @<SESSION> --no-subagents --file <REPO>/Cargo.toml --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
          "observed": "Binary, verbatim: \"var vQo=16384\" and the builder \"async function QSr(e){let n=K1e(e.readFileState); ... let v=e.readFileState.get(_);if(!v)return null;if(v.offset!==void 0||v.limit!==void 0)return null; ... if(await Bx(C)<=v.timestamp)return null; ... if(U.data.file.truncatedByTokenCap===!0)return null;if(z$(v,U.data.file.content))return null;let j=EJt(v.content,U.data.file.content);if(j===\\\"\\\")return null;return{type:\\\"edited_text_file\\\",filename:C,snippet:j}\" followed by \"let f=0;for(let _ of d){if(_.type!==\\\"edited_text_file\\\")continue;if(f>=vQo)_.snippet=\\\"\\\";else f+=_.snippet.length}\". Corpus: csift's attachment census reports 1334 edited_text_file records across all projects; of 1047 raw lines retrieved, 1047 carry the key set [\"filename\",\"snippet\",\"type\"] and zero carry filePath or content; 61 have a zero-length snippet and 986 do not. Snippet byte lengths over those 1047: min 0, median 7925, p90 8212, max 8223. csift recover on one target reported causes external_edit x3 with detail \"edited_text_file attachment (file changed outside the tool stream); this can be an external edit, the project's formatter, or a hook\".",
          "rule": "One attachment record per line whose attachment payload type is edited_text_file; key sets unioned per record; a snippet counts as empty when its JSON string length is zero.",
          "note": "The claim's load-bearing assertions all hold and the binary makes them exact: read-set gating, full-view-read gating, the 16384 budget, and the empty-snippet degraded form are all directly readable in one function. Three refinements: the budget is cumulative per batch rather than per file (which explains 61 empties among 1047 records while no single snippet exceeds 8223 bytes, the diff renderer's own ceiling); the filePath and content fallbacks in csift are unobserved against current CC and cost nothing; and the \"one shot per read epoch\" clause is the one part no instrument here decided, since it would need a controlled sequence of writes without an intervening Read. All three pinned code sites (src/recover/carriers.rs 218-230, src/recover/types.rs 50-54, src/recover/replay.rs 207-213) match verbatim at the stated lines."
        }
      ]
    },
    {
      "id": "FH-008",
      "area": "freshness-file-history",
      "behavior": "The edited_text_file snippet carries a leading line-number gutter whose separator is a TAB (U+0009) in Claude Code 2.1.258, on 986 of 986 non-empty payloads measured. The U+2192 arrow form is not produced by 2.1.258 or by 2.1.241 (the character does not occur in either binary's strings) and does not occur anywhere in this store, so the \"older form\" is not observable here.",
      "depends": "csift strips BOTH gutter forms and skips any line with no recognizable gutter rather than fabricating a number; an arrow-only parser would drop every current-format snippet.",
      "code": [
        {
          "path": "src/recover/diff.rs",
          "lines": "185-189",
          "snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number).\npub(crate) fn strip_gutter(snippet: &str) -> Vec<(usize, String)> {"
        }
      ],
      "instrument": "Inspect one payload: `jq -r 'select(.attachment.type==\"edited_text_file\") | .attachment.snippet' <file> | head -3 | cat -A` and look at the byte immediately after the leading digits. Counting rule: one probe per attachment payload.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.8.0",
        "source": "SPEC.md section 6.7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "jq -r 'select(.type==\"attachment\")|.attachment.snippet|select(length>0)|split(\"\\n\")[0]' etf_raw.jsonl | perl -ne 'if(/^(\\d+)(.)/){printf(\"U+%04X\\n\", ord($2))}else{print \"NO-GUTTER\\n\"}' | sort | uniq -c; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c $'\\u2192'; strings -n 6 ~/.local/share/claude/versions/2.1.241 | rg -c $'\\u2192'",
          "observed": "986 of 986 non-empty snippets have U+0009 immediately after the leading digits of the first line; zero read NO-GUTTER and zero read any other codepoint. rg found no U+2192 anywhere in the strings of 2.1.258 or of 2.1.241. No arrow-gutter snippet exists anywhere in the live corpus sample of 1047 edited_text_file payloads.",
          "rule": "One probe per non-empty attachment payload, reading the single byte immediately following the leading run of digits on the payload's first line.",
          "note": "The half that governs correctness today is instrumented and holds: an arrow-only parser would drop every current-format snippet, and csift's TAB arm covers 986 of 986. The arrow half is unverifiable on this machine, because the oldest Claude Code build present is 2.1.241 and it already has no arrow; deciding it would need a build old enough to emit one, or a transcript from that build. Keeping the arrow arm costs nothing and is the safe choice, so the code needs no change. src/recover/diff.rs:185-189 matches verbatim."
        }
      ]
    },
    {
      "id": "FH-009",
      "area": "freshness-file-history",
      "behavior": "Claude Code's integrity error bodies at 2.1.258 are \"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\", \"File has not been read yet. Read it first before writing to it.\", \"String to replace not found in file.\" followed by a newline and a \"String: \" line quoting the failed old_string, and \"File does not exist.\" in several forms. The first three arrive wrapped in a <tool_use_error> element and carry no path, so the file must be recovered through the tool_use_id join. The fourth does NOT: the observed form appends \"Note: your current working directory is <absolute path>.\" and is unwrapped, and the binary carries a further \"File does not exist: <filePath>\" form and a \"Did you mean ...?\" suffix. There is no separate \"Read it first\" variant: every observed not-read-yet body carries the full sentence, and the bare \"File has not been read yet\" string in the binary is a UI-side prefix test, not a tool_result body.",
      "depends": "csift classifies only the modified-since-read case as a hard boundary and counts the rest as annotations, attributing each to a file through the `tool_use_id` join; a wrong classification either fabricates boundaries or lets a stale buffer through as current.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "281-297",
          "snippet": "/// Classify a tool_result error body as an integrity error, or `None` if it is some\n/// other tool error. Only [`IntegrityKind::ModifiedSinceRead`] becomes a boundary;\n/// the others are COUNTED annotations (the op never landed).\npub(crate) fn classify_integrity_error(content: &serde_json::Value) -> Option<IntegrityKind> {\n    let text = crate::model::tool_result_content_text(content);\n    if text.contains(\"has been modified since read\") || text.contains(\"File has been modified\") {\n        Some(IntegrityKind::ModifiedSinceRead)\n    } else if text.contains(\"has not been read yet\") || text.contains(\"Read it first\") {\n        Some(IntegrityKind::NotReadYet)\n    } else if text.contains(\"String to replace not found in file\") {\n        Some(IntegrityKind::StringNotFound)\n    } else if text.contains(\"File does not exist\") {\n        Some(IntegrityKind::FileDoesNotExist)\n    } else {\n        None\n    }\n}"
        }
      ],
      "instrument": "`rg -c 'has been modified since read|String to replace not found in file' ~/.claude/projects --glob '*.jsonl'`, then `csift files @<session> --format json | jq 'select(.kind==\"boundary\")'`. Counting rule: one boundary per erroring tool_result carrier.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'File has been modified since read[^\"`]{0,90}|has not been read yet[^\"`]{0,80}|String to replace not found[^\"`]{0,60}|File does not exist[^\"`]{0,50}' | sort -u; for pat in 'File has been modified since read' 'File has not been read yet' 'String to replace not found in file' 'File does not exist'; do rg -NI \"$pat\" <CORPUS>/*.jsonl | jq -r --arg p \"$pat\" 'select(.type==\"user\")|.message.content[]?|select(.type==\"tool_result\")|select(((.content|tostring)|test($p)) and (.is_error==true))|(.content|tostring)[0:170]' | sort | uniq -c; done; csift recover @<SESSION> --no-subagents --file <REPO>/tests/cli_integration.rs --coverage --format json | jq -c 'select(.kind==\"coverage\")|{events, causes:[.boundaries[].cause]}'",
          "observed": "All four literals exist in 2.1.258. The observed error bodies are longer than the pinned literals: \"<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>\" (8 records), \"<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>\" (44), \"<tool_use_error>String to replace not found in file.\\nString: <the failed old_string>\" (7), and \"File does not exist. Note: your current working directory is <an absolute path>.\" (2). The last carries an inline absolute path and is NOT wrapped in a tool_use_error element. The binary also holds \"File does not exist: ${e.filePath}\" and a \"Did you mean\" variant. Joining three rejections to their tool_use records recovered Edit with file_path in each case. csift recover reported causes modified_since_read x2 and events.integrity_error 2.",
          "rule": "One body per erroring tool_result block, deduplicated by the first 170 characters; only blocks with is_error true are counted, which excludes the many records where the same strings appear as echoed file content.",
          "note": "The classifier is safe as written: every observed body is matched by the substring test it targets, and the ordering keeps a String-not-found body (which quotes arbitrary user text) from falling through to a later arm. The clause that needs striking is \"whose text carries no inline path\", which is true of three bodies and false of the fourth; a consumer that assumed it universally would be surprised by a File-does-not-exist body carrying the shell cwd. src/recover/carriers.rs:281-297 matches verbatim."
        }
      ]
    },
    {
      "id": "FH-010",
      "area": "freshness-file-history",
      "behavior": "Write and Edit both gate on an mtime check against the last Read, with a content-hash absolution (a base-36 hash of the read content) that Soe() allows only when the recorded read had offset <= 1 and was not a partial view. A byte-identical rewrite therefore never trips the gate. The correction: a windowed read does not merely lose its absolution channel and then reject on mtime movement. Both tools branch on \"no read state OR partial view\" BEFORE reaching the mtime comparison and reject from that arm with \"File has not been read yet. Read it first before writing to it.\" (errorCode 2 on Write), so after a windowed read the write is refused whether or not the file's mtime moved, unless a separate guard-skip predicate applies. The notebook editor carries a third copy of the mtime gate with no absolution branch at all (errorCode 10).",
      "depends": "csift's `recover` treats a modified-since-read rejection as a hard integrity boundary that invalidates every known line before it; if the gate's criterion changed, recover would either over-report gaps or present stale content as current.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "7-11",
          "snippet": "/// Extract a `FullSnapshot` / `PartialRead` (Read) or a `FullSnapshot` (Write) / `Edit`\n/// from a `toolUseResult` carrier, plus the two Claude Code freshness signals riding\n/// the same carrier: a Bash result's `staleReadFileStateHint` (CC's own modified-file\n/// attribution) and an Edit result's `staleRecovered` flag. `record_cwd` is the\n/// carrying record's own `cwd`, the base the hint's relative paths resolve against."
        },
        {
          "path": "src/cli/files_recover_args/recover.rs",
          "lines": "129-137",
          "snippet": "        FRESHNESS SIGNALS (Claude Code's own, adopted as boundaries)\\n  \\\n          A Bash result's `staleReadFileStateHint` is Claude Code itself reporting \\\n        that the command modified files in its read set, BY NAME (paths relative to \\\n        the shell cwd, resolved before matching): an authoritative HARD \\\n        `hint_modified` boundary, and the one signal that attributes a formatter-class \\\n        rewrite to concrete files. A successful Edit flagged `staleRecovered` means \\\n        the disk had drifted since the last read but the edit still applied: an \\\n        authoritative `stale_recovered` annotation (nothing invalidated, other \\\n        changes exist outside the stream). An `edited_text_file` external-edit \\"
        }
      ],
      "instrument": "`rg -c 'File has been modified since read' ~/.claude/projects --glob '*.jsonl'` (370 occurrences across 79 files measured) and confirm every carrier has `is_error:true`; live check: Read a file, modify it outside Claude Code, then Edit it. Counting rule: one occurrence per matching tool_result text.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9; AGENTS.md section 3.11"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function Soe\\(e\\)\\{.{0,320}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,260}File has been modified since read.{0,120}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,120}isPartialView.{0,120}' | sort -u; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'function k\\(e\\)\\{return Bun\\.hash.{0,60}'",
          "observed": "The absolution predicate, verbatim: \"function Soe(e){if((e.offset??1)>1||e.isPartialView)return!1;if(e.limit===void 0)return!0;return e.content!==\\\"\\\"&&_n(e.content,\" ... The Write gate: \"if(Math.floor(I)>F.timestamp){let U=Soe(F),j=!1;if(U){let W=await C.readFileBytes(d);j=Rtr(F,W.toString(\\\"utf8\\\"))}if(!j)return{result:!1,message:\\\"File has been modified since read, ...\\\",errorCode:3}}\". The Edit gate: \"if(cB(_)>U.timestamp)if(Soe(U)&&z$(U,B));else{...}\". Both tools first branch on \"if(!F||F.isPartialView)\" / \"if(!U||U.isPartialView)\" and return \"File has not been read yet. Read it first before writing to it.\" from that arm. The content hash is \"function k(e){return Bun.hash(e).toString(36)}\". The literal appears 4 times in the binary: Write (errorCode 3), Edit (errorCode 7), notebook edit (errorCode 10), and the standalone message.",
          "rule": "One gate per tool, read off the decompiled control flow around each occurrence of the literal error message; the predicate is read from its own named function rather than inferred from call sites.",
          "note": "The criterion csift depends on is intact at 2.1.258 and is now readable as source rather than inferred: mtime comparison, full-read-only hash absolution, idempotent rewrites passing. The refinement makes the rejection stricter, not looser, than the claim states, so recover's treatment of a modified-since-read rejection as a hard boundary is unaffected; what changes is which message a windowed read produces, and a consumer expecting \"modified since read\" after a windowed read will instead see \"not been read yet\". Both pinned code sites (src/recover/carriers.rs 7-11, src/cli/files_recover_args/recover.rs 129-137) match verbatim at the stated lines."
        }
      ]
    },
    {
      "id": "FH-011",
      "area": "freshness-file-history",
      "behavior": "A read-before-write or staleness REJECTION always lands as a tool_result with `is_error:true`: the validator's `behavior:'ask'` field is inert in the runner (measured 25 of 25 rejection records in one corpus).",
      "depends": "csift's recover boundary detection keys on the error flag plus the message class, so it has no blind spot from a non-errored rejection path; if rejections could land as successes the integrity boundaries would be missed entirely.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "1-3",
          "snippet": "//! Carrier-side extraction: toolUseResult (Read/Write/Edit + the Bash freshness\n//! hint), attachments (edited_text_file / file), the integrity-error classifier, and\n//! the structured-patch parser."
        }
      ],
      "instrument": "Search `~/.claude/projects` for `File has been modified since read` and check the enclosing tool_result block's `is_error`; expect true on every one. Counting rule: one check per matching tool_result block.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -NI --glob '*.jsonl' '<tool_use_error>' <one-project-dir> | jq -r '(.message.content//[])|if type==\"array\" then . else [] end|.[]|select(.type==\"tool_result\")|(if (.content|type)==\"string\" then .content else ((.content//[])|map(select(.type==\"text\")|.text)|join(\"\\n\")) end) as $t|select($t|startswith(\"<tool_use_error>\"))|select($t|test(\"^<tool_use_error>(File has been modified since read|File has not been read yet|String to replace not found|File does not exist)\"))|[($t|capture(\"^<tool_use_error>(?<k>File has been modified since read|File has not been read yet|String to replace not found|File does not exist)\").k),((.is_error//\"ABSENT\")|tostring)]|@tsv' | sort | uniq -c   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}behavior:\"ask\".{0,200}'",
          "observed": "77 rejection carriers, is_error:true on 77 of 77, zero with the key absent or false: 'File has been modified since read' 8, 'File has not been read yet' 60, 'String to replace not found' 9. Binary, the Edit validator's own return: 'if(s(\"tengu_edit_tool_stale_read\",{wouldHaveResult:CJt(ke),recovered:we}),!we)return{result:!1,behavior:\"ask\",message:\"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.\",errorCode:7}'. The Write validator's twin returns the same message with no behavior field: '{result:!1,message:\"File has been modified since read, ...\",errorCode:3}'.",
          "rule": "One carrier per tool_result block whose text STARTS with '<tool_use_error>' followed by one of the four integrity messages. The startswith anchor is load-bearing: a loose substring test over the same scope returned 70 extra hits that were all quotations of the literal inside ordinary tool output (a source file read back, a grep result, an earlier test log), none of them rejections. Counting those as carriers would report 70 non-errored rejections that do not exist.",
          "note": "Both halves confirmed by separate instruments. The 'behavior:\"ask\"' field is literally present in the Edit tool's staleness return in 2.1.258 (errorCode:7), and it is inert at the runner: every rejection reached the transcript as an errored tool_result, none as a permission prompt. Related observation worth carrying forward, since it bounds when the gate fires at all: the Write validator has an early bypass ('if(Eu(d))return{result:!0}') and a skip path guarded by 'let W=!F&&!yIn(d)&&!mYe(U,n.remoteCall)&&CJ(jn,d,n,pe(n))' whose telemetry event is named 'tengu_write_tool_not_read_hypothetical' with a 'guardSkipped' field, and the shared helper 'FRo' returns without throwing when there is no prior read, no pre-read guard and reads are auto-allowed. Live in this session, a Write and an Edit to files never opened with the Read tool both succeeded with no rejection, so the not-read-yet gate is not universally armed."
        }
      ]
    },
    {
      "id": "FH-012",
      "area": "freshness-file-history",
      "behavior": "Bash NEVER invalidates readFileState - that is exactly how staleness arises - but Claude Code lexically recognizes read-shaped Bash commands and SEEDS read-state from them, so a bash `cat <file>` legalizes a later Write of that file with no intervening Read.",
      "depends": "`recover` and `files` treat a gated bash read idiom as a real read anchor for the same reason the harness does; assuming only the Read tool seeds state under-counts legal Writes and over-reports integrity boundaries.",
      "code": [
        {
          "path": "src/bash_mutations/anchors.rs",
          "lines": "4-14",
          "snippet": "//! A small closed set of shell shapes carries DETERMINISTIC file content in the\n//! transcript itself: a quoted-delimiter heredoc's body is byte-verbatim in the\n//! tool_use input; `cat <file>` / `head -n N <file>` / `sed -n 'A,Bp' <file>` stdout\n//! (under the caller's completeness gate) IS the file window; `echo`/`printf` with\n//! purely literal arguments write known bytes; `truncate -s 0` writes the empty file.\n//! Everything else stays in the boundary/heuristic lanes - correctness first, but\n//! honesty about the decidable subset is not surrender on it.\n//!\n//! ADMISSION LAWS (each refusal falls back to today's behavior, never a wrong anchor):\n//! - A READ anchor demands a SINGLE simple segment: a compound command's stdout is a\n//!   concatenation nothing can attribute to one file."
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "22-28",
          "snippet": "    // ── (0) Bash result hint: `staleReadFileStateHint` names files THIS command\n    //    modified out of the read set. CC computed the list from its own readFileState\n    //    (an mtime stat over every read file), so a named match is authoritative. The\n    //    paths are rendered relative to the shell's cwd; resolve before matching. A\n    //    truncated tail (\"… and N more\") names no paths, so only the named first-5 can\n    //    ever match: a target hidden in the remainder is NOT detected here (documented\n    //    limit; the window's bash disclosure still covers the command itself)."
        }
      ],
      "instrument": "Live: `cat <file>` through the Bash tool, then Write to it - the Write must succeed with no freshness rejection. Corpus: find a Write immediately following a bash `cat` of the same path with no Read in between and no rejection. Counting rule: one tool_result per attempt, one witness sequence per session.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9; AGENTS.md section 3.11"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Jzo\\(.{0,1500}'  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '(async )?function hWn\\(.{0,900}'  AND a live A/B: Bash 'cat <scratch>/probe.txt' as a lone command, then a later out-of-band Bash 'printf ... > <scratch>/probe.txt', versus two control files rewritten identically that were never cat-ed by a lone command.",
          "observed": "Binary, the read-shape recognizer: 'function Jzo(e){if(/[|<>]/.test(e))return[];...}' dispatching to a sed -n 'A,Bp' arm and a table 'eqo=new Map([[\"cat\",new Set([\"-n\",\"--number\"])],[\"nl\",new Set],[\"bat\",...],[\"batcat\",...]])'. Binary, the seeder run after every non-interrupted foreground Bash call: 'async function hWn(e,n,r,o,d){let f=Jzo(e).filter((v)=>!v.requiresExitZero||o===0);if(f.length===0)return;...let C=ct(v.filePath);if(n.get(C))return;...if(I.size>10485760)return;...n.set(C,{content:U.content,timestamp:Math.floor(I.mtimeMs),offset:U.offset,limit:U.limit,...})}'. Live: after the lone cat and the out-of-band rewrite, the harness returned 'Note: <path> changed on disk since you last read it.' followed by the file's numbered content, for a file never opened with the Read tool. The two control files, rewritten by the same idiom but never cat-ed by a lone command, produced no note.",
          "rule": "One seeding probe per (bash command, file). A control must be a lone command: the recognizer refuses the whole command string when it contains '|', '<' or '>', so a cat that shares a line with a redirect seeds nothing. The first attempt at this A/B was void for exactly that reason - the cat rode a compound command carrying '>' - and had to be rerun with the cat isolated.",
          "note": "Both clauses hold. Seeding: a lone cat / nl / bat / batcat, or a sed -n 'A,Bp', enters readFileState with the file's content, its mtime as the read timestamp, and (for sed) an offset/limit partial view. Non-invalidation: the seeder's 'if(n.get(C))return' means bash never overwrites an existing entry, and the write side only computes a hint, never deletes state - the one readFileState.delete site found is 'evictRewoundFileTracking', the rewind feature. The 10485760-byte (10 MiB) size cap on seeding is a bound the claim does not mention."
        }
      ]
    },
    {
      "id": "FH-013",
      "area": "freshness-file-history",
      "behavior": "A file change that produces none of the three freshness signals - no `staleReadFileStateHint`, no `staleRecovered`, no `edited_text_file` attachment - leaves NO transcript trace at all, so absence of a signal is never evidence of absence of a change.",
      "depends": "csift cannot infer a modification from a bash write alone, so every recovered window carries explicit opaque accounting (`boundaries`, `bash_file`, `bash_opaque`) plus a suggested search instead of a silent gap, and a `complete` row with non-zero columns means complete FROM THE TOOL STREAM, not verified against disk.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "138-143",
          "snippet": "/// Write `recovery-report.tsv` under `out_dir` and print the one-line summary. The\n/// three accounting columns disclose what each recovered file's window held beyond\n/// the replay: `boundaries` (integrity boundaries), `bash_file` (parsed bash\n/// mutations of the file), `bash_opaque` (mutating-class + PowerShell commands whose\n/// file set is unknowable). A `complete` row with non-zero columns is complete FROM\n/// THE TOOL STREAM, not verified against disk."
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "321-327",
          "snippet": "/// Collect the SCOPE-ACCOUNTING commands of one transcript: every mutating-CLASS\n/// marker a Bash command yields (`fmt:cargo`, `interp:python`, `pkg:npm`,\n/// `extract:tar`, `git:<sub>` - commands that mutate files they never name), plus\n/// every `PowerShell` tool call (its command text is never lexically parsed, so any\n/// file it touched is invisible; see AGENTS 3.9). Target-independent by nature: these\n/// commands CANNOT be joined to a `--file`, which is exactly why they are counted and\n/// disclosed per window instead of silently ignored."
        }
      ],
      "instrument": "`csift recover <target> --file <path> --coverage --format json | jq '{events, boundaries}'` on a session where a whole-tree formatter ran: windows with no signal are reported opaque rather than clean. Counting rule: one opaque window per turn that produced no freshness signal.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "SPEC.md section 4.9; AGENTS.md section 3.11; CHANGELOG 0.8.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Live: create and then rewrite three scratch files with Bash 'printf ... > <path>', then census the session transcript for the three signals - rg -N '<probe name>' <transcript> | jq -r 'select(.toolUseResult.staleReadFileStateHint != null)|1' | wc -l, and the same for .toolUseResult.staleRecovered==true and .attachment.type==\"edited_text_file\".  csift side: csift recover @<session> --no-subagents --file <repo>/src/search/matcher.rs --coverage",
          "observed": "All three probe files: staleReadFileStateHint 0, staleRecovered 0, edited_text_file 0 - no transcript record of any kind carried a freshness signal for them, including the one file whose change the harness had visibly noticed in the tool result. csift recover printed the accounting rather than a clean window: 'events: 5 read (1 full, 4 windowed) - 2 edit - 2 modified-file-hint', 'integrity boundaries: 2 (2 hard - 0 soft)', 'opaque in window: 490 mutating-class command(s) whose file set is not in the command text (extract:tar x24, fmt:cargo x195, git:checkout x5, git:mv x3, git:reset x4, interp:python x258, pkg:cargo)', and the suggested search line.",
          "rule": "One probe per (file, bash rewrite). A signal counts only when a transcript record carries the key - the model-facing note in a tool result is not a record and does not count. One opaque row per mutating-class command in the window, as csift counts them.",
          "note": "Stronger than the claim states. Even the change the harness DID detect and report to the model in-band left no durable freshness signal anywhere in the transcript, so the absence is not merely 'no signal for unnoticed changes' but 'no signal for a bash write, noticed or not'. csift's disclosure side behaves as described: the window reports 490 unattributable commands and two authoritative hint boundaries instead of a silent gap."
        }
      ]
    },
    {
      "id": "FH-014",
      "area": "freshness-file-history",
      "behavior": "Claude Code writes a `file-history-snapshot` at the PROMPT-SUBMISSION boundary rather than on any turn-segmentation rule, and slash commands and meta prompts fire one too, which is why an in-process `/model` write is caught at all. The snapshot count therefore tracks prompts, not csift turns, and may fall either side of the turn count: three older transcripts measured 91/76, 57/53 and 28/4 snapshots-to-turns, while a current-Claude-Code window measured 23 snapshots against 27 turns and 20 prompt submissions. Since about 2.1.227 a newly tracked file's first backup is carried by a separate `file-history-delta` line instead of waiting for the next snapshot, so counting only snapshot lines undercounts file-history events - the same transcript held 95 snapshots and 146 deltas.",
      "depends": "csift attributes an external write to the INTERVAL between two snapshot lines rather than to an instant, and its `files` timeline row names the version transition plus that interval; assuming a per-turn cadence would mis-place every attribution.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "34-37",
          "snippet": "/// Synthesize `external write` timeline rows for the settings family from the\n/// snapshot version sequences. `mutations` are this transcript's tool-extracted\n/// rows (line-ordered per path is NOT required; membership in the interval is by\n/// jsonl line number)."
        }
      ],
      "instrument": "`csift stats @<session>` for the turn count, then count `\"type\":\"file-history-snapshot\"` lines in the same transcript; expect more snapshots than turns. Counting rule: one snapshot per matching line, one turn per csift turn index.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Per transcript: count lines with type \"file-history-snapshot\" and lines with type \"file-history-delta\" in jsonl order (python3, json.loads on matching lines only), against csift stats @<session> --no-subagents --format json | jq -r 'select(.kind==\"session\")|.turns'. Window form: csift stats @<session> --no-subagents --since 2026-09-02 and csift search '' @<session> --no-subagents --since 2026-09-02 --count-by label. Adjacency form: for each snapshot line, walk back to the nearest record that is not itself a file-history line and record its type.",
          "observed": "Older transcripts: 91 snapshots vs 76 turns; 57 vs 53; 28 vs 4 - snapshots exceed turns. The live transcript, which spans Claude Code 2.1.227 through 2.1.258: 95 snapshot lines vs 192 turns, alongside 146 file-history-delta lines (241 file-history lines in total). Its 2026-09-02 window (2.1.257/2.1.258): 23 snapshots, 27 csift turns, 18 user.message plus 2 harness.command.invocation records. Predecessor of each of those 23 snapshots: attachment 19, user 3, session-bridge record 1.",
          "rule": "One snapshot per raw jsonl line of that type; one turn per csift turn index at csift 0.10.0 with --no-subagents; one prompt submission per user.message or harness.command.invocation record in the same window; one adjacency row per snapshot line.",
          "note": "The mechanism survives and the adjacency evidence is direct: 22 of 23 snapshots in the current-Claude-Code window sit immediately after the prompt's own attachment or user record. What does not survive is 'more snapshots than turns' as a signature - it inverted in the current window. The claim's own example transcript still carries exactly 91 snapshots, but its csift turn count reads 76 under csift 0.10.0, not the 55 recorded; anything keyed to that ratio should be rechecked against the version of csift that will read it."
        }
      ]
    },
    {
      "id": "FH-015",
      "area": "freshness-file-history",
      "behavior": "Claude Code snapshots every Edit/Write-tracked file per prompt and bumps `trackedFileBackups[<path>].version` ONLY when the file's bytes changed - one measured path sat at v2 across 88 consecutive snapshots - so a version JUMP with no tool write of that path in the interval is an external write.",
      "depends": "`files --by timeline` synthesizes its `external write` row from exactly that instrument and `recover` rebases its replay at a divergent snapshot; without it a replayed file reports a state that never existed on disk.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "4-8",
          "snippet": "//! Claude Code rewrites its settings files IN-PROCESS (/model, /config, theme,\n//! permission \"always allow\", plugin toggles) - no tool record, usually no printed\n//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "409-414",
          "snippet": "                out.counts.history_snapshot += 1;\n                // The file-history INSTRUMENT (v0.9.4): Claude Code backs the tracked\n                // file up per prompt and bumps `version` only when the bytes changed.\n                // A version CHANGE therefore captures the disk truth at that instant -\n                // including harness-side writes that leave NO tool record (measured:\n                // half of all settings.json mutations corpus-wide). Three cases:"
        }
      ],
      "instrument": "Walk `.snapshot.trackedFileBackups | to_entries[] | [.key, .value.version]` in line order over one transcript: long constant runs broken by single increments; then `csift files @<session> --format json | jq 'select(.op==\"external_write\")'`. Counting rule: one version transition per (snapshot line, tracked path) pair.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/files/external.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bX\\(e,n,r,o\\).{0,2100}'  AND  the corpus run census: per (transcript, tracked path), walk trackedFileBackups[<path>].version in jsonl line order and record the length of each constant run and the number of changes.",
          "observed": "Binary change test: 'function I3r(e,n,r){if(e===null!==(n===null))return!0;if(e===null||n===null)return!1;if(!e.isFile()||!n.isFile())return!0;if(e.mode!==n.mode||e.size!==n.size)return!0;if(e.mtimeMs<n.mtimeMs)return!1;return r()}', where r() is 'let[_,v]=await Promise.all([M8(e),M8(o,{noFollow:!0})]);if(_===null||v===null)return!0;return _!==v' - a content comparison of the file against its stored backup. In the snapshot builder, when that test is false the previous entry is reused unchanged ('ave(f,v,I.realParentDir!==void 0&&W===I.version?I:{...I,version:W,...});return'), and only otherwise is a new backup written at 'U=B+1' via 'ave(f,v,await XQt(C,U))'. Corpus: 4,061 snapshot lines across 62 transcripts carrying snapshots, 56,940 version changes, longest constant run 1,241 snapshots at one version.",
          "rule": "One entry per (snapshot line, tracked path) pair in jsonl line order; a run is a maximal stretch of adjacent pairs with an equal version; a change is one adjacent pair with unequal versions.",
          "note": "Confirmed at the mechanism level, not just correlationally: the bump is gated on mode, size and then a content comparison against the stored backup, so a byte-identical rewrite with a bumped mtime provably does not bump the version. The claim's '88 consecutive snapshots at one version' is a floor - the corpus maximum is 1,241."
        }
      ]
    },
    {
      "id": "FH-016",
      "area": "freshness-file-history",
      "behavior": "The `version` counter RESETS mid-session, so the sequence is a series of GENERATIONS rather than one monotone counter and a decrease is a new generation, never an anomaly: a corpus census measured 159 decreases across the 62 transcripts that carry snapshot lines, concentrated in 4 of them and absent from the other 58.",
      "depends": "csift reads version JUMPS only within a generation and treats a decrease as bookkeeping; a naive `version > prev` test would silently drop every post-reset event, and cross-generation comparison is invalid by construction.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "16-21",
          "snippet": "//! HONEST LIMITS (documented, disclosed in SPEC/SKILL): the version counter RESETS\n//! mid-session (a process restart starts a new generation) - jumps are only read\n//! within a generation, so a write hiding across a reset is not reported; a tool\n//! write and a silent write in the SAME snapshot interval merge into one bump and\n//! the silent half is invisible here (recover's content comparison is the complete\n//! form); a session that never tool-touched the file has no tracking at all."
        },
        {
          "path": "src/recover/types.rs",
          "lines": "80-88",
          "snippet": "    /// A `file-history-snapshot` recorded a disk backup of `--file` at this time.\n    /// Since v0.9.4 the marker carries its INSTRUMENT payload: the per-path\n    /// `version` (monotone within a GENERATION; the counter resets mid-session on\n    /// process restart - 148 real occurrences - so a DECREASE means a new\n    /// generation, never an anomaly), the store `backupFileName`, its `backupTime`,\n    /// and - attached by the scan post-pass ONLY on a version CHANGE, and only when\n    /// the store file exists AND its mtime agrees with `backupTime` (the @vN name\n    /// COLLIDES across a generation reset, so an unverified read can return the\n    /// wrong generation's bytes) - the snapshot CONTENT. The replay layer uses a"
        },
        {
          "path": "src/recover/snapshots.rs",
          "lines": "10-14",
          "snippet": "//! and its mtime must agree with the marker's `backupTime` within a tolerance -\n//! the `@vN` file name COLLIDES across a mid-session generation reset (the version\n//! counter restarts; measured 148 resets), so an unverified read can silently\n//! return the WRONG generation's bytes. Non-UTF-8 store bytes are refused (the\n//! replay buffer is line-textual)."
        }
      ],
      "instrument": "Per (transcript, tracked path), walk the `trackedFileBackups[<path>].version` sequence in jsonl line order and count strict decreases. Counting rule: one reset per adjacent pair where `version[i] < version[i-1]`.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; src/recover/snapshots.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Corpus census over every project directory under ~/.claude/projects, top-level transcripts only: for each (transcript, tracked path), walk trackedFileBackups[<path>].version in jsonl line order and count adjacent pairs with version[i] < version[i-1].  Plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bX\\(e,n,r,o\\).{0,2100}' and rg -o 'KBe *= *[0-9]+'.",
          "observed": "62 transcripts carry snapshot lines; 4,061 snapshot lines; 56,940 version changes; 159 version decreases; decreases appear in exactly 4 transcripts (56, 81, 13 and 9 decreases) and in none of the other 58. Binary: the next version is 'let B=0;for(let z of d.snapshots){let fe=Ad(z.trackedFileBackups,v);if(fe&&KCe(fe.version))B=Math.max(B,fe.version);let me=qQt(fe?.backupFileName);if(me!==void 0)B=Math.max(B,me)}let U=B+1', and the retained snapshot list is capped at 'KBe=100'.",
          "rule": "One reset per adjacent pair, per tracked path, per transcript, where version[i] < version[i-1] in jsonl line order. Subagent transcripts are excluded (they carry no file-history lines).",
          "note": "Direction, shape and concentration all reproduce; only the totals moved with corpus growth (148 to 159 decreases, 61 to 62 transcripts), and the concentration is still exactly 4 transcripts. One mechanism refinement from the binary: the next version is the maximum version seen across the RETAINED snapshots - capped at 100 - plus one, with versions also parsed out of the @vN backup file names. Eviction past that 100-snapshot cap can therefore lower the maximum on its own, so a process restart is one route to a reset rather than demonstrably the only one. csift's rule of reading jumps only within a generation is unaffected either way."
        }
      ]
    },
    {
      "id": "FH-017",
      "area": "freshness-file-history",
      "behavior": "`isSnapshotUpdate` is not a usable change flag, but not in the direction recorded. Measured over 2,281 snapshot pairs, the flag was NEVER true without a version change - the cross-tab is (true,false) 0, (true,true) 586, (false,true) 1,278, (false,false) 417 - so a true flag implies a change while a false flag says nothing, and 1,278 real version changes carried false. Since 2026-08 the flag has been uniformly false (95 snapshot lines, 0 true), having been true 586 times between 2026-05 and 2026-07.",
      "depends": "csift reads only `version` to detect a change and never consults the flag (no src/ reference to `isSnapshotUpdate` exists; it appears only in test fixtures), so its external-write instrument stays sound: a version JUMP is reported whether the flag rode true or false, and a flag-true snapshot with no version change reports nothing. Keying on the flag instead would not INVERT the detector, it would silently under-report - it would have missed the 1,278 changes that carried false against the 586 it would have caught, and detects nothing at all since 2026-08, when the flag went uniformly false.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "6-8",
          "snippet": "//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
        },
        {
          "path": "src/files/external.rs",
          "lines": "65-71",
          "snippet": "        for (path, entry) in tfb {\n            if !is_settings_family(path) {\n                continue;\n            }\n            let Some(version) = entry.get(\"version\").and_then(serde_json::Value::as_u64) else {\n                continue;\n            };"
        },
        {
          "path": "src/files/external.rs",
          "lines": "84-88",
          "snippet": "        for (line, turn, ts, version) in seq {\n            if let Some((prev_line, prev_v)) = prev {\n                // A DECREASE = a generation reset (the counter restarts on process\n                // restart); only same-generation jumps are readable here.\n                if *version > prev_v {"
        }
      ],
      "instrument": "For each snapshot line after the first, record `isSnapshotUpdate` and whether any tracked path's version differs from the previous snapshot, then cross-tabulate. Counting rule: one row per snapshot line after the first.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "Over two project directories under ~/.claude/projects: for each snapshot line after the first in its transcript, record isSnapshotUpdate and whether any tracked path's version differs from the previous snapshot, then cross-tabulate; separately bucket the raw isSnapshotUpdate value by the month of snapshot.timestamp.",
          "observed": "2,318 snapshot lines, 2,281 cross-tab rows: (isSnapshotUpdate=false, changed=false) 417, (false, true) 1,278, (true, true) 586, (true, false) 0. Raw flag values by month: 2026-05 false 162 / true 45; 2026-06 false 911 / true 299; 2026-07 false 564 / true 242; 2026-08 false 61 / true 0; 2026-09 false 34 / true 0.",
          "rule": "One row per snapshot line after the first in its transcript; 'changed' means at least one tracked path's version differs from that path's version in the immediately preceding snapshot line of the same transcript; a path present in only one of the two snapshots counts as changed.",
          "note": "The claim's cross-tab does not reproduce in any direction: it recorded (true,false) 51 times and concluded every version change arrives with the flag false, whereas 586 changes arrived with it true and the (true,false) cell is empty across 2,281 rows. On top of that the flag stopped being emitted as true after 2026-07, so in current Claude Code it carries no information at all. What is unaffected is the consequence the ledger entry exists to protect: csift reads only `version`, never the flag, and that remains correct - more so now, since keying on the flag today would detect nothing."
        }
      ]
    },
    {
      "id": "FH-018",
      "area": "freshness-file-history",
      "behavior": "A snapshot interval containing BOTH a tool write and a silent write produces only ONE version increment, so the version-jump rule alone cannot see the silent half; only replaying the interval's edits onto the previous snapshot content and comparing with the new snapshot content is complete.",
      "depends": "csift's `files` external-write row is deliberately the incomplete-but-cheap form and `recover` carries the complete form - comparing the replayed buffer to a VERIFIED snapshot blob and rebasing on disagreement; without the rebase a reconstruction can report 100% complete for a state that never existed on disk.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "18-21",
          "snippet": "//! within a generation, so a write hiding across a reset is not reported; a tool\n//! write and a silent write in the SAME snapshot interval merge into one bump and\n//! the silent half is invisible here (recover's content comparison is the complete\n//! form); a session that never tool-touched the file has no tracking at all."
        }
      ],
      "instrument": "Find a session with two Edits of the same tracked path between adjacent snapshot lines and confirm the version advanced by exactly one. Counting rule: one interval per adjacent snapshot pair, one increment per tracked path.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "For each adjacent snapshot-line pair in a transcript, count the structured edits (Edit/Write/MultiEdit/NotebookEdit tool_use records) whose jsonl line number falls strictly between them, grouped by target path; for each path with two or more, bucket the difference between the tracked path's version in the later and the earlier snapshot.",
          "observed": "227 qualifying intervals across the scanned transcripts: bump +1 in 148, bump +0 in 79, bump of +2 or more in 0. The largest edit burst inside a single interval was 19 structured edits of one path, and it still moved the version by no more than one.",
          "rule": "One interval per adjacent snapshot-line pair; edits attributed by jsonl line number strictly between the two snapshot lines; only intervals holding two or more structured edits of the SAME tracked path are counted; a relative tracked path is matched to an absolute tool path as a path-component suffix.",
          "note": "Confirmed with no exception in 227 intervals: the version is a per-snapshot state marker, not an edit counter, so the number of writes inside an interval is unrecoverable from it. The 79 intervals that bumped by zero are the sharper form of the same limit - even two or more tool writes can leave the version untouched, when the interval's net effect on the bytes was nil or the backup was taken before the edits landed. Both cases make csift's reliance on content comparison rather than jump counting the only complete form."
        }
      ]
    },
    {
      "id": "FH-019",
      "area": "freshness-file-history",
      "behavior": "`backupTime` records when Claude Code backed the file up, not when the file changed, so it is a bound on the write instant rather than the write instant. The backup never precedes the write (429 of 429 lags non-negative), but the size of the lag depends on which carrier records it: a newly tracked file's first backup rides a `file-history-delta` written at the moment of the write (146 measurements, median 0.25 s, max 3.20 s), while a later bump of an already tracked file waits for the next snapshot (283 measurements, min 29.9 s, median 1,409 s).",
      "depends": "csift reports the version transition plus the surrounding INTERVAL rather than an instant, so a consumer never reads the backup time as the write time; presenting `backupTime` as the mutation instant would misdate every silent write.",
      "code": [
        {
          "path": "src/recover/types.rs",
          "lines": "80-91",
          "snippet": "    /// A `file-history-snapshot` recorded a disk backup of `--file` at this time.\n    /// Since v0.9.4 the marker carries its INSTRUMENT payload: the per-path\n    /// `version` (monotone within a GENERATION; the counter resets mid-session on\n    /// process restart - 148 real occurrences - so a DECREASE means a new\n    /// generation, never an anomaly), the store `backupFileName`, its `backupTime`,\n    /// and - attached by the scan post-pass ONLY on a version CHANGE, and only when\n    /// the store file exists AND its mtime agrees with `backupTime` (the @vN name\n    /// COLLIDES across a generation reset, so an unverified read can return the\n    /// wrong generation's bytes) - the snapshot CONTENT. The replay layer uses a\n    /// verified content to detect and REBASE across harness-side writes that left\n    /// no tool record (e.g. Claude Code rewriting settings.json on /model); an\n    /// unverified marker stays the old coverage annotation."
        }
      ],
      "instrument": "In a session where a slash command rewrote settings, compare the command record's timestamp with the following snapshot entry's `backupTime`; expect the backup to trail by seconds. Counting rule: one lag measurement per version change with a nearby command record.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Snapshot form: for each version change of a tracked path between adjacent snapshot lines, measure the seconds from the latest structured edit of that path inside the interval to the new entry's backupTime. Delta form: for each file-history-delta line, measure the seconds from the latest preceding structured edit of the same path to the delta's timestamp. Plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '(async )?function XQt\\(.{0,600}'.",
          "observed": "Snapshot form, 283 measurements: min 29.9 s, median 1,409 s, max 338,710 s, negative 0. Delta form, 146 of 148 delta lines matched to a preceding structured write of the same path: min 0.02 s, median 0.25 s, p90 0.39 s, max 3.20 s, all 146 under 5 s; every one of the 148 deltas carried version 1 and realParentDir. Binary: 'async function XQt(e,n){...await rQe(e,o);...return{backupFileName:r,version:n,backupTime:new Date,realParentDir:f}}' - backupTime is stamped when the copy is made.",
          "rule": "One lag measurement per version change that has at least one structured edit of the same path inside its interval; one delta measurement per delta line with a preceding structured edit of the same path; lag is backupTime minus the edit record's timestamp, so a negative value would mean the backup preceded the write.",
          "note": "The reason the entry gives is right and is now confirmed in the binary - the backup routine stamps backupTime with new Date() at the moment it copies the file. The 'roughly 13 seconds' figure is one incident, not a general lag, and there is no single characteristic value: the two carriers differ by three orders of magnitude. csift's practice of reporting the transition plus the surrounding interval rather than an instant is what makes both carriers safe to consume."
        }
      ]
    },
    {
      "id": "FH-020",
      "area": "freshness-file-history",
      "behavior": "The file-history tracked set is populated by STRUCTURED writes only - in one measured session tracked paths numbered 113, structurally-written paths 113, and tracked-but-never-structurally-written 0 - so a bash-only write never creates a tracked entry and bash writes never enter the store.",
      "depends": "csift never expects a snapshot boundary for a bash-written file and instead discloses bash mutations through per-window opaque accounting; assuming coverage from the store would report a false clean window for bash-heavy sessions.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "321-327",
          "snippet": "/// Collect the SCOPE-ACCOUNTING commands of one transcript: every mutating-CLASS\n/// marker a Bash command yields (`fmt:cargo`, `interp:python`, `pkg:npm`,\n/// `extract:tar`, `git:<sub>` - commands that mutate files they never name), plus\n/// every `PowerShell` tool call (its command text is never lexically parsed, so any\n/// file it touched is invisible; see AGENTS 3.9). Target-independent by nature: these\n/// commands CANNOT be joined to a `--file`, which is exactly why they are counted and\n/// disclosed per window instead of silently ignored."
        }
      ],
      "instrument": "For one session, build the set of `trackedFileBackups` keys and the set of Edit/Write `filePath` values; expect the tracked set to be a subset of the structured-write set. Counting rule: one path per distinct string in each set.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Over two project directories under ~/.claude/projects: build the union of trackedFileBackups keys across every snapshot line of a top-level transcript, and the set of Edit/Write/MultiEdit/NotebookEdit file_path values from that transcript and from every one of its subagent transcripts (journal.jsonl excluded), then count tracked paths matching no structured write.",
          "observed": "810 distinct tracked paths across the 37 top-level transcripts carrying snapshots in those two directories; 0 tracked paths unmatched by a structured write. A first pass reported 5 apparent exceptions; a per-path tool census showed all five had in fact been written by Edit or Write in the same transcript, and the exceptions were an artefact of the matcher, not of the data.",
          "rule": "One path per distinct trackedFileBackups key; one structured write per distinct file_path (or notebook_path) on an Edit/Write/MultiEdit/NotebookEdit tool_use. A relative tracked path counts as written when it is a path-component suffix of some absolute tool path - the leading './' may be stripped but the leading dot of a dotfile may NOT, which is exactly the bug that produced the 5 phantom exceptions.",
          "note": "Confirmed at roughly seven times the scale of the original measurement, with zero exceptions. Two operational cautions for anyone rerunning it. The tracked set of a top-level session includes files its SUBAGENTS structurally wrote, so a comparison against the top-level transcript alone will show false exceptions. And matching relative tracked paths against absolute tool paths needs a path-component suffix test that preserves dotfile names; a naive strip of leading '.' and '/' characters silently unmatches every dotfile and every path under a dot-directory."
        }
      ]
    },
    {
      "id": "FH-021",
      "area": "freshness-file-history",
      "behavior": "The tracked-path set spans ordinary source files at scale: a census over the 59 transcripts carrying file-history snapshots found 1,739 distinct tracked-path keys against 11 settings-family keys (0.6%), and only 5 of those 59 transcripts (8.5%) tracked any settings file at all. Under a 200 MB transcript cutoff the same census reads 1,227 keys, 3 settings-family (0.2%), 3 of 56 transcripts (5.4%).",
      "depends": "csift scope-limits external-write reporting to basenames `settings.json` and `settings.local.json` under a `.claude` parent precisely because wider reporting would flood every `files` timeline; the instrument is per-session evidence, not a corpus-wide sensor.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "13-14",
          "snippet": "//! listing \"harness writes\" beyond settings would flood every timeline (measured\n//! corpus: 1701 tracked paths, 11 settings-family)."
        }
      ],
      "instrument": "Collect the union of `trackedFileBackups` keys corpus-wide and count how many match the settings-family shape; separately count sessions with at least one such key. Counting rule: one path per distinct key string, one session per transcript file.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "src/files/external.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t harness.meta.snapshot --raw --max-count 0  (corpus-wide dump of every file-history-snapshot / file-history-delta line), piped into a python3 census that unions the `snapshot.trackedFileBackups` keys and classifies each with csift's own is_settings_family rule (basename settings.json|settings.local.json AND immediate parent `.claude`);  csift search '' -t harness.meta.snapshot -l --max-count 0 | wc -l  for the denominator;  csift search '\\.claude/settings(\\.local)?\\.json' -t harness.meta.snapshot -l --max-count 0 | wc -l  for the numerator; a second python3 pass re-ran the same census split by transcript byte size against the 200 MB cutoff.",
          "observed": "5,490 raw lines returned (3,951 file-history-snapshot + 1,539 file-history-delta; 3,930 distinct snapshot records; 4 malformed lines skipped, reported on stderr). Union of tracked-path keys = 1,739, of which 11 are settings-family = 0.6%. 59 transcripts carry snapshot records; 5 of them (8.5%) track at least one settings file. Restricted to transcripts under 200 MB: 56 transcripts, 1,227 tracked-path keys, 3 settings-family (0.2%), 3 of 56 (5.4%) tracking a settings file. The largest top-level transcript in the corpus is 687.5 MB, so the 200 MB cutoff is not vacuous. Code site verified verbatim: src/files/external.rs:10-14 and 25-32 match the claim's snippets character for character.",
          "rule": "One path per distinct trackedFileBackups key string (keys are counted verbatim, so a repo-relative spelling and an absolute spelling of the same file are two keys - this is the same rule csift's scope law applies). One session per transcript file. A transcript 'tracks a settings file' iff at least one snapshot record in it carries a settings-family key. Percentages are settings-family keys over all keys, and settings-tracking transcripts over snapshot-carrying transcripts.",
          "note": "The scope law it justifies is untouched - the ratio moved from 0.65% to 0.6%, which strengthens rather than weakens the argument for reporting only the settings family. The claim's two halves were measured at two different scopes: 1,701/11 is the unfiltered union while '4 of 58 sessions' carried a 200 MB cutoff. Reported both scopes above so the next reader does not have to guess. The settings-family count staying at exactly 11 across a corpus that grew by 38 tracked paths is consistent with settings files being tracked only when a session tool-edited one."
        }
      ]
    },
    {
      "id": "FH-022",
      "area": "freshness-file-history",
      "behavior": "Claude Code rewrites its own settings files IN-PROCESS - /model, /config, theme changes, permission always-allow, plugin toggles - with NO tool record: corpus-wide, 44 of 62 settings mutations carry no tool write in their snapshot interval by csift's own external_write instrument (71%), 47 of 62 by an independent replication (76%). Silent writes are the MAJORITY of settings mutations, not half.",
      "depends": "the per-prompt file-history snapshot is the only durable instrument, so `files` emits an `external write` timeline row from a version jump with no tool write in the interval and `recover` treats a content disagreement as a hard boundary; without it half of all settings mutations are invisible to every csift surface and a replayed settings file reports a state that never existed on disk.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "1-8",
          "snippet": "//! Settings-family external writes: the file-history snapshot instrument applied to\n//! the `files` timeline.\n//!\n//! Claude Code rewrites its settings files IN-PROCESS (/model, /config, theme,\n//! permission \"always allow\", plugin toggles) - no tool record, usually no printed\n//! trace. The instrument: CC snapshots every Edit/Write-tracked file per prompt and\n//! bumps `trackedFileBackups[<path>].version` only when the bytes changed; a version\n//! JUMP with no tool write of that path in the interval is an external write."
        }
      ],
      "instrument": "Over a session that tracks `~/.claude/settings.json`, list every version jump and check whether any Edit/Write tool_use naming that path falls in the interval; expect roughly half with none. Live: change a setting through the harness, then `csift files @main --by timeline --format json | jq 'select(.op==\"external_write\")'`. Counting rule: one mutation per version jump, classified silent or tool-accompanied.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md sections 6.6 and 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/files/external.rs comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "For each of the 5 transcripts that track a settings file:  csift files @<session> --by timeline --format json | rg -c '\"op\":\"external_write\"'  ; plus an independent python3 replication that walks each transcript in line order, builds the per-path version sequence from snapshot.trackedFileBackups, counts every version INCREASE as one settings mutation, and marks it silent when no Edit/Write/MultiEdit/NotebookEdit tool_use naming that path (suffix-tolerant match between the relative and absolute spelling) falls in the open line interval (prev_snapshot_line, this_snapshot_line]. Binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'trackedFileBackups|backupFileName|file-history-snapshot|file-history-delta|isSnapshotUpdate|backupTime|realParentDir'.",
          "observed": "62 settings-family version jumps in total across the 5 transcripts (3 + 32 + 5 + 15 + 7). csift's own instrument emits 44 external_write rows over the same 5 transcripts (71%). The independent replication classifies 47 of 62 as silent (76%) and 15 as tool-accompanied (24%); the 3-row gap is csift additionally crediting its bash-heuristic mutations inside the interval. Exactly 1 generation reset (a version DECREASE) occurred on a settings path, so the jump count is essentially reset-free. Binary tokens present in 2.1.258: trackedFileBackups 3, backupFileName 7, file-history-snapshot 5, file-history-delta 5, isSnapshotUpdate 3, backupTime 2, realParentDir 2 string-table lines.",
          "rule": "One mutation per version INCREASE of one settings-family key between two consecutive snapshot records in the same transcript; a version DECREASE is a generation reset and is not a mutation. A mutation is silent iff no structured file-writing tool_use naming that path appears on a jsonl line strictly after the previous snapshot line and at or before this one. Percentages are silent mutations over all mutations.",
          "note": "The mechanism half of the claim is confirmed and the total (62 mutations) reproduces exactly; only the silent fraction moved, and it moved AGAINST the claim's conservatism - 71-76% rather than 'half'. The clause 'usually no printed trace: 4 of 5 silent writes printed nothing a string table could match' was NOT rerun: deciding it requires driving five real setting changes through a live harness and watching the terminal, which mutates the operator's settings files and is outside what a read-only verifier may do. What would decide it: a throwaway --claude-home / CLAUDE_CONFIG_DIR home, five interactive setting changes through /model, /config, theme, an always-allow grant and a plugin toggle, and a transcript-plus-terminal capture of each."
        }
      ]
    },
    {
      "id": "FH-023",
      "area": "freshness-file-history",
      "behavior": "Claude Code's own file-history checkpoint store lives at `<claude-home>/file-history/<top-level-session-uuid>/<hash>@v<N>`, where `<hash>` is the first 16 hex chars of the sha256 of the ABSOLUTE file path and the file content is the PLAIN checkpoint snapshot with no JSON wrapper.",
      "depends": "`recover --list-backups` computes the key without needing any transcript record, so a wrong hash input (a relative path, a plan sigil) finds nothing at all rather than the wrong file.",
      "code": [
        {
          "path": "src/recover/backups.rs",
          "lines": "1-7",
          "snippet": "//! `recover --list-backups`: list Claude Code's OWN file-history checkpoint store for\n//! one absolute path.\n//!\n//! Store layout (verified against the live store): `<claude-home>/file-history/\n//! <top-level-session-uuid>/<hash>@v<N>`, where `<hash>` is the first 16 hex chars of\n//! sha256(absolute file path) and the file content is the PLAIN checkpoint snapshot\n//! (no JSON wrapper). Facts that bound what a listing may claim:"
        },
        {
          "path": "src/recover/backups.rs",
          "lines": "36-40",
          "snippet": "/// The store key: sha256 of the absolute path, first 16 hex chars.\nfn path_hash(path: &str) -> String {\n    let digest = Sha256::digest(path.as_bytes());\n    digest[..8].iter().map(|b| format!(\"{b:02x}\")).collect()\n}"
        }
      ],
      "instrument": "`printf '%s' '<abs path>' | shasum -a 256 | cut -c1-16` then `ls ~/.claude/file-history/*/<that hash>@v*`, and compare with `csift recover <target> --file <abs path> --list-backups --format json | jq 'select(.kind==\"backup\")'`. Counting rule: one row per store file matching the hash prefix.",
      "located": {
        "claude_code": "2.1.246",
        "csift": "0.8.1",
        "source": "SPEC.md sections 6.7 and 6 v0.8.1 ledger; CHANGELOG 0.8.1; src/recover/backups.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{300}@v\\$\\{.{200}'  and  rg -o '.{200}file-history.{300}'  for the store-path builder; on disk: a python3 pass recomputing sha256(realParentDir + '/' + tracked key)[:16] for live trackedFileBackups entries and comparing it with the record's own backupFileName; printf '%s' \"$PWD/src/recover/backups.rs\" | shasum -a 256 | cut -c1-16 from the csift checkout, then find ~/.claude/file-history -name '<hash>@v*'; wc -c and diff of the located blob against the working-tree file; python3 -c 'json.load(open(blob))'; find ~/.claude/file-history -type f | rg -v '/[0-9a-f]{16}@v[0-9]+$'.",
          "observed": "2.1.258 contains the name builder verbatim: function O3r(e,n){return`${T3r(\"sha256\").update(e).digest(\"hex\").slice(0,16)}@v${n}`} and the producer regex var D3r=/^[0-9a-f]{16}@v\\d+$/. The store path builder is case\"fileHistory\":return r(t,\"file-history\",n.sessionId,n.backupFileName) with function uH(e,n){...return O8(r,\"file-history\",n||Q(),e)}. Live records confirm the hash input is the ABSOLUTE path even when the key is relative: a key spelled `src/path.rs` whose realParentDir ends in `/src` carries a 16-hex backupFileName token, and sha256 of the JOINED absolute path reproduces that token exactly, character for character; an independently, absolutely-spelled key under ~/.claude/plans likewise reproduces its own recorded token. (The digest values themselves are withheld here: a store hash is a one-way digest of a home-directory path and publishing it would let a reader confirm a guessed home path by brute force. Both are recomputable in one command by the rule below.) The csift-checkout probe: the recomputed hash for the working-tree file src/recover/backups.rs resolved to exactly ONE blob in the whole store, at ~/.claude/file-history/<session-uuid>/<hash>@v2, 8,609 bytes, byte-identical to the working-tree src/recover/backups.rs (diff empty, wc -c equal) and not parseable as JSON (json.load raises JSONDecodeError). 6,554 of 6,555 store files match ^[0-9a-f]{16}@v[0-9]+$ (the one exception is a .DS_Store). 52 store directories, all 52 uuid-shaped; 34 resolve to a top-level transcript, 0 to a subagent transcript, 18 to no transcript now on disk. Code sites verified verbatim: src/recover/backups.rs:1-7 and 36-40.",
          "rule": "One row per store file matching <16 hex>@v<digits>. A hash is confirmed iff sha256 of the absolute path, first 16 hex chars, equals the leading token of a store file name or of a record's backupFileName. 'Plain snapshot, no JSON wrapper' is confirmed iff the blob bytes equal the source file's bytes and json.load raises.",
          "note": "Refutation attempt failed in three independent directions: the binary's own name builder, the live records' backupFileName field, and a fresh hash computed from a csift source path all agree. The strongest refutation attempt was to look for a store directory keyed on a subagent id (which would break the 'top-level-session-uuid' half) - zero of 52 store directories are subagent-shaped. One forward-compat observation that does not change the claim: the storage-key VALIDATOR in 2.1.258 is JQ=/^[0-9a-f]{16}(?:[0-9a-f]{48})?@v\\d+$/, tolerating a full 64-hex-char digest form, while the writer always emits slice(0,16). No 64-hex file exists in the live store, so csift's 16-char assumption is safe today; a future switch to the long form would be the thing that breaks it."
        }
      ]
    },
    {
      "id": "FH-024",
      "area": "freshness-file-history",
      "behavior": "The store's write path is TOOL-LAYER ONLY (the rewind feature): bash writes and manual edits never land there - 98% of structured Edit/Write targets are in the store versus 0.4% of bash-only written paths - so absence proves nothing about a file's history. The store is PRUNED: only the newest 100 snapshots are retained and the backups they no longer reference are deleted (12% of all recorded backup names are already gone), but version 1 is EXEMPT from eviction by an explicit guard, so @v1 is almost always still present (1 of 623 recorded @v1 names missing).",
      "depends": "`recover --list-backups` lists the store with those provenance bounds printed and csift never merges checkpoint content into a reconstruction - a checkpoint has no transcript anchor, and the transcript stays the ground truth.",
      "code": [
        {
          "path": "src/recover/backups.rs",
          "lines": "10",
          "snippet": "//! - the store is PRUNED: old checkpoints get deleted wholesale (`@v1` often gone);"
        }
      ],
      "instrument": "List a session's store directory and compare its entries with the session's structured Edit/Write targets and with its bash-written paths: bash-only paths are absent, and the lowest surviving version is frequently above v1. Counting rule: one row per store file, one path per distinct tool target.",
      "located": {
        "claude_code": "2.1.246",
        "csift": "0.8.1",
        "source": "CHANGELOG 0.8.1; src/recover/backups.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "On disk: a python3 pass over 8 sessions that have both a store directory and a transcript, comparing sha256(path)[:16] of every absolute path in  csift files @<session> --by timeline --format json --no-subagents  against the hash set in that session's store directory, split by the row's own `heuristic` flag (false = structured Edit/Write, true = bash-derived). A second python3 pass collected every backupFileName that a transcript actually RECORDED and checked whether the blob still exists in that session's store directory. Binary: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function A3r\\(.{0,600}' for the evictor, plus the same filter compared across ~/.local/share/claude/versions/{2.1.229,2.1.241,2.1.251,2.1.257,2.1.258}.",
          "observed": "TOOL-LAYER ONLY holds hard: over the 8 sessions, 869 of 885 structured Edit/Write target paths (98%) are present in that session's store directory, versus 4 of 980 bash-only written paths (0.4%). PRUNING is real but bounded: of 6,535 backupFileName values recorded across the 59 snapshot-carrying transcripts, 769 (12%) are no longer on disk. The '@v1 often gone' clause is REFUTED: of the 623 recorded @v1 names, exactly 1 is missing (0.2%). The evictor explains why - async function A3r(e,n,r){...for(let _ of e)for(let v of Object.values(_.trackedFileBackups))if(v.backupFileName!==null&&!d.has(v.backupFileName)&&v.version!==1&&D3r.test(v.backupFileName))f.add(v.backupFileName)... - version 1 is explicitly exempt from eviction, and the same `version!==1` guard is present in 2.1.229, 2.1.241, 2.1.251 and 2.1.257 as well, so this is a long-standing exemption and not a change. The retention constant is KBe=100: snapshots beyond the newest 100 are dropped and their unreferenced, non-v1 backups deleted. The signal that likely produced the wrong clause is real but means something else: 2,088 of 2,770 (session-dir, path-hash) groups (75.4%) have a lowest surviving version above v1 - because the version counter is carried forward across a resume (the next version is max over the retained snapshot history plus one), so v1 was never WRITTEN in that directory rather than pruned from it. Code sites verified verbatim: src/recover/backups.rs:7-16.",
          "rule": "One path per distinct absolute path in a session's files timeline, classified structured vs bash-only by the row's `heuristic` flag with bash-only defined as bash-derived and never a structured target in the same session; present-in-store iff sha256(path)[:16] appears as the leading token of any file in that session's store directory. For pruning: one row per backupFileName string a transcript recorded, counted missing iff no file of that name exists in that session's store directory.",
          "note": "The claim's load-bearing half - absence proves nothing, so csift may only LIST the store - survives the strongest refutation available (a 980-path bash-only sample produced 4 hits, and even those are explainable as paths tool-edited in a session whose backups were hard-linked in on resume). The failing half is a mis-inference rather than drift: the `version!==1` exemption predates the CC version the claim was located at, so 'often gone' was never true; what is true is that a file's FIRST backup in a given session directory is usually not v1, because the counter carries across a resume."
        }
      ]
    },
    {
      "id": "FH-025",
      "area": "freshness-file-history",
      "behavior": "The @vN token is NOT an ordering key: the counter resets per session directory and is reused across directories, and because it also resets mid-session the names COLLIDE inside ONE session directory - measured, 418 names reused for two or more distinct backupTime instants, and of the 297 with a surviving blob, 183 blob mtimes belong to the LATEST generation and 0 to an earlier one, the earlier generation's blob having been overwritten at the same name. Only the backup instant - the store file's mtime, checked against the marker's backupTime - identifies the generation. Separately, a blob can show a link count of 2 because the session-restore path HARD-LINKS a prior session's backups into the new session directory under the same name (119 such inode groups, every one spanning two directories, none inside one).",
      "depends": "csift orders backups by the store file's mtime and trusts a blob's bytes only when that mtime agrees with the marker's recorded `backupTime` inside a 120-second window, otherwise degrading to a content-less marker; reading `@vN` by name alone would splice the wrong generation's bytes into a `recover` rebase.",
      "code": [
        {
          "path": "src/recover/snapshots.rs",
          "lines": "11-12",
          "snippet": "//! the `@vN` file name COLLIDES across a mid-session generation reset (the version\n//! counter restarts; measured 148 resets), so an unverified read can silently"
        }
      ],
      "instrument": "For a session whose transcript shows a version decrease, `ls -l` its store directory and compare blob mtimes with the `backupTime` values in the transcript: expect at least one `@vN` name whose mtime belongs to the later generation. `csift recover <target> --file <path> --list-backups --format json | jq -r '.backup_time'` prints the order the version numbers do not follow. Counting rule: one row per store file.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.1",
        "source": "SPEC.md sections 6.7 and 6 v0.8.1 ledger; src/recover/backups.rs and src/recover/snapshots.rs module docs; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 passes over the 59 snapshot-carrying transcripts (ids from  csift search '' -t harness.meta.snapshot -l --max-count 0 ): (a) count version DECREASES per tracked path per transcript = generation resets; (b) group by backupFileName and flag any name carrying more than one distinct backupTime = an in-directory @vN collision; (c) for each colliding name with a surviving blob, compare the blob's filesystem mtime against every recorded backupTime and record which one it falls within 120s of; (d) the same mtime-vs-backupTime comparison over all NON-colliding surviving blobs, for the trust rule's base rate; (e) an os.stat nlink census over every store blob, grouping links by inode. Binary: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function \\$3r\\(.{0,420}' and rg -o '(link|linkSync) as [A-Za-z0-9_$]{2,6}'.",
          "observed": "182 generation resets across 4 transcripts (the code doc says 148; same rule, larger corpus). 418 backupFileName values are reused for two or more distinct backupTime instants INSIDE one session directory, spread over 5 transcripts; 297 of those still have a blob on disk (121 do not). Of the 297: 183 blob mtimes fall within 120s of the LATEST recorded backupTime, 0 fall within 120s of any EARLIER one, and 114 match none - i.e. the later generation overwrote the earlier generation's blob at the same name in every measurable case, and reading @vN by name alone would return the later generation's bytes. Concrete instance: one @v3 name (digest withheld, see FH-023) carries backupTime 2026-08-24T09:15:14.514Z and 2026-09-02T09:02:14.261Z, and the surviving blob's mtime is within 0.0s of the LAST one. The 120s window is well calibrated: over 5,472 non-colliding surviving blobs, 4,751 (86.8%) agree with backupTime to within 1s, 154 more within 120s, and 567 (10.4%) exceed the window and are refused. Hard links: 119 inodes carry nlink 2 (238 blobs of 6,554); all 119 groups span exactly two DIFFERENT session directories and all 119 share the same @vN name; 0 groups sit inside a single session directory. The binary names the cause - the restore path async function $3r(e,n,r){let d=uH(e,n),f=O8(r,e)...await C3r(d,f)} with `link as C3r`, i.e. resume/restore hard-links a prior session's backups into the new session directory under the same name (the ordinary backup write uses `copyFile as rQe`). Code sites verified verbatim: src/recover/backups.rs:10-12, src/recover/snapshots.rs:5-14 and 18-21 (MTIME_TOLERANCE_SECS = 120).",
          "rule": "One reset per version DECREASE of the same tracked path between consecutive snapshot or delta records in one transcript. One collision per backupFileName that carries two or more distinct backupTime values within one transcript. A blob's generation is decided by the recorded backupTime nearest its mtime, and only when that distance is 120s or less. One inode group per distinct st_ino among store blobs with st_nlink > 1.",
          "note": "This is the strongest-confirmed claim of the five and the instrument sharpened it: the directional result (0 of 297 colliding blobs belonging to an EARLIER generation, 183 to the latest) means reading @vN by name is not merely unordered but systematically returns the newest generation's bytes, so the failure mode csift guards against is the one that actually occurs. The 120s tolerance is empirically the right size - 86.8% of agreements land inside 1s, so widening it would buy almost nothing while admitting collisions. One sub-clause needed correcting: hard links are not a content-dedupe of identical blobs but the restore path's link() into a resumed session's directory; the observable consequence for csift (a blob reachable from two session directories) is unchanged."
        }
      ]
    },
    {
      "id": "FH-026",
      "area": "freshness-file-history",
      "behavior": "A TEXT-branch Read result (toolUseResult.type == \"text\") is persisted as toolUseResult.file = {filePath, content, numLines, startLine, totalLines} plus an OPTIONAL sixth key truncatedByTokenCap, set when a WHOLE-FILE read was auto-paginated for exceeding the output token cap - content then holds only the partial FIRST page. The type qualifier is load-bearing: the other Read branches carry their own file shapes and none of the five text keys is guaranteed there (file_unchanged carries filePath alone; parts carries count/filePath/originalSize/outputDir; pdf carries base64/filePath/originalSize).",
      "depends": "csift's `push_read_event` decides full-snapshot versus partial splice from line arithmetic alone (`startLine == 1 && numLines >= totalLines`) and never reads the flag, so a capped read normally lands as the conservative partial splice with explicit gaps - but `recover --coverage` cannot name the token cap as the reason, and that arithmetic is the only thing standing between a capped page and a full-file anchor.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-182",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;\n    if is_full {"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "17-22",
          "snippet": "    /// Full ground-truth content (an anchor): a Write result, a full Read\n    /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n    FullSnapshot {\n        content: String,\n        total_lines: usize,\n        source: SnapSource,"
        }
      ],
      "instrument": "`strings -a <the Claude Code 2.1.258 executable> | grep -o 'truncatedByTokenCap:M().optional().describe(\"[^\"]*\"'` prints the quoted sentence verbatim (counting rule: one hit per schema literal). Corpus: over every jsonl under ~/.claude/projects count echoes carrying the key, one observation per Read echo - measured 2026-09-02: 174.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > /tmp/cc258.strings; python3 -c \"import re;s=open('/tmp/cc258.strings',errors='replace').read();print(re.search(r'file:c\\\\(\\\\{filePath.{0,700}',s).group(0)[:700])\"  AND  python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nks=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n      try: t=json.loads(line).get('toolUseResult')\n      except Exception: continue\n      if isinstance(t,dict) and isinstance(t.get('file'),dict):\n        ks[(t.get('type'),tuple(sorted(t['file'].keys())))]+=1\nprint(ks)\nEOF  AND  grep -rn 'truncatedByTokenCap' src/  (run in the csift repo)",
          "observed": "Binary schema verbatim: file:c({filePath:i().describe(\"The path to the file that was read\"),content:i().describe(\"The content of the file\"),numLines:A().describe(\"Number of lines in the returned content\"),startLine:A().describe(\"The starting line number\"),totalLines:A().describe(\"Total number of lines in the file\"),truncatedByTokenCap:M().optional().describe(\"True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page). A programmatic signal for internal consumers; survives output reconstruction (unlike the render-time banner).\") -- the claim's quoted sentence is present word for word. Corpus over 7,817 jsonl files: 8,758 records carry a dict toolUseResult.file. Split by toolUseResult.type: type=\"text\" -> 8,434 with the five keys ('content','filePath','numLines','startLine','totalLines') and 174 with the same five plus 'truncatedByTokenCap' (the claim's 174, matched exactly). The remaining 150 are NON-text Read branches with entirely different file shapes: type=\"file_unchanged\" -> ('filePath',) x120, type=\"parts\" -> ('count','filePath','originalSize','outputDir') x26, type=\"pdf\" -> ('base64','filePath','originalSize') x4. In the csift repo `grep -rn 'truncatedByTokenCap' src/` returns nothing, so the depends clause (\"never reads the flag\") is confirmed.",
          "rule": "One observation per record whose toolUseResult.file is a dict, keyed by (toolUseResult.type, sorted tuple of file's keys); recursive over ~/.claude/projects, subagent transcripts included. Binary: one hit per schema literal.",
          "note": "The behavior and the 174 count survive intact; only the scope needed narrowing. 1.7% of file-bearing Read echoes (150 of 8,758) are non-text branches whose file object does not have the enumerated shape, so a consumer that reads numLines/startLine/totalLines without first checking toolUseResult.type would read absent keys. csift is unaffected: push_read_event is only reached from the text branch and takes num_lines/total_lines as Options. code-site note: Both sites confirmed verbatim at the claimed paths and line numbers: src/recover/carriers.rs 178-182 and src/recover/types.rs 17-22. The function containing the arithmetic is push_read_event (src/recover/carriers.rs:167)."
        }
      ]
    },
    {
      "id": "FH-027",
      "area": "freshness-file-history",
      "behavior": "`truncatedByTokenCap` is spread into the result object only as the literal `true` - the construction site is `...Ue!==void 0&&{truncatedByTokenCap:!0}` where `Ue` is the truncation banner - so the key is never written `false` and its absence is the only way `false` is expressed.",
      "depends": "Any csift consumer of the key must treat absence as false rather than expecting an explicit `false`; the pattern matches the absence-means-false convention csift already relies on elsewhere, and a future `recover` gate keying on `== Some(false)` would silently never fire.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "179-181",
          "snippet": "    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "`grep -o '.\\{450\\}truncatedByTokenCap.\\{450\\}'` over `strings -a` of the 2.1.258 binary shows the single construction site with the `{truncatedByTokenCap:!0}` spread. Corpus check: over every jsonl under ~/.claude/projects, count the distinct JSON values of the key, one observation per echo carrying it - measured now: 174 occurrences, value `true` on 174, `false` on 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport re\nsrc=open('/tmp/cc258.strings',errors='replace').read()\ni=src.find('truncatedByTokenCap:!0')\nseg=src[max(0,i-3000):i+120]\nfor m in re.finditer(r'Ue\\\\s*=\\\\s*[^;,]{0,200}', seg): print('ASSIGN:', m.group(0))\nprint(seg[-450:])\nprint('consumer tests:', src.count('truncatedByTokenCap===!0'))\nEOF  AND the same whole-corpus walk as FH-026, tallying json.dumps(file['truncatedByTokenCap'])",
          "observed": "Single construction site, verbatim: let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}},...pt&&{artifactRead:pt}}; -- the spread is exactly the claimed ...Ue!==void 0&&{truncatedByTokenCap:!0}. Ue is confirmed to be the truncation banner by its own assignment in the same scope: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total ...`:lfe+`${r}: showing the first ${nt.length} of ${me.length} characters ...` where lfe=\"[Truncated: PARTIAL view \\u2014 \". Consumers read it as an identity test against true, at 3 sites (U.data.file.truncatedByTokenCap===!0 twice, nt.toolUseResult?.file?.truncatedByTokenCap===!0 once) plus one d?.truncatedByTokenCap!==!0 early-return. Corpus: 174 records carry the key; distinct JSON values = {true: 174}, false: 0.",
          "rule": "One observation per Read echo carrying file.truncatedByTokenCap, bucketed by the key's serialized JSON value; whole corpus. Binary: one hit per construction/consumption site.",
          "note": "Confirmed on both instruments and reinforced by Claude Code's own consumers, which all test ===!0 rather than reading a boolean - the same absence-means-false convention the claim predicts. The identical spread idiom governs three sibling keys on the write/edit carriers (memdirStamped, gitDiff, staleRecovered), so the convention is systematic, not incidental to this one field. code-site note: src/recover/carriers.rs 179-181 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-028",
      "area": "freshness-file-history",
      "behavior": "The two forms are SUCCESSIVE, not concurrent, and neither current one is pathless. Through 2.1.202 the banner was injected into the rendered tool_result as '<system-reminder>[Truncated: PARTIAL view \\u2014 showing lines 1-N of T total ...]' - pathless, 504 occurrences, none after 2.1.202. From about 2.1.206 the carrier moved to a separate read_truncation_notice attachment whose banner ALWAYS names the absolute path (183 of 183), and no copy is written into the tool_result at all. 2.1.258 additionally builds a THIRD, path-bearing sentence at reconstruction time (function qes): '[Truncated: PARTIAL view \\u2014 <abs path>: showing N of T lines. Call Read with offset/limit to page through. ...]' or, when the file cannot be paginated by line, '... <abs path>: this view is incomplete and the file cannot be paginated by line. ...' - injected into the reconstructed payload, guarded against double-application, and never persisted (0 corpus records).",
      "depends": "csift parses no banner text on any surface - `rg -c \"PARTIAL view|read_truncation_notice\" src/ tests/` matches nothing - so all three grammars, the carrier migration and the path/pathless split are inert for it. Its partial-read instrument is the STRUCTURED read coordinates instead: `toolUseResult.file` and the `file` attachment supply `startLine` / `numLines` / `totalLines`, and `push_read_event` classifies a Read as a FullSnapshot only when `start_line == 1 && observed >= total && total > 0`, else as a PartialRead. The exposure this claim guards is therefore not the banner wording but the absence of coordinates: a truncated view delivered with no `numLines` / `totalLines` falls back to `total = observed` and classifies FULL, so a partial view would be replayed as a complete file.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "193-204",
          "snippet": "    } else {\n        events.push(FileEvent {\n            line_no,\n            turn_index,\n            timestamp_utc: ts.clone(),\n            kind: EventKind::PartialRead {\n                start_line: start_line.max(1),\n                lines,\n                total_lines: total,\n            },\n        });\n    }"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "64-75",
          "snippet": "            let start_line = file\n                .get(\"startLine\")\n                .and_then(serde_json::Value::as_u64)\n                .unwrap_or(1) as usize;\n            let total_lines = file\n                .get(\"totalLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);\n            let num_lines = file\n                .get(\"numLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-181",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Corpus: for every line containing `Truncated: PARTIAL view`, split on the em-dash-space and test whether the remainder starts with `showing`; count separately for `attachment.banner` and for `tool_result` content. One observation per carrier occurrence. Measured now: attachment banners 170 path-bearing / 0 pathless; tool_result copies 4 path-bearing / 528 pathless.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects'); EMD='\\u2014 '\npos=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if 'PARTIAL view' not in line: continue\n      try: o=json.loads(line)\n      except Exception: continue\n      c=(o.get('message') or {}).get('content')\n      texts=[]\n      if isinstance(c,list):\n        for b in c:\n          if isinstance(b,dict) and b.get('type')=='tool_result':\n            cc=b.get('content')\n            if isinstance(cc,str): texts.append(cc)\n            elif isinstance(cc,list): texts+=[s['text'] for s in cc if isinstance(s,dict) and isinstance(s.get('text'),str)]\n      for t in texts:\n        i=t.find('[Truncated: PARTIAL view '); j=t.find(EMD,i)\n        if i<0 or j<0: continue\n        rest=t[j+len(EMD):]\n        pb=not rest.startswith('showing') and not rest.startswith('this view is incomplete')\n        head = i==0 or t[:i].strip() in ('','<system-reminder>')\n        pos[('path' if pb else 'pathless','head' if head else 'embedded',o.get('version'))]+=1\nprint(pos)\nEOF  AND  csift search 'Truncated: PARTIAL view' -t agent.tool.result --count-by version  AND  csift search 'Truncated: PARTIAL view' --attachments -t harness.meta.attachment --count-by version  AND a python scan of /tmp/cc258.strings for every 'PARTIAL view' construction",
          "observed": "BINARY (2.1.258): only two banner constructions exist and BOTH name the path. (1) read time: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total ...`:lfe+`${r}: showing the first ...`, carried to the transcript as an attachment {type:\"read_truncation_notice\", banner, toolUseID}. (2) reconstruction time, function qes: lfe+`${d.filePath}: showing ${d.numLines} of ${d.totalLines} lines. Call ${dt} with offset/limit to page through. Do NOT answer from this page alone if the answer may be further in the file.]` with a no-coordinates variant lfe+`${d.filePath}: this view is incomplete and the file cannot be paginated by line. ...]`, guarded by if(typeof _.content===\"string\"&&_.content.startsWith(\"<system-reminder>\"+lfe))return. There is NO pathless construction anywhere in the 2.1.258 binary. CORPUS: 527 tool_result-carried banner occurrences. Splitting by position: 504 are HEAD position (the tool_result content is exactly '<system-reminder>' + banner, i.e. a genuine injected copy) and every single one of those 504 is pathless AND carries a record version <= 2.1.202 (2.1.150 x2, 2.1.156 x12, 2.1.159 x65, 2.1.170 x3, 2.1.177 x279, 2.1.186 x1, 2.1.187 x1, 2.1.191 x73, 2.1.196 x29, 2.1.199 x6, 2.1.200 x8, 2.1.202 x25). ZERO head-position copies exist at any version above 2.1.202, pathless or path-bearing. The remaining 23 pathless + 7 path-bearing occurrences are EMBEDDED mid-content at versions 2.1.179-2.1.258 - Read echoes of files that quote a banner, not injected copies. The reconstruction sentence from qes appears in ZERO corpus records. csift corroborates the carrier migration independently: 'Truncated: PARTIAL view' -t agent.tool.result gives 544 records spanning 2.1.150-2.1.202 plus 19 stragglers at 2.1.231/2.1.258, while -t harness.meta.attachment gives 184 records spanning 2.1.206-2.1.258 (one outlier at 2.1.177). Attachment side: 183 banners, 183 path-bearing, 0 pathless.",
          "rule": "One observation per banner occurrence, classified on two axes: carrier (attachment.banner vs a tool_result content string) and position (HEAD = the text before the '[Truncated:' bracket is empty or exactly '<system-reminder>'; EMBEDDED otherwise). Path-bearing = the text after the U+2014 separator does not begin 'showing' or 'this view is incomplete'. Whole corpus.",
          "note": "Drift is in the carrier, not just the wording: current Claude Code writes the banner once, as a read_truncation_notice attachment, and writes no tool_result copy. The claim's 528-pathless / 4-path-bearing tool_result split is a whole-corpus HISTORY count, and reading it as a statement about current behavior would send a detector hunting for a copy that is no longer emitted. The claim's warning still has teeth in the opposite direction: a detector written today against the attachment-only, path-bearing form would silently see nothing on any transcript at version <= 2.1.202, where 504 of the corpus's 690 genuine banner emissions live. code-site note: src/recover/carriers.rs 193-204 confirmed verbatim (the PartialRead else-arm). csift parses no banner text on any surface, so this drift changes no csift behavior."
        }
      ]
    },
    {
      "id": "FH-029",
      "area": "freshness-file-history",
      "behavior": "The line-branch banner states the exact next-page coordinates as `Call Read with offset=${Ne+1} limit=${Ne} for the next page`, i.e. the next window starts one past the last line delivered and requests the same page size.",
      "depends": "This makes a capped read's missing range machine-derivable from the banner text alone, which is the cheapest way for csift to disclose the gap in a `recover --coverage` window without reading the file.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "190-196",
          "snippet": "                source,\n            },\n        });\n    } else {\n        events.push(FileEvent {\n            line_no,\n            turn_index,"
        }
      ],
      "instrument": "Read one banner: parse any record under ~/.claude/projects whose `attachment.type` is `read_truncation_notice` and print `attachment.banner`; expect the literal `Call Read with offset=<N+1> limit=<N> for the next page, or Grep to find a specific section.` where N is the `showing lines 1-N` upper bound. Measured now: matches on every one of the 139 line-branch banners.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('Ue=!je&&Ne<ke');print(repr(src[i:i+330]))\"  AND  python3 - <<'EOF'\nimport json,os,re,collections\nROOT=os.path.expanduser('~/.claude/projects')\nRE=re.compile(r'showing lines 1-(\\\\d+) of (\\\\d+) total \\\\((\\\\d+) tokens, cap (\\\\d+)\\\\)\\\\. Call (\\\\w+) with offset=(\\\\d+) limit=(\\\\d+) for the next page, or (\\\\w+) to find a specific section\\\\.')\nk=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if 'read_truncation_notice' not in line: continue\n      try: a=json.loads(line).get('attachment')\n      except Exception: continue\n      if not (isinstance(a,dict) and a.get('type')=='read_truncation_notice'): continue\n      b=a.get('banner') or ''\n      m=RE.search(b)\n      if m: k['ok' if int(m.group(6))==int(m.group(1))+1 and int(m.group(7))==int(m.group(1)) else 'MISMATCH']+=1\n      elif 'cannot be paginated by line' in b: k['char-branch']+=1\n      else: k['unmatched']+=1\nprint(k)\nEOF",
          "observed": "Binary template verbatim: Ue=!je&&Ne<ke?lfe+`${r}: showing lines 1-${Ne} of ${ke} total (${Ot.tokenCount} tokens, cap ${I}). Call ${dt} with offset=${Ne+1} limit=${Ne} for the next page, or ${Wo} to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]` -- the offset argument is literally the interpolation Ne+1 and the limit argument is literally Ne, where Ne is the same value rendered as the upper bound of 'showing lines 1-${Ne}'. Corpus: of 183 read_truncation_notice attachments, 152 take the line branch and 152 of 152 satisfy offset == N+1 and limit == N; 0 mismatches; the other 31 are the character branch. The historical tool_result copies add 523 line-branch instances, also 523 of 523 conforming. Concrete instance: 'showing lines 1-206 of 255 total (26183 tokens, cap 25000). Call Read with offset=207 limit=206 for the next page, or Grep to find a specific section.'",
          "rule": "One observation per read_truncation_notice attachment; a banner counts 'ok' only when the regex-captured offset equals the captured N plus one AND the captured limit equals N. Whole corpus.",
          "note": "Holds exactly, at both the template and the instance level, with zero counterexamples in 675 line-branch banners across every version in the corpus. Only the instrument's headline number moved with corpus growth: 152 line-branch attachment banners now, against the 139 the claim recorded. The derivation the depends clause wants is sound - the missing range is (N+1 .. T) read straight off 'showing lines 1-N of T total' - but note the coordinates are only present on the line branch; see FH-030 for the 17% of banners that carry none. code-site note: src/recover/carriers.rs 190-196 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-030",
      "area": "freshness-file-history",
      "behavior": "The character-branch banner is a different sentence - `showing the first <X> of <Y> characters (<T> tokens, cap <C>); this file has very long lines and cannot be paginated by line.` - and reports CHARACTERS, not lines, so it carries no line coordinates at all.",
      "depends": "A banner parser that assumes the `showing lines A-B` grammar silently fails on this variant; for csift it is the one case where the capped window cannot be expressed as a line range at all.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "190-196",
          "snippet": "                source,\n            },\n        });\n    } else {\n        events.push(FileEvent {\n            line_no,\n            turn_index,"
        }
      ],
      "instrument": "Corpus: count records whose `attachment.banner` contains `very long lines and cannot be paginated by line`, one observation per record. Measured now: 31 of 170 notice attachments.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('showing the first');print(repr(src[i-40:i+430]))\"  AND the same read_truncation_notice walk as FH-029, counting banners containing 'very long lines and cannot be paginated by line'",
          "observed": "Binary template verbatim, as the else-branch of the same ternary: lfe+`${r}: showing the first ${nt.length} of ${me.length} characters (${Ot.tokenCount} tokens, cap ${I}); this file has very long lines and cannot be paginated by line. Use ${Wo} to find a specific section, or ${dt} with offset/limit to page through it. Do NOT answer from this excerpt alone if the answer may be elsewhere in the file.]` -- the quantities interpolated are nt.length and me.length, i.e. string lengths in characters, and no line number appears anywhere in the sentence. Corpus: 31 of 183 read_truncation_notice attachments (17.0%) contain 'very long lines and cannot be paginated by line'; the other 152 are the line branch; 31 + 152 = 183, so the two branches partition the set exactly with no third form. The claim's 31 is unchanged; its denominator has grown from 170 to 183.",
          "rule": "One observation per record whose attachment.type is read_truncation_notice, bucketed by whether the banner contains the literal 'very long lines and cannot be paginated by line'. Whole corpus.",
          "note": "Holds, and the branch selector is worth recording because it is not what the sentence implies: the ternary is Ue=!je&&Ne<ke?line:char, where je is set when the cut landed mid-line, so the character branch is chosen by the CUT falling inside a line, not by any property of the file measured up front. The reconstruction-time banner found under FH-028 has its own coordinate-free variant ('this view is incomplete and the file cannot be paginated by line'), so a parser guarding against the line grammar must tolerate two coordinate-free sentences, not one. code-site note: src/recover/carriers.rs 190-196 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-031",
      "area": "freshness-file-history",
      "behavior": "A Write carrier that CREATED a file is persisted as {type:\"create\", filePath, content, structuredPatch:[], originalFile:null, userModified} plus up to two CONDITIONALLY SPREAD optional keys - memdirStamped (58 of 2,598 creates) and gitDiff (schema-declared gitDiff:kQe().optional(), 0 occurrences corpus-wide). originalFile is null by construction on a create, and on an update it is null when \"the previous content was too large to include\" - measured at 65 of 291 updates (22.3%).",
      "depends": "csift uses an Edit's `originalFile` versus the replayed buffer as an AUTHORITATIVE drift boundary, so a null `originalFile` disarms that cross-check for that op; the majority of real Edits are in exactly that state, which is why the boundary set is explicitly an honestly-bounded detectable subset rather than a guarantee.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "91-93",
          "snippet": "    // ── (2) Write result: {type:create|update, filePath, content, …} ──\n    // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n    let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'originalFile:i().nullable().describe(\"The original file content before the write[^\"]*\"' - expect the describe string naming both null cases. Counting rule: one hit per schema literal.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "SPEC.md section 6.7 extraction table; schema description measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();[print(repr(m.group(0))) for m in re.finditer(r'originalFile:[A-Za-z_$]*\\\\(\\\\)\\\\.nullable\\\\(\\\\)\\\\.describe\\\\(\\\"[^\\\"]*\\\"',src)]; i=src.find('let ke={type:\\\"create\\\"');print(repr(src[i:i+230]))\"  AND  python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); keys=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n      try: t=json.loads(line).get('toolUseResult')\n      except Exception: continue\n      if not (isinstance(t,dict) and isinstance(t.get('filePath'),str)): continue\n      ty=t.get('type')\n      if ty in ('create','update'):\n        keys[(ty,tuple(sorted(t.keys())))]+=1\n        k[(ty,'origNull' if t.get('originalFile') is None else 'origStr')]+=1\n        if ty=='create': k[('create','sp_empty' if t.get('structuredPatch')==[] else 'sp_other')]+=1\nprint(k); print(keys)\nEOF",
          "observed": "Binary schema verbatim: originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\") -- both null cases named exactly as the claim quotes. The create construction is also literal in the binary: let ke={type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:v??!1,...z&&{memdirStamped:!0},..._e&&{gitDiff:_e}}, and its update sibling: Ie={type:\"update\",filePath:e,content:n,structuredPatch:we,originalFile:fe?null:W,userModified:v??!1,...z&&{memdirStamped:!0},..._e&&{gitDiff:_e}} -- so originalFile:null is a compile-time constant on create, not a runtime outcome. Corpus: 2,889 Write carriers total. Creates 2,598: originalFile null on 2,598 of 2,598 (100%), structuredPatch [] on 2,598 of 2,598, userModified false on 2,598 of 2,598. Updates 291: originalFile null on 65 (22.3%), a string on 226. Key sets: creates are ('content','filePath','originalFile','structuredPatch','type','userModified') x2,540 plus a seven-key variant adding 'memdirStamped' x58; updates are the same six-key set x290 plus one memdirStamped variant. 'gitDiff' occurs 0 times on any Write carrier corpus-wide despite being schema-declared.",
          "rule": "One observation per record whose toolUseResult is a dict with a string filePath and type in {create, update}; tallied by type, by whether originalFile is JSON null, and by the sorted tuple of the carrier's own keys. Whole corpus.",
          "note": "The substance holds without exception: originalFile is a literal null in the create construction and is null on 100% of the 2,598 real creates. Two refinements. First, the key enumeration is a base, not a closed set - two keys are spread in conditionally, so a consumer must not treat an unexpected key as a malformed carrier. Second, the update rate is worth pinning at 22.3%: 65 of 291 updates carry a null originalFile, which is where the depends clause's disarmed cross-check actually bites on the Write side. code-site note: src/recover/carriers.rs 91-93 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-032",
      "area": "freshness-file-history",
      "behavior": "An Edit carrier is persisted as {filePath, oldString, newString, originalFile (nullable), structuredPatch, userModified, replaceAll} plus up to three CONDITIONALLY SPREAD optional keys - staleRecovered (92 corpus-wide), memdirStamped (43) and gitDiff (schema-declared, 0 occurrences corpus-wide) - and carries NO type field, that absence being what distinguishes it from a Write carrier. Its originalFile, schema-declared .nullable() (\"The original file contents before editing\"), is NULL on the MAJORITY of real edits: 9,690 null against 3,682 present over 13,372 Edit carriers corpus-wide, and 8,444 null against 2,977 string over the 11,434 echoes of a 120-transcript window.",
      "depends": "csift discriminates the two carrier shapes by the presence of `oldString`/`newString` rather than by `type`, then keeps the strings plus `structuredPatch` plus `originalFile`; its AUTHORITATIVE `original_file_disagreement` boundary can therefore fire on only about one edit in four, so a clean `recover --coverage` is evidence over that fraction and the skipped check is never reported as skipped. A `type` field appearing on Edit carriers would flip every edit into the Write full-snapshot arm and wipe the buffer at each one.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "97-98",
          "snippet": "    let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n    let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "131-136",
          "snippet": "                // Boundary cross-check: originalFile vs replayed buffer.\n                if let Some(orig) = original_file {\n                    if had_full_anchor && buffer_disagrees_with_original(&buf, orig) {\n                        out.boundaries.push(Boundary {\n                            line_no: e.line_no,\n                            turn_index: e.turn_index,"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "531-535",
          "snippet": "pub(crate) fn buffer_disagrees_with_original(buf: &SparseBuffer, original_file: &str) -> bool {\n    let orig_lines = split_lines(original_file);\n    if orig_lines.is_empty() {\n        return false;\n    }"
        }
      ],
      "instrument": "Whole corpus: for every dict `toolUseResult` with a `filePath` and no `create`/`update` `type`, tally whether `originalFile` is null, whether `structuredPatch` is truthy and whether `replaceAll` is truthy - counting rule one count per carrier RECORD; measured 2026-09-02: 13,372 with a structuredPatch, 9,690 null originalFile against 3,682 present, 70 with `replaceAll:true`. Narrower window: the same tally over the 120 most-recently-modified top-level transcripts, one tally per Edit echo - measured 2026-09-02: null 8,443, str 2,975 (11,418 total; the live window drifts by single digits between runs). Schema: `strings -a <the Claude Code 2.1.258 executable> | grep -F 'originalFile:i().nullable().describe(\"The original file contents before editing\")' | wc -l` expects 1.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.7 extraction table; census measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 | grep -F 'originalFile:i().nullable().describe(\"The original file contents before editing\")' | wc -l  AND  python3 -c \"src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('return{data:{filePath:F,oldString');print(repr(src[i:i+230]))\"  AND  python3 - <<'EOF'\nimport json,os,glob,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); keys=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if '\\\"filePath\\\"' not in line or 'toolUseResult' not in line: continue\n      try: t=json.loads(line).get('toolUseResult')\n      except Exception: continue\n      if not (isinstance(t,dict) and isinstance(t.get('filePath'),str)): continue\n      if t.get('type') in ('create','update'): continue\n      keys[tuple(sorted(t.keys()))]+=1\n      k['type_key_present=%s'%('type' in t)]+=1\n      of=t.get('originalFile','MISSING')\n      k['null' if of is None else ('missing' if of=='MISSING' else 'str')]+=1\n      if t.get('structuredPatch'): k['structuredPatch']+=1\n      if t.get('replaceAll'): k['replaceAll']+=1\nprint(k); print(keys)\nEOF  (and the same tally restricted to the 120 most-recently-modified files matched by ~/.claude/projects/*/*.jsonl)",
          "observed": "Schema literal count = 1, as the claim expects. The Edit construction is literal in the binary and carries NO type key: return{data:{filePath:F,oldString:me,newString:j,originalFile:fe,structuredPatch:ke,userModified:_??!1,replaceAll:B,...we&&{staleRecovered:!0},...Ie&&{memdirStamped:!0},...De&&{gitDiff:De}}} -- so the discriminator is structural, not statistical. Corpus, matching the claim's counting rule exactly: 13,385 carriers with a string filePath and no create/update type; 'type' key present on 0 of 13,385; 13,372 with a truthy structuredPatch; originalFile null 9,690 against a string 3,682 (both the claim's numbers, to the unit); replaceAll true on 70. Key sets: ('filePath','newString','oldString','originalFile','replaceAll','structuredPatch','userModified') x13,237, the same plus 'staleRecovered' x92, the same plus 'memdirStamped' x43, and 13 residue carriers that are not Edits at all - ('filePath','hasTaskTool','isAgent','plan') x8 and the same plus 'planWasEdited' x5. 'gitDiff' occurs 0 times across all 13,372 Edit carriers. Narrower window, the 120 most-recently-modified top-level transcripts: null 8,444, string 2,977 (claim: 8,443 and 2,975 - a drift of 1 and 2, inside the claim's own stated single-digit tolerance). Ratio: 3,682/13,372 = 27.5%, i.e. the claim's \"about one edit in four\".",
          "rule": "One count per carrier RECORD whose toolUseResult is a dict with a string filePath and whose type is neither create nor update; tallied by originalFile nullity, by truthy structuredPatch, by truthy replaceAll, and by the sorted key tuple. Whole corpus, then the same tally over the 120 most-recently-modified files matching ~/.claude/projects/*/*.jsonl.",
          "note": "Every number in the claim reproduces to the unit on the whole corpus, and the window numbers moved by 1 and 2 as the claim itself predicted. Two refinements. gitDiff is real in the schema but has never fired here, so it is a documented possibility rather than an observed field - and a further optional, memdirStamped, was missing from the enumeration. The type-absence discriminator is now confirmed structurally rather than by census: the Edit result object is built with no type key at all, so the claim's worry (a type field appearing on Edit carriers and flipping every edit into the Write arm) would require a change to that construction, not a data accident. csift's own discriminator reads oldString/newString, which is the same structural fact from the other side. code-site note: All three sites confirmed verbatim: src/recover/carriers.rs 97-98, src/recover/replay.rs 131-136, src/recover/replay.rs 529-533."
        }
      ]
    },
    {
      "id": "FH-033",
      "area": "freshness-file-history",
      "behavior": "The `edited_text_file` attachment's payload keys are exactly `type`, `filename` and `snippet` - never `filePath` or `content` - and an EMPTY `snippet` string is a real, emitted form.",
      "depends": "csift's `filename`-then-`filePath` and `snippet`-then-`content` fallbacks are dead alternatives on current Claude Code, and the empty-snippet case is the degraded external-edit boundary csift already names in the boundary detail.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "219-224",
          "snippet": "    if atype == Some(\"edited_text_file\") {\n        let path = att\n            .get(\"filename\")\n            .or_else(|| att.get(\"filePath\"))\n            .and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        }
      ],
      "instrument": "Corpus: count `tuple(sorted(attachment.keys()))` for every record whose `attachment.type` is `edited_text_file`, one observation per record. Measured now over all of ~/.claude/projects: 1339 records, all with the key set `('filename','snippet','type')`; 85 of the 1339 carry an empty `snippet` (6.3%).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nks=collections.Counter(); e=collections.Counter(); byver=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if 'edited_text_file' not in line: continue\n      try: o=json.loads(line)\n      except Exception: continue\n      a=o.get('attachment')\n      if isinstance(a,dict) and a.get('type')=='edited_text_file':\n        ks[tuple(sorted(a.keys()))]+=1\n        e['empty' if a.get('snippet')=='' else 'nonempty']+=1\n        if a.get('snippet')=='': byver[o.get('version')]+=1\nprint(ks); print(e); print(byver)\nEOF  AND  python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();[print(repr(src[max(0,m.start()-160):m.start()+120])) for m in re.finditer(r'type:\\\"edited_text_file\\\"',src)]; i=src.find('if(_.type!==\\\"edited_text_file\\\")continue');print(repr(src[i-60:i+170])); print(re.search(r'vQo=[0-9]+',src).group(0))\"",
          "observed": "Corpus: 1,339 records whose attachment.type is edited_text_file; the key set is ('filename','snippet','type') on 1,339 of 1,339 - a single key set, with no filePath and no content anywhere. 85 of the 1,339 carry an empty snippet (6.3%), and those empties are spread across 12 Claude Code versions from 2.1.177 to 2.1.258, including 2 records at 2.1.258 itself, so the empty form is current, not a fossil. Binary: the construction is return{type:\"edited_text_file\",filename:C,snippet:j}, three keys, and it is guarded by if(j===\"\")return null - yet the empty form still ships, because a LATER pass blanks it: let f=0;for(let _ of d){if(_.type!==\"edited_text_file\")continue;if(f>=vQo)_.snippet=\"\";else f+=_.snippet.length} with vQo=16384. Attachments are walked in order, their snippet lengths accumulated, and every edited_text_file past a cumulative 16,384 characters has its snippet overwritten with the empty string.",
          "rule": "One observation per record whose attachment.type is edited_text_file, keyed by the sorted tuple of the attachment's own keys, then bucketed by whether snippet is the empty string, then by the record's Claude Code version. Whole corpus.",
          "note": "Holds on every count, to the unit (1,339 and 85), and the empty form is confirmed live at 2.1.258 rather than inferred. The mechanism is now pinned and is worth recording because it is not a per-snippet cap: the budget vQo=16384 is CUMULATIVE across the attachments of one batch, applied after construction, so whether a given external edit arrives with content depends on how many other edited files preceded it in the same batch - not on that file's own size. The claim's depends clause is confirmed from the other side too: the construction emits exactly three keys, so csift's filePath and content fallbacks can never be reached on current Claude Code. code-site note: src/recover/carriers.rs 219-224 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-034",
      "area": "freshness-file-history",
      "behavior": "The 8,192-character cap applies to the diff BODY before the tail is appended, and the cut backs up to the last newline at or before 8,192 (falling back to a hard 8,192 slice when there is no earlier newline). So the EMITTED snippet is body + `\\n... [N lines truncated] ...` and can exceed 8,192 - 8,223 characters observed - while a truncated snippet can also be far shorter than the cap, 597 characters observed, when the body's last newline before position 8,192 sits early because of one very long line.",
      "depends": "csift treats the stripped snippet as observed content for the external-edit boundary detail; the 8192-char cap means half of all real snippets are themselves truncated, so the snippet is never a complete picture of the external change.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "225-230",
          "snippet": "            let snippet_text = att\n                .get(\"snippet\")\n                .or_else(|| att.get(\"content\"))\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default();\n            let snippet = strip_gutter(snippet_text);"
        }
      ],
      "instrument": "Seek `function EJt(` in a `strings -a` dump of the 2.1.258 binary (`context:8`, the `TQe` cap, and the `... [${F} lines truncated] ...` tail are one function), then `grep -o 'TQe=[0-9]*'` for the cap - expect `TQe=8192`. Corpus: count `edited_text_file` snippets containing `lines truncated] ...`, one observation per attachment - measured now: 651 of 1254 non-empty snippets (52%).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();i=src.find('function EJt(');print(repr(src[i:i+470]));j=src.find('function j8t(');print(repr(src[j:j+330]));print(re.search(r'TQe=[0-9]+',src).group(0))\"  AND  python3 - <<'EOF'\nimport json,os,collections\nROOT=os.path.expanduser('~/.claude/projects')\nk=collections.Counter(); mx=collections.Counter()\nfor dp,dn,fn in os.walk(ROOT):\n  for f in fn:\n    if not f.endswith('.jsonl'): continue\n    for line in open(os.path.join(dp,f),errors='replace'):\n      if 'edited_text_file' not in line: continue\n      try: a=json.loads(line).get('attachment')\n      except Exception: continue\n      if not (isinstance(a,dict) and a.get('type')=='edited_text_file'): continue\n      s=a.get('snippet')\n      if not isinstance(s,str) or s=='': continue\n      t='lines truncated] ...' in s\n      k['trunc' if t else 'whole']+=1\n      mx['max_trunc' if t else 'max_whole']=max(mx['max_trunc' if t else 'max_whole'],len(s))\n      if not t and len(s)>8192: k['whole_over_cap']+=1\nprint(k); print(mx)\nEOF",
          "observed": "Binary, one function, verbatim: function EJt(e,n){let r=rne(\"file.txt\",\"file.txt\",e,n,void 0,void 0,{context:8,timeout:qCe});if(!r)return\"\";let o=iz(),d=r.hunks.map((B)=>({startLine:B.oldStart,content:B.lines.filter((U)=>!U.startsWith(\"-\")&&!U.startsWith(\"\\\\\")).map((U)=>U.slice(1)).join(`\\n`),tabAwareSeparator:o})).map(j8t).join(`\\n`);if(d.length<=TQe)return d;let f=d.lastIndexOf(`\\n`,TQe),_=f>0?d.slice(0,f):d.slice(0,TQe),v=1,C=1,F=_n(d,`\\n`,_.length+v)+C;return`${_}\\n... [${F} lines truncated] ...`} -- every element the claim names is present: context:8, the removal and '\\\\ No newline' filter, the .slice(1) marker-column strip, and the tail. TQe=8192. The gutter renderer is function j8t({content:e,startLine:r,tabAwareSeparator:n=!1}) which walks lines emitting `${o}${sep}${text}` from o=startLine, so the numbering does start at the hunk's oldStart. Corpus: 1,254 non-empty snippets, 651 carry 'lines truncated] ...' (51.9%). Longest snippet WITHOUT the tail: 8,186 characters, and 0 of the 603 untruncated snippets exceed 8,192 - so the cap is real and never violated on the body. Longest snippet overall: 8,223 characters, i.e. 31 over the cap, which is the appended tail. Truncated snippets range 597 to 8,223 characters.",
          "rule": "One observation per record whose attachment.type is edited_text_file with a non-empty snippet, bucketed by whether the snippet contains the literal 'lines truncated] ...', with the maximum character length tracked per bucket. Whole corpus.",
          "note": "Every structural element of the claim is confirmed in a single binary function, and the headline 651 of 1,254 (52%) reproduces to the unit. The refinement matters for a consumer that would size a buffer or test length<=8192 to decide truncation: neither works. The reliable truncation test is the literal tail, and the reliable statement about the cap is that it bounds the body, not the field. One consequence for the depends clause: because the cut is at a newline before the cap rather than at the cap, the shortfall is unbounded - a snippet can stop at 597 characters and still claim to represent an external change, so the snippet is not merely incomplete but incomplete by an amount the record does not disclose except as a line count in the tail. code-site note: src/recover/carriers.rs 225-230 confirmed verbatim."
        }
      ]
    },
    {
      "id": "FH-035",
      "area": "freshness-file-history",
      "behavior": "`staleRecovered:true` rides on the EDIT echo only (never the Write or Read shape) and means Claude Code found the file changed on disk since the last Read yet applied the edit because `oldString` stayed unique.",
      "depends": "csift reads the flag INSIDE the Edit arm, which returns before the Write full-content arm, and turns it into the non-invalidating `stale_recovered` annotation boundary; if the flag ever moved to the Write shape it would be read by no arm at all.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "131-138",
          "snippet": "        // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n        // disk since the last read, but old_string stayed unique so the edit applied.\n        // The disk holds changes this stream never saw - an authoritative annotation.\n        if tur\n            .get(\"staleRecovered\")\n            .and_then(serde_json::Value::as_bool)\n            .unwrap_or(false)\n        {"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "87-151",
          "snippet": "        return;\n    }\n\n    // A Write result: full-content anchor.\n    if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n        let total = line_count(content);"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "62-67",
          "snippet": "    /// A SUCCESSFUL Edit whose `toolUseResult.staleRecovered:true` reports the file\n    /// had been modified on disk since the last read; the edit still applied cleanly\n    /// (old_string stayed unique). The buffer's edited span is right, but the disk\n    /// holds other changes this stream never saw: an authoritative, NON-invalidating\n    /// annotation boundary.\n    StaleRecovered,"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'staleRecovered' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and 'staleRecovered' in t:\n      k['edit' if 'oldString' in t else ('write' if 'content' in t else 'other')]+=1\nprint(k)\nEOF\nCounting rule: one tally per echo carrying a `staleRecovered` key, split by shape. Measured 2026-09-02: edit 80, write 0, other 0. Earliest Claude Code version in the corpus carrying the key: 2.1.207.",
      "located": {
        "claude_code": "2.1.233",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11; carrier-shape census measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.207",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'staleRecovered' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and 'staleRecovered' in t:\n      k['edit' if 'oldString' in t else ('write' if 'content' in t else 'other')]+=1\nprint(k)\nEOF  (and the same walk over the whole corpus via os.walk, additionally tallying the value and the record version)  AND  python3 -c \"import re;src=open('/tmp/cc258.strings',errors='replace').read();print(src.count('staleRecovered'));[print(repr(src[max(0,m.start()-150):m.start()+120])) for m in re.finditer('staleRecovered',src)]\"",
          "observed": "Whole corpus: 92 echoes carry a staleRecovered key; split by shape, edit 92, write 0, other 0; the value is true on 92 of 92 and false on 0. The 120-transcript window reproduces the claim's figure exactly: edit 80, write 0, other 0. Versions carrying the key run 2.1.207 (earliest, 4 echoes) through 2.1.258 (1 echo), so the flag is both first-seen where the claim says and still live on the current build. Binary: 'staleRecovered' occurs 5 times and every occurrence is inside the Edit tool - the result construction return{data:{filePath:F,oldString:me,newString:j,originalFile:fe,structuredPatch:ke,userModified:_??!1,replaceAll:B,...we&&{staleRecovered:!0},...}}, the Edit tool's mapToolResultToToolResultBlockParam destructure, and two sites in the edit helper that produces it. Neither Write construction (type:\"create\" or type:\"update\") mentions it.",
          "rule": "One tally per echo carrying a staleRecovered key, split by carrier shape - 'edit' when oldString is present, else 'write' when content is present, else 'other' - then by the key's JSON value and the record's Claude Code version. Run over the whole corpus and over the 120 most-recently-modified files matching ~/.claude/projects/*/*.jsonl.",
          "note": "Holds on both instruments, and the census result is now backed by a structural reason rather than a sample: the key is spread into the Edit result object only, and the string does not appear in either Write construction, so the claim's failure mode (the flag moving to the Write shape, where csift's Edit-arm read would never see it) cannot happen by data variation - it would take a change to that construction. Worth noting alongside FH-027: staleRecovered uses the same conditional-spread idiom, so it too is never written false, and csift's .and_then(as_bool).unwrap_or(false) reads absence correctly. code-site note: All three sites confirmed verbatim: src/recover/carriers.rs 131-138, src/recover/carriers.rs 146-151, src/recover/types.rs 62-67."
        }
      ]
    },
    {
      "id": "FH-036",
      "area": "freshness-file-history",
      "behavior": "When an Edit applies to a file that changed on disk since the last Read, Claude Code both sets `toolUseResult.staleRecovered` and appends a prose note to the tool_result string (` (note: the file had been modified on disk since you last read it — the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)`).",
      "depends": "csift reads ONLY the structured flag to raise its `stale_recovered` annotation boundary, so in a carrier-less workflow lane - where the flag does not exist and only the prose note survives - that authoritative freshness signal is lost and `recover --coverage` reports one fewer soft boundary than really occurred.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "131-135",
          "snippet": "        // `staleRecovered:true` on a SUCCESSFUL Edit: CC found the file modified on\n        // disk since the last read, but old_string stayed unique so the edit applied.\n        // The disk holds changes this stream never saw - an authoritative annotation.\n        if tur\n            .get(\"staleRecovered\")"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'staleRecovered:f[^`]\\{0,260\\}' for the note literal; python3 over ~/.claude/projects counting Edit carriers (a dict `toolUseResult` carrying `oldString`) whose `staleRecovered` is truthy. Counting rule: one count per carrier RECORD. Observed 92 out of 13372 Edit carriers.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.11 and SPEC.md section 6.7 extraction table; note literal and counts measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -o 'the file had been modified on disk since you last read it[^\"`]\\{0,220\\}' and grep -o '.\\{300\\}the file had been modified on disk since you last read it.\\{0,240\\}'; then python3 walking ~/.claude/projects, byte-prefiltering each jsonl line on b'oldString', parsing the survivors, counting records whose toolUseResult is a dict containing 'oldString' (Edit carriers) and, among those, the ones whose toolUseResult.staleRecovered is truthy, plus whether the rendered tool_result text of each such record contains the prose note.",
          "observed": "Binary: `mapToolResultToToolResultBlockParam(e,n){let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e,v=o?\".  The user modified your proposed changes before accepting them. \":\"\",C=f?\" (note: the file had been modified on disk since you last read it \\u2014 the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)\":o||_?\"\":O3t;` - the structured flag `staleRecovered` and the prose note are produced by the same expression. Corpus: 13372 Edit carriers, 92 with staleRecovered truthy, and 92 of those 92 also carry the prose note in the rendered tool_result text. 7819 jsonl files walked, 0 unparseable candidate lines. csift code site src/recover/carriers.rs:131-135 matches the claimed snippet verbatim (comment lines 131-133, `if tur` 134, `.get(\"staleRecovered\")` 135).",
          "rule": "One count per carrier RECORD: a jsonl record whose top-level `toolUseResult` is a JSON object containing the key `oldString` is one Edit carrier; it is a stale carrier iff `toolUseResult.staleRecovered` is truthy. Prose-note pairing counts a stale carrier as paired iff the string `the file had been modified on disk since you last read it` occurs in the concatenation of message.content (string form) and every tool_result block's content (string or [{text}] form).",
          "note": "Both halves confirmed on the current binary and the current corpus, and the claim's numbers reproduce exactly (92 of 13372). Added observation the claim did not state: the structured flag and the prose note are not merely correlated, they are emitted by one ternary in the Edit tool's result mapper, and all 92 stale carriers in the corpus carry both. That strengthens the claim's `depends`: the prose note is the only survivor in a carrier-less lane precisely because the flag lives on toolUseResult while the note is spliced into the tool_result string."
        }
      ]
    },
    {
      "id": "FH-037",
      "area": "freshness-file-history",
      "behavior": "Claude Code's file-mutating structured tool set is exactly the four-element array `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`, wrapped in a `Set` and consulted by the predicate that decides whether a `PostToolUse` / `PostToolUseFailure` / `PostToolBatch` event touched a file (the test also resolves the name through an alias expansion, so an MCP tool aliased onto one of the four counts).",
      "depends": "csift's `FileOp` enum recognises exactly the same four structured tools as authoritative mutations, so `files --by-file` create-vs-edit accounting and `recover`'s replay stay aligned with the harness's own definition; a fifth mutating tool added to that array would produce mutations csift silently omits.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "146-152",
          "snippet": "            let (op, key) = match name {\n                \"Write\" => (FileOp::Write, \"file_path\"),\n                \"Edit\" => (FileOp::Edit, \"file_path\"),\n                \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n                \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n                _ => continue,\n            };"
        }
      ],
      "instrument": "`strings -n 30 <the Claude Code 2.1.258 executable> | grep -oE '.{160}\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\].{80}'` -> one hit showing `var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr);function cGe(e,n){...return LPt.has(r)||...}`, and `grep -oE 'function FPt\\(e,n\\)\\{.{0,420}'` -> `switch(e.hook_event_name){case\"PostToolUse\":case\"PostToolUseFailure\":return cGe(e.tool_name,n);case\"PostToolBatch\":return (e.tool_calls??[]).some(...)}`. Counting rule: exact array literal match, one occurrence.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (tool-registry probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -oE '.{170}\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\].{110}' dump; grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' dump | wc -l; grep -oE 'function cGe\\(e,n\\)\\{.{0,300}' dump; grep -oE 'function FPt\\(e,n\\)\\{.{0,420}' dump; python3 regex over the dump for 'function T5\\(.{0,420}' and for the call sites of FPt(.",
          "observed": "Exactly 1 occurrence of the array literal: `var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr);function cGe(e,n){if(e===void 0)return!1;let r=bu(e);return LPt.has(r)||T5(r,n).some((o)=>LPt.has(bu(o)))}function FPt(e,n){switch(e.hook_event_name){case\"PostToolUse\":case\"PostToolUseFailure\":return cGe(e.tool_name,n);case\"PostToolBatch\":return(e.tool_calls??[]).some((r)=>cGe(r.tool_name,n));default:return!1}}`. Alias expansion: `function T5(e,r){if(!r)return[];let t=[];for(let[n,s]of Object.entries(r))if(s===e)t.push(n);return t}` - a reverse lookup over an alias map, so any alias key whose value is one of the four passes. Name normaliser: `function bu(e){return Object.hasOwn(i,e)?i[e]:e}`. Observed consumer: `if(FPt(z,e.toolAliases()))return _(I,\"after_edit_held\"),{}` - the second argument is the live tool-alias map. csift code site src/model/mutation.rs:146-152 matches the claimed snippet verbatim.",
          "rule": "Exact array-literal byte match over the whole strings dump, counted with `grep -o ... | wc -l`; one occurrence expected. Predicate shape read verbatim from the bytes immediately following the literal.",
          "note": "Confirmed verbatim, including the alias-expansion clause. Two additions a rerunner should know: (1) the observed consumer of FPt is a remote device-hook gate that holds the callback after an edit (`after_edit_held`), which is what makes the four-element set load-bearing rather than decorative; (2) the bundle contains a SECOND, unrelated function also minified to `FPt` (an away-summary helper, `function FPt(e,n){if(!n.onMetadataChanged)return;...}`), so a bare `function FPt(` grep returns two hits - match on the `switch(e.hook_event_name)` body to get the right one."
        }
      ]
    },
    {
      "id": "FH-038",
      "area": "freshness-file-history",
      "behavior": "Claude Code carries a SECOND, narrower three-element tool list `[\"Edit\",\"Write\",\"NotebookEdit\"]` (`var Ufo`, membership via `function fot(e){return Ufo.includes(e)}`) alongside the four-element mutating set. It is NOT a second definition of \"mutating tool\": both of its call sites are permission-decision telemetry, gating whether a `tool_decision` metric gets an extra `language` dimension resolved from the tool call's path. No freshness, snapshot, or hook-payload signal is governed by it.",
      "depends": "The three-element list governs only a telemetry dimension, so it cannot make csift's four-element `FileOp` over-read any consumed signal. The claim's hazard is not reachable through this list; the real exposure to a fifth mutating tool remains the four-element `opr` set covered by FH-037.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "16-24",
          "snippet": "pub enum FileOp {\n    /// `Write` tool - writes a file whole (a create when the path was new).\n    Write,\n    /// `Edit` tool - a single in-place string replacement in an existing file.\n    Edit,\n    /// `NotebookEdit` tool - edits a Jupyter notebook cell (`notebook_path`).\n    NotebookEdit,\n    /// `MultiEdit` tool - multiple edits to one file in a single call.\n    MultiEdit,"
        }
      ],
      "instrument": "`strings -n 30 <the Claude Code 2.1.258 executable> | grep -oE '.{100}\\[\"Edit\",\"Write\",\"NotebookEdit\"\\].{60}'` -> `var Ufo=[\"Edit\",\"Write\",\"NotebookEdit\"];function fot(e){return Ufo.includes(e)}`. Counting rule: exact array literal, one occurrence, distinct from the four-element `opr`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (tool-registry probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -oE '.{110}\\[\"Edit\",\"Write\",\"NotebookEdit\"\\].{80}' dump; grep -o '\\[\"Edit\",\"Write\",\"NotebookEdit\"\\]' dump | wc -l; grep -oE 'function fot\\(e\\)\\{return Ufo.includes\\(e\\)\\}.{0,700}' dump; grep -o 'fot(' dump | wc -l; grep -oE '.{200}[^a-zA-Z]fot\\([a-zA-Z_$.]{1,20}\\).{0,160}' dump.",
          "observed": "Exactly 1 occurrence of the three-element literal: `var Ufo=[\"Edit\",\"Write\",\"NotebookEdit\"];function fot(e){return Ufo.includes(e)}`, distinct from the four-element `opr`. Its two membership call sites are both permission-decision telemetry: `if(fot(o.name))pot(o,d,I,j).then((z)=>Gkt()?.add(1,z))` and `if(fot(e.name))pot(e,Ie,xn,An).then((Cn)=>Gkt()?.add(1,Cn))`. The consumer is `async function pot(e,n,r,o){let d;if(e.getPath&&n){let f=e.inputSchema.safeParse(n);if(f.success){let _=e.getPath(f.data);if(_)d=await gDe(_)}}return{decision:r,source:o,tool_name:e.name,...d&&{language:d}}}` - it resolves the tool call's path and attaches a detected `language` to a metric counter. `grep -o 'fot(' | wc -l` = 6, of which 2 are these membership tests, 1 is the definition, and 3 belong to an unrelated same-named helper. csift code site src/model/mutation.rs:16-24 matches the claimed snippet verbatim.",
          "rule": "Exact array-literal byte match over the strings dump (one occurrence); then every `fot(` occurrence read in context and classified by its enclosing expression as definition / membership test / unrelated homonym.",
          "note": "The literal fact (a second, narrower array exists, one occurrence, distinct from the four-element one) survives refutation exactly as written. What needed correction is the framing and therefore the hazard: reading the call sites shows the three-element list is a telemetry subset, not a rival definition of the mutating-tool set, so the claim's `depends` (\"csift's four-element assumption over-reads it\") does not hold. A rerunner should classify each `fot(` occurrence in context - the bundle reuses both `fot` and `pot` for unrelated helpers (`function fot(e){return e?180000:60000}`), so symbol name alone is not evidence."
        }
      ]
    },
    {
      "id": "FH-039",
      "area": "freshness-file-history",
      "behavior": "Claude Code rewrites its own settings in-process on model, config, theme, permission and plugin changes with NO tool record, and the settings paths it knows about are broader than two: the binary carries `.claude/settings.json` (145 string-table occurrences), `.claude/settings.local.json` (88), `managed-settings.json` (36) and `.claude.json` (8).",
      "depends": "csift's settings-family detector matches exactly a basename of `settings.json` or `settings.local.json` whose immediate parent directory is `.claude`, so a version bump on a managed-settings file, on the per-project state file, or on an MCP/plugin config is reported as an ORDINARY external write and the replay's settings-mutation annotation is not applied.",
      "code": [
        {
          "path": "src/files/external.rs",
          "lines": "10-14",
          "snippet": "//! SCOPE LAW (operator-ruled): reported ONLY for the settings family - basename\n//! `settings.json` / `settings.local.json` under a `.claude` parent. The tracked set\n//! spans thousands of ordinary source paths and CC writes bookkeeping constantly;\n//! listing \"harness writes\" beyond settings would flood every timeline (measured\n//! corpus: 1701 tracked paths, 11 settings-family)."
        },
        {
          "path": "src/files/external.rs",
          "lines": "25-32",
          "snippet": "/// True for the settings family: `settings.json` / `settings.local.json` directly\n/// under a `.claude` directory (absolute or relative spelling alike).\npub(crate) fn is_settings_family(path: &str) -> bool {\n    let mut parts = path.rsplit(['/', '\\\\']);\n    let base = parts.next().unwrap_or_default();\n    let parent = parts.next().unwrap_or_default();\n    matches!(base, \"settings.json\" | \"settings.local.json\") && parent == \".claude\"\n}"
        }
      ],
      "instrument": "`strings -n 8 <the Claude Code 2.1.258 executable> | grep -oE '\\.claude/settings[a-z.]*json|managed-settings\\.json|\"\\.claude\\.json\"' | sort | uniq -c`. Counting rule: one per string-table occurrence; compare the resulting path set against `is_settings_family`'s two accepted basenames.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.4",
        "source": "measured 2026-09-02 (settings-family probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <the Claude Code 2.1.258 executable> | grep -oE '\\.claude/settings[a-z.]*json|managed-settings\\.json|\"\\.claude\\.json\"' | sort | uniq -c | sort -rn; then `csift files . --by timeline | rg 'external write'` in the csift repo; then python3 walking ~/.claude/projects, byte-prefiltering on b'file-history-snapshot', collecting every tracked path key from file-history-snapshot / file-history-delta records and classifying each basename by csift's is_settings_family rule.",
          "observed": "String-table census: 145 `.claude/settings.json`, 88 `.claude/settings.local.json`, 36 `managed-settings.json`, 8 `\".claude.json\"` - the claim's four numbers reproduce exactly. csift's predicate at src/files/external.rs:25-32 accepts only basenames `settings.json` / `settings.local.json` under a `.claude` parent, so it rejects `managed-settings.json` and `.claude.json` by construction. Silent-write clause observed live: `csift files . --by timeline` prints 3 `external write` rows, e.g. `L4973  2026-06-29 11:51:38 AEST(UTC+10)  turn 20  external write  ~/.claude/settings.json  (inferred: file-history v3->v4 with no tool record in L3896..L4973)`. File-history census over the whole corpus: 4065 file-history records, 1668 distinct tracked paths, 11 of them settings-family by csift's predicate; the six other tracked paths whose basename merely contains the word `settings` are ordinary source and note files that csift correctly rejects, and neither `managed-settings.json` nor `.claude.json` is file-history-tracked anywhere in this corpus. Both csift code sites (src/files/external.rs:10-14 and 25-32) match the claimed snippets verbatim.",
          "rule": "One count per string-table occurrence for the binary census (grep -o over the strings dump, then uniq -c). One count per distinct tracked path for the file-history census: a path key inside a file-history-snapshot / file-history-delta record whose value object carries `version`, `content` or `hash`. One count per printed timeline row for the external-write observation.",
          "note": "All four census numbers reproduce exactly, the csift predicate's two-basename scope is confirmed verbatim, and the \"no tool record\" clause is now instrument-backed rather than asserted (3 external-write rows on ~/.claude/settings.json in the csift repo's own scope, each naming a file-history version transition with no tool record in the interval). One boundary on the claim's `depends`: the miss it predicts for `managed-settings.json` and `.claude.json` is structurally real (csift's predicate rejects both) but unobserved here - neither path is file-history-tracked in this corpus, so nothing could have been mis-reported. Deciding whether the miss ever bites needs a machine that actually runs managed settings (MDM/policy-installed) or whose `.claude.json` is tracked."
        }
      ]
    },
    {
      "id": "FH-040",
      "area": "freshness-file-history",
      "behavior": "Token-cap truncation is reachable only on a whole-file read - the guard is `Ge=(f??1)<=1&&_===void 0&&v===void 0`, i.e. offset<=1 AND limit undefined AND PAGES undefined (`v` is the `pages` parameter, not a second limit) - and on that path `startLine` is forced to `Math.max(1,f)`. Because the tool entry defaults `offset:n=1`, `f` is always a number and the guard restricts it to <=1, so a truncated echo always reports the literal integer `startLine:1` (never null or NaN).",
      "depends": "csift's `is_full` requires `start_line == 1`, which a truncated echo always satisfies, so `startLine` can never be the guard that demotes a capped read; only `observed >= total` stands between a capped page and a full-snapshot anchor.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "181",
          "snippet": "    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Corpus: for every echo carrying `truncatedByTokenCap`, count `startLine != 1`; one observation per echo. Measured now over all of ~/.claude/projects: 0 of 174. Binary: the `let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f` construction, reachable via `python3 -c` seeking that literal in a `strings -a` dump.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 seeking `Ge=(f??1)<=1&&_===void 0&&v===void 0` in the raw 2.1.258 executable and printing 2600 bytes of preceding context, plus a regex sweep of the preceding 30000 bytes for the parameter destructuring; grep -oE 'startLine:Ue!==void 0\\?Math\\.max\\(1,f\\):f.{0,160}' over the strings dump; then python3 over ~/.claude/projects counting echoes with a `toolUseResult.file` object containing `truncatedByTokenCap` and, among them, those whose `startLine` is not 1.",
          "observed": "Guard verbatim: `De=me,Ne=_e,Fe=_,Ue,je=!1,Ge=(f??1)<=1&&_===void 0&&v===void 0;try{await Ejn(me,d,I,F.credentials,Ge)}catch(Ot){if(Ot instanceof jue&&Ge){` - truncation is reachable only when Ge holds. Destructuring: `async function vGo(e){let{file_path:n,fullFilePath:r,resolvedFilePath:o,ext:d,offset:f,limit:_,pages:v,maxSizeBytes:C,maxTokens:I,context:F,messageId:B,io:U}=e` - so f=offset, _=limit, v=PAGES, not a second limit. Tool entry: `async function TGo({file_path:e,offset:n=1,limit:r=void 0,pages:o},d,f)` - offset defaults to 1. Echo construction: `let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}}`. Corpus: 174 truncated echoes, 0 with startLine != 1. csift code site src/recover/carriers.rs:181 matches the claimed snippet verbatim.",
          "rule": "One observation per echo: a record whose `toolUseResult.file` is an object containing the key `truncatedByTokenCap`; the echo counts against the claim iff its `startLine` is anything other than the integer 1 (a null or NaN-serialised value would therefore have been counted).",
          "note": "The quoted guard is byte-exact and the conclusion is confirmed twice over, on the binary and on 174 corpus echoes. Two corrections: the parenthetical gloss \"offset<=1, no limit\" drops the third conjunct, which is the `pages` parameter (PDF page selection), and the guarantee is stronger than the claim states - the tool-entry default `offset:n=1` rules out the `Math.max(1,undefined)` NaN edge, so startLine is always exactly 1. The claim's `depends` on csift stands unchanged: with startLine pinned to 1, only `observed >= total` separates a capped page from a full-snapshot anchor - and FH-043's experiment shows that comparison also passes."
        }
      ]
    },
    {
      "id": "FH-041",
      "area": "freshness-file-history",
      "behavior": "The default file-read output cap is 25000 tokens (`var gYr=25000`), overridable per-process by `CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS`, and the default max file size is 262144 bytes (`var nhe=262144`); both defaults are also remotely overridable by a gate payload carrying `maxTokens`/`maxSizeBytes`.",
      "depends": "The cap is a moving target, so csift must never hard-code a token threshold to infer truncation - the only stable instrument is the `truncatedByTokenCap` key or the banner, both of which csift currently ignores in `recover`.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "179-181",
          "snippet": "    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "`strings -a <the Claude Code 2.1.258 executable> | grep -o 'var gYr=[0-9]*'` prints `var gYr=25000`; the same grep for `var nhe=[0-9]*` prints `var nhe=262144`. The env override appears as `let e=a.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS;if(e!==void 0&&e>0)return e` in the `hYr` function.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <the Claude Code 2.1.258 executable> > dump; grep -o 'var gYr=[0-9]*' dump; grep -o 'var nhe=[0-9]*' dump; grep -oE 'function hYr\\(\\)\\{.{0,300}' dump; grep -oE '.{160}maxSizeBytes.{0,160}' dump.",
          "observed": "`var gYr=25000` and `var nhe=262144`, one occurrence each. Env override verbatim: `function hYr(){let e=a.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS;if(e!==void 0&&e>0)return e;return}`. Gate payload verbatim: `function hX(){let e=Os();if(e.defaultFileReadingLimits!==void 0)return e.defaultFileReadingLimits;let n=P(\"tengu_amber_wren\",{}),r=typeof n?.maxSizeBytes===\"number\"&&Number.isFinite(n.maxSizeBytes)&&n.maxSizeBytes>0?n.maxSizeBytes:nhe,d=hYr()??(typeof n?.maxTokens===\"number\"&&Number.isFinite(n.maxTokens)&&n.maxT...` and the memoised result `e.defaultFileReadingLimits={maxSizeBytes:r,maxTokens:d,includeMaxSizeInPrompt:f,targetedRangeNudge:_}`. csift code site src/recover/carriers.rs:179-181 matches the claimed snippet verbatim.",
          "rule": "Exact `var <name>=<digits>` byte match over the strings dump, one occurrence each; override paths read verbatim from the bytes of the two functions that compute the effective limits.",
          "note": "Both constants and both override paths confirmed byte-exact. Three precision points a rerunner should carry: the env var overrides only the TOKEN cap (there is no env override for maxSizeBytes); the env wins over the gate (`d=hYr()??(gate maxTokens)`) while maxSizeBytes takes the gate value or falls back to `nhe`; and a per-call override also exists (`fileReadingLimits:{maxTokens:1/0,maxSizeBytes:268435456}` is passed on an inner call), which the claim does not mention but which reinforces its point that the cap is a moving target and no threshold should be hard-coded."
        }
      ]
    },
    {
      "id": "FH-042",
      "area": "freshness-file-history",
      "behavior": "Truncation has TWO branches: a line branch that keeps whole lines (`nt=gn.slice(0,yn).join('\\n')`) and a character branch for files whose first line alone exceeds the cap (`nt=me.slice(0,pn)`) that cuts MID-LINE, drops a trailing lone high surrogate, and then reports `numLines` as `newline-count + 1`, counting the truncated fragment as a whole line. BOTH branches carry up to six 0.7x shrink retries (`yn*0.7` on the line branch, `pn*0.7` on the character branch); the character branch is entered when the line branch's result still exceeds the cap or is blank (`if(Tn(nt)>I||nt.trim()===\"\")`).",
      "depends": "csift's `split_lines` treats every line of `content` as ground truth and writes it into the sparse buffer, so on the character branch the LAST spliced line is a partial line presented as a known line - a fabrication the anti-fabrication guards do not catch.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "177-178",
          "snippet": ") {\n    let lines: Vec<String> = split_lines(content);"
        }
      ],
      "instrument": "Seek the literal `De=nt,Ne=je?` in a raw byte read of the 2.1.258 executable and print the surrounding ~1400 bytes; the two branches, the six-retry loops and the `je` flag are one expression. Corpus evidence that the character branch fires: count records whose `attachment.type` is `read_truncation_notice` and whose text carries the banner `this file has very long lines and cannot be paginated by line` - measured 2026-09-02: 31, all at CC 2.1.211, plus 3 older `tool_result` string carriers (2.1.159, 2.1.177, 2.1.231).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 seeking the literal `De=nt,Ne=je?` in the raw 2.1.258 executable and printing 1400 bytes before and 1500 after; then python3 walking ~/.claude/projects, byte-prefiltering each line on the exact banner literal `this file has very long lines and cannot be paginated by line`, parsing the survivors and bucketing them by record `type`, by `version`, and by which carrier field holds the banner.",
          "observed": "Both branches in one expression: line branch `let gn=me.split('\\n'),cn=Math.max(0.5,me.length/Math.max(1,Ot.tokenCount)),Tn=(pn)=>pn.length/cn,yn=Math.max(1,Math.min(gn.length,Math.floor(gn.length*I/Math.max(1,Ot.tokenCount)*0.85))),nt=gn.slice(0,yn).join('\\n');for(let pn=0;pn<6;pn++){if(Tn(nt)<=I||yn<=1)break;yn=Math.max(1,Math.floor(yn*0.7)),nt=gn.slice(0,yn).join('\\n')}`; char branch, entered on `if(Tn(nt)>I||nt.trim()===\"\")`: `let pn=Math.max(1,Math.floor(I*cn*0.85));for(let fn=0;fn<6;fn++){if(nt=me.slice(0,pn),Tn(nt)<=I)break;pn=Math.max(1,Math.floor(pn*0.7))}let hn=nt.charCodeAt(nt.length-1);if(hn>=55296&&hn<=56319)nt=nt.slice(0,-1);je=!0`; then `De=nt,Ne=je?_n(nt,'\\n')+1:yn`. Corpus with the exact banner literal: 52 records total - 31 records whose `attachment.type` is `read_truncation_notice` (all at CC 2.1.211), 3 `tool_result` string carriers (one each at 2.1.159, 2.1.177, 2.1.231), and 18 records at CC 2.1.258 that are this verification session's own Bash tool output quoting the binary. csift code site src/recover/carriers.rs:177-178 matches the claimed snippet verbatim.",
          "rule": "Corrected counting rule, contamination-proof: count one observation per record whose `attachment.type` is exactly `read_truncation_notice` AND whose serialized record contains the banner literal `this file has very long lines and cannot be paginated by line`. A bare needle count over all records is NOT rerunnable - it also catches any session that merely quoted the banner, including the verifying session itself.",
          "note": "The mechanism is byte-exact as claimed, including the mid-line cut, the lone-high-surrogate drop and the `numLines = newline-count + 1` miscount. Three corrections. (1) The six 0.7x shrink retries are on BOTH branches, not only the line branch. (2) The stated count of 31 is right but its stated counting rule is not rerunnable: a bare needle count now returns 52 because 18 of those records are the verifying session's own grep output quoting the binary - self-contamination that will recur for anyone who greps the binary in a Claude Code session. Restricting to `read_truncation_notice` attachments reproduces 31 exactly and is contamination-proof. (3) A detail the claim does not state, measured in the FH-043 experiment: the banner is NOT inside `toolUseResult.file.content` - it rides the notice attachment and the `<system-reminder>` prefix on the rendered tool_result, which is why csift's carrier reader never sees it."
        }
      ]
    },
    {
      "id": "FH-043",
      "area": "freshness-file-history",
      "behavior": "Because the character branch reports `numLines` as the line count of the partial page while `totalLines` stays the whole file's count, a one-very-long-line file over the token cap yields `startLine:1, numLines:1, totalLines:1` with `truncatedByTokenCap:true` - numerically indistinguishable from a complete read. Directly reproduced on Claude Code 2.1.258 with a 66000-character single-line file: the echo carried exactly that quartet and only 32742 of 66000 characters.",
      "depends": "csift's `is_full = start_line == 1 && observed >= total && total > 0` promotes that capped page to a `FullSnapshot` anchor. Confirmed end-to-end on csift 0.10.0: `csift recover --file` exits 0, reports `1 lines, complete`, and writes 32742 of the source's 66000 characters - truncated bytes handed back as a complete restorable file, with no disclosure. The fix is to demote whenever `truncatedByTokenCap` is present.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-182",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;\n    if is_full {"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "17-22",
          "snippet": "    /// Full ground-truth content (an anchor): a Write result, a full Read\n    /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n    FullSnapshot {\n        content: String,\n        total_lines: usize,\n        source: SnapSource,"
        }
      ],
      "instrument": "Write a single-line file with no newlines, sized over the read token cap and under the byte cap (66000 hex characters is comfortably both); have Claude Code Read it once in an isolated run; read the resulting echo's `toolUseResult.file` fields from that run's transcript; then run `csift recover <transcript> --file <file> --out <out>` and byte-compare. Measured 2026-09-02 on CC 2.1.258 + csift 0.10.0: echo `startLine:1 numLines:1 totalLines:1 truncatedByTokenCap:true` with 32742 of 66000 characters; recover reported `complete` and returned a strict prefix.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Built a 66000-character single-line file (0 newlines, 66000 bytes, under the 262144-byte size cap and over the 25000-token read cap) in a temp dir; ran `claude -p \"Use the Read tool exactly once on <that file> and then reply with only the word DONE. Do not print any file content.\" --allowedTools Read` on Claude Code 2.1.258; located that run's top-level transcript under ~/.claude/projects by mtime and read the echo's `toolUseResult.file` fields with python3; then ran `csift recover <that transcript> --file <that file> --out <out>` on csift 0.10.0 and byte-compared the recovered output against the source.",
          "observed": "Echo on disk, CC version 2.1.258: `startLine: 1`, `numLines: 1`, `totalLines: 1`, `truncatedByTokenCap: True`, `content` = 32742 characters containing 0 newlines, and that content is a strict prefix of the 66000-character source. csift end-to-end: `csift recover ... --file ... --out ...` exited 0 and reported on stderr `(recovered <file> -> <out>, 1 lines, complete; no bash mutation of this file and no opaque mutating-class command detected in the window)`; the recovered file is 32742 characters against the source's 66000 and is a STRICT PREFIX of it. Corpus baseline before the experiment: 174 truncated echoes, 0 with `startLine==1 && numLines>=totalLines`. csift code sites src/recover/carriers.rs:178-182 and src/recover/types.rs:17-22 match the claimed snippets verbatim.",
          "rule": "One observation per echo: a record whose `toolUseResult.file` object contains `truncatedByTokenCap` AND has `startLine == 1` AND `numLines >= totalLines`. The end-to-end check is a byte comparison: recovery is a false 'complete' iff `csift recover` exits 0, its report says `complete`, and the recovered bytes are a strict prefix of (not equal to) the source bytes.",
          "note": "The claim's mechanism is exactly right and its hedge is now obsolete. The corpus baseline still reproduces (0 of 174), but a purpose-built read exercised the hazard on the current binary and the current csift, and the end-to-end consequence the claim only predicted - `recover` reporting `complete` while returning a strict prefix - actually happened. This moves the finding from a structural argument to a reproduced defect; the counting rule above is the rerun recipe."
        }
      ]
    },
    {
      "id": "FH-044",
      "area": "freshness-file-history",
      "behavior": "Claude Code's own transcript-replay treats a read as partial if ANY of three carriers says so: `toolUseResult.file.truncatedByTokenCap===true`, a `read_truncation_notice` attachment naming that `tool_use_id`, or the rendered `tool_result` text starting with `<system-reminder>` immediately followed by the `[Truncated: PARTIAL view — ` banner prefix (the separator is U+2014, not a hyphen).",
      "depends": "csift reads none of the three in `recover`, so a resumed session and csift disagree about which reads were complete; the three-carrier OR is the ready-made specification for a csift truncation gate.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "176-183",
          "snippet": "    events: &mut Vec<FileEvent>,\n) {\n    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;\n    if is_full {\n        events.push(FileEvent {"
        }
      ],
      "instrument": "Seek the literal `let W=C.toolUseResult?.file?.truncatedByTokenCap===!0||v.has(I.tool_use_id)||I.content.startsWith(` in a `strings -a` dump of the 2.1.258 binary; the surrounding function rebuilds `readFileState` from persisted messages and sets `isPartialView` from that disjunction.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 scanning the raw 2.1.258 executable for every occurrence of `truncatedByTokenCap===!0`, printing 320 bytes before and 420 after each, plus 2100 bytes before the first occurrence to recover the construction of the notice set; then python3 seeking `lfe=\"[Truncated` and `[Truncated: PARTIAL view` in the raw bytes.",
          "observed": "4 occurrences of `truncatedByTokenCap===!0` in the binary. The replay one is verbatim: `let W=C.toolUseResult?.file?.truncatedByTokenCap===!0||v.has(I.tool_use_id)||I.content.startsWith(\"<system-reminder>\"+lfe),fe=I.content.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/g,\"\")...` and the setter `o.set(F.filePath,{content:fe,timestamp:me,offset:_e?1:F.offset??1,limit:_e?void 0:F.limit,...W&&{isPartialView:!0}})`. The notice set is built in the same function: `function zgt(e,n,r=XEo){let o=qw(r),d=new Map,f=new Map,_=new Map,v=new Set;for(let C of e){if(C.type===\"attachment\"&&C.attachment.type===\"read_truncation_notice\"){v.add(C.attachment.toolUseID);continue}...` - the function logs under the name `extractReadFilesFromMessages`, i.e. it rebuilds readFileState from persisted messages. Banner prefix: `var lfe=\"[Truncated: PARTIAL view \\u2014 \"` - the separator is U+2014, confirmed. csift code site src/recover/carriers.rs:176-183 matches the claimed snippet verbatim.",
          "rule": "One occurrence per byte match of `truncatedByTokenCap===!0` over the whole executable; the replay disjunction is the occurrence whose enclosing function also constructs a Set from `read_truncation_notice` attachments and writes `isPartialView`.",
          "note": "All three carriers of the OR confirmed byte-exact in one expression, together with the construction of the middle one (a Set of `attachment.toolUseID` from `read_truncation_notice` attachments) and the U+2014 separator in the banner prefix constant. The claim's characterisation of the function is also right: it rebuilds `readFileState` from persisted messages and stamps `isPartialView` from the disjunction. Worth recording for a rerunner: the other three occurrences of the same literal are unrelated consumers - one refuses to build an `edited_text_file` freshness attachment from a truncated re-read, and one falls back on an at-mention path - so match on the enclosing function, not the literal alone."
        }
      ]
    },
    {
      "id": "FH-045",
      "area": "freshness-file-history",
      "behavior": "The `truncatedByTokenCap` key predates the `read_truncation_notice` attachment by roughly 56 patch releases: the key is observed from Claude Code 2.1.150 while the earliest notice attachment in the same corpus is 2.1.206.",
      "depends": "A csift truncation gate should key on the programmatic field first and treat the attachment and the `<system-reminder>` banner as corroboration, because only the field covers older transcripts.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "176-181",
          "snippet": "    events: &mut Vec<FileEvent>,\n) {\n    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Corpus: bucket by the record's `version` field, counting one observation per echo carrying `truncatedByTokenCap` and one per record whose `attachment.type` is `read_truncation_notice`. Measured now: the key appears at 2.1.150 (2), 2.1.156 (2), 2.1.159 (10) and onward; the attachment first at 2.1.206 (9).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 walking ~/.claude/projects, byte-prefiltering each jsonl line on b'truncatedByTokenCap' and b'read_truncation_notice', parsing the survivors, and bucketing by the record's top-level `version` field: one bucket entry per echo whose `toolUseResult.file` object contains `truncatedByTokenCap`, one per record whose `attachment.type` is `read_truncation_notice`.",
          "observed": "174 truncated echoes across 25 Claude Code versions, earliest buckets 2.1.150 (2), 2.1.156 (2), 2.1.159 (10), 2.1.170 (3), 2.1.177 (19) - reproducing the claim's first three exactly. 183 notice-attachment records across 17 versions, earliest bucket 2.1.206 (9) - reproducing the claim's first bucket exactly. 2.1.206 minus 2.1.150 is 56 patch releases. csift code site src/recover/carriers.rs:176-181 matches the claimed snippet verbatim.",
          "rule": "One observation per echo (a record whose `toolUseResult.file` object contains the key `truncatedByTokenCap`) and one per notice record (a record whose `attachment.type` equals `read_truncation_notice`), each bucketed by the record's own top-level `version` string; the earliest non-empty bucket is the first-seen version.",
          "note": "Both first-seen versions and all four quoted bucket counts reproduce exactly, and the 56-release gap is arithmetic on them. One honest boundary the claim does not state: these are FLOORS bounded by the Claude Code versions this machine actually ran, not true first-release versions - the corpus jumps 2.1.150 -> 2.1.156 -> 2.1.159 with nothing between, so either field could have shipped in an unrun earlier version. Deciding the true introduction release would need release-artifact archaeology across every 2.1.x build, which no instrument on this machine can do. The claim's operational conclusion is unaffected: on any corpus, the programmatic field covers strictly older transcripts than the attachment."
        }
      ]
    },
    {
      "id": "FH-046",
      "area": "freshness-file-history",
      "behavior": "The external-edit scanner skips a file entirely when its remembered read state carries an `offset` or a `limit`, and skips it again when a fresh read comes back with `truncatedByTokenCap===true`; a token-cap-truncated read also writes `limit` and `isPartialView:true` into that state.",
      "depends": "So an `edited_text_file` boundary can only ever exist for a file that was read WHOLE and uncapped - csift's hard external-edit boundary is structurally absent for every windowed or token-capped file, which `recover`'s opaque accounting must not read as evidence of no change.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "216-222",
          "snippet": "    let atype = att.get(\"type\").and_then(serde_json::Value::as_str);\n\n    // (7a) edited_text_file → an external edit (hard boundary).\n    if atype == Some(\"edited_text_file\") {\n        let path = att\n            .get(\"filename\")\n            .or_else(|| att.get(\"filePath\"))"
        }
      ],
      "instrument": "Seek `if(v.offset!==void 0||v.limit!==void 0)return null` and `if(U.data.file.truncatedByTokenCap===!0)return null` in a `strings -a` dump of the 2.1.258 binary - both guards live in the same scanner function, a few hundred bytes apart. The state write is the `j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe,...Ue!==void 0&&{isPartialView:!0}})` expression on the Read path.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings.txt && rg -o -a '.{700}set!==void 0\\|\\|v\\.limit!==void 0\\)return null.{700}' cc258.strings.txt && rg -o -a '.{420}isPartialView:!0.{100}' cc258.strings.txt && python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); i=b.find(b'j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe'); print(repr(b[i-2600:i+200]))\" && sed -n '205,235p' src/recover/carriers.rs",
          "observed": "Both guards are verbatim in one function whose only return value is the external-edit attachment: 'async function QSr(e){...let v=e.readFileState.get(_);if(!v)return null;if(v.offset!==void 0||v.limit!==void 0)return null;...let U=await im.call(F,e);if(U.data.type===\"text\"){if(U.data.file.truncatedByTokenCap===!0)return null;if(z$(v,U.data.file.content))return null;let j=EJt(v.content,U.data.file.content);if(j===\"\")return null;return{type:\"edited_text_file\",filename:C,snippet:j}}'. The Read state write is verbatim: 'j.set(r,{content:De,timestamp:Math.floor(Pe),offset:f,limit:Fe,...Ue!==void 0&&{isPartialView:!0}});', and the 2600 preceding bytes show the token-cap branch assigning 'Fe=Ne,Ue=' the truncation notice (either 'showing lines 1-${Ne} of ${ke} total (${Ot.tokenCount} tokens, cap ${I})' or 'showing the first ${nt.length} of ${me.length} characters'), so a capped read writes both limit and isPartialView:!0. The Read entry function binds them as parameters: 'async function vGo(e){let{file_path:n,fullFilePath:r,resolvedFilePath:o,ext:d,offset:f,limit:_,pages:v,...}=e' - f IS the caller's offset argument. csift site: src/recover/carriers.rs:216-222 matches the ledger snippet verbatim (216 'let atype = att.get(\"type\").and_then(serde_json::Value::as_str);', 219 'if atype == Some(\"edited_text_file\") {').",
          "rule": "One matched byte string per guard in the shipped executable, both inside the single function that returns {type:\"edited_text_file\"}; a matched string is the observation. Code site checked by exact line number plus byte-identical snippet.",
          "note": "Both guards and the state write survive intact at 2.1.258. Supporting corpus fact: 1339 edited_text_file attachment records exist across the transcripts scanned, so the scanner does fire in practice - consistent with a whole-file read leaving offset/limit undefined in the remembered state."
        }
      ]
    },
    {
      "id": "FH-047",
      "area": "freshness-file-history",
      "behavior": "The unchanged-file dedup has TWO branches, both gated by one lookup that returns undefined - and so disables both - under the tengu_read_dedup_killswitch gate, on a remote call, or when dedupUnchangedReads===false. Branch A (source:\"seeded\") fires when the remembered entry is startup-seeded, is NOT a partial view, and the NEW call asks for the whole file (offset===1 and limit===undefined); it compares the file's timestamp for exact equality with the remembered one. Branch B fires when the remembered entry is NOT a partial view AND itself carries a defined offset (i.e. it came from a windowed Read, not from an edit/write-seeded entry), the new call's offset and limit match the remembered ones exactly, and the file's timestamp is exactly equal to the remembered one. Both branches additionally require a pending-handover guard to be false. Because a whole-file Read stores offset===undefined, branch B never dedups a plain whole-file re-read.",
      "depends": "The `!isPartialView` precondition is why a token-cap-truncated read is always re-read in full later, which is the only reason csift's contentless `file_unchanged` handling has not yet produced a wrong recovery.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "85-86",
          "snippet": "        let norm_total = self.normalize_total(total_lines);\n        self.seen_total_lines = Some(norm_total.max(self.seen_total_lines.unwrap_or(0)));"
        }
      ],
      "instrument": "Seek `type:\"file_unchanged\"` in a `strings -a` dump of the 2.1.258 binary, skipping the zod `describe(` occurrence, and print the preceding 1100 bytes: the two dedup branches, the `!fe.isPartialView` guard, the `fe.offset===n&&fe.limit===r` equality and the `tengu_read_dedup_killswitch` gate are all in that span.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); [print(repr(b[b.find(s):b.find(s)+330])) for s in (b'let fe=P(\\\"tengu_read_dedup_killswitch\\\"', b'if(fe&&fe.seededFromContext', b'if(fe&&!fe.isPartialView&&fe.offset!==void 0)')]\" && rg -n 'let norm_total = self.normalize_total' src/recover/buffer.rs",
          "observed": "Three verbatim spans. Gate: 'let fe=P(\"tengu_read_dedup_killswitch\",!1)||d.remoteCall!==void 0||d.dedupUnchangedReads===!1?void 0:_.get(j);'. Branch A: 'if(fe&&fe.seededFromContext&&!fe.isPartialView&&n===1&&r===void 0)try{let Ie=await RSr(j,d.storageV5);if(Ie===fe.timestamp&&!ke(Ie)){...return ...{data:{type:\"file_unchanged\",file:{filePath:j},source:\"seeded\"}}}}catch{}'. Branch B: 'if(fe&&!fe.isPartialView&&fe.offset!==void 0){if(fe.offset===n&&fe.limit===r)try{let Pe=await Bx(j);if(Pe===fe.timestamp&&!ke(Pe)){...return{data:{type:\"file_unchanged\",file:{filePath:e}}}}}catch{}}'. The enclosing function binds n=offset and r=limit ('let we={file_path:e,fullFilePath:j,ext:U,offset:n,limit:r,pages:o,...}' immediately after branch B). The zod result schema confirms the payload carries no content: 'c({type:x(\"file_unchanged\"),file:c({filePath:i().describe(\"The path to the file\")}),source:x(\"seeded\").optional()...})'. csift site: src/recover/buffer.rs:85-86 matches the ledger snippet verbatim.",
          "rule": "Every occurrence of the literal 'file_unchanged' in the executable was enumerated (7 total: 1 zod schema, 1 tool_result mapper, 2 dedup returns, 3 consumers); the two returns inside the Read entry function are the dedup, and each branch's full guard chain was read from the raw bytes.",
          "note": "The claim's single-branch summary is right about branch B but omits the seeded branch, the extra 'prior entry has a defined offset' precondition, the pending-handover guard, and that the mtime test is exact equality rather than 'unchanged'. The csift-side consequence (the contentless payload) is unaffected."
        }
      ]
    },
    {
      "id": "FH-048",
      "area": "freshness-file-history",
      "behavior": "A contentless `file` echo drives csift's push_read_event to observed = 0 and total = 1, producing a PartialRead that reports the file as one line long. THREE persisted Read result variants carry a file.filePath with no content string and so take this path: file_unchanged (120 records), parts - the PDF page-extraction result (26 records), and pdf (4 records).",
      "depends": "`SparseBuffer::splice` raises `seen_total_lines` with a `.max()` against the existing value, so the bogus total of 1 cannot shrink a real total learned from an earlier read - that `.max()` is the only thing standing between a `file_unchanged` echo and a wrong `recover --coverage` denominator.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "10-86",
          "snippet": "        }\n        let norm_total = self.normalize_total(total_lines);\n        self.seen_total_lines = Some(norm_total.max(self.seen_total_lines.unwrap_or(0)));"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-181",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Read the csift unit test `src/recover/tests/coverage.rs` for the coverage arithmetic, then re-derive by hand: with `content` empty, `split_lines` returns an empty vector (pinned by `split_lines_drops_only_trailing_newline` in `src/recover/tests/render.rs`), so `observed=0` and `total = 0.max(1+0) = 1`, and `is_full` is false because `0 >= 1` is false.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/recover/buffer.rs, the seen_total_lines max() guard"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift --claude-home ./fx2 recover --file /tmp/demo/x.txt --coverage   # fixture fx2 = one synthetic project dir holding one transcript: a genuine user line, a Read tool_use, and a user tool_result whose toolUseResult is {\"file\":{\"filePath\":\"/tmp/demo/x.txt\"}} (the contentless echo) ;; csift --claude-home ./fx recover --file /tmp/demo/x.txt --coverage   # fixture fx = the same, but with a prior Read tool_result whose toolUseResult is {\"file\":{\"filePath\":\"/tmp/demo/x.txt\",\"content\":\"alpha\\nbeta\\ngamma\\n\",\"startLine\":1,\"numLines\":3,\"totalLines\":3}} ;; a python3 walk of ~/.claude/projects tallying records whose toolUseResult.file has a filePath, keyed by (toolUseResult.type, whether file.content is a string)",
          "observed": "Contentless echo alone: 'recoverable: 0/1 lines (0%)  fragments: 1' / 'covered line ranges: (none)' / 'events: 1 read (0 full, 1 windowed)' - a PartialRead with a total of 1 and nothing observed. Full 3-line read then the same echo: 'recoverable: 3/3 lines (100%)  fragments: 1' / 'covered line ranges: [1..3]' / 'events: 2 read (1 full, 1 windowed)' - the max() guard held the real total. Corpus: records carrying toolUseResult.file.filePath with NO string content = 120 of type file_unchanged, 26 of type parts, 4 of type pdf; 8608 of type text DO carry a content string. Schemas confirm the shapes: parts persists file:{filePath, originalSize, count, outputDir}, pdf persists file:{filePath, base64, originalSize}. csift sites: src/recover/buffer.rs:84-86 and src/recover/carriers.rs:178-181 match the ledger snippets verbatim.",
          "rule": "One count per transcript RECORD whose toolUseResult is an object with a file object containing filePath, bucketed by toolUseResult.type and by whether file.content is a string. Fixture verdicts are read off csift's own --coverage line: 'recoverable: <known>/<total> lines'.",
          "note": "Verified end to end by running csift, not by reading it: the echo-only fixture reports 0/1 lines, and the same echo after a real 3-line read still reports 3/3, which is the seen_total_lines max() guard doing exactly what the claim says. Only the variant enumeration needed widening - `pdf` was missing."
        }
      ]
    },
    {
      "id": "FH-049",
      "area": "freshness-file-history",
      "behavior": "The gutter separator is a TAB by default and becomes a COLON only when the tab-aware mode is on AND the whole CONTENT blob being rendered starts with a tab or contains a newline immediately followed by a tab (`let i=n&&(e.startsWith(\"\\t\")||e.includes(\"\\n\\t\"))?\":\":\"\\t\"`) - i.e. the separator is chosen ONCE per blob, when any line of it is tab-indented, not per line. The tab-aware mode is itself behind a gate that defaults to false (`function iz(){return kU(\"tengu_tab_read_sep\",!1)}`). Claude Code's own stripper accordingly accepts three separators: `function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}`.",
      "depends": "csift's `strip_gutter` accepts only TAB and U+2192 and `continue`s on anything else, so a colon-guttered `edited_text_file` snippet line is silently dropped from the external-edit snippet - no fabrication, but a lost line with no disclosure.",
      "code": [
        {
          "path": "src/recover/diff.rs",
          "lines": "199-206",
          "snippet": "        let rest = &trimmed[digits.len()..];\n        let text = if let Some(t) = rest.strip_prefix('\\t') {\n            t\n        } else if let Some(t) = rest.strip_prefix('\\u{2192}') {\n            t\n        } else {\n            continue;\n        };"
        },
        {
          "path": "src/recover/diff.rs",
          "lines": "185-189",
          "snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number).\npub(crate) fn strip_gutter(snippet: &str) -> Vec<(usize, String)> {"
        }
      ],
      "instrument": "Read the condition from the RAW executable bytes, not from a `strings` dump: the template literal holds a real newline followed by a real TAB, and `strings` splits the line at the newline and drops the one-byte tab remainder, which makes the condition read as the much broader `e.includes(\"\\n\")`. Use python3 to slice around `function j8t({content:e,startLine:r`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (read-window fidelity probe)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"b=open('<the 2.1.258 executable>','rb').read(); i=b.find(b'function j8t({content:e,startLine:r'); print(repr(b[i:i+330]))\" ;; python3 -c \"... b.find(b'function EJt(e,n){let r=rne(\\\"file.txt\\\"') ...\" ;; rg -o -a 'function iz\\(\\).{0,60}' cc258.strings.txt ;; a python3 walk of ~/.claude/projects collecting, for every attachment record of type edited_text_file, the character following the leading digits of each snippet line",
          "observed": "Raw bytes: 'function j8t({content:e,startLine:r,tabAwareSeparator:n=!1}){if(!e)return\"\";let i=n&&(e.startsWith(\"\\t\")||e.includes(`\\n\\t`))?\":\":\"\\t\",s=[],o=r,c=0,f=e.indexOf(`\\n`);while(f!==-1)s.push(W8t(e.slice(c,f),o++,i)),c=f+1,f=e.indexOf(`\\n`,c);return s.push(W8t(e.slice(c),o,i)),s.join(`\\n`)}function W8t(e,r,n){let i=e.endsWith(\"\\r\")?e.slice(0,-1):e;return`${r}${n}${i}`}function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}'. The mode is a gate: 'function iz(){return kU(\"tengu_tab_read_sep\",!1)}'. The external-edit snippet builder routes through the same renderer: 'function EJt(e,n){...let o=iz(),d=r.hunks.map((B)=>({startLine:B.oldStart,content:...,tabAwareSeparator:o})).map(j8t).join(`\\n...\\n`);...}'. Corpus: 1339 edited_text_file attachment records; 85 empty snippets; 1254 non-empty snippets whose gutter separators form the single set {TAB}; 0 snippets containing a colon or U+2192 gutter; 151912 gutter lines observed, all TAB.",
          "rule": "Separator = the single character following the leading digits of a snippet line matched by ^\\s*\\d+(.). One observation per SNIPPET, recorded as the SET of separators seen across that snippet's gutter lines (per-line totals reported separately). Binary claims are matched byte strings in the shipped executable.",
          "note": "The corpus half reproduces exactly (1254 TAB-only snippets, 85 empty, 0 colon, 0 arrow). The binary half needed correcting twice: the condition is newline+TAB (any tab-indented line), not any newline, and the choice is per content blob rather than per line. Both csift sites are verbatim at src/recover/diff.rs:185-189 and 199-206, and the exposure the claim describes is real but currently unreachable in this corpus, since the colon separator needs a gate that is off by default."
        }
      ]
    },
    {
      "id": "FH-050",
      "area": "freshness-file-history",
      "behavior": "Claude Code strips a Write result of type update through stripForStorage, which rewrites the stored object to {...e, content:\"\", originalFile:null} unless one of TWO early returns fires: the content is already empty with an empty-or-absent originalFile, or the structuredPatch is an empty array with a null originalFile (the second guard is newer than 2.1.229). The strip is not applied at persist time to every result: it runs as a sweep over the message array that skips the most recent 200 messages (the interactive call site uses that default; the headless flush path passes a window of 0, stripping every eligible message). An update-type Write carrier can therefore land in the transcript with an EMPTY content string and no original content at all.",
      "depends": "csift's carrier extractor treats any toolUseResult with a filePath and a content string but no oldString/newString as a full-snapshot anchor and calls buf.reset_to_full(content, ...). A stripped update carrier therefore resets the sparse reconstruction buffer to a zero-line file: reset_to_full clears the known-line map outright, so the prior content does not survive as explicit `??? lines A..B unknown` gaps - the total collapses too, and coverage reports 0/0 lines while --salvage reports no content seen at all.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "149-151",
          "snippet": "    // A Write result: full-content anchor.\n    if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n        let total = line_count(content);"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "89",
          "snippet": "                buf.reset_to_full(content, *total_lines, e.line_no);"
        }
      ],
      "instrument": "Measured incidence: 19 of 291 update carriers have content == '', all at versions 2.1.208 (16 of 110) and 2.1.211 (3 of 4); 0 of the 102 update carriers at versions after 2.1.211 are empty, including the one at 2.1.258. 1 of 2598 create carriers is empty (at 2.1.217, a genuinely empty file).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02 (carrier-shape probe)"
      },
      "first_seen_claude_code": "2.1.208",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import re;b=open('<the 2.1.258 executable>','rb').read();print(re.search(rb'stripForStorage\\(e\\)\\{if\\(typeof e!==\\\"object\\\"\\|\\|e===null\\)return e;if\\(e\\.type!==\\\"update\\\"\\)return e;[^}]{0,240}',b).group(0))\" ;; python3 -c \"... find(b'if(!F?.stripForStorage)continue') and every occurrence of the sweep function name ...\" ;; a python3 walk of ~/.claude/projects counting records whose toolUseResult is an object of type create/update, bucketed by whether content == '' and keyed by the record's version ;; csift --claude-home ./fx3 recover --file /tmp/demo/x.txt --coverage and --salvage on a fixture whose Write carrier is {\"type\":\"update\",\"filePath\":\"/tmp/demo/x.txt\",\"content\":\"\",\"originalFile\":null,\"structuredPatch\":[]} after a full 3-line read",
          "observed": "Verbatim at 2.1.258: 'stripForStorage(e){if(typeof e!==\"object\"||e===null)return e;if(e.type!==\"update\")return e;if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e;if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e;return{...e,content:\"\",originalFile:null}}' - TWO early returns, and the same function at 2.1.229 has only the first, so the structuredPatch guard is newer than 2.1.229. The caller is a windowed sweep: 'function $_t(e,n,r=200,o=!1){let d=e.length-r;if(d<=0)return e;...if(v>=d||C.type!==\"user\"||C.isVirtual||C.toolUseResult==null||!Array.isArray(C.message.content))continue;...let B=(()=>{try{return F.stripForStorage(C.toolUseResult,o)}catch(j){return h(j),C.toolUseResult}})();...}' with two call sites: '$_t(Ko,Go.options.tools)' at query end (default window 200) and '$_t(o,je,0,!0)' on the headless flush path (window 0 = strip everything). Corpus: 291 update carrier records (272 non-empty, 19 empty) and 2598 create carrier records (2597 non-empty, 1 empty); every empty update carrier is at version 2.1.208 (16 of 110) or 2.1.211 (3 of 4); across versions after 2.1.211 there are 102 update carriers and 0 empty ones, including 1 at 2.1.258. Fixture: the stripped update carrier after a full 3-line read yields 'recoverable: 0/0 lines (0%)' / 'covered line ranges: (none)' / 'events: 1 read (1 full, 0 windowed) - 1 write', and --salvage prints '(no content seen for this file in range)'. csift sites: src/recover/carriers.rs:149-151 and src/recover/replay.rs:89 match the ledger snippets verbatim.",
          "rule": "One count per carrier RECORD: a transcript record whose toolUseResult is an object with type create or update; bucketed by content == '' and keyed by the record's own version field. Binary claims are matched byte strings in the shipped executable; the fixture verdict is read off csift's own --coverage line.",
          "note": "The mechanism is present and byte-identical in 2.1.258, so this is not drift, but two things needed fixing. The strip is windowed - only messages older than the most recent 200 are swept at query end - which is why an append-only transcript sees it so rarely, and the corpus shows zero empty update carriers at any version after 2.1.211. And the csift consequence is harsher than the claim states: the prior lines are erased rather than turned into disclosed gaps, so the recovery reports a zero-length file rather than a holey one."
        }
      ]
    },
    {
      "id": "IMG-001",
      "area": "images",
      "behavior": "A pasted or attached image (and a tool-result screenshot) rides INLINE on the record as a `{type:\"image\", source:{type:\"base64\", media_type:\"image/png\", data:\"<base64>\"}}` block - the bytes live in the jsonl line itself and nothing is externalised to a sidecar file (verified against real `~/.claude/projects` data on 2026-06-16). One record commonly carries several such blocks.",
      "depends": "csift's `image` decodes the inline bytes straight back to files and addresses each occurrence by the stable locator `L<line>i<n>` (the 1-based jsonl line of the carrying record plus the 1-based ordinal of the image within that record), which is stable because the transcript is append-only; an externalised-pointer assumption would make `image --out` produce nothing.",
      "code": [
        {
          "path": "src/image.rs",
          "lines": "3-6",
          "snippet": "//! A user-pasted/attached image (and a tool-result screenshot) rides INLINE on a record as\n//! an `{type:\"image\", source:{type:\"base64\", media_type:\"image/png\", data:\"<base64>\"}}`\n//! block - verified against real `~/.claude/projects` data (2026-06-16). The bytes live in\n//! the jsonl, so `image` decodes them straight back to files; nothing is externalised."
        },
        {
          "path": "src/image.rs",
          "lines": "8-11",
          "snippet": "//! Stable image id = `L<line>i<n>`: the 1-based JSONL line of the carrying record plus the\n//! 1-based ordinal of the image among that record's image blocks. It is stable because the\n//! transcript is append-only, and it is consistent with the `Lnnnnn` line references used\n//! across `recover` / `turns` / `search` (so an id surfaced there feeds straight back here)."
        }
      ],
      "instrument": "`csift image @<session>` lists every image with its media type and estimated bytes; cross-check against `rg -c '\"type\":\"base64\"' <transcript>`. Counting rule: one row per image block, direct blocks and tool_result-nested elements alike.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.3.0",
        "source": "SPEC.md section 6.9; src/image.rs:3-6 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift show ~/.claude/projects/<enc>/<session>.jsonl --line 4351 --raw | jq -c '{type, role: .message.role, blocks: [.message.content[]? | {t: .type, src: (.source.type // null), mt: (.source.media_type // null), dlen: (.source.data|if .==null then null else length end)}]}'   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id L42693i1,L42103i1 --out out   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,80}type:\"base64\",media_type.{0,120}'",
          "observed": "One `type:\"user\"` record carried blocks [text, image x6]; every image block was `source.type == \"base64\"` with `source.media_type` in {image/jpeg, image/png} and an inline `source.data` of 555588 / 359416 / 246952 / 279804 / 282420 / 307516 base64 characters. Extraction from a different transcript wrote a 450165-byte file that `file` reports as `JPEG image data ... 2000x1605` and a 217338-byte `PNG image data`, both byte-for-byte the sizes csift's est_bytes predicted. Claude Code 2.1.258 binary carries the constructor `{block:{type:\"image\",source:{type:\"base64\",media_type:ANe(d),data:d.toString(\"base64\")}},dimensions:m.dimensions}` and the media-type sniffer `function ANe(e){return pR(e)??\"image/png\"}`. Corpus-wide (14 project dirs, *.jsonl only) there are 3021 `\"media_type\"` occurrences and 0 image references with a non-base64 source kind.",
          "rule": "One image block per `{type:\"image\"}` object on a record; source kind counted once per image reference from the csift JSON `source_kind` field; the binary strings are counted as present/absent, quoted verbatim.",
          "note": "Both code sites exist verbatim at the claimed locations: src/image.rs:3-6 and src/image.rs:8-11. Nothing was externalised on any inspected record: the base64 payload sits in the transcript line itself, and `image --out` decoded it to valid image files without touching any sidecar. One record carrying several image blocks is confirmed directly (6 on one line; the per-line histogram over one transcript's 73 image-carrying lines runs 1,2,3,4,6,8,10,33,38 images per record)."
        }
      ]
    },
    {
      "id": "IMG-002",
      "area": "images",
      "behavior": "An image reaches the transcript through one of two carriers: a direct `{type:\"image\"}` content block (the user-sent / assistant case) or an `{type:\"image\"}` element nested inside a `tool_result` block's `content` ARRAY (a tool screenshot).",
      "depends": "csift walks both carriers in document order under a single 1-based running ordinal, so the `L<line>i<n>` locator stays stable across the two shapes; handling only the direct block makes tool screenshots invisible to `image` and to `image --out`.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "231-234",
          "snippet": "/// Collect every image carried by one record, in document order: a direct `Block::Image`\n/// (the user-sent / assistant case) OR an `{type:\"image\"}` element nested in a\n/// `Block::ToolResult.content` array (a tool screenshot). `img_index` is a 1-based running\n/// ordinal across both sources, so the id `L<line>i<n>` is stable."
        },
        {
          "path": "src/image/refs.rs",
          "lines": "255-259",
          "snippet": "            Block::ToolResult {\n                content: Some(content),\n                ..\n            } => {\n                if let Some(arr) = content.as_array() {"
        }
      ],
      "instrument": "`csift image <target>` and compare its row count to a walk that counts `{\"type\":\"image\"}` objects both as top-level content blocks and as elements nested in `tool_result` content arrays. Counting rule: one image per image object, per record, in document order.",
      "located": {
        "claude_code": null,
        "csift": "0.6.0",
        "source": "SPEC.md section 6; src/image.rs:3-6 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "for L in 4351 10894; do csift show ~/.claude/projects/<enc>/<session>.jsonl --line $L --raw | jq -c '{type, role: .message.role, blocks: [.message.content[]? | {t: .type, nested: (if .type==\"tool_result\" then (if (.content|type)==\"array\" then [.content[]?.type] else (.content|type) end) else null end), src: (.source.type // null), mt: (.source.media_type // null)}]}'; done   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,80}type:\"base64\",media_type.{0,120}'",
          "observed": "Direct carrier: `{\"type\":\"user\",\"role\":\"user\",\"blocks\":[{\"t\":\"text\"},{\"t\":\"image\",\"src\":\"base64\",\"mt\":\"image/jpeg\"}, ... 5 more image blocks]}`. Nested carrier, in the same transcript: `{\"type\":\"user\",\"role\":\"user\",\"blocks\":[{\"t\":\"tool_result\",\"nested\":[\"image\"]}]}` - a `tool_result` block whose `content` is an ARRAY holding an `image` element. Claude Code 2.1.258 binary emits exactly that second shape: `return{tool_use_id:n,type:\"tool_result\",content:[{type:\"image\",source:{type:\"base64\",media_type:o,data:r.data}}]}`. csift's JSON gave the direct-carrier record img_index 1..6 (a 1-based running ordinal) and the nested-carrier record img_index 1.",
          "rule": "One image per `{type:\"image\"}` object, counted per record in document order across both carrier shapes; carrier shape read off the parsed block tree, not a text match.",
          "note": "Code sites verified verbatim: src/image/refs.rs:231-234 (the doc comment) and src/image/refs.rs:255-259 (the `Block::ToolResult { content: Some(content), .. }` arm). The nested carrier is not hypothetical here - the three consecutive unnumbered images in the measured transcript (seq null, one per record) are all tool-result screenshots, and a listing that walked only direct blocks would have dropped them."
        }
      ]
    },
    {
      "id": "IMG-003",
      "area": "images",
      "behavior": "An image block's `source` carries a kind field; every image block observed on disk is `base64` (payload in `data`) plus a `media_type`, and the raw line always carries the `\"media_type\"` key in its compact form. The `url` kind is an Anthropic-API source shape that Claude Code 2.1.258 never writes: 0 image references out of 2977 in this corpus use it, and the binary contains no `type:\"url\"` construction. csift keeps the kind verbatim and would report a url-sourced image as having no bytes to extract, but that arm is defensive rather than exercised.",
      "depends": "csift selects candidate lines with an OR of three needles - the compact pair `\"type\":\"image\"`, the key-only `\"media_type\"`, and the bare value substring `base64` (src/image/refs.rs:96-98, combined by `.any()` at line 101). The serialization tolerance comes from the UNION: the pair needle alone would miss a line reserialized with whitespace around the colon, but the key-only and bare-value needles both survive it.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "28-36",
          "snippet": "    /// `\"base64\"` | `\"url\"` | another source kind (kept verbatim).\n    pub(crate) source_kind: String,\n    pub(crate) media_type: String,\n    /// Base64 character length (0 for a non-base64 source).\n    pub(crate) b64_len: usize,\n    /// Estimated decoded byte size (base64 only; 0 otherwise).\n    pub(crate) est_bytes: usize,\n    /// The image URL, for a `source.type == \"url\"` image (no inline bytes to extract).\n    pub(crate) url: Option<String>,"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "95-99",
          "snippet": "            [\n                memmem::Finder::new(br#\"\"type\":\"image\"\"#),\n                memmem::Finder::new(br#\"\"media_type\"\"#),\n                memmem::Finder::new(b\"base64\"),\n            ]"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "173-179",
          "snippet": "    let (b64_len, est_bytes, url, fingerprint, data) = if source_kind == \"url\" {\n        let url = source\n            .get(\"url\")\n            .and_then(Value::as_str)\n            .map(str::to_string);\n        let fp = format!(\"url:{}\", url.as_deref().unwrap_or(\"\"));\n        (0, 0, url, fp, None)"
        }
      ],
      "instrument": "`grep -c '\"media_type\"' ~/.claude/projects/*/*.jsonl` then `csift image @<session> --format json | jq -r '[.source_kind, .media_type, .b64_len] | @tsv'`. Counting rule: one reference per image block; a url source reports zero base64 length.",
      "located": {
        "claude_code": null,
        "csift": "0.6.0",
        "source": "AGENTS.md section 5; SPEC.md section 6"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for d in ~/.claude/projects/*/; do rg -oIN -g '*.jsonl' '\"media_type\"\\s*:\\s*\"[^\"]*\"' \"$d\"; done | sort | uniq -c   AND   csift image ~/.claude/projects/<enc> --format json | jq -r 'select(.kind==\"image\") | .source_kind' | sort | uniq -c   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'type:\"url\"'   AND   sed -n '93,101p' src/image/refs.rs   # from the csift repo root",
          "observed": "Corpus census over all 14 project dirs, *.jsonl only: 1748 `\"media_type\":\"image/png\"`, 1269 `\"media_type\":\"image/jpeg\"`, 4 `\"media_type\":\"application/pdf\"` - 3021 occurrences, every one the compact no-space form, and every image block carried the key. csift's source_kind census over the same corpus: base64 on 100% of image references (largest single project dir: 582 of 582), url on 0. The Claude Code 2.1.258 binary contains 0 occurrences of the string `type:\"url\"`. The csift prefilter at src/image/refs.rs:101 is `NEEDLES.iter().any(|f| f.find(line).is_some())` - an OR over the three needles at lines 96-98.",
          "rule": "One media_type occurrence per regex match on a transcript line; one source_kind per csift image reference after content dedup; binary strings counted as occurrences of the literal.",
          "note": "Two refinements. (1) The url arm is untestable against behavior here because Claude Code never produces it - the instrument that would decide it is a transcript written by a client that sends `{\"type\":\"image\",\"source\":{\"type\":\"url\",\"url\":...}}`; note that such a block has no `media_type` under the Anthropic API, which would make the claim's 'the raw line always carries the media_type key' false for that shape. csift already defends: `media_type` falls back to `application/octet-stream` (src/image/refs.rs:168-172) and `ext()` to `bin`. (2) The claim's wording implies a key-only needle does the selecting; the code actually ORs a compact key:value pair in with two serialization-safe needles. All four claimed code sites exist verbatim (refs.rs:28-36, 95-99, 173-179)."
        }
      ]
    },
    {
      "id": "IMG-004",
      "area": "images",
      "behavior": "The `media_type` varies per image rather than always being `image/png`: this corpus carries `image/png` (1748) and `image/jpeg` (1269) side by side, and the Claude Code 2.1.258 binary carries the four accepted types `image/png`, `image/jpeg`, `image/gif`, `image/webp`. The non-standard `image/jpg` spelling is NOT observed in any transcript here (0 of 3021 media_type occurrences) and 2.1.258 actively normalizes it away when building a media type (`i===\"jpg\"?\"jpeg\":i`), so csift's jpg arm is defensive rather than a live shape.",
      "depends": "csift derives each output file's extension from that image's own `media_type` (falling back to `bin`, never fabricated) and folds both jpeg spellings onto one output format; assuming PNG writes wrongly-named files, and a media type outside the mapped set must degrade rather than guess.",
      "code": [
        {
          "path": "src/image/convert.rs",
          "lines": "5-12",
          "snippet": "/// A source `media_type` → an [`ImageOutFormat`]. `None` for a media type outside the four\n/// Claude-API image types (shouldn't occur - CC only stores those).\npub(crate) fn format_of_media_type(mt: &str) -> Option<ImageOutFormat> {\n    match mt {\n        \"image/png\" => Some(ImageOutFormat::Png),\n        \"image/jpeg\" | \"image/jpg\" => Some(ImageOutFormat::Jpeg),\n        \"image/gif\" => Some(ImageOutFormat::Gif),\n        \"image/webp\" => Some(ImageOutFormat::Webp),"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "59-66",
          "snippet": "    /// File extension implied by the media type (`bin` when unknown - never fabricated).\n    pub(crate) fn ext(&self) -> &'static str {\n        match self.media_type.as_str() {\n            \"image/png\" => \"png\",\n            \"image/jpeg\" | \"image/jpg\" => \"jpg\",\n            \"image/gif\" => \"gif\",\n            \"image/webp\" => \"webp\",\n            \"image/svg+xml\" => \"svg\","
        }
      ],
      "instrument": "`csift image @<session> --format json | jq -r .media_type | sort | uniq -c`; expected: both jpeg spellings present in real data, one output extension. Counting rule: one media type per image block, after content dedup.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.3.0",
        "source": "SPEC.md section 6.9; dev session 2026-08-17"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for d in ~/.claude/projects/*/; do rg -oIN -g '*.jsonl' '\"media_type\"\\s*:\\s*\"[^\"]*\"' \"$d\"; done | sort | uniq -c   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function C\\(e,i,r\\)\\{let n=i===\"jpg\"\\?\"jpeg\":i;.{0,120}'   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | sed -n '137076,137082p'   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id L42693i1,L42103i1 --out out && file out/*",
          "observed": "Corpus census (*.jsonl, 14 project dirs): image/png 1748, image/jpeg 1269, image/jpg 0, image/gif 0, image/webp 0. Claude Code 2.1.258 NORMALIZES the jpg spelling away at construction time: `function C(e,i,r){let n=i===\"jpg\"?\"jpeg\":i;return{base64:e.toString(\"base64\"),mediaType:`image/${n}`,originalSize:r}}`. The binary's supported-image-type list is four adjacent strings `image/jpeg` / `image/png` / `image/gif` / `image/webp`; the literal `image/jpg` occurs 4 times in the binary, all outside that list (one bare occurrence sits in the browser-bridge string region). Extraction check: an `image/jpeg` source wrote `...-L42693i1.jpg` (450165 bytes, `file`: JPEG image data) and an `image/png` source wrote `...-L42103i1.png` (217338 bytes, `file`: PNG image data).",
          "rule": "One media_type per regex match on a transcript line, whole-corpus, *.jsonl only; binary strings counted as occurrences of the literal; one output file per extracted image, extension read from the written filename.",
          "note": "The claim's operative half - the extension must come from the image's own media_type, never assumed PNG - is confirmed by extraction: two images from one transcript wrote a .jpg and a .png, sized exactly as predicted. The `image/jpg` half is not reproducible at 2.1.258: 0 occurrences on disk and an explicit normalization in the binary. It would take a transcript written by an older Claude Code, or by the browser-bridge path where the bare `image/jpg` literal lives, to put that spelling on disk. Both code sites exist verbatim (src/image/convert.rs:5-12, src/image/refs.rs:59-66); note refs.rs `ext()` maps five more types beyond convert.rs's four (svg+xml, bmp, tiff, heic, avif) before the `bin` fallback."
        }
      ]
    },
    {
      "id": "IMG-005",
      "area": "images",
      "behavior": "Claude Code writes the model-facing image handle into the carrying record's own text as the literal marker `[Image #N]`, one marker per image block, in block order. A handle is unique within one prompt, but the counter does NOT restart at #1 on every prompt: measured across one transcript the distinct handles climb monotonically 1..624 over many prompts. The counter does reset at some coarser boundary - a second transcript shows `#1` naming 7 different images spread over four weeks - which is what makes a handle non-unique within a transcript (IMG-006).",
      "depends": "csift recovers `#N` by positionally zipping a record's `[Image #N]` markers with its image blocks and leaves the number unset when the two counts differ (a mismatch means a back-reference to an image compacted out of the record), so addressing falls back to the always-unique `L<line>i<n>` locator; a change to the marker format loses the model-facing handle entirely.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "216-221",
          "snippet": "pub(crate) fn parse_image_markers(text: &str, out: &mut Vec<usize>) {\n    let mut rest = text;\n    const PAT: &str = \"[Image #\";\n    while let Some(i) = rest.find(PAT) {\n        let after = &rest[i + PAT.len()..];\n        let digits: String = after.chars().take_while(char::is_ascii_digit).collect();"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "275-282",
          "snippet": "    // Assign `#N` by POSITIONAL zip - only when the marker count matches the image count\n    // (CC guarantees `[Image #N]` is unique within a prompt; a mismatch means a back-\n    // reference to a compressed-out image, so we leave `seq = None` rather than misassign).\n    if markers.len() == out.len() {\n        for (r, &n) in out.iter_mut().zip(markers.iter()) {\n            r.seq = Some(n);\n        }\n    }"
        }
      ],
      "instrument": "`rg -o '\\[Image #[0-9]+\\]' <transcript> | sort | uniq -c` for the raw markers against `csift image @<session> --format json | jq -c '{handle, seq, line}'`. Counting rule: one marker per regex match, one row per content-deduped image.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.0",
        "source": "SPEC.md section 6; SPEC.md section 6 v0.6.2 ledger item 1; src/image/refs.rs:17-23 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\")|[.line,.img_index,.seq,.b64_len]|@tsv' | sort -n   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 1   AND   rg -oIN '\\[Image #[0-9]+\\]' ~/.claude/projects/<enc>/<session2>.jsonl | sed 's/[^0-9]//g' | sort -n | uniq   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | sed -n '147155,147162p'",
          "observed": "One record (line 4351) carries 6 image blocks; csift assigned them seq 1,2,3,4,5,6 by position, and the ambiguity listing shows that record's own text opening `[Image #1] [Image #2] [Image #3] [Image #4] [Image #5] [Im...` - the markers ride in the carrying record's text, one per block, in order. Across the same transcript's 191 deduped images, 188 got a seq and 3 stayed null (marker/block count mismatch), and the 3 nulls are exactly the tool-result screenshots. The 2.1.258 binary carries the literals `[Image #`, `[Image]`, `imageId`, and the error text `Cannot destructure property 'imageId' from null or undefined value`. Counter cadence: in a SECOND transcript the distinct handles run monotonically 1..624 across many prompts rather than restarting at #1 each prompt.",
          "rule": "One marker per `\\[Image #[0-9]+\\]` regex match; one row per content-deduped image; seq assigned only when a record's marker count equals its image-block count.",
          "note": "The positional-zip mechanism and the leave-unset-on-mismatch rule are both confirmed directly, and both code sites exist verbatim (src/image/refs.rs:216-221, 275-282). Only the phrase 'numbering pasted images PER PROMPT' needed correcting: per-prompt uniqueness holds, per-prompt restart does not."
        }
      ]
    },
    {
      "id": "IMG-006",
      "area": "images",
      "behavior": "Because the numbering restarts per prompt, low `[Image #N]` numbers are REUSED across prompts, so `#N` is not globally unique within one transcript and one number can name several distinct images.",
      "depends": "csift resolves an `--id N` selection only when the number names ONE distinct image (distinctness by content fingerprint) and otherwise ERRORS with the full occurrence list - turn, `L<line>i<n>` locator, uuid, time and an excerpt around the marker - rather than silently picking one; the locator is the unambiguous fallback.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "20-23",
          "snippet": "    /// `None` when the marker count doesn't match (then only `L<line>i<n>` addresses it).\n    /// NOT globally unique - CC reuses low numbers across prompts, so a `#N` that names >1\n    /// DISTINCT image is AMBIGUOUS: `--id #N` then ERRORS with the occurrence list rather than\n    /// silently guessing (disambiguate with the locator or `--since`/`--turn`/`--uuid`)."
        },
        {
          "path": "src/image/selection.rs",
          "lines": "9-11",
          "snippet": "    /// `#N` / bare `N` - the `[Image #N]` handle the model uses. Resolves to the unique image\n    /// with that handle in scope; if it names >1 DISTINCT image (CC reuses `#N` across prompts)\n    /// it is AMBIGUOUS and ERRORS with the occurrence list rather than silently picking one."
        },
        {
          "path": "src/image/selection.rs",
          "lines": "281-286",
          "snippet": "        msg.push_str(&format!(\n            \"--id #{n} is ambiguous: it names {} different images in this transcript (Claude Code \\\n             reuses `#N` across prompts). Pick one by its exact `--id L<line>i<n>`, or narrow the \\\n             scope with --since/--until (a time window) / --turn / --uuid:\",\n            occs.len()\n        ));"
        }
      ],
      "instrument": "`csift image @<session>` and look for one `#N` handle repeated on different lines; `rg -o '\\[Image #[0-9]+\\]' <transcript> | sort | uniq -c` shows the reuse. Counting rule: distinct images (by content fingerprint) per `#N` handle within one transcript.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.0",
        "source": "SPEC.md section 6; SPEC.md section 6 v0.6.2 ledger item 1; src/image/refs.rs:17-23 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 1   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\" and .seq==1)|[.line,.b64_len]|@tsv'",
          "observed": "`--id 1` exits non-zero with `csift: error: --id #1 is ambiguous: it names 7 different images in this transcript (Claude Code reuses `#N` across prompts). Pick one by its exact `--id L<line>i<n>`, or narrow the scope with --since/--until (a time window) / --turn / --uuid:` followed by 7 occurrence lines, each with its own locator, turn, timestamp, uuid prefix and marker excerpt. The 7 are genuinely distinct content: base64 lengths 555588, 651860, 448616, 455720, 678200, 675456, 659632 on lines 4351, 13132, 25350, 47923, 56770, 83310, 94987, spanning turns t39 to t997 and four weeks of wall time.",
          "rule": "Distinct images per `#N` handle within one transcript, distinctness by the csift content fingerprint (base64 length + head + tail); an ambiguous handle is one whose distinct count exceeds 1.",
          "note": "The refusal path is real, not documentation: the command errors with the full occurrence list rather than picking one, and the locator disambiguates. All three code sites exist verbatim (src/image/refs.rs:20-23, src/image/selection.rs:9-11, 281-286). Note the counting rule matters here - a fingerprint taken from the base64 HEAD alone would wrongly merge these, see IMG-008."
        }
      ]
    },
    {
      "id": "IMG-007",
      "area": "images",
      "behavior": "A transcript's `[Image #N]` handles are not a dense 1..N index: the numbers are inherited from paste time, so the handles present carry HOLES where that number's image never landed in this transcript. Measured: 120 distinct handles over the range #1..#131 (11 holes) in one transcript, and holes throughout a #1..#624 range in another. The claim's other half - handles starting past #1 - was NOT reproduced here; both measured transcripts start at #1.",
      "depends": "csift's `image --id` takes the bare handle number or the `L<line>i<n>` locator, and on a miss it names the handles that DO exist (plus the count of unnumbered images) and states that a missing number is a source gap, not a dropped image; a consumer that assumes a contiguous range asks for images that were never written.",
      "code": [
        {
          "path": "src/image/selection.rs",
          "lines": "154-156",
          "snippet": "        // Name the handles that DO exist: `#N` is inherited from CC's paste-time\n        // `[Image #N]` numbering, so a transcript's handles can start past #1 and carry\n        // holes - a bare \"matched no image\" reads like a csift drop when it is a source gap."
        },
        {
          "path": "src/image/selection.rs",
          "lines": "185-189",
          "snippet": "            \"--id matched no image: {} — {inventory}. `#N` handles are inherited from Claude \\\n             Code's paste-time `[Image #N]` numbering, NOT a dense 1..N index csift assigns, \\\n             so a transcript's handles can start past #1 and carry holes; a missing number is \\\n             a source gap (that number's image never landed in this transcript), not a \\\n             dropped image. Run `csift image` on the same target to see the full listing.\","
        }
      ],
      "instrument": "`csift image @<uuid> --format json | jq -r .seq | sort -n` against the dense 1..max range. Counting rule: distinct handles present versus the dense range; non-contiguous handles are expected.",
      "located": {
        "claude_code": null,
        "csift": "0.6.2",
        "source": "CHANGELOG 0.6.2; SKILL.md wrong-assumption row on image #N handles"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -oIN '\\[Image #[0-9]+\\]' ~/.claude/projects/<enc>/<session>.jsonl | sed 's/[^0-9]//g' | sort -n | uniq   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 20   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --id 999",
          "observed": "Transcript A: 120 distinct `[Image #N]` handles spanning #1..#131 - 11 numbers missing (#20, #103, #115-#120, #128-#130) out of the dense range. Transcript B: handles reach #624 with holes at #61, #121, #127, #129, #193-#194, #282-#283, #291, #341-#342, #358-#370, #507, #517, #520-#524, #537-#542, #546-#551, #555, #559, #568, #603-#604, #622. Asking for a hole (`--id 20`) and asking for an out-of-range number (`--id 999`) both produce the same non-zero error naming the handles that exist: `--id matched no image: #20 - present here: #1 #2 ... #25 (+95 more) + 3 unnumbered image(s) (address by the L<line>i<n> locator). `#N` handles are inherited from Claude Code's paste-time `[Image #N]` numbering, NOT a dense 1..N index csift assigns, so a transcript's handles can start past #1 and carry holes; a missing number is a source gap ...`. Both measured transcripts START at #1.",
          "rule": "Distinct handle numbers present, from the deduplicated set of `\\[Image #[0-9]+\\]` matches in one transcript, compared against the dense 1..max range; a hole is a number in that range with no match.",
          "note": "The holes half is confirmed twice over, and the error path names the present handles plus the count of unnumbered images exactly as claimed. The start-past-#1 half needs a transcript whose first pasted image already carried a high handle - plausible under the same inheritance mechanism, but not observed on this machine. Both code sites exist verbatim (src/image/selection.rs:154-156, 185-189)."
        }
      ]
    },
    {
      "id": "IMG-008",
      "area": "images",
      "behavior": "The same image is re-injected within one transcript - a prompt re-sends attached images and a compaction re-includes them - so one pasted image can appear as several separate image blocks. Measured, the effect is modest rather than large: 248 blocks over 196 distinct images in one transcript (1.27x, most-repeated image 4 copies) and 1164 blocks over 902 distinct in another (1.29x, most-repeated 9 copies). A raw block count therefore over-reports the image inventory by roughly a quarter, not by an order of magnitude.",
      "depends": "csift's listing content-dedups by a cheap base64 fingerprint (`<len>:<head>:<tail>`) keeping the LATEST occurrence and its current `#N`, while two distinct-content images sharing one `#N` both survive so the handle reuse stays visible; a raw block count over-reports the image inventory.",
      "code": [
        {
          "path": "src/image/selection.rs",
          "lines": "57-60",
          "snippet": "/// Dedup the SAME image re-injected across context windows (by content fingerprint), keeping\n/// the LATEST occurrence (its current `#N`). `images` is sorted ascending, so walking in\n/// reverse and keeping first-seen yields the latest; then order by `#N` (then line) for the\n/// listing so a reader scanning for \"#32\" finds it in sequence."
        },
        {
          "path": "src/image/refs.rs",
          "lines": "25-27",
          "snippet": "    /// A cheap content fingerprint (`<len>:<head>:<tail>` of the base64) - dedups the SAME\n    /// image re-injected across context windows so the listing shows it once.\n    pub(crate) fingerprint: String,"
        }
      ],
      "instrument": "Compare the deduped row count of `csift image @<session>` against `rg -c '\"media_type\"' <transcript>` (raw block count). Counting rule: distinct content fingerprints versus image blocks.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.3.0",
        "source": "SPEC.md section 6.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -oIN '\"media_type\":\"image/[a-z]+\",\"data\":\"[A-Za-z0-9+/=]+\"' ~/.claude/projects/<enc>/<session>.jsonl | awk '{n=length($0); print n\":\"substr($0,1,60)\":\"substr($0,n-31,32)}' | sort | uniq -c   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -c 'select(.kind==\"summary\")'   AND   rg -oIN '\"media_type\":\"image/' <same file> | wc -l",
          "observed": "Transcript A: 248 raw image blocks; the csift-shaped fingerprint (base64 length + 60-char head + 32-char tail) groups the 245 blocks it matched into 196 distinct images - multiplicity histogram 158 singles, 31 doubles, 3 triples, 4 quadruples, max 4 copies of any one image. csift's own deduped listing: 191 rows from 248 blocks. Transcript B: 1164 raw blocks, 1147 matched, 902 distinct - histogram 715/155/20/7/2/1/2 at multiplicities 1/2/3/4/5/6/9, max 9 copies; csift listing 897 rows from 1164 blocks. Adversarial control: fingerprinting on the base64 HEAD ALONE (first 48 chars) collapsed transcript A's 245 blocks to 170 groups and reported a bogus max multiplicity of 15 - same-dimension screenshots share a PNG prefix, so head-only merges distinct images.",
          "rule": "Raw blocks counted one per `\"media_type\":\"image/` match in one transcript; distinct images counted one per (base64 length, head, tail) triple; multiplicity = raw blocks sharing one triple.",
          "note": "The mechanism and its necessity both hold; only the magnitude ('many times') needed correcting to a measured ratio. The head-only control is the strongest support for the code as written: csift's `<len>:<head>:<tail>` form (src/image/refs.rs:25-27, src/image/selection.rs:57-60, both verbatim) is what keeps 26 distinct screenshots from collapsing into one row, and the two-distinct-images-sharing-#1 case survives dedup exactly as claimed (IMG-006 lists all 7 of that transcript's #1 images)."
        }
      ]
    },
    {
      "id": "IMG-009",
      "area": "images",
      "behavior": "Pasted images are ordinary rather than rare in an image-carrying session. Measured on this corpus's largest such transcript: 1164 image blocks spread over 393 records, 268 of them human-turn (`user.message`) records out of that file's 840 - about one human turn in three. The originally recorded 528 / 159 / 774 is a point-in-time snapshot of a transcript that has since grown; the direction it reported is now stronger, not weaker.",
      "depends": "csift scans for images with the same mmap + byte-prefilter + parse-only-candidates path the other full-scan commands use and surfaces the malformed-line count, so the inventory is cheap to take on a large transcript; a text-only reader of the same session drops every image with no marker left behind.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "299-302",
          "snippet": "/// Scan ONE transcript for images. Mirrors `recover`/`files`: mmap + a pre-JSON byte\n/// prefilter + parse only candidate lines (line-numbered, 1:1 with the file). Returns the\n/// images and the malformed-line count (surfaced, never hidden).\npub(crate) fn images_in_file(path: &Path, with_data: bool) -> Result<(Vec<ImageRef>, usize)> {"
        }
      ],
      "instrument": "`csift image @<uuid> --format json | jq -c '{handle, seq, line, media_type}'`. Counting rule: 528 counted one per `{type:\"image\"}` block before content dedup; 159 of 774 counted one per human-turn record carrying at least one such block, in a single transcript.",
      "located": {
        "claude_code": null,
        "csift": "0.6.2",
        "source": "CHANGELOG 0.6.2; SKILL.md wrong-assumption row on image #N handles"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -r 'select(.kind==\"image\")|.line' | sort -nu > imglines.txt; csift search '' <same file> --no-subagents -t user.message --max-count 0 --format json | jq -r '.hits[]?|.line' | sort -nu > umlines.txt; rg -nI -o '\"media_type\":\"image/' <same file> | cut -d: -f1 | sort -nu > rawimglines.txt; comm -12 rawimglines.txt umlines.txt | wc -l   AND   time csift image <same file> --no-subagents --format json > /dev/null",
          "observed": "In the largest image-carrying transcript in this corpus (720932882 bytes): 1164 raw image blocks over 393 distinct record lines; 268 of those 393 lines are `user.message` records, out of 840 `user.message` records in the file - 31.9%, about one human turn in three. csift scanned that 720MB file in 0.344s wall (0.33s user, 165% cpu) and reported `skipped_lines: 0` with 897 deduped images. A 415MB transcript scanned in 0.680s including its 12 subagent transcripts (265 images, skipped_lines 0). The claimed 528 / 159 / 774 triple does not match this or any other transcript here; the same file today reads 1164 / 268 / 840.",
          "rule": "Image blocks counted one per `\"media_type\":\"image/` match; image-carrying human-turn records counted as the intersection of the distinct line numbers of those matches with the line numbers csift labels `user.message`; the denominator is that transcript's total `user.message` record count.",
          "note": "The specific triple is not reproducible because transcripts are append-only and keep growing - a snapshot number in a ledger goes stale by construction, so the claim is better stated as a ratio with its counting rule. The performance half is confirmed independently: 0.344s for 720MB with an exact malformed-line census (0). The code site exists verbatim (src/image/refs.rs:299-302)."
        }
      ]
    },
    {
      "id": "IMG-010",
      "area": "images",
      "behavior": "Claude Code names its on-disk image cache files `<[Image #N] handle>.<media-type subtype>` under ~/.claude/image-cache/<session>/ - the extension is the media type's subtype taken verbatim, so an `image/jpeg` image is cached as `.jpeg` while csift extracts the same image to `.jpg`. The two names are attributes of different writers, not of different images. Note the subtype rule means a hypothetical `image/jpg` would cache as `.jpg`, not `.jpeg`.",
      "depends": "csift maps both media types to one output format and writes `.jpg`, so a `.jpeg` file found beside a session is Claude Code's cache and not csift output; conflating the two attributes files to an extraction that never ran.",
      "code": [
        {
          "path": "src/image/convert.rs",
          "lines": "5-12",
          "snippet": "/// A source `media_type` → an [`ImageOutFormat`]. `None` for a media type outside the four\n/// Claude-API image types (shouldn't occur - CC only stores those).\npub(crate) fn format_of_media_type(mt: &str) -> Option<ImageOutFormat> {\n    match mt {\n        \"image/png\" => Some(ImageOutFormat::Png),\n        \"image/jpeg\" | \"image/jpg\" => Some(ImageOutFormat::Jpeg),\n        \"image/gif\" => Some(ImageOutFormat::Gif),\n        \"image/webp\" => Some(ImageOutFormat::Webp),"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "59-66",
          "snippet": "    /// File extension implied by the media type (`bin` when unknown - never fabricated).\n    pub(crate) fn ext(&self) -> &'static str {\n        match self.media_type.as_str() {\n            \"image/png\" => \"png\",\n            \"image/jpeg\" | \"image/jpg\" => \"jpg\",\n            \"image/gif\" => \"gif\",\n            \"image/webp\" => \"webp\",\n            \"image/svg+xml\" => \"svg\","
        }
      ],
      "instrument": "`csift image @<session> --format json | jq -r .media_type | sort | uniq -c` for the transcript side, then list the extensions Claude Code's image cache wrote for the same images. Counting rule: one media type per image block, one file per cached image.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.7.6",
        "source": "dev session 2026-08-17"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}image-cache.{0,700}'   AND   find ~/.claude -type f \\( -iname '*.jpeg' -o -iname '*.jpg' -o -iname '*.png' -o -iname '*.gif' -o -iname '*.webp' \\) | sed 's/.*\\.//' | sort | uniq -c   AND   ls -l ~/.claude/image-cache/*/   AND   csift image ~/.claude/projects/<enc>/<session>.jsonl --no-subagents --format json | jq -c 'select(.kind==\"image\")|{seq,est_bytes,media_type}'   AND   csift image <same file> --no-subagents --id L42693i1 --out out && file out/*",
          "observed": "The 2.1.258 binary builds the cache path as `var g=\"image-cache\", O=200; function f(){return d(Se(),g,Q())} function p(e,r){let o=r.split(\"/\")[1]||\"png\";return d(f(),`${e}.${o}`)}` and calls it as `p(e.id, e.mediaType||\"image/png\")` - so the extension is the media type's SUBTYPE verbatim: `image/jpeg` yields `.jpeg`, and `image/jpg` would yield `.jpg`, not `.jpeg`. On disk today ~/.claude/image-cache holds 7 files across 2 sessions, all `.png`; across all of ~/.claude there are 596 image files (581 .jpg, 14 .png, 1 .gif) and ZERO `.jpeg`. The cache basename is the `[Image #N]` handle: the file `9.png` is 328603 bytes and that session's transcript image `#9` decodes to exactly 328603 bytes. csift on the other side wrote `...-L42693i1.jpg` for an `image/jpeg` source (450165 bytes, `file`: JPEG image data). The only `.jpeg` literal elsewhere in the binary is `public.jpeg`, an operating-system pasteboard type identifier listed beside `public.png` / `public.heic` / `public.avif`, not a filename extension.",
          "rule": "Cache filename rule read off the binary's path builder, then checked against the on-disk cache; extensions counted one per file under ~/.claude; media types counted one per csift image reference; the handle-to-file join is byte-size equality between the cached file and the transcript image's decoded length.",
          "note": "The `.jpeg`-vs-`.jpg` divergence is confirmed as a rule but not as a live artifact: no `.jpeg` file exists under ~/.claude right now because both cached sessions happened to paste PNG-sourced images and the cache is pruned (the binary carries `Cleaned up old image cache:` and a cap constant of 200). Deciding it as an artifact needs a jpeg-sourced paste in a session whose cache has not yet been pruned. Both code sites exist verbatim (src/image/convert.rs:5-12, src/image/refs.rs:59-66), and the extraction check confirms csift writes `.jpg` for `image/jpeg`. code-site note: The mechanism half of this claim now has a binary receipt as well: `var g=\"image-cache\"` with `function p(e,r){let o=r.split(\"/\")[1]||\"png\";return d(f(),`${e}.${o}`)}` in Claude Code 2.1.258."
        }
      ]
    },
    {
      "id": "IMG-011",
      "area": "images",
      "behavior": "A `Read` of an image file writes a `toolUseResult` whose top-level `type` is `image` and whose `file` object has NO `filePath` at all: it carries `base64` (the payload), `type` (the media type - `image/png` and `image/jpeg` are what the corpus holds), `originalSize`, and an OPTIONAL `dimensions:{originalWidth, originalHeight, displayWidth, displayHeight}` present on 435 of 444 such results.",
      "depends": "csift's `recover` Read arm keys on a `file` object with a matching `filePath` and calls `path_matches(target_file, path.unwrap_or_default())`, so an image Read reaches that arm with an EMPTY path string; `path_matches` returns false on an empty target match, which is the only reason an image Read is never mistaken for a file event - the `unwrap_or_default()` is load-bearing, not incidental.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "56-58",
          "snippet": "    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "173-177",
          "snippet": "pub(crate) fn path_matches(target: Option<&str>, path: &str) -> bool {\n    let Some(t) = target else { return false };\n    if t == path {\n        return true;\n    }"
        }
      ],
      "instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, bucketing `Counter(tuple(sorted(file.keys())))` by the top-level `type`. Counting rule: one observation per record with an object `file`. Result: 444 `image` results; `file.filePath` present on 0 of 444; key set `{base64, dimensions, originalSize, type}` on 435 and `{base64, originalSize, type}` on 9; `file.type` values `image/png` 371 and `image/jpeg` 73. Schema authority: the Read tool-result schema in the Claude Code 2.1.258 binary, whose `image` variant declares exactly those four members.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) python3 -c \"import json,os,collections; R=os.path.expanduser('~/.claude/projects'); C=collections.Counter(); T=collections.Counter(); n=0; fp=0; [ (n:=n+1, C.update([tuple(sorted(t['file']))]), T.update([t['file'].get('type')])) for dp,_,fs in os.walk(R) for f in fs if f.endswith('.jsonl') for l in open(os.path.join(dp,f),errors='replace') if 'toolUseResult' in l for r in [json.loads(l)] for t in [r.get('toolUseResult')] if isinstance(t,dict) and isinstance(t.get('file'),dict) and t.get('type')=='image' ]\" (run as the multi-line equivalent; walks every *.jsonl under ~/.claude/projects, json-parses each line containing toolUseResult, keeps records whose toolUseResult.file is an object, buckets by top-level type)  (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'c\\(\\{type:x\\(\"image\"\\).{300}'  (c) csift recover --file <an image file read in that session> @<session> --no-subagents --coverage",
          "observed": "(a) 7829 transcript files scanned; toolUseResult types carrying an object `file`: text 8609, image 444, file_unchanged 120, parts 26, pdf 4. image results = 444; `file.filePath` present on 0 of 444; file key sets {base64,dimensions,originalSize,type} on 435 and {base64,originalSize,type} on 9; file.type = image/png 371, image/jpeg 73.  (b) verbatim binary string: 'c({type:x(\"image\"),file:c({base64:i().describe(\"Base64-encoded image data\"),type:e.describe(\"The MIME type of the image\"),originalSize:A().describe(\"Original file size in bytes\"),dimensions:c({originalWidth:A().optional()...displayHeight:A().optional().describe(\"Displayed image height in pixels (after resizing)\")}).optional().describe(\"Image dimension info for coordinate mapping\")})})' - exactly the four members base64/type/originalSize/dimensions, dimensions optional, no filePath declared.  (c) csift printed 'no recoverable history for <that image file> in range' (exit 0) even though the Read of it is on the transcript (tool_use line 558, result line 561).",
          "rule": "One observation per record whose toolUseResult.file is an object, counted once per file on disk (the corpus holds duplicated project directories, so 444 observations resolve to 315 distinct record uuids; every ratio quoted here is observation-level and the dedup does not change any of them qualitatively - filePath is absent on 0 under either rule). The recover check counts anchors minted for a --file target: 0 read events for an image Read, versus 1 for the PDF case of IMG-012.",
          "note": "Reproduces exactly, at both instruments. The binary schema is the stronger form of the claim: filePath is not merely absent from the corpus, it is not a declared member of the image variant. Two secondary observations worth carrying: (1) 444 file-level observations are 315 distinct record uuids, because two project directories hold copies of the same session; (2) image/png and image/jpeg are a corpus fact, not a schema limit - Claude Code's media-type maps in the same binary also carry image/gif and image/webp, so a future corpus can hold those. The depends clause is confirmed behaviourally, not just by reading: recover finds no history for a file whose only Read produced an image result."
        }
      ]
    },
    {
      "id": "IMG-012",
      "area": "images",
      "behavior": "A `Read` of a PDF that Claude Code splits into page images writes a `toolUseResult` whose top-level `type` is `parts` and whose `file` object is exactly `{count, filePath, originalSize, outputDir}` - it NAMES an on-disk output directory instead of carrying the pages in `file`; a top-level `firstPage` rides beside it only on results written by Claude Code 2.1.251 and later (2 of 26 observed, and both of those are the 2.1.251 records).",
      "depends": "csift's `recover` Read arm keys on the presence of a `file` object with a matching `filePath`, and a `parts` result carries one (the PDF path), so it enters the same path as a text Read but supplies no `content` (the `unwrap_or_default()` yields an empty string). The `total > 0` gate in `push_read_event` is what stops that contentless variant from being minted as a FULL snapshot: the event still lands, on the `else` arm, as a zero-line windowed read, so `recover --file <the pdf>` reports `0/1 lines (0%)` with `1 read (0 full, 1 windowed)` rather than a false full read.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "59-63",
          "snippet": "            let content = file\n                .get(\"content\")\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default()\n                .to_string();"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "72-77",
          "snippet": "            let num_lines = file\n                .get(\"numLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);\n            push_read_event(\n                line_no,"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-181",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, filtered to `type == 'parts'` and tallying both the `file` key set and the top-level key set. Counting rule: one observation per record with an object `file`. Result: 26 `parts` results; `file` key set exactly `{count, filePath, originalSize, outputDir}` on 26 of 26; top-level key set `{file, type}` on 24 and `{file, firstPage, type}` on 2.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) python3 walk over ~/.claude/projects filtered to toolUseResult.type == 'parts', tallying the `file` key set, the top-level key set, and the count of {type:\"image\"} elements nested in the record's tool_result blocks  (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'c\\(\\{type:x\\(\"parts\"\\).{700}'  (c) csift recover --file <the PDF> @<session> --coverage",
          "observed": "(a) 26 parts results; file key set exactly {count, filePath, originalSize, outputDir} on 26 of 26; top-level key set {file, type} on 24 and {file, firstPage, type} on 2.  (b) verbatim binary string: 'c({type:x(\"parts\"),file:c({filePath:i().describe(\"The path to the PDF file\"),originalSize:A().describe(\"Original file size in bytes\"),count:A().describe(\"Number of pages extracted\"),outputDir:i().describe(\"Directory containing extracted page images\")}),firstPage:A().optional().describe(\"Document page number of the first extracted page (1 when no range was requested); labels the page images in the model-facing tool_result\")' - exactly those four file members, firstPage optional.  (c) csift printed 'recoverable: 0/1 lines (0%)  fragments: 1' and 'events: 1 read (0 full, 1 windowed)', exit 0 - one event was minted, none of it recoverable.",
          "rule": "One observation per record with an object `file` and type 'parts', over every *.jsonl under ~/.claude/projects (26 observations, 26 distinct record uuids - no duplication in this bucket). The recover check counts events by kind as csift's own --coverage line reports them.",
          "note": "The measured numbers reproduce exactly and the binary schema confirms the key set. Two corrections. (1) The depends clause overstated the gate: recover does NOT record nothing - it records one windowed read event with zero recoverable lines. What the `total > 0` gate prevents is the FullSnapshot arm, i.e. a false full read, which is the load-bearing half of the claim; the event itself is still created on the else arm. (2) `firstPage` is not random: the 2 results carrying it are exactly the 2 written by Claude Code 2.1.251, and the 24 without it were written by 2.1.196 / 2.1.207 / 2.1.220 / 2.1.231, so the correct reading is a version boundary rather than an intermittent field. Also noted, not a correction: the production site in the binary can attach a top-level `v5SidecarScope:{...,pageNames}` ('return{success:!0,data:{type:\"parts\",file:{filePath:e,originalSize:s,outputDir:b,count:B},...d!==void 0&&{v5SidecarScope:{...d,pageNames:_}}}}'), but the persisted union does not declare that member and it appears on 0 of 26 records."
        }
      ]
    },
    {
      "id": "IMG-013",
      "area": "images",
      "behavior": "An image `Read` stores the payload TWICE inside the one jsonl line: once as the `toolUseResult.file.base64` echo and once as an `{type:\"image\"}` element nested in the record's `tool_result` content array. Every one of the 444 observed image results carries exactly one such nested element, the element's `source.media_type` equals the echo's `file.type` on 444 of 444, and the two base64 strings are byte-identical on 435 of 444.",
      "depends": "csift's `image` walks `rec.blocks()` - the message content - and never reads `toolUseResult` (kept unparsed), so it counts the nested element once and the echo copy never double-counts a row; a hand-rolled census over raw `base64` bytes counts one image Read twice, and a byte-size estimate taken over the raw line doubles the real payload.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "235-239",
          "snippet": "pub(crate) fn record_images(rec: &Record, with_data: bool) -> Vec<ImageRef> {\n    let mut out = Vec::new();\n    let Some(blocks) = rec.blocks() else {\n        return out;\n    };"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "255-259",
          "snippet": "            Block::ToolResult {\n                content: Some(content),\n                ..\n            } => {\n                if let Some(arr) = content.as_array() {"
        }
      ],
      "instrument": "python3 over ~/.claude/projects: for every record whose `toolUseResult.type` is `image`, walk `message.content` for `{type:\"image\"}` elements nested in `tool_result` blocks and compare each element's `source.data` with `toolUseResult.file.base64`. Counting rule: one observation per record, with the nested-element tally taken per record. Measured 2026-09-02: 444 records; nested-element count 1 on 444 of 444; `source.media_type == file.type` on 444 of 444; base64 byte-identical on 435 of 444.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 -c \"...\" walk over ~/.claude/projects: for every record whose toolUseResult.type is 'image', collect the {type:\"image\"} elements nested in the record's tool_result content arrays, then compare each element's source.media_type with toolUseResult.file.type and its source.data with toolUseResult.file.base64",
          "observed": "444 image records; nested-element count 1 on 444 of 444 (no record with 0 or 2+); source.media_type == file.type on 444 of 444; base64 byte-identical on 435 of 444; the 9 non-identical pairs are all (echo length 0, block length 118908/118908/129212/129212/130588/138368/141016/153900/153900).",
          "rule": "One observation per record with an object `file` and type 'image'; the nested-element tally is taken per record, over all *.jsonl under ~/.claude/projects. 444 observations resolve to 315 distinct record uuids (duplicated project directories); the per-record ratios are unchanged by the dedup because the duplicates are exact copies.",
          "note": "Reproduces exactly, every number. The double-storage is real and current: the payload sits both in the toolUseResult echo and in one nested content block, so a raw-bytes census over the line double-counts an image Read and a byte-size estimate over the raw line doubles the payload. csift's `image` walks rec.blocks() only, and the confirming instrument is IMG-014's csift run: it emits exactly one row per image record (id L<line>i1), never two."
        }
      ]
    },
    {
      "id": "IMG-014",
      "area": "images",
      "behavior": "The echo copy is not a reliable carrier: on 9 of 444 image `Read` results the `toolUseResult.file.base64` is an EMPTY string while the nested content block still holds the full payload (118,908 to 153,900 base64 characters across those 9), so the content block is the authoritative source of the bytes.",
      "depends": "csift's `image` reads the block's `source.data` and returns no reference at all when that key is missing, so `image --out` writes real bytes on exactly those records; a tool that preferred the `toolUseResult` echo would write a zero-byte file and report success, and a size report taken from the echo would read 0 for a 100KB image.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "181-184",
          "snippet": "        let d = source.get(\"data\").and_then(Value::as_str)?;\n        // Cheap content fingerprint: length + base64 head/tail. Dedups the SAME image\n        // re-injected across context windows without holding/hashing the full payload.\n        let head: String = d.chars().take(32).collect();"
        }
      ],
      "instrument": "The pairing census of the previous claim, filtered to the records where the echo and the nested block disagree. Counting rule: one observation per record. Measured 2026-09-02: 9 of 444 disagree, and in all 9 the echo string has length 0 while the block's `source.data` measures 118,908-153,900 characters. Cross-check: `csift image @<session> --format json | jq -r .b64_len` reports the block's length on those rows, never 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) the IMG-013 pairing census, filtered to the records where the echo and the nested block disagree  (b) csift image @<session> --no-subagents --format json | (extract line, media_type, b64_len) for the two sessions holding those records",
          "observed": "(a) 9 of 444 disagree; in all 9 the echo string has length 0 while the nested block's source.data measures 118,908 / 129,212 / 130,588 / 138,368 / 141,016 / 153,900 characters.  (b) csift reported b64_len 130588, 138368, 141016 (three rows, one per record, ids L<line>i1) in one session and 153900, 118908, 129212 in the other - six rows, none 0, matching the census lengths one for one.",
          "rule": "One observation per record with an object `file` and type 'image'. The 9 disagreeing observations are 6 distinct record uuids (three of them appear in two copies of the same session under two project directories); the csift cross-check enumerates the 6 distinct records, one row each.",
          "note": "Reproduces exactly, and the csift cross-check the claim proposes was actually run: on every record whose toolUseResult.file.base64 is the empty string, csift's b64_len is the nested block's length, never 0. Counting-rule footnote for a stranger rerunning it: '9 of 444' is the file-level observation count and the corpus duplicates one session across two project directories, so the distinct-record form of the same fact is '6 of 315'. Either way, the direction of the claim is unchanged - the echo is unreliable, the content block is authoritative."
        }
      ]
    },
    {
      "id": "IMG-015",
      "area": "images",
      "behavior": "The `pages` key is stripped before a `parts` Read result is persisted - 0 of 26 observed records carry it at the top level or inside `file`, and the 2.1.258 schema documents it as in-process only, 'not retained on the tool_use_result, so this key is absent on the emitted/persisted result'. That is a fact about the KEY, not about the bytes. Since 2.1.251 the page images DO reach the transcript, delivered in the persisted tool_result CONTENT as one `{type:\"image\"}` block per extracted page (block count equals `file.count`, bytes identical to the `page-N.jpg` files in the recorded `outputDir`). On records written by 2.1.196 through 2.1.231 that same content is instead the plain string `PDF pages extracted: N page(s) from <path>`, with no image blocks anywhere, and the output directory is the only copy.",
      "depends": "csift's `image` scans inline base64 in BOTH a direct image block and an `{type:\"image\"}` element nested in a tool_result content array, so on a 2.1.251-or-later `parts` Read the pages are surfaced as ordinary `L<line>i<n>` rows and written out by `image --out` - a PDF page is recoverable from the transcript alone, no csift change required. On an older record there is nothing inline, `image` correctly reports nothing, and the recorded output directory is the only pointer: the invisibility is a version boundary, not a permanent source fact. `recover --file <the pdf>` has no text content to replay in either era, since the `parts` `file` object carries no `content` string.",
      "code": [
        {
          "path": "src/image/refs.rs",
          "lines": "299-302",
          "snippet": "/// Scan ONE transcript for images. Mirrors `recover`/`files`: mmap + a pre-JSON byte\n/// prefilter + parse only candidate lines (line-numbered, 1:1 with the file). Returns the\n/// images and the malformed-line count (surfaced, never hidden).\npub(crate) fn images_in_file(path: &Path, with_data: bool) -> Result<(Vec<ImageRef>, usize)> {"
        },
        {
          "path": "src/image/refs.rs",
          "lines": "231-234",
          "snippet": "/// Collect every image carried by one record, in document order: a direct `Block::Image`\n/// (the user-sent / assistant case) OR an `{type:\"image\"}` element nested in a\n/// `Block::ToolResult.content` array (a tool screenshot). `img_index` is a 1-based running\n/// ordinal across both sources, so the id `L<line>i<n>` is stable."
        },
        {
          "path": "src/image/refs.rs",
          "lines": "255-266",
          "snippet": "            Block::ToolResult {\n                content: Some(content),\n                ..\n            } => {\n                if let Some(arr) = content.as_array() {\n                    for el in arr {\n                        if el.get(\"type\").and_then(Value::as_str) == Some(\"image\") {\n                            if let Some(src) = el.get(\"source\") {\n                                if let Some(mut r) = image_ref_from_source(src, with_data) {\n                                    r.img_index = out.len() + 1;\n                                    out.push(r);\n                                }"
        }
      ],
      "instrument": "Measured 2026-09-02 over the 7,814 transcript files under ~/.claude/projects with a python3 walk that json-parses every line carrying `toolUseResult` and keeps the ones whose `toolUseResult.file` is an object, filtered to `type == 'parts'` and testing for a `pages` key at the top level and inside `file`. Counting rule: one observation per record. Result: 0 of 26 carry `pages` at either level. Schema authority: the Read tool-result schema in the Claude Code 2.1.258 binary documents the page images as not retained on the persisted tool result, and the result is stripped of that key before it is written.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "(a) python3 walk over ~/.claude/projects filtered to toolUseResult.type == 'parts', testing for a `pages` key at the top level and inside `file`, and counting the {type:\"image\"} elements nested in the record's tool_result content, bucketed by the record's `version` field  (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'pages:R\\(c\\(\\{base64:i\\(\\).{600}'  (c) csift image @<session> --no-subagents --format json, filtered to the jsonl line of a 2.1.251 parts record  (d) ls -la on the outputDir that same record names",
          "observed": "(a) 26 parts results; `pages` present at top level on 0 and inside `file` on 0. Nested {type:\"image\"} elements per parts record: 0 on 24 records, 5 on one, 3 on one. Split by writer version: the two records WITH nested image blocks were written by 2.1.251 (and are the two carrying top-level firstPage); the 24 with none were written by 2.1.196 (1), 2.1.207 (2), 2.1.220 (15), 2.1.231 (6), and on those the tool_result content is not an array at all but the plain string 'PDF pages extracted: N page(s) from <path>'. On both 2.1.251 records the nested image count equals file.count.  (b) verbatim binary string: 'pages:R(c({base64:i().describe(\"Base64-encoded page image; empty when the page could not be processed\"),mediaType:e.describe(\"The MIME type of the image\"),error:i().optional()...})).optional().describe(\"Extracted page images, in page order. Present only transiently in-process: the page image bytes are delivered solely as image blocks in the model-facing tool_result content and are not retained on the tool_use_result, so this key is absent on the emitted/persisted result\")'.  (c) csift emitted five rows for that one jsonl line - L<line>i1..i5, media_type image/jpeg, b64_len 58928 / 124844 / 111072 / 125080 / 125124.  (d) the named outputDir holds page-1.jpg .. page-5.jpg, 44,196 / 93,631 / 83,303 / 93,810 / 93,841 bytes; 44,196 bytes base64-encodes to exactly 58,928 characters, matching csift's first row.",
          "rule": "One observation per record with an object `file` and type 'parts' (26 observations, 26 distinct record uuids). Nested-image count is per record; the version split is the record's own `version` field. The csift count is rows emitted for a single jsonl line.",
          "note": "The literal `pages`-key half of the claim still holds - 0 of 26 records carry it at either level, and the 2.1.258 schema documents that key as in-process only and 'absent on the emitted/persisted result'. But the conclusion drawn from it no longer describes current Claude Code, and it was the operative half. NEW BEHAVIOR: since 2.1.251 the page bytes DO reach the transcript. The persisted tool_result content for a parts Read is now an array carrying one {type:\"image\"} block per extracted page (5 and 3 blocks, each equal to file.count) plus one text element, and those blocks are byte-for-byte the page-N.jpg files in the outputDir. On every record written by 2.1.196 through 2.1.231 the same content is a plain string, 'PDF pages extracted: N page(s) from <path>', with no image blocks anywhere - that is the shape the claim was written against. Consequences for csift: the pages are NOT invisible to `image` any more - `csift image` surfaces them as ordinary L<line>i<n> rows and `image --out` will write them, so a PDF page is now recoverable from the transcript alone, and the claim's 'the absence is a source fact, not a csift miss' is false on current data. What is still true: outputDir remains the on-disk copy, and `recover --file <the pdf>` still has no text content to replay (see IMG-012). The claim should be rewritten as a version-boundary fact rather than a permanent absence."
        }
      ]
    },
    {
      "id": "REG-001",
      "area": "live-registry",
      "behavior": "Claude Code keeps a session registry under its home directory, one JSON row per live top-level session at `<claude-home>/sessions/<pid>.json`. The observed key set is `pid`, `sessionId`, `cwd`, `startedAt` (millisecond epoch), `procStart` (the owner process's creation instant), `version`, `kind`, `entrypoint`, `status`, `updatedAt` and `statusUpdatedAt`, plus row-dependent `peerProtocol`, `messagingSocketPath`, `name`, `nameSince`, `nameSource`, `peerFeatures`, `pidDomain`, `bridgeSessionId` and `tmux`. `kind` is a closed four-value set (`interactive` | `bg` | `daemon` | `daemon-worker`), of which only `interactive` occurs on an interactive host; `entrypoint` is NOT a closed pair - the writer copies `$CLAUDE_CODE_ENTRYPOINT` through verbatim and the reader accepts any string, so `cli` and `sdk-cli` are two members of an open set. The row is keyed on disk by pid, and the session it describes is named by the `sessionId` field, not by the file name.",
      "depends": "`csift status` and `csift wait` join this row as one of three verdict surfaces, looking it up by scanning the directory for the matching `sessionId` and reading `pid`, `status`, `statusUpdatedAt`, `procStart` and `pidDomain`; a renamed key silently drops the registry leg out of the join and every verdict degrades to the no-registry lane.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "1-5",
          "snippet": "//! The harness session registry: `<claude-home>/sessions/<pid>.json`.\n//!\n//! Field shape (verified on disk, CC 2.1.258): `{pid, sessionId, cwd, startedAt(ms),\n//! procStart(str), version, kind:\"interactive\", entrypoint:\"cli\"|\"sdk-cli\", pidDomain,\n//! status, updatedAt, statusUpdatedAt, bridgeSessionId?, tmux?, ...}`. `status` is the"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "40-44",
          "snippet": "pub(crate) fn registry_row_for(session_id: &str) -> Result<Option<RegistryRow>> {\n    let dir = crate::path::claude_home()?.join(\"sessions\");\n    if !dir.is_dir() {\n        return Ok(None);\n    }"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "57-59",
          "snippet": "        if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n            continue;\n        }"
        }
      ],
      "instrument": "`python3 -c \"import json,glob,pathlib;[print(sorted(json.load(open(p)))) for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))]\"` - every row prints a superset of {pid, sessionId, cwd, startedAt, procStart, version, kind, entrypoint, status, updatedAt, statusUpdatedAt}; then `csift status @<session> --format json | jq '.evidence[] | select(.surface==\"registry\")'` shows the row csift joined. Counting rule: one observation per `.json` file in that directory (non-`.json` sidecars excluded by extension), at one instant - the directory is current state, not history.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; AGENTS.md section 1; CHANGELOG 0.9.0; src/live/registry.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import json,glob,os,pathlib;[print(os.path.basename(p),json.load(open(p)).get('pid'),sorted(json.load(open(p)))) for p in sorted(glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json')))]\"  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'pid:process.pid,sessionId:.{0,220}'  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var .{1,3}=\\[\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"\\]'",
          "observed": "7 rows at the first sampling instant (10 rows 40 min later, as short-lived sessions registered and exited). All 7/7 carry a superset of {pid, sessionId, cwd, startedAt, procStart, version, kind, entrypoint, status, updatedAt, statusUpdatedAt}; the extras seen are exactly {bridgeSessionId, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pidDomain}; `tmux` appeared on 0/7. File-name stem == the row's own `pid` field on 7/7. `sessionId` matched the 36-char uuid shape on 7/7 (and 8/8, 10/10 at later instants); `startedAt` was a 13-digit int on 7/7. Value census: kind=interactive 7/7; entrypoint=cli 7/7; version spread over six binary generations; pidDomain=darwin on 4/7 and absent on 3/7 (older rows). Binary writer, verbatim: `S({pid:process.pid,sessionId:Q(),cwd:ye(),startedAt:Date.now(),procStart:await ja(process.pid),version:{...}.VERSION,peerProtocol:Fzt,peerFeatures:Pwr(),kind:o,entrypoint:a.CLAUDE_CODE_ENTRYPOINT,pidDomain:await YG(),...C&&{tmux:C},...{messagingSocketPath:...},...{name:I?.name,nameSource:...,nameSince:Date.now(),logPath:...,agent:...,jobId:...,spare:...}})` written to `Ct(_,`${process.pid}.json`)`; the directory reader filters `/^\\d+\\.json$/`. `kind` is validated by a closed set: `var $e=[\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"];function Ge(e){return $e.includes(e)?e:void 0}`.",
          "rule": "One observation per `*.json` file in ~/.claude/sessions at one instant (the `<pid>.<64hex>.key` messaging-auth sidecars in the same directory are excluded by extension). A key counts as present if it appears at the JSON top level; a value census counts one value per file. Binary side: one quoted string per pattern from `strings -n 6` over the 2.1.258 binary.",
          "note": "Holds as an on-disk observation; refined because the claim's parenthetical value sets read as closed and only one of them is. `kind` really is closed (a 4-value validator in the binary), `entrypoint` is not (verbatim env passthrough; the binary's own entrypoint switch also names claude-vscode, remote*, sdk-ts, sdk-py, mcp, local-agent and more). `tmux` was on 0/7 rows here, which is consistent with the claim calling it row-dependent, not a refutation. csift code sites confirmed verbatim: src/live/registry.rs lines 1-5, 40-44 and 57-59 are unchanged."
        }
      ]
    },
    {
      "id": "REG-002",
      "area": "live-registry",
      "behavior": "The registry row's `status` is the binary's closed four-value set `busy | shell | idle | waiting`, byte-identical across every binary generation on this machine (2.1.229 through 2.1.258). `waiting` is written whenever a dialog blocks the session - a queued elicitation (`input needed`), the top dialog's own label, a worker request, a sandbox request, or an open local command dialog - with the reason mirrored into a companion `waitingFor` string field on the same row; `busy` covers loading and delegating; `idle` is the default. On disk `idle`, `busy` and `shell` were observed. The set is the harness's, not csift's, and must be treated as extensible.",
      "depends": "`csift status` maps registry `waiting` onto its blocked-on-human verdict and ranks the remaining values for running-shape; a new running-ish value csift does not list would make an actively working session read as idle at end of turn and satisfy `wait --until stop` early.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "5-9",
          "snippet": "//! status, updatedAt, statusUpdatedAt, bridgeSessionId?, tmux?, ...}`. `status` is the\n//! binary's closed set `busy | shell | idle | waiting`: `waiting` whenever the session is\n//! blocked on a dialog (a question, a permission prompt, a plan approval, a sandbox or\n//! worker request), `busy` while loading or delegating, else `idle`. A print-mode\n//! (`claude -p`) session writes a row too, with `status: null` that never transitions."
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "291-298",
          "snippet": "    // Two HITL legs beside the sidecar: the registry's `waiting` status (the binary sets\n    // it whenever a dialog blocks the session: a question, a permission prompt, a plan\n    // approval, a sandbox/worker request), and an unreturned AskUserQuestion/ExitPlanMode\n    // at the tail (a MULTI-question ask is written at question time since CC 2.1.258;\n    // a single-question ask stays buffered until answered - the sidecar's shape).\n    let registry_waiting = registry\n        .and_then(|r| r.status.as_deref())\n        .is_some_and(|s| s == \"waiting\");"
        }
      ],
      "instrument": "`python3 -c \"import json,glob,pathlib,collections;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))))\"` while several sessions run, and repeat while one sits on a permission prompt to catch `waiting`. Counting rule: one status observation per registry file per sampling instant; the file is point-in-time, so a value seen zero times is not evidence it cannot occur.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; src/live/registry.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var .{1,3}=\\[\"busy\",\"shell\",\"idle\",\"waiting\"\\];function .{1,3}\\(e\\)\\{return .{1,3}\\.includes\\(e\\)\\?e:void 0\\}'  AND  rg -o 'function XBe\\(T\\)\\{let I=o_o\\(T\\).{0,320}'  AND  for v in 2.1.229 2.1.234 2.1.241 2.1.251 2.1.257 2.1.258; do strings -n 6 ~/.local/share/claude/versions/$v | rg -o '\\[\"busy\",[^]]{0,80}\\]' | sort -u; done  AND  python3 -c \"import json,glob,pathlib,collections;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))))\"",
          "observed": "Binary, verbatim: `var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0}` - the reader's closed set. The producer, verbatim: `function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"idle\",waitingFor:void 0,working:T.isQueryActive}}function o_o(T){if(T.queuedElicitation)return\"input needed\";if(T.topDialogWaitingFor!==void 0)return T.topDialogWaitingFor;if(T.pendingWorkerRequest)return\"worker request\";if(T.pendingSandboxRequest)return\"sandbox request\";if(T.isShowingLocalJSXCommand&&!T.isResponseStreaming&&!T.delegatedActive)return\"dialog open\";return}`. Across all six binary generations present on this machine (2.1.229, 2.1.234, 2.1.241, 2.1.251, 2.1.257, 2.1.258) the array is byte-identical: `[\"busy\",\"shell\",\"idle\",\"waiting\"]` - 6/6. The only arrays containing `\"blocked\"` in any of those binaries are `[\"active\",\"idle\",\"blocked\"]` (the `tempo` field), `[\"allow\",\"ask\",\"blocked\"]` / `[\"allow\",\"ask\",\"ask-session\",\"blocked\"]` (permission), `[\"proceed\",\"confirm\",\"blocked\"]` and a task-list vocabulary - never the registry status set. On-disk census over three sampling instants: {idle: 4, busy: 2, shell: 1} then {idle: 6, busy: 2, shell: 1, ...} - `waiting` observed 0 times (no dialog was blocking any session while sampling).",
          "rule": "One status observation per registry `.json` file per sampling instant; a value seen zero times on disk is not evidence it cannot occur, which is why the binary's validator array is the authority for the SET and the disk census only for what occurred. Binary side: one distinct array per generation, deduped with `sort -u`.",
          "note": "The main body of the claim is confirmed by the binary itself, including the exact dialog sources it lists. Refined on two points: the `blocked` sub-clause is unsupported for every generation testable here, and the row also carries a `waitingFor` companion naming WHICH dialog blocked - a field csift does not read and a stronger HITL signal than `status:waiting` alone. csift code sites confirmed verbatim: src/live/registry.rs 5-9 and src/live/verdict.rs 291-298 unchanged."
        }
      ]
    },
    {
      "id": "REG-003",
      "area": "live-registry",
      "behavior": "The registry status `shell` is not a working state: the harness computes `idle` and then relabels it `shell` while a local background shell task is open, so `shell` means idle at end of turn WITH a background shell still running. `busy` is the only registry status that signals the session is doing work.",
      "depends": "csift's running-shape test accepts only an unreturned tail call or registry `busy`, and folds `shell` into the end-of-turn shape instead, where it also serves as a cross-check on the background section (a `shell` row with nothing counted means a background shell was launched in an unscanned lane or excluded by the lens). Reading `shell` as running - as csift did through v0.10.0 - makes an idle session with a dev server open never satisfy `wait --until stop`.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "303-308",
          "snippet": "    // The registry's `shell` status is IDLE WITH A BACKGROUND SHELL RUNNING (the binary\n    // computes `idle` and then relabels it `shell` while a local_bash task is open) -\n    // never a running shape. `busy` is the only registry running signal.\n    let registry_status = registry.and_then(|r| r.status.as_deref());\n    let registry_shell = registry_status == Some(\"shell\");\n    let running_shape = main_tail.unreturned_use.is_some() || registry_status == Some(\"busy\");"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "309-314",
          "snippet": "    let eot_shape = main_tail.unreturned_use.is_none()\n        && (main_tail\n            .last_stop_reason\n            .as_deref()\n            .is_some_and(|s| s == \"end_turn\")\n            || registry_shell);"
        }
      ],
      "instrument": "Launch one long `run_in_background` shell command in a session, let the turn finish, then read that session's `~/.claude/sessions/<pid>.json`: `status` is `shell` while the command runs and returns to `idle` once it completes, with no prompt in flight either time. Counting rule: one status reading per known background-shell state (open, then closed), taken from a second session so the observed one stays idle.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "src/live/verdict.rs rank_verdict comment; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'let PIr=W\\(Y_o\\),Nne=VA===\"idle\"&&PIr\\?\"shell\":VA.{0,260}'  AND  rg -o 'function Y_o\\(FIr\\)\\{return Fhn\\(FIr.tasks\\)\\}' AND rg -o 'function Fhn\\(h\\)\\{.{0,90}\\}function l9t\\(h\\)\\{.{0,70}\\}'  AND  a 250 ms poll of ~/.claude/sessions/*.json over 240 s recording every (status, statusUpdatedAt) change  AND  csift status @<session> --format json | jq '.background.tasks[].kind'",
          "observed": "Binary, verbatim: `let PIr=W(Y_o),Nne=VA===\"idle\"&&PIr?\"shell\":VA` - the computed status VA is relabelled `shell` only when it is `idle` and PIr is true - and the very next effect writes exactly that relabelled value: `B_o=()=>{l$e({status:Nne,waitingFor:oUe},tUe).catch(J_o)}`. PIr's predicate, verbatim: `function Y_o(FIr){return Fhn(FIr.tasks)}` with `function Fhn(h){return Object.values(h).some(l9t)}function l9t(h){return h.type===\"local_bash\"&&!Fs(h.status)}` - i.e. some unsettled local_bash (background shell) task. Live: in a 240 s poll I caught one session go `shell -> busy` and then `busy -> shell`, the second transition landing 14 666 ms after that turn's newest user-turn record - an end-of-turn transition into `shell`, never into a running state. That same session's background section, read by csift's whole-transcript scan, showed 52 open background tasks including a `shell` kind while its registry row read `shell`.",
          "rule": "One quoted producer expression from the binary (the relabel and the write are adjacent statements in one function, so the chain is direct, not inferred). Live side: one status reading per registry file per 250 ms tick over 240 s; a transition counts when the parsed `status` differs from the previous tick's for the same file. Background side: one `kind` per open task row in csift's JSON, counted once.",
          "note": "Confirmed at the producer, not just by correlation: `idle` is computed first and only then relabelled, so `shell` is idle-at-end-of-turn with a background shell open. One caveat a reader should know: the harness's own fleet UI renders `busy` and `shell` with the same working glyph (`if(m===\"busy\"||m===\"shell\")return{word:co.working,...}`), so a screenshot of Claude Code's UI is NOT evidence about the registry's meaning - the written state is what counts. Note also that the csift binary on PATH here is 0.10.0, which predates the shell-to-end-of-turn fold; the fold is present in the working tree, and src/live/verdict.rs lines 303-308 and 309-314 match the claim's snippets verbatim."
        }
      ]
    },
    {
      "id": "REG-004",
      "area": "live-registry",
      "behavior": "`statusUpdatedAt` alone is TRANSITION-written, never a heartbeat: the writer stamps it only when the patch carries a `status`, so an hours-old `statusUpdatedAt` means the state has not changed rather than that the session is gone - a specimen sat `busy` for 2 h 07 m with its pid alive and its transcript written 8 minutes earlier. `updatedAt` is NOT co-equal: sibling writers stamp it on non-status patches (2 of 10 rows had it ahead of `statusUpdatedAt`, by 91 ms and by 93 s), and the file's mtime is not an instrument at all - writers that carry neither timestamp rewrite the row silently (4 of 7 rows had mtime ahead of both, by up to 25.0 h, and three rewrites with a 0 ms `statusUpdatedAt` delta were caught live).",
      "depends": "csift renders the `statusUpdatedAt` age as evidence but never derives liveness from registry recency, joining the transcript tail and a pid probe instead; if the harness ever switched to heartbeat writes, the age column would change meaning and the stale-dead branch would need re-deriving.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "30-31",
          "snippet": "    /// Millisecond epoch of the last status TRANSITION (not a heartbeat).\n    pub(crate) status_updated_at_ms: Option<i64>,"
        },
        {
          "path": "src/live.rs",
          "lines": "9-12",
          "snippet": "//!    transitions land sub-second, but the file is written ONLY on transitions (never a\n//!    heartbeat: an hours-old `statusUpdatedAt` just means the state has not changed;\n//!    the real failure mode is a SIGKILLed session's stale entry, guarded by pid\n//!    liveness + a process-start-time check against pid reuse);"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "171-173",
          "snippet": "            age_secs: r\n                .status_updated_at_ms\n                .map(|ms| (jiff::Timestamp::now().as_millisecond() - ms).max(0) / 1000),"
        }
      ],
      "instrument": "For each `~/.claude/sessions/*.json`, compare `updatedAt` against `statusUpdatedAt` and watch `statusUpdatedAt` across a poll. Do NOT use the file mtime: it advances on writes that touch neither timestamp, so an mtime/timestamp gap is expected, not a refutation.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "AGENTS.md section 1; src/live/registry.rs module doc; src/live.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import json,glob,os,pathlib;[print(json.load(open(p)).get('updatedAt')-json.load(open(p)).get('statusUpdatedAt'), int(os.stat(p).st_mtime*1000)-max(json.load(open(p)).get('updatedAt'),json.load(open(p)).get('statusUpdatedAt'))) for p in glob.glob(str(pathlib.Path.home()/'.claude/sessions/*.json'))]\"  AND  a 200 ms poll of the same directory over 150 s recording every content change  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function l\\$e\\(e,n\\).{0,230}' AND rg -o 'async function XEt\\(e,n\\)\\{await Sn\\(\\{bridgeSessionId:e\\},n\\)\\}'",
          "observed": "First instant, 7 rows: `updatedAt - statusUpdatedAt` = 0 ms on 7/7, but `mtime - updatedAt` = 4, 3, 9, 14 ms on four rows and 253 811 ms, 5 676 415 ms and 90 149 163 ms (25.0 h) on the other three - so 0/7 rows had all three equal. Second instant, 10 rows: 2/10 had `updatedAt` AHEAD of `statusUpdatedAt` by 91 ms and 93 131 ms. During a 150 s poll, three separate files were rewritten with `statusUpdatedAt` unchanged (delta exactly 0 ms) and mtime landing 992-1 497 ms later. The writer explains all of it, verbatim: `async function l$e(e,n){let r=Date.now(),...,f=await Sn({...e,updatedAt:r,...e.status!==void 0&&{statusUpdatedAt:r},...},n);...}` - `statusUpdatedAt` is stamped ONLY when the patch carries a status - while sibling writers touch the file without it, e.g. `async function XEt(e,n){await Sn({bridgeSessionId:e},n)}` (neither timestamp) and `async function UHr(e,n){await Sn({messagingSocketPath:e,updatedAt:Date.now()},n)}` (updatedAt only). Substance re-measured on a fresh specimen: one row sat `busy` for 7 635 s (2 h 07 m) with its pid alive under `ps -p` and its transcript last written 468 s ago.",
          "rule": "One comparison per registry file per sampling instant: `updatedAt - statusUpdatedAt` and `int(os.stat(p).st_mtime*1000) - max(updatedAt, statusUpdatedAt)`, both in milliseconds. Poll side: a change counts when the (status, statusUpdatedAt, updatedAt, mtime) tuple for one file differs from the previous 200 ms tick.",
          "note": "The claim's own stated refutation trigger fired - rows whose mtime advances past `statusUpdatedAt` - but it fired on the INSTRUMENT, not on Claude Code: the behavior is unchanged and is now confirmed at the writer rather than by sample agreement. The `6 of 6 rows agree to the millisecond` figure was an artifact of a small, freshly-transitioned sample; it does not reproduce (0 of 7 here). Anything downstream that treated mtime as a transition marker should stop. csift is unaffected - it reads `statusUpdatedAt` only - and the cited sites src/live/registry.rs 30-31, src/live.rs 9-12 and src/live/verdict.rs 171-173 match verbatim."
        }
      ]
    },
    {
      "id": "REG-005",
      "area": "live-registry",
      "behavior": "Registry status transitions land sub-second: a session's own row read `status: \"busy\"` with `statusUpdatedAt` equal to that turn's prompt-submit instant.",
      "depends": "csift trusts a fresh `busy` as running-shaped evidence with no debounce; a lagging write would make `status` report idle-at-end-of-turn during the first moments of a turn.",
      "code": [
        {
          "path": "src/live.rs",
          "lines": "8-9",
          "snippet": "//! 1. the harness's session registry (`<claude-home>/sessions/<pid>.json`) - `status`\n//!    transitions land sub-second, but the file is written ONLY on transitions (never a"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "71",
          "snippet": "            status_updated_at_ms: v.get(\"statusUpdatedAt\").and_then(serde_json::Value::as_i64),"
        }
      ],
      "instrument": "Submit a prompt in one session, then immediately read that session's `~/.claude/sessions/<pid>.json` from a second session: `status` is `busy` and `statusUpdatedAt` is within a second of the prompt instant. Counting rule: one paired reading (submit instant against `statusUpdatedAt`); it needs two concurrent sessions on one machine, since the observing read must not itself be the turn.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "A 200 ms poll of ~/.claude/sessions/*.json over 240 s; on any transition into busy/waiting/shell, read that row's `statusUpdatedAt` and compare it against the newest user-turn record timestamp in the same session's transcript via `csift show @<session> --turn -1 --format json` (field `ts_utc`)",
          "observed": "One `shell -> busy` transition caught: `statusUpdatedAt` landed 8 ms BEFORE that turn's newest user-turn record timestamp (signed delta -8 ms), and my poll detected the written file 227 ms after `statusUpdatedAt` with a 200 ms tick - so the file was on disk within roughly one tick of the write. Separately, four brand-new registry rows appeared during a 150 s poll with a detect-lag of 151, 583, 612 and 756 ms between their `statusUpdatedAt` and my observation, all under one second. The mechanism agrees: the write is an effect keyed on the status value and `l$e` stamps `statusUpdatedAt: Date.now()` at write time, so the stamp is the write instant, not a replayed event time.",
          "rule": "One paired reading per caught transition: (registry `statusUpdatedAt`) minus (the newest `ts_utc` among the records of that session's last turn), in milliseconds; plus (observation instant) minus `statusUpdatedAt` as an upper bound on write-to-visible latency, which cannot be smaller than the poll interval. Two concurrent sessions are required, since the observing read must not itself be the turn - satisfied here, the observed session was not the one running the probe.",
          "note": "Measured, not inferred: 8 ms between the prompt-submit instant and the registry `busy` stamp, on a session other than the observer. The sign is negative (the registry stamp very slightly precedes the transcript record's timestamp), which is consistent with the transcript record being an async flush of the completed message while the registry write is a synchronous effect on the state change. csift code sites src/live.rs 8-9 and src/live/registry.rs line 71 match verbatim."
        }
      ]
    },
    {
      "id": "REG-006",
      "area": "live-registry",
      "behavior": "The session registry covers TOP-LEVEL sessions only: a subagent transcript has no `<pid>.json` row anywhere, and no per-subagent registry exists in the harness.",
      "depends": "csift looks the row up by the OWNER (top-level) session id derived from the transcript path, and when a subagent target finds no row it emits an explicit note and falls back to tail plus children evidence rather than fabricating a row; session lineage is never read out of the registry.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "10-11",
          "snippet": "//! TRANSITION-writes only - never a heartbeat. Coverage: TOP-LEVEL sessions only; a\n//! subagent has no row anywhere and csift never fabricates one."
        },
        {
          "path": "src/live/status.rs",
          "lines": "63-66",
          "snippet": "    let owner_id =\n        crate::subagent::parent_session_id_from_path(main).unwrap_or_else(|| session_id.clone());\n\n    let registry = registry_row_for(&owner_id)?;"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "175-180",
          "snippet": "    } else if is_subagent_target {\n        notes.push(\n            \"the registry covers top-level interactive sessions only - a subagent has no \\\n             row; verdict from tail + children evidence\"\n                .to_string(),\n        );"
        }
      ],
      "instrument": "`jq -r '.sessionId' ~/.claude/sessions/*.json | sort -u` yields only uuid-shaped ids, never a bare subagent hex; then run `csift status` against a subagent target and read the notes - expect the explicit no-registry note, never a fabricated status. Counting rule: distinct `sessionId` values across the directory, one row per live top-level process; zero rows for subagent ids.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; AGENTS.md section 1; src/live/registry.rs module doc; dev session 2026-08-30; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 counting uuid-shaped vs bare-hex `sessionId` values across ~/.claude/sessions/*.json, intersected against the set of subagent ids taken from every `.../subagents/**/agent-*.jsonl` stem on disk  AND  csift status @<subagent> --format json for two subagent targets - one whose owner session IS in the registry, one whose owner is NOT",
          "observed": "8 registry rows at that instant, `sessionId` matched the 36-char uuid shape on 8/8, bare-hex on 0/8, 8 distinct values (7/7 and 10/10 uuid-shaped at two other instants). 7 536 subagent transcripts on disk; the intersection of their canonical agent ids with the registry's `sessionId` set was 0. csift on a subagent whose owner is unregistered returned verdict `unknown`, zero registry evidence rows, and exactly the note `the registry covers top-level interactive sessions only - a subagent has no row; verdict from tail + children evidence`. csift on a subagent whose owner IS registered attached the OWNER's row as evidence (`surface: registry`, `status busy (pid ...)`), never a fabricated subagent row.",
          "rule": "Distinct `sessionId` values across the directory, one row per live top-level process; a subagent id counts as covered only if it appears verbatim as some row's `sessionId`. Subagent ids are the canonical `agent-` stripped stems, one per transcript file.",
          "note": "Both halves confirmed with an instrument: zero of 7 536 subagent ids has a row, and csift's two fallback paths behave as claimed - owner-row join when the owner is live, explicit no-row note otherwise. csift code sites src/live/registry.rs 10-11, src/live/status.rs 63-66 and src/live/verdict.rs 175-180 match verbatim."
        }
      ]
    },
    {
      "id": "REG-007",
      "area": "live-registry",
      "behavior": "Claude Code's task tools persist the session task list as one JSON file per task under `<claude-home>/tasks/<owner>/`, and the owner directory takes either of TWO name forms on real disks: the full session uuid, or the newer `session-` prefix followed by the first 8 characters of that uuid.",
      "depends": "csift's tasks section probes BOTH directory names for one owner and merges them; reading only the uuid form silently reports no tasks for every session written under the newer layout, and an absent directory means the session never used the task tools (no section, not an error).",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "1-5",
          "snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file"
        },
        {
          "path": "src/live/tasks.rs",
          "lines": "37-41",
          "snippet": "    let tasks_root = home.join(\"tasks\");\n    let mut dirs = vec![tasks_root.join(owner_id)];\n    if let Some(prefix) = owner_id.get(..8) {\n        dirs.push(tasks_root.join(format!(\"session-{prefix}\")));\n    }"
        }
      ],
      "instrument": "`ls ~/.claude/tasks/ | sed 's/^session-.*/session-form/;s/^[0-9a-f-]\\{36\\}$/uuid-form/' | sort | uniq -c` - both shapes appear; then `csift status @<session> --format json | jq '{tasks, tasks_completed}'` for a session under each shape (a null `tasks` means no directory, `[]` means a directory with nothing open). Counting rule: one observation per owner directory, classified by name shape. The e2e tests `p13_settled_children_fold_and_tasks_section` and `p14_no_tasks_dir_means_null_not_empty` in tests/cli/live/status.rs pin both forms and the absent-directory case.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; AGENTS.md section 1; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "ls ~/.claude/tasks/ | sed 's/^session-.*/session-form/;s/^[0-9a-f-]\\{36\\}$/uuid-form/' | sort | uniq -c  AND  for one owner present in BOTH forms, count `*.json` in each directory and compare against `csift status @<session> --format json | jq '{n_open: (.tasks|length), tasks_completed}'`  AND  the same csift call for a session with no tasks directory at all",
          "observed": "168 owner directories: 152 `session-<8 hex>` form, 16 full-uuid form, 0 of any other shape. Merge check on one owner present in both forms: its uuid-named directory held 7 task files (1 not-completed, 6 completed) and its `session-<first 8>` sibling held 21 (9 not-completed, 12 completed); csift reported exactly 10 open and 18 completed - the union of both directories, with no cap and nothing double-counted. A second owner, present only in the `session-` form with 27 files, reported 10 open / 17 completed = 27. A session with neither directory reported `tasks: null` (not `[]`) and `tasks_completed: null`.",
          "rule": "One observation per owner directory, classified by name shape; for the merge, one file per `*.json` in each of the two candidate directories, with `open` counted as status != completed, and the csift numbers must equal the sum over both directories.",
          "note": "Both directory forms exist on a live disk in quantity, and the merge is not just claimed but arithmetically checked (7 + 21 files -> 10 open / 18 completed). The three-way distinction the claim draws - null for no directory, [] for an empty one, rows otherwise - reproduced. csift code sites src/live/tasks.rs 1-5 and 37-41 match verbatim."
        }
      ]
    },
    {
      "id": "REG-008",
      "area": "live-registry",
      "behavior": "Each task file carries `{id, subject, description, status, blocks, blockedBy}` on every file, plus `activeForm` on most (316 of 402 here) and occasionally `owner` or `metadata`; `blocks` and `blockedBy` are arrays. The ids are STRINGS on disk - a short all-digit ordinal of 1 to 3 characters, e.g. `\"13\"` - and the file name is that id. The `status` set is OPEN: `pending`, `in_progress` and `completed` are the values observed on disk, and the harness's own task-update schema additionally accepts `deleted`.",
      "depends": "csift treats anything that is not `completed` as an open row rendered with its verbatim status (in-progress rows first, then numeric id order, blockers named), and tolerates a bare-number id; a closed status enum or a numeric-id assumption would drop rows the harness still considers open.",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "5-10",
          "snippet": "//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim\n//! status. This is a live-truth read (current values only, no history) - the same\n//! carve-out `status` itself lives under."
        },
        {
          "path": "src/live/tasks.rs",
          "lines": "63-66",
          "snippet": "            if status == \"completed\" {\n                report.completed += 1;\n                continue;\n            }"
        },
        {
          "path": "src/live/tasks.rs",
          "lines": "91-97",
          "snippet": "/// Ids are strings on disk (\"13\") but tolerate a bare number.\nfn json_id(v: Option<&serde_json::Value>) -> String {\n    match v {\n        Some(serde_json::Value::String(s)) => s.clone(),\n        Some(serde_json::Value::Number(n)) => n.to_string(),\n        _ => \"?\".to_string(),\n    }"
        }
      ],
      "instrument": "`python3 -c \"import json,glob,collections,pathlib;print(collections.Counter(json.load(open(p)).get('status') for p in glob.glob(str(pathlib.Path.home()/'.claude/tasks/*/*.json'))))\"` for the status census and `jq 'keys, (.id|type)' ~/.claude/tasks/*/*.json | sort -u` for the shape. Counting rule: one observation per task `.json` file across both owner-directory forms.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.13; SPEC.md section 6 v0.9.4 ledger; AGENTS.md section 1; CHANGELOG 0.9.4; src/live/tasks.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import json,glob,pathlib,collections;ps=glob.glob(str(pathlib.Path.home()/'.claude/tasks/*/*.json'));print(len(ps));print(collections.Counter(json.load(open(p)).get('status') for p in ps));print(collections.Counter(k for p in ps for k in json.load(open(p))))\"  AND  a per-file id type/shape census  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"pending\",\"in_progress\",\"completed\",\"deleted\"\\]' with surrounding schema",
          "observed": "402 task files across the 168 owner directories, 0 unparseable. Status census: completed 270, pending 99, in_progress 33 - exactly the three claimed values, nothing else. Ids: 402/402 JSON strings, 402/402 all-digits, lengths 1 (112), 2 (282), 3 (8); the file-name stem equalled the `id` on 402/402. Key frequency: id 402, subject 402, description 402, status 402, blocks 402, blockedBy 402, but activeForm only 316/402 (78.6%), plus `owner` on 36 and `metadata` on 26. `blocks`/`blockedBy` were JSON arrays on 402/402. The binary's task-update input schema, verbatim, carries a FOURTH value: `qmo=m(()=>c({taskId:i(),status:ee([\"pending\",\"in_progress\",\"completed\",\"deleted\"]).optional(),subject:i().optional(),activeForm:i().optional()}))`, and the create schema makes activeForm optional too: `zmo=m(()=>c({subject:i(),activeForm:i().optional()}))`.",
          "rule": "One observation per task `.json` file across both owner-directory forms (402 files); a key counts as present if it appears at the JSON top level, so a key's count below 402 means it is optional. Status census counts one value per file. Binary side: the quoted schema is the tool's own input validator, so it bounds what the harness will ACCEPT, which is wider than what a disk snapshot shows.",
          "note": "Ids, the status trio and the arrays all reproduce exactly. Refined on three points the census forced: `activeForm` is present on 78.6% of files rather than all, two further keys occur (`owner`, `metadata`), and the harness accepts a fourth status `deleted` that no file on this disk carries - which is the one value that would render wrongly under the not-completed-means-open rule. Whether a `deleted` task's file survives on disk or is unlinked could not be determined here, since none exists. csift code sites src/live/tasks.rs 5-10, 63-66 and 91-97 match verbatim."
        }
      ]
    },
    {
      "id": "REG-009",
      "area": "live-registry",
      "behavior": "Only a BACKGROUND session writes a live in-flight registry: at `<claude-home>/jobs/<short-id>/state.json` it records `inFlight: {tasks, queued, kinds[], drainableMonitors, wake?}`. An interactive session returns early and writes nothing there, so no on-disk file states how many background tasks an interactive session currently has armed.",
      "depends": "csift's live layer does not read this file, so for an INTERACTIVE session the whole-file transcript scan is the only background-task instrument; if a live-armed count is ever wanted for a background session, this is the only place one exists.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "37-39",
          "snippet": "//! Never-returned launches sit 56-375 MB before EOF on real files, so this is a whole-\n//! file scan behind a five-needle byte prefilter (measured +0.2-0.4 s worst case), not a\n//! tail read."
        }
      ],
      "instrument": "`cat ~/.claude/jobs/*/state.json` on a machine that has only run interactive sessions: either no directory at all, or a row whose `inFlight` counts are zero with an empty `kinds` array. Counting rule: one file per background-job directory; the `kinds` array is populated only for a background session, so absence on an interactive-only machine is the expected observation, not a refutation.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "ls -la ~/.claude/jobs/ and python3 reading every ~/.claude/jobs/*/state.json  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function bre\\(e,n\\)\\{let r=a.CLAUDE_JOB_DIR;.{0,520}'  AND  rg -o 'inFlight:c\\(\\{tasks:A\\(\\),queued:A\\(\\),kinds:R\\(i\\(\\)\\).{0,190}'",
          "observed": "One job directory on this interactive-only machine (an 8-hex short id) plus a top-level `pins.json`; its `state.json` carries `inFlight: {\"tasks\": 0, \"queued\": 0, \"kinds\": [], \"drainableMonitors\": 0}` with `template: bg`, `state: done`, `tempo: idle`, and siblings `timeline.jsonl` and `tmp/`. The writer, verbatim: `async function bre(e,n){let r=a.CLAUDE_JOB_DIR;if(!r||a.CLAUDE_CODE_SESSION_KIND!==\"bg\")return;await p8e(async()=>{...await As(r,{...o,inFlight:{tasks:e.count,queued:d.queued,kinds:[...e.kinds],...e.drainableMonitors!==void 0&&e.drainableMonitors>0&&{drainableMonitors:e.drainableMonitors},...d.wake!==void 0&&{wake:d.wake}},updatedAt:new Date().toISOString()},n)...})}` - the guard returns before any write unless the process has a job directory AND its session kind is `bg`. The declared shape matches: `inFlight:c({tasks:A(),queued:A(),kinds:R(i()),drainableMonitors:A().int().nonnegative().optional(),wake:c({at:A().optional(),reason:i().optional(),fires:A().int().nonnegative(),keepalive:x(!0).optional()}).optional()})`. For contrast, the same machine carried 7-10 session-registry rows and 168 task directories at the same instant.",
          "rule": "One file per background-job directory. The `kinds` array is populated only for a background session, so all-zero counts and an empty `kinds` on an interactive-only machine is the expected observation, not a refutation. The early-return guard is read off the writer, so the absence of an interactive in-flight file is established at the producer rather than by absence of evidence.",
          "note": "Both halves confirmed. The interactive negative is the interesting one and it is now positive evidence rather than an absence argument: the writer's first statement bails unless `CLAUDE_JOB_DIR` is set and `CLAUDE_CODE_SESSION_KIND` is `bg`, so for an interactive session no on-disk file states how many background tasks are armed and csift's whole-transcript scan remains the only instrument. csift code site src/live/background.rs 37-39 matches verbatim."
        }
      ]
    },
    {
      "id": "REG-010",
      "area": "live-registry",
      "behavior": "On unix hosts the registry's `procStart` is the owner process's creation instant rendered as an asctime-like string in UTC (`Sun Aug 16 09:04:23 2026`), because the harness literally runs `ps -o lstart=` with `LC_ALL=C` and `TZ=UTC`; a bare `ps lstart` renders the SAME instant in the machine's local zone and locale - measured as a constant 10-hour gap with a 0-second residual on all 7 rows read in a UTC+10 zone. The Windows FILETIME (100-nanosecond ticks since 1601-01-01) is carried in a SEPARATE string key `procStartFt` beside `procStart`, not in `procStart` itself: the row parser accepts both keys, the platform helper writes exactly one of them, and consumers read `procStartFt ?? procStart`.",
      "depends": "csift parses the registry value as UTC and the `ps` value as local, then compares INSTANTS with a 2-second tolerance for its pid-reuse guard; comparing the two as strings or as same-zone values flags pid reuse on every row and turns every live session into a stale-dead verdict. When either side is absent or unparseable csift degrades to a pid-only probe and discloses that the reuse guard was skipped.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "13-21",
          "snippet": "//! `procStart` is the OWNER PROCESS's creation instant in a PLATFORM-SPECIFIC rendering:\n//! on unix an asctime string in UTC (`Sun Aug 16 09:04:23 2026`), on Windows a FILETIME\n//! integer (100ns ticks since 1601-01-01, e.g. `134328101803820142`). `pidDomain` names\n//! the pid space the row was written in (`darwin`, `linux`, or `win32:<hostname>`); a row\n//! from another domain cannot be probed here and the verdict says so. `ps lstart` renders\n//! in the LOCAL zone - a naive string/local comparison flags pid reuse on EVERY row.\n//! Parse both sides to instants and compare with a small tolerance; when either side is\n//! absent or unparseable, degrade to a pid-only probe AND say so in the evidence (the\n//! reuse guard was skipped, honest, never silent)."
        },
        {
          "path": "src/live/registry.rs",
          "lines": "175-188",
          "snippet": "/// Parse the registry's `procStart`: an asctime-like UTC string (`Sun Aug 16 09:04:23\n/// 2026`, unix) or a FILETIME integer (`134328101803820142`, Windows: 100ns ticks since\n/// 1601). `None` on any mismatch - the caller degrades to pid-only + a note.\npub(crate) fn parse_registry_proc_start(s: &str) -> Option<jiff::Timestamp> {\n    let s = s.trim();\n    if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {\n        return filetime_to_timestamp(s.parse::<u64>().ok()?);\n    }\n    let bd = jiff::fmt::strtime::parse(\"%a %b %e %H:%M:%S %Y\", s).ok()?;\n    let dt = bd.to_datetime().ok()?;\n    dt.to_zoned(jiff::tz::TimeZone::UTC)\n        .ok()\n        .map(|z| z.timestamp())\n}"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "143-152",
          "snippet": "        PsProbe::Alive(actual) => match (proc_start.and_then(parse_registry_proc_start), actual) {\n            (Some(reg), Some(act)) => {\n                if (reg.as_second() - act.as_second()).abs() <= 2 {\n                    PidLiveness::Alive {\n                        reuse_guard: ReuseGuard::Checked,\n                    }\n                } else {\n                    PidLiveness::Reused\n                }\n            }"
        }
      ],
      "instrument": "For one live session read `procStart` from its `~/.claude/sessions/<pid>.json` and run `ps -p <pid> -o lstart=`: the two strings differ by exactly the machine's UTC offset while naming one instant. Counting rule: one paired reading per live pid, and seconds between the two parsed instants after zone assignment (expect 2 or fewer); agreement in a UTC+0 zone is evidence of nothing, so run it where the offset is non-zero. The unit test `registry_proc_start_parses_as_utc` in src/live/tests/surfaces.rs pins the parse side.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; AGENTS.md section 1; CHANGELOG 0.9.0; src/live/registry.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "For every row: read `procStart` from ~/.claude/sessions/<pid>.json and run `ps -p <pid> -o lstart=`, then compare the two as instants (registry side assigned UTC, ps side assigned the local zone)  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Eie\\(e\\)\\{try\\{let t=DQ\\(`LC_ALL=C TZ=UTC ps -o lstart= -p \\$\\{e\\}`.{0,60}'  AND  rg -o 'procStart:typeof o.procStart===\"string\".{0,120}' AND rg -o 'function OU\\(e\\)\\{return c\\(\\)\\?\\{procStart:void 0,procStartFt:e\\}:\\{procStart:e,procStartFt:void 0\\}\\}'  AND  csift status @<session> --format json | jq '.evidence[] | select(.surface==\"pid\")'",
          "observed": "7 of 7 live rows: the registry string and `ps lstart` name the same instant with exactly the machine's UTC+10 offset between them and a 0-second residual after zone assignment (e.g. registry `Tue Sep  1 23:54:26 2026` against ps `Wed  2 Sep 09:54:26 2026`); all 7 pids were alive. The producer explains the UTC rendering exactly: `function Eie(e){try{let t=DQ(`LC_ALL=C TZ=UTC ps -o lstart= -p ${e}`,{timeout:1000});return t?t.trim():void 0}catch{return}}` and its async twin runs `ps -o lstart=` with `env:{...,LC_ALL:\"C\",TZ:\"UTC\"}` - the registry value IS `ps lstart` forced to the C locale and UTC. The Windows half is NOT the same field: the row parser reads two keys, verbatim `procStart:typeof o.procStart===\"string\"?o.procStart:void 0,...typeof o.procStartFt===\"string\"&&{procStartFt:o.procStartFt}` (both string-typed), the platform helper writes one or the other, `function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}`, and every consumer prefers the FILETIME key, e.g. `let l=d.procStartFt??d.procStart`. End to end, csift's two-sided parse works here: its pid evidence read `alive (start-time guard matched)`, i.e. the two instants agreed within its 2-second tolerance.",
          "rule": "One paired reading per live pid: parse the registry string as UTC and the `ps` string as local, then take the absolute difference in seconds (expect <= 2). Agreement in a UTC+0 zone would be evidence of nothing; this host is UTC+10, so the 10-hour string gap and the 0-second instant gap are both meaningful.",
          "note": "The unix half is confirmed twice: by measurement on 7 live rows and by the producer command itself, which pins WHY the string is UTC. The Windows parenthetical is corrected - the FILETIME lives under its own key rather than in `procStart` - and this is actionable for csift, since a `procStartFt`-only row silently loses the pid-reuse guard. What remains undecidable here: whether a Windows host writes the FILETIME into the registry row's `procStart` (the row writer passes the platform value straight through under that name) or into `procStartFt` (the shape the messaging-key writer uses). One `<pid>.json` written by a session on a Windows host settles it; a darwin binary cannot. csift code sites src/live/registry.rs 13-21, 139-148 and 171-184 match verbatim."
        }
      ]
    },
    {
      "id": "REG-011",
      "area": "live-registry",
      "behavior": "The `ps -p PID -o lstart=` form used to probe pid liveness is not universally available on unix: busybox `ps` (Alpine and similar) rejects `-p` and `lstart` outright and fails for a LIVE pid exactly as for a dead one, while `/proc/<pid>` answers liveness directly on Linux and does not exist on macOS, where the `ps` form is reliable.",
      "depends": "csift falls back to a `/proc/<pid>` directory test when the `ps` form fails, keeps the no-such-process verdict only when that is absent too, and discloses the skipped reuse guard; without the fallback `status` read a live session as stale-dead on musl hosts, which the release matrix's musl lanes caught.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "201-203",
          "snippet": "/// One `ps -p PID -o lstart=` call: a failing/empty result = no such process; success\n/// yields the start instant when the LOCAL-rendered format parses (two observed field\n/// orders tried), else `Alive(None)` - the caller then skips the reuse guard AND says so."
        },
        {
          "path": "src/live/registry.rs",
          "lines": "213-222",
          "snippet": "    if !out.status.success() || text.is_empty() {\n        // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n        // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n        // present = alive with the start time unknown (the reuse-guard skip is\n        // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n        // reliable) = the no-such-process verdict stands.\n        if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n            return PsProbe::Alive(None);\n        }\n        return PsProbe::NoProcess;"
        }
      ],
      "instrument": "In a busybox container run `ps -p 1 -o lstart=` (non-zero exit or empty output) beside `test -d /proc/1` (succeeds), then `csift status` for a live session there must not report stale-dead and must name the reuse guard as skipped. Counting rule: one probe outcome per platform family (busybox, glibc Linux, macOS); only a real run on a busybox host exercises the fallback. The unit tests `probe_pid_own_process_guard_states` and `probe_pid_reports_a_reaped_pid_dead` in src/live/tests/surfaces.rs pin the alive and dead arms.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.1",
        "source": "SPEC.md section 6 v0.9.1 ledger; CHANGELOG 0.9.1; src/live/registry.rs ps_probe comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "docker run --rm busybox:latest sh -c 'ps -p 1 -o lstart=; echo exit=$?; ps -o lstart=; echo exit=$?; test -d /proc/1 && echo proc1=present; busybox | head -1'  ||  docker run --rm debian:stable-slim sh -c 'apt-get -qq update >/dev/null && apt-get -qq install -y procps >/dev/null; LC_ALL=C ps -p 1 -o lstart=; test -d /proc/1 && echo present; ps --version'  ||  on the macOS host: ps -p $$ -o lstart=; echo exit=$?; ls -d /proc",
          "observed": "busybox v1.38.0: `ps: invalid option -- 'p'` with exit=1, and `ps: bad -o argument 'lstart', supported arguments: user,group,comm,args,pid,ppid,pgid,etime,nice,rgroup,ruser,time,tty,vsz,sid,stat,rss` with exit=1 - both for pid 1, which is the container's own live init; `test -d /proc/1` succeeded (proc1=present). glibc Linux (procps-ng 4.0.4): `ps -p 1 -o lstart=` printed `Wed Sep  2 09:47:54 2026` and /proc/1 was present. macOS (Darwin 24.6.0, arm64): `ps -p $$ -o lstart=` printed `Wed  2 Sep 19:46:12 2026` with exit=0, and `ls -d /proc` returned `ls: /proc: No such file or directory`.",
          "rule": "One probe outcome per platform family (busybox container, glibc Linux container, macOS host), each family probed once. A container's pid 1 is live by construction, so a failing ps there is a measured false-dead rather than a dead pid; /proc presence is a single directory test.",
          "note": "Code site verified verbatim in the current file: src/live/registry.rs lines 197-199 (the ps_probe doc comment) and 209-218 (the failure arm with the /proc fallback), both matching the ledger snippets character for character; the named unit tests probe_pid_own_process_guard_states (src/live/tests/surfaces.rs:44) and probe_pid_reports_a_reaped_pid_dead (src/live/tests/surfaces.rs:312) exist. Scope limit worth stating: only the two primitives the fallback keys on were measured in the busybox container. The end-to-end half - csift status not reporting stale-dead there - was not exercised, because the csift binary on this machine is macOS arm64 and the container carries no ~/.claude corpus; deciding that half needs a musl build run inside a busybox host against a fixture home. Cross-claim aside for REG-010's owner, found while reading the same probe path in the binary: the Windows rendering is a SEPARATE FIELD, not the same field re-encoded - the binary carries `function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}`, so a Windows row writes `procStartFt` and leaves `procStart` undefined."
        }
      ]
    },
    {
      "id": "REG-012",
      "area": "live-registry",
      "behavior": "`ps -o lstart=` renders its date in two field orders - `Sun Aug 16 09:04:23 2026` (%a %b %e) and `Sun 16 Aug 09:04:23 2026` (%a %e %b) - but the discriminator is the ps implementation together with LC_TIME, not the host family. A single macOS host emits both: C and en_US give month-first, en_AU and en_GB give day-first. glibc procps-ng emits month-first in every locale tried, so it contributes only one order. Both orders reach csift in production: on a machine whose LC_TIME is day-first the default probe returns the second order and the pid-reuse guard still matches.",
      "depends": "csift tries both patterns before giving up and degrading to a disclosed pid-only probe; matching only one order silently disables the pid-reuse guard on the other family of hosts, which shows up as an `alive (pid only)` evidence value rather than as an error.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "224-234",
          "snippet": "    let local = crate::timez::local_tz();\n    for fmt in [\"%a %b %e %H:%M:%S %Y\", \"%a %e %b %H:%M:%S %Y\"] {\n        if let Ok(bd) = jiff::fmt::strtime::parse(fmt, &text) {\n            if let Ok(dt) = bd.to_datetime() {\n                if let Ok(z) = dt.to_zoned(local.clone()) {\n                    return PsProbe::Alive(Some(z.timestamp()));\n                }\n            }\n        }\n    }\n    PsProbe::Alive(None)"
        }
      ],
      "instrument": "Compare `LC_ALL=C ps -p $$ -o lstart=` against `LC_ALL=en_AU.UTF-8 ps -p $$ -o lstart=` on ONE host (macOS shows both orders, glibc procps-ng shows one), then confirm the guard still binds by reading the `pid` evidence row of `csift status @<session-uuid> --format json` under each locale.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/registry.rs ps_probe comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "locale; ps -p $$ -o lstart=; LC_ALL=C ps -p $$ -o lstart=; LC_ALL=en_US.UTF-8 ps -p $$ -o lstart=; LC_ALL=en_AU.UTF-8 ps -p $$ -o lstart=; LC_ALL=en_GB.UTF-8 ps -p $$ -o lstart=  ||  docker run --rm debian:stable-slim sh -c 'apt-get -qq install -y procps locales; LC_ALL=C ps -p 1 -o lstart=; locale-gen en_AU.UTF-8; LC_ALL=en_AU.UTF-8 ps -p 1 -o lstart=; ps --version'  ||  csift status @<session-uuid> --format json | (select the evidence row whose surface is pid)  and  LC_ALL=C csift status @<session-uuid> --format json | (same selection)",
          "observed": "One macOS host, LC_TIME=en_AU.UTF-8 by default, produced BOTH orders: default and LC_ALL=en_AU.UTF-8 and LC_ALL=en_GB.UTF-8 all printed `Wed  2 Sep 19:46:24 2026` (day before month, day space-padded to two columns), while LC_ALL=C and LC_ALL=en_US.UTF-8 printed `Wed Sep  2 19:46:24 2026` (month before day). The glibc Linux host (procps-ng 4.0.4) printed `Wed Sep  2 09:47:54 2026` under BOTH LC_ALL=C and LC_ALL=en_AU.UTF-8 - one order only. csift's pid evidence row was `{\"age_secs\": null, \"surface\": \"pid\", \"value\": \"alive (start-time guard matched)\"}` in the default locale AND under LC_ALL=C.",
          "rule": "One rendering per (ps implementation, LC_TIME) pair, not per host: the same host is counted twice when LC_TIME changes the order. A third unmatched order would not error - it surfaces as the pid evidence value `alive (pid only)` instead of `alive (start-time guard matched)`, so the csift run is the discriminating counter.",
          "note": "Code site verified verbatim: src/live/registry.rs lines 220-230, with the two format strings `\"%a %b %e %H:%M:%S %Y\"` and `\"%a %e %b %H:%M:%S %Y\"` on line 221 exactly as the ledger quotes. The day-first form pads the day to two columns, so the literal text carries a DOUBLE space (`Wed  2 Sep`); the %e directive absorbs it, confirmed end to end by the guard matching rather than degrading. The claim's substance - at least two orders exist and both must be tried - stands; only the attribution to host families and the per-host-family counting rule needed correcting, and that matters because it means a single-platform release matrix cannot rule the second order out."
        }
      ]
    },
    {
      "id": "REG-013",
      "area": "live-registry",
      "behavior": "A registry row outlives the process it describes: SIGKILL writes no closing transition, so the file stays on disk carrying its last `status` and `statusUpdatedAt` while its pid names nothing. It is NOT kept until it is replaced, though - any peer session may run the registry sweep, which deletes every `<pid>.json` whose pid is dead (and reports `Prior session exited uncleanly` for interactive rows). Measured removal latency after a SIGKILL ranged from under 200 ms to still-present 35 minutes later on the same machine, so a stale row is a race won or lost, never a guarantee.",
      "depends": "the owner-pid probe is the only thing that retires a stale row: csift ranks a dead or reused pid above every other surface and reports stale-dead, naming whether the tail was mid-tool or settled when the process ended; trusting the row alone would report long-dead sessions as running.",
      "code": [
        {
          "path": "src/live.rs",
          "lines": "10-12",
          "snippet": "//!    heartbeat: an hours-old `statusUpdatedAt` just means the state has not changed;\n//!    the real failure mode is a SIGKILLed session's stale entry, guarded by pid\n//!    liveness + a process-start-time check against pid reuse);"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "124-128",
          "snippet": "/// Probe pid liveness WITHOUT signaling (and without a second `unsafe` site - the\n/// crate's single-allow law outranks the raw-syscall form): one process query answers\n/// both questions - a failing/empty query = no such process; a start time within\n/// tolerance of the registry's `procStart` = the same process (reuse guarded). A row\n/// from another pid domain is never probed (its pid belongs to another machine or OS)."
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "290",
          "snippet": "    let dead = matches!(liveness, Some(PidLiveness::Dead | PidLiveness::Reused));"
        }
      ],
      "instrument": "Spawn a throwaway session, poll ~/.claude/sessions/*.json at a fixed interval, SIGKILL the registered pid, and keep polling: record both whether the row survives and how long. Confirm death with `ps -p <pid>`; read the sweep contract out of the binary with `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'sessionRegistry\\] sweep.{1500}'`.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "A scratchpad python pty.fork probe spawned a throwaway Claude Code session in a trusted directory with the inherited child-session env markers stripped (CLAUDE_CODE_CHILD_SESSION, CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CLAUDE_PID, CLAUDE_CODE_SESSION_ID, the messaging socket/token), while a second process polled ~/.claude/sessions/*.json every 150 ms, sent SIGKILL to the registered pid at t+40 s, and kept polling to t+75 s; then `ps -p <pid> -o pid=,comm=` and a re-read of the row minutes later. Binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'sessionRegistry\\] sweep.{1500}'",
          "observed": "The row appeared 5.23 s after spawn with `\"status\": null`, then transitioned to `\"idle\"` 0.33 s later (t+5.56 s). SIGKILL was sent at t+40.02 s; the 150 ms poller printed no removal through t+75 s, and a re-read ~10 minutes later still found the file with its pre-kill `status: idle` and an unchanged `statusUpdatedAt`, while `ps -p <pid>` exited 1 with empty output. A separate earlier probe's row was already gone at the first poll 0.2 s after its SIGKILL and stayed gone at 1/2/3/5/8/12/20/30/45/60 s. The binary explains both: the sweep logs `[sessionRegistry] sweep ${d?`permitted (domain ${f})`:\"declined by isRegistrySweepPermitted() - dead records are left in place (neither counted nor deleted)\"}`, and its dead-pid arm deletes the `<pid>.json` and reports `Prior session exited uncleanly: <sessionId> (v<version>)` when the swept row's `kind===\"interactive\"`.",
          "rule": "One row reading before the kill, then one per 150 ms poll after it, plus one late re-read; `ps -p <pid>` exit status is the liveness oracle (exit 1 with no output = no such process). Four spawn/kill cycles were run; two registered a row, of which one kept the row well past the kill and one lost it inside 200 ms.",
          "note": "Code sites verified verbatim in the current files: src/live.rs lines 10-12, src/live/registry.rs lines 120-124, src/live/verdict.rs line 290. Two honest scope limits. First, the stale row I captured carried `idle`, not `busy`; the mechanism is status-agnostic (whatever the last transition wrote is what persists), so the claim's `busy` example is an illustration rather than something measured here. Second, the csift-side half - that `status` then reports stale-dead - could not be exercised: a session killed before its first prompt writes no transcript, and `csift status @<session-uuid>` bails with `no session file found for session id [...]`. Exercising that needs a session that took at least one turn before being killed. The refinement matters operationally: because a peer can sweep the row away within a fraction of a second, csift's stale-dead verdict is reachable only while the row survives, and after a sweep the same dead session degrades to the no-registry lane instead."
        }
      ]
    },
    {
      "id": "REG-014",
      "area": "live-registry",
      "behavior": "A registry row's `kind` is a closed four-value enum - `interactive`, `bg`, `daemon`, `daemon-worker` - stamped once when the row is registered, from the session's own kind (a `bg` row additionally carries `jobId` and `spare`). Row updates merge a patch into the existing row, and no code path in 2.1.258 rewrites `kind` to a default. What is genuinely not durable is the ROW: the registry sweep deletes any `<pid>.json` whose pid is dead, regardless of kind, which is why a background job's row disappears once the job is killed.",
      "depends": "csift parses only the liveness-relevant fields (pid, status, statusUpdatedAt, procStart, pidDomain) and ignores `kind` entirely, so no verdict, lane or lineage is ever derived from it.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "25-27",
          "snippet": "/// One registry row (only the liveness-relevant fields; the rest is tolerated + ignored).\n#[derive(Debug, Clone)]\npub(crate) struct RegistryRow {"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "71",
          "snippet": "status_updated_at_ms: v.get(\"statusUpdatedAt\").and_then(serde_json::Value::as_i64),"
        }
      ],
      "instrument": "Read the enum and the writer straight out of the binary (`strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'kind:.{0,60}\"interactive\".{0,120}'`), then watch a live row: poll ~/.claude/sessions/*.json and dump each new row's full JSON, correlating appearances and removals against ~/.claude/daemon.log `[bg]` lines.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "A 100 ms watcher recorded the full JSON of every row appearing under ~/.claude/sessions/*.json across four spawn/SIGKILL cycles of a throwaway session, plus a python census of every row's kind/status/entrypoint and pid liveness; then strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'kind:[A-Za-z_$]+\\(\\)\\.(enum|optional)\\(.{0,200}|kind:.{0,60}\"interactive\".{0,120}' and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{80}o===\"interactive\"\\?\\{name:.{1400}'; and tail of ~/.claude/daemon.log for the background-job window.",
          "observed": "The binary carries a closed schema `kind:w.enum([\"interactive\",\"bg\",\"daemon\",\"daemon-worker\"])`. The writer stamps it once at registration: `{pid:process.pid,sessionId:...,cwd:...,startedAt:Date.now(),procStart:...,version:...,peerProtocol:...,peerFeatures:...,kind:o,entrypoint:...,pidDomain:...,...{jobId:o===\"bg\"&&a.CLAUDE_JOB_DIR?...:void 0,spare:d?!0:void 0}}`, and the update path re-reads the file and writes `{...existing,...patch}` (no default reintroduces `kind`). Live: every row whose JSON I actually parsed over ~50 minutes carried `kind: \"interactive\"` - 7 pre-existing rows plus 4 probe rows. Two further rows appeared and vanished inside a background-job window (~/.claude/daemon.log: `[bg] bg spare spawned host pid=<n>`, `[bg] bg claimed-spare <id> (slash)`, then `[bg] bg settled <id> (killed)`), but they were removed before their `kind` was read.",
          "rule": "One `kind` reading per registry row per 100 ms poll; a row counts as observed only if its JSON was parsed while the file still existed. Binary strings count as one observation each and are quoted verbatim.",
          "note": "Code sites verified verbatim: src/live/registry.rs lines 25-27 and 65-74; the parsed field set is exactly pid, status, statusUpdatedAt, procStart, pidDomain, so `kind` is indeed never read by csift and no verdict, lane or lineage can depend on it. The claim's second half (the row vanishing when the job is killed) is explained and consistent with what I saw. The first half - a `bg` row reverting to `interactive` - could NOT be reproduced: no `kind:\"bg\"` row could be captured, because background jobs are spawned by the harness daemon rather than on demand from this lane, and the binary shows no mechanism that would rewrite the field. What would decide it: watch a single row for a daemon-spawned background job (identified from the ~/.claude/daemon.log line `[bg] bg spawned <id>`), sampling its `kind` while the job runs and again after `[bg] bg settled <id> (killed)`. Either way csift's posture is unaffected, since it ignores the field."
        }
      ]
    },
    {
      "id": "REG-015",
      "area": "live-registry",
      "behavior": "Neither harness store carries background-task state: no session registry row mentions a monitor or a task id, and the task directory holds only the session task list, never a 9-character background task id.",
      "depends": "csift therefore reconstructs background tasks from the transcript alone (a whole-file scan behind a byte prefilter) and reads the task directory only as the task-list section; a future store that did carry live task state would let that scan be replaced by a cheap read.",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "1-3",
          "snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a"
        },
        {
          "path": "src/live/background.rs",
          "lines": "1-2",
          "snippet": "//! Background tasks: the off-turn work a session launched and whether it ever came\n//! back. Three kinds, measured on the corpus (v0.10.0):"
        }
      ],
      "instrument": "`grep -ril 'monitor\\|taskId' ~/.claude/sessions/` and `grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/` - both list nothing. Counting rule: one hit equals one file; zero files from either grep is the expected observation, taken while at least one background task is known to be open so absence is not merely an idle machine.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; src/live/tasks.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "grep -ril 'monitor\\|taskId' ~/.claude/sessions/ | wc -l  ;  grep -rlE '\"b[a-z0-9]{8}\"' ~/.claude/tasks/ | wc -l  ;  run in the same command as csift status @<session-uuid> --format json (reading the evidence row whose surface is background) to establish the background-open precondition; plus a python key-union census over ~/.claude/sessions/*.json and ~/.claude/tasks/*/*.json",
          "observed": "Both greps returned 0 files. The simultaneous csift evidence row was `{\"age_secs\": null, \"surface\": \"background\", \"value\": \"2 open; 165 completed, 1 failed, 15 killed, 0 stopped\"}`, so at least two background tasks were open at that instant. Scope scanned: 16 directory entries under ~/.claude/sessions (8 rows plus 8 key sidecars) and 402 task JSON files. Key union across the registry rows was 19 keys - bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version - none naming a monitor or a task. Key union across all 402 task files was activeForm, blockedBy, blocks, description, id, metadata, owner, status, subject.",
          "rule": "One hit equals one file; both greps are recursive over the whole store, so zero files means no file in either store contains the needle. The background-open precondition is met by the csift `background` evidence row read in the same command, which is what makes the zero a real absence rather than an idle machine.",
          "note": "Code sites verified verbatim: src/live/tasks.rs lines 1-3 and src/live/background.rs lines 1-2. The key-union census strengthens the greps: neither store has any field that could carry a background task id under a different name, so the whole-file transcript scan really is the only instrument for background tasks. One aside for REG-008's owner, seen in the same census: the 402 task files carry two keys beyond the seven that claim lists - `metadata` and `owner` - and neither is a background task id, so REG-015 is unaffected."
        }
      ]
    },
    {
      "id": "REG-016",
      "area": "live-registry",
      "behavior": "The session-registry directory holds more than rows: beside every `<claude-home>/sessions/<pid>.json` sits a companion file `<pid>.<64 lowercase hex>.key` sharing the row's pid stem, one per row (7 rows and 7 companions, a 1:1 pid-stem pairing that held across 13414 directory listings over 90 seconds). The companion is NOT unparseable: it is itself a small JSON object (87 or 108 bytes; keys peerToken and procStart, sometimes pidDomain), so a reader that treats every directory entry as a registry row parses it SUCCESSFULLY and must reject it on content - it carries no `sessionId` - rather than on a parse error. The directory can also hold a third entry kind that is neither: Claude Code writes and later unlinks a `.fleetview-heartbeat` dotfile in the same directory whose whole content is a millisecond epoch as a bare integer string (absent at all 13414 listings here, so its lifetime was not observed). csift's `.json` extension filter excludes all of these before any read.",
      "depends": "csift's registry lookup filters on the `.json` extension before it reads anything, so the companions never enter the scan; without that filter every live session costs one read plus one failed parse, and because an unparseable entry is skipped silently the waste would never surface as an error. The filter also means any future sidecar the harness writes under a `.json` name would be indistinguishable from a row and would have to be rejected on content instead.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "45-50",
          "snippet": "    for entry in std::fs::read_dir(&dir)? {\n        let entry = entry?;\n        let p = entry.path();\n        if p.extension().and_then(|e| e.to_str()) != Some(\"json\") {\n            continue;\n        }"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "38-44",
          "snippet": "/// Scan the registry dir for the row whose `sessionId` matches. `Ok(None)` when the dir\n/// is absent or no row matches (a subagent target, a non-interactive session, an old CC).\npub(crate) fn registry_row_for(session_id: &str) -> Result<Option<RegistryRow>> {\n    let dir = crate::path::claude_home()?.join(\"sessions\");\n    if !dir.is_dir() {\n        return Ok(None);\n    }"
        }
      ],
      "instrument": "`ls -a ~/.claude/sessions | sed 's/.*\\.//' | sort | uniq -c` - the `json` and `key` counts are equal, and `ls -a` is needed because the third entry kind is a dotfile. Then match each companion against `<digits>.<64 hex>.key`, check that its pid stem names an existing row, and JSON-parse it (it parses; the discriminator against a row is the absent `sessionId`, not a parse failure). Counting rule: one observation per directory entry, bucketed by extension, per listing - a single listing gives a per-instant pairing, and repeating the listing (13414 times over 90s here) is what turns it into a windowed statement. The pairing is still not a durable invariant: a session starting or exiting mid-listing can be seen half-paired.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "dev session 2026-09-02; src/live/registry.rs registry_row_for"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "ls -a ~/.claude/sessions | sed 's/.*\\.//' | sort | uniq -c   ;   then, per companion, check the name against <digits>.<64 lowercase hex>.key and check that its pid stem names an existing <pid>.json   ;   then python3 -c 'json.loads(open(k).read())' over each .key   ;   then a 90-second poll at 5ms listing the directory and reading every *.json (script kept in the run scratchpad, not the repo)",
          "observed": "7 json + 7 key + 2 blank (the . and .. entries of ls -a), no dotfiles; all 7 companions have a digits-only pid stem, a 64-character all-lowercase-hex middle segment, and an existing <pid>.json sibling; json stems minus key pid stems = 0 and key pid stems minus json stems = 0; all 7 .key files are 87 or 108 bytes and ALL 7 PARSE AS JSON, each a JSON object whose keys are peerToken + procStart, three of them also pidDomain; the 90s poll made 13414 listings, read 93898 *.json files (exactly 7.0 per listing), saw 0 parse failures, 0 empty reads, and 0 directory entries with any extension other than json or key",
          "rule": "One observation per directory entry, bucketed by extension, per listing. The single-instant census is 7/7; the pairing is then checked as two set differences over pid stems (both empty = 1:1). The .key content check counts one JSON-parse attempt per companion file. The poll re-runs the extension census 13414 times over 90 seconds, so 'only json and key ever appeared' is a 90-second statement, not a one-instant one.",
          "note": "Both code sites confirmed verbatim in the current file: src/live/registry.rs lines 38-44 hold the doc comment through the `if !dir.is_dir()` guard exactly as quoted, and lines 45-50 hold the `read_dir` loop through the `Some(\"json\")` extension filter exactly as quoted; no path or line correction needed. What the instrument refutes is the claim's consequence clause, which was never itself measured: the companion parses fine as JSON, so the cost a naive reader pays is one wasted read plus a content-level rejection per live session, not a parse failure. That is the same waste and the same silence, but the ledger should not assert a parse failure that does not happen. The `.fleetview-heartbeat` dotfile is a second reason the extension filter is load-bearing and was not in the claim at all."
        }
      ]
    },
    {
      "id": "REG-017",
      "area": "live-registry",
      "behavior": "Claude Code ships an atomic-write helper that writes through a temporary file and a rename, plus a documented non-atomic fallback whose error is prefixed `atomic write failed first: `. Both paths are present in the shipped binary. But the binary DOES attribute one of the four JSON sidecars a reader joins: the session-registry row `<claude-home>/sessions/<pid>.json` is written with plain `fs/promises.writeFile` on both its registration and its update path (and the sibling `.fleetview-heartbeat` likewise), NOT through the atomic helper - the writing module's own import header binds `writeFile as Mg` alongside the `join` and `readFile` aliases that same function uses. A plain writeFile truncates in place, so the registry row is not merely 'to be assumed' observable half-written: it is positively established as such, and csift's skip-on-unparseable guard is required rather than merely prudent. The other three sidecars - subagent `meta.json`, workflow run manifests, harness task files - remain unattributed by this instrument; a serialized write chain (`pidFileWriteChain`) orders the registry row's writers against each other but does nothing for a concurrent external reader.",
      "depends": "csift reads all four sidecars as a whole-file read plus a parse and silently skips a file that fails either step, so one caught mid-write yields a DOWNGRADED answer with no disclosure: `status` falls back to its no-registry lane, a task row disappears from the tasks section, and a subagent loses its type and its teammate discrimination. Retrying the read once, or disclosing the skip as evidence, is the only way that failure could become visible.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "51-56",
          "snippet": "        let Ok(raw) = std::fs::read_to_string(&p) else {\n            continue; // a mid-write or unreadable row is not this session's problem\n        };\n        let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n            continue;\n        };"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "84-93",
          "snippet": "pub(crate) fn read_meta(meta_path: Option<&Path>) -> MetaFields {\n    let Some(p) = meta_path else {\n        return MetaFields::default();\n    };\n    let Ok(bytes) = std::fs::read(p) else {\n        return MetaFields::default();\n    };\n    let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) else {\n        return MetaFields::default();\n    };"
        }
      ],
      "instrument": "`strings -n 40` over the installed Claude Code binary grepped for `atomic write failed first` shows the helper (temp file, symlink-refusing O_EXCL open, fsync, rename) and its in-place fallback; `strings -n 12 <binary> | grep -oE 'appendFileSync|renameSync|mkdtempSync' | sort | uniq -c` gives 24 / 15 / 11 and bounds call sites from above without attributing any. To ATTRIBUTE a sidecar, do not count - resolve the alias: find the writer by a literal only it contains (for the registry row, ``Ct(e1(),`${process.pid}.json`)``), then find the nearest preceding `import{...}from\"fs/promises\"` header in the same module text and require that every identifier the writer calls is bound there. The registry row resolves to plain `writeFile`. A live race (poll `~/.claude/sessions/*.json` at high frequency and count reads that fail to parse) can corroborate but must report its denominator: a 90-second 5ms poll here made 13414 listings and 93898 reads yet saw 0 row mtime changes, so its 0 parse failures prove nothing - a race with no write in the window is unqualified.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "dev session 2026-09-02; src/live/registry.rs registry_row_for; src/subagent/meta.rs read_meta"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 40 ~/.local/share/claude/versions/2.1.258 | rg 'atomic write failed first'   ;   strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE 'appendFileSync|renameSync|mkdtempSync' | sort | uniq -c   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Sn\\(e,n\\)\\{let r=Ct\\(e1\\(\\),`\\$\\{process\\.pid\\}\\.json`\\).{0,60}'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'let f=K\\(await ace\\(r,\"utf8\"\\)\\);return await Mg\\(r,S\\(\\{\\.\\.\\.f,\\.\\.\\.e\\}\\)\\),!0'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'readFile as ace,unlink as Oi,writeFile as Mg\\}from\"fs/promises\"'   ;   plus the 90-second 5ms poll of ~/.claude/sessions described under REG-016",
          "observed": "The atomic helper is present verbatim: a temp name `.tmp.${process.pid}.${q(6).toString(\"hex\")}`, an O_WRONLY|O_CREAT|O_EXCL open with O_NOFOLLOW, an fsync, then `Renaming ${m} to ${o}` and `File ${o} written atomically`; the fallback is present verbatim as `File ${o} written via in-place fallback` guarded by an O_TRUNC reopen, and the documented error decoration is ` (atomic write failed first: ${l(r)})`. Counts reproduce exactly: appendFileSync 24, renameSync 15, mkdtempSync 11. The session-registry writer is `function Sn(e,n){let r=Ct(e1(),`${process.pid}.json`),o=BG(),d=o.pidFileWriteChain.then(async()=>{...})}` and its direct-filesystem branch is `let f=K(await ace(r,\"utf8\"));return await Mg(r,S({...f,...e})),!0`; the registration branch is `else await Mg(v,N)` and the heartbeat is `await Mg(Ct(e1(),wl),String(Date.now()))` with `var wl=\".fleetview-heartbeat\"`. That module's own import header binds all three aliases the writer uses: `readFile as ace,unlink as Oi,writeFile as Mg}from\"fs/promises\"` in the same statement as `join as Ct` from \"path\". So Mg is fs/promises.writeFile - a plain truncating write, NOT the atomic helper. The 90-second poll observed 0 `.tmp.` sightings and 0 parse failures over 93898 reads, but ALSO 0 row mtime changes, so it witnessed no write at all and decided nothing.",
          "rule": "For the string counts: one occurrence per matching literal in one binary's string table; a count bounds call sites from above and attributes none of them to a sidecar. For the attribution: an alias is resolved by finding the nearest preceding `import{...}from` header in the same module text and requiring that ALL of the identifiers the target function actually calls (here Ct, ace, Mg) are bound by that one header - a three-way agreement, not a single-name guess. For the poll: writes-observed is the denominator (row mtime changes); with a denominator of 0 the parse-failure and temp-file counts are vacuous and the poll is unqualified.",
          "note": "Both code sites confirmed verbatim in the current files: src/live/registry.rs lines 51-56 hold the read-then-parse pair with the `// a mid-write or unreadable row is not this session's problem` comment exactly as quoted, and src/subagent/meta.rs lines 84-93 hold `read_meta` through its `from_slice` guard exactly as quoted; no path or line correction needed, and the `depends` reasoning about the silent downgrade is unaffected. The refinement is to the premise only. I could not resolve the remaining three sidecars: a partial trace found the subagent metadata writer calling a 3-arity helper `Un(path, content, mode)` and separately found a `function Un(t,n,e,r){return QQ(t,n,{mode:e,renameFn:r})}` whose body is a temp-and-rename writer, but those two sit in different string-table regions and the identifier is chunk-locally mangled, so the join does not meet the three-way-agreement bar the registry row met and I am not asserting it. Deciding those three needs either the same alias-join done properly per module, or a race with a non-zero write denominator - which on this machine means catching a session status TRANSITION, not merely a busy session, since 90 seconds of continuous activity produced zero row writes."
        }
      ]
    },
    {
      "id": "TAIL-001",
      "area": "live-tail",
      "behavior": "A tool call in flight appears on disk as an assistant `tool_use` block whose `id` has no later `tool_result` carrying that `tool_use_id`; a process that died mid-tool leaves exactly the same shape, so the transcript alone cannot separate the two.",
      "depends": "csift's `tail_shape` reads a bounded 512 KB final window, walks it BACKWARD collecting `tool_result` ids first, and reports the newest use id absent from that set as the unreturned call; `status`/`wait` classify the tail by SHAPE (unreturned use, last stop_reason, last record instant) and the pid probe is what disambiguates in-flight from dead mid-tool.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "3-7",
          "snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),\n//! the last assistant `stop_reason`, and the last record's instant. A record is only\n//! trusted from a COMPLETE line (torn tails are skipped by the newline framing)."
        },
        {
          "path": "src/live/tail.rs",
          "lines": "65-67",
          "snippet": "    // Backward walk: result ids seen so far are LATER in file order, so a use id absent\n    // from that set is unreturned as of the tail.\n    let mut later_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();"
        },
        {
          "path": "src/live/tail.rs",
          "lines": "93-107",
          "snippet": "            if shape.unreturned_use.is_none() {\n                for b in blocks {\n                    if let crate::model::Block::ToolUse {\n                        id: Some(id), name, ..\n                    } = b\n                    {\n                        if !later_result_ids.contains(id) {\n                            shape.unreturned_use = Some((\n                                name.clone().unwrap_or_else(|| \"(unnamed)\".to_string()),\n                                rec.timestamp.clone(),\n                            ));\n                        }\n                    }\n                }\n            }"
        }
      ],
      "instrument": "During a long SYNCHRONOUS Bash call, `csift status @<session>` names the unreturned tool and its age; confirm with `csift show @<session> --turn -1 --format json` that no `tool_result` carries that `tool_use_id`. Counting rule: one unreturned use = a `tool_use` id with no matching `tool_use_id` later in file order inside the tail window. The unit test `tail_shape_reads_pairing_stop_reason_and_holds_torn_tails` in src/live/tests/surfaces.rs pins the pairing.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; AGENTS.md section 3.9; src/live/tail.rs:3-10 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "From inside a still-running synchronous Bash call: (1) `csift status @trap:<ThreeCamelWords1234> --format json`; (2) in the SAME call, resolve the caller's own transcript and pair its blocks - `python3 - <own transcript>` collecting every `tool_use` id and every `tool_result` `tool_use_id` and printing the ids with no result; (3) dead-lane census over a seeded random sample of 600 files from `find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`, reading each file's last 512 KB and asking whether its tail carries an unreturned use.",
          "observed": "(1) verdict row `{\"age_secs\": 0, \"surface\": \"tail\", \"value\": \"unreturned Bash call\"}`, `tail_state: \"in a Bash call for 0s\"`, verdict `running`. (2) `records: 33 tool_use blocks: 11 tool_result blocks: 10` and `UNPAIRED tool_use (line,name,block keys): [(33, 'Bash', ['caller', 'id', 'input', 'name', 'type'])]` - exactly one unreturned use, the call doing the asking, at the last line. (3) of 600 sampled lanes, 597 had a tail record older than 1 h and 2 of those 597 (0.3%) still carry an unreturned `tool_use` at the tail - the identical on-disk shape with no live process behind it.",
          "rule": "One unreturned use = a `tool_use` id with no `tool_result` carrying that id later in file order within the window read. One observation per lane for the dead-lane census; a lane counts as dead when its newest timestamped record is more than 1 h old.",
          "note": "Both halves observed directly: the live half in the caller's own lane, the dead-mid-tool half as 2 real lanes whose tail is byte-identical in shape to a live in-flight call. The collision is genuine but rare in this corpus (0.3% of dead lanes), which is why the pid probe rather than the transcript is what separates them. All three code sites in src/live/tail.rs (lines 3-7, 65-67, 93-107) match the current file verbatim; TAIL_WINDOW_BYTES is still `512 * 1024`."
        }
      ]
    },
    {
      "id": "TAIL-002",
      "area": "live-tail",
      "behavior": "Assistant `stop_reason` is written per API MESSAGE, not per record: on MAIN transcripts it is effectively always present (135 null out of 133,624 assistant records = 0.10% corpus-wide, per-file median 0.00% and worst file 1.94%), while on SUBAGENT transcripts it is null on 65.16% of assistant records (193,062 of 296,275 across 7,515 lanes; per-lane quartiles 58.8% / 64.8% / 71.0%), because a subagent flushes per content block and only the message's terminating record carries the field.",
      "depends": "csift's `tail_shape` takes `last_stop_reason` from the newest assistant record only, trusts `end_turn` as the settled shape on the MAIN lane, and on a child lane uses it only as the SECOND conjunct of `generating`; keying on a null mid-message value alone would misclassify most subagent lanes.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "24-26",
          "snippet": "    /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n    /// null is NORMAL mid-message on subagents).\n    pub(crate) last_stop_reason: Option<String>,"
        },
        {
          "path": "src/live/tail.rs",
          "lines": "76-80",
          "snippet": "        if shape.last_stop_reason.is_none() && rec.r#type.as_deref() == Some(\"assistant\") {\n            if let Some(sr) = rec.message.as_ref().and_then(|m| m.stop_reason.as_deref()) {\n                shape.last_stop_reason = Some(sr.to_string());\n            }\n        }"
        },
        {
          "path": "src/live/children.rs",
          "lines": "23-29",
          "snippet": "pub(crate) struct ChildState {\n    pub(crate) session_id: String,\n    /// `in-flight` (unreturned tail call) | `generating` (recent tail record and the\n    /// last assistant record is not an end_turn - the model is mid-generation; a\n    /// paired tail alone proves nothing) | `settled`.\n    pub(crate) state: &'static str,\n    pub(crate) detail: String,"
        }
      ],
      "instrument": "Count `stop_reason` per assistant record on a main transcript and on one of its `subagents/agent-*.jsonl`: `csift search '\"type\":\"assistant\"' @<session> --raw | python3 -c \"import sys,json,collections;print(collections.Counter(json.loads(l).get('message',{}).get('stop_reason') for l in sys.stdin))\"`. Counting rule: one observation per `type:\"assistant\"` record, null counted as its own bucket, main-lane and subagent files counted separately.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "dev session 2026-08-30; AGENTS.md section 1; SPEC.md section 6.13"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Regex census of `\"stop_reason\":(null|\"...\")` on every line containing `\"type\":\"assistant\"`, run separately over the 64 top-level transcripts (`find ~/.claude/projects -maxdepth 2 -name '*.jsonl'`) and the 7,520 subagent transcripts (`find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`).",
          "observed": "MAIN: 133,624 assistant records across the 54 main transcripts that have any; 135 carry `stop_reason: null` = 0.10%; 0 records omit the field. Per-file null rate over the 45 mains with >= 50 assistant records: min 0.00%, p50 0.00%, max 1.94%. SUBAGENT: 296,275 assistant records across 7,515 lanes; 193,062 null = 65.16%; 0 omit the field. Per-lane null rate over the 2,009 lanes with >= 50 assistant records: min 18.2%, p25 58.8%, p50 64.8%, p75 71.0%, max 100.0%.",
          "rule": "One observation per `type:\"assistant\"` record; a record counts as null when `stop_reason` is present-and-null or absent (both buckets tracked; absent was 0 everywhere). Main-lane and subagent files counted in separate populations; per-file rates computed only where a file has >= 50 assistant records.",
          "note": "The behavior holds and the cited sample figures sit inside the measured distribution, but they named unnamed individual sessions that a stranger cannot re-find. Replaced with whole-corpus rates plus the per-lane quartiles, which are rerunnable from the two find expressions above."
        }
      ]
    },
    {
      "id": "TAIL-003",
      "area": "live-tail",
      "behavior": "The two lanes flush on different schedules. A SUBAGENT transcript flushes per content block, and its launching record is on disk BEFORE the launched command begins executing (measured 0.000 s). The MAIN conversation writes the whole assistant message as one batch of per-block records at message end, so the tool_use record appears 0.19-2.51 s after its own timestamp (n = 20, p50 about 0.33 s) - and a thinking or text sibling written in that same batch can carry a timestamp up to 9.4 s older. The main tail can therefore still read settled for a second or two after a tool has already started.",
      "depends": "csift reads the tail as of what is on disk and never waits; the same flush window is why a first `@trap` marker resolution on the main lane normally misses and a re-run of the same marker resolves.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "22-29",
          "snippet": "    /// flight (or dead mid-tool; the pid probe disambiguates). `(tool name, use ts)`.\n    pub(crate) unreturned_use: Option<(String, Option<String>)>,\n    /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n    /// null is NORMAL mid-message on subagents).\n    pub(crate) last_stop_reason: Option<String>,\n    /// The newest record's timestamp (any type that carries one).\n    pub(crate) last_ts_utc: Option<String>,\n    /// Records inspected (evidence sizing; 0 = empty/unreadable file)."
        },
        {
          "path": "src/live/tail.rs",
          "lines": "65-67",
          "snippet": "    // Backward walk: result ids seen so far are LATER in file order, so a use id absent\n    // from that set is unreturned as of the tail.\n    let mut later_result_ids: std::collections::HashSet<String> = std::collections::HashSet::new();"
        }
      ],
      "instrument": "Run a command that immediately reads its own transcript's tail: on a subagent lane the launching record is already present, on the main lane it is absent for the first seconds. Counting rule: one observation per launch, measured from the dispatch instant to the record's first presence on disk.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "dev session 2026-08-30; AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) Subagent lane, self-referential: inside one Bash call, poll the caller's own transcript every 20 ms for a literal marker that appears only in THAT command's text, printing the elapsed time when it lands. (b) Main lane: a 0.1 s poller over the transcripts of every session the registry (`~/.claude/sessions/*.json`) marks `busy`/`shell`, recording for each newly appended record the observation instant minus that record's own `timestamp`.",
          "observed": "(a) `own tool_use record visible on disk 0.000s after this command started executing` - the launching record was already flushed before the shell ran. (b) 214 records observed on three live main transcripts (CC 2.1.258, 2.1.257, 2.1.251). Assistant records whose only block is `tool_use` (n = 20): lags 0.19, 0.22, 0.23, 0.23, 0.24, 0.24, 0.25, 0.26, 0.28, 0.37, 0.39, 0.42, 0.43, 0.45, 0.52, 0.93, 1.39, 1.53, 1.91, 2.51 s - p50 about 0.33 s, max 2.51 s. All records of one assistant message land in a single batch at the same observation instant (e.g. `t=63.38` for a thinking + text + tool_use triple), so the earlier blocks of that batch carry older timestamps: over all 45 assistant records the lag runs 0.19 to 9.4 s.",
          "rule": "(a) one measurement per launch, from the start of the command's own execution to the first byte-presence of its marker. (b) one measurement per appended record: lag = wall clock at the poll that first saw the completed line, minus the record's own `timestamp`; polling granularity 0.1 s, and the floor across all record types was 0.05 s.",
          "note": "The direction holds and the mechanism holds, but the main-lane number is smaller than the ledger's 1-3.4 s: the tool_use record itself landed within 2.51 s in every one of 20 observations, p50 about a third of a second. The 3 s exclusion window the hook recipe uses is still safe, but for a different reason than the ledger states - what is genuinely up to ~9 s old at first sight is the EARLIER block of a batched message, not the tool_use record. Subagent half measured on this session's own lane; main half measured on two other live sessions (2.1.258 and 2.1.257) since a subagent cannot dispatch on the main lane."
        }
      ]
    },
    {
      "id": "TAIL-004",
      "area": "live-tail",
      "behavior": "Within one subagent lane, consecutive-record time gaps during GENUINE work reach p50 1.4 s, p90 9.7 s, p99 63.4 s and p99.9 317.0 s (n = 477,499 gaps over 7,520 lanes) because a long generation writes nothing for minutes, while abandoned lanes sit far out - p1 of the dead set is 16.3 h and its median 1,438 h. The two populations overlap only in the tail: 0.112% of genuine-work gaps exceed the 300 s window, and the dead-lane age distribution is continuous down to the threshold rather than separated from it by a gap.",
      "depends": "csift's `CHILD_RECENT_SECS = 300` is set from that distribution, and a child lane is `generating` only on the CONJUNCTION tail-record age <= 300 s AND last assistant `stop_reason != end_turn`; the retired 15 s window read a lane mid-generation as settled 1 time in 17 (under-counting `live_count`, so `waiting-children` collapsed to `idle-eot`), and the mtime-based `active` state is gone.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "14-20",
          "snippet": "/// Tail-record age (seconds) under which a settled-looking child is treated as still\n/// GENERATING when its last assistant record is not an end_turn. Measured: intra-lane\n/// record gaps reach p99.9 = 295s during genuine work (a long generation writes\n/// nothing for minutes), while dead lanes sit >= 31h out - four orders of magnitude of\n/// separation, so 300s misses almost no real work and resurrects no dead lane. The\n/// old 15s window read a lane mid-generation as settled 1 time in 17.\npub(crate) const CHILD_RECENT_SECS: i64 = 300;"
        },
        {
          "path": "src/live/children.rs",
          "lines": "72-74",
          "snippet": "        } else if tail_age.is_some_and(|a| a <= CHILD_RECENT_SECS)\n            && shape.last_stop_reason.as_deref() != Some(\"end_turn\")\n        {"
        }
      ],
      "instrument": "Over a corpus of `subagents/agent-*.jsonl`, compute consecutive record timestamp deltas inside each file and the age of each file's tail record, then take percentiles. Counting rule: one gap per adjacent timestamped-record pair WITHIN one file (lanes with fewer than two timestamped records excluded), one age per lane; gap percentiles pooled over gaps, age percentiles over lanes. Age is always measured from the tail RECORD timestamp, never file mtime. The unit test `children_report_generating_needs_recency_and_no_end_turn` in src/live/tests/surfaces.rs pins the conjunct.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger item 4; CHANGELOG 0.9.4; AGENTS.md section 1; src/live/children.rs:14-20 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Over every file from `find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl`, extract each line's `timestamp`, take consecutive deltas WITHIN each file, and take the age of each file's last timestamped record; then percentiles over the pooled gaps and over the per-lane ages.",
          "observed": "477,499 gaps over 7,520 lanes with >= 2 timestamped records: p50 = 1.4 s, p90 = 9.7 s, p99 = 63.4 s, p99.9 = 317.0 s, p99.99 = 1048.3 s, max = 58,692.1 s. 533 gaps (0.112%) exceed 300 s; 6.0% exceed 15 s. Lane tail-record age: 9 lanes at or under 300 s (all genuinely live at the time of the scan), 7,511 above it with min 0.10 h, p1 16.28 h, p50 1,438 h.",
          "rule": "One gap per adjacent timestamped-record pair WITHIN one file (lanes with fewer than two timestamped records contribute no gaps); one age per lane, always from the tail RECORD timestamp, never file mtime. Gap percentiles pooled over gaps, age percentiles over lanes.",
          "note": "p50 and p90 reproduce exactly; p99 and p99.9 have drifted up (61.1 -> 63.4 s, 295.4 -> 317.0 s) as the corpus grew from 467,244 to 477,499 gaps, so p99.9 now sits ABOVE the CHILD_RECENT_SECS = 300 constant rather than just below it - the window misses about 1 genuine-work gap in 900. Two other wording corrections: the abandoned-lane p1 is 16.3 h in the present corpus, not 30.9 h (more recent lanes have accumulated), and 'four orders of magnitude of separation' describes the bulk of the two populations, not a gap in the distribution - the minimum dead-lane age IS the threshold. The retired 15 s window is vindicated precisely: 6.0% of gaps exceed 15 s = 1 in 16.7, which is the code comment's '1 time in 17'. Both code sites in src/live/children.rs (14-20, 72-74) match the current file verbatim."
        }
      ]
    },
    {
      "id": "TAIL-005",
      "area": "live-tail",
      "behavior": "Most DEAD subagent lanes do not end at an `end_turn`: 5,337 of 7,320 lanes with no further activity (73%) carry a last assistant `stop_reason` other than `end_turn`, so a rule keyed on `stop_reason` alone would report nearly three quarters of them as still running.",
      "depends": "tail-record recency is therefore the load-bearing conjunct of csift's `generating` state - the `<= 300 s` age test is what keeps those 73% classified `settled`.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "72-78",
          "snippet": "        } else if tail_age.is_some_and(|a| a <= CHILD_RECENT_SECS)\n            && shape.last_stop_reason.as_deref() != Some(\"end_turn\")\n        {\n            // A paired tail with a fresh record and no end_turn = mid-generation (the\n            // model is thinking/writing between tool calls; stop_reason alone would\n            // mark 73% of DEAD lanes live, so recency is the load-bearing conjunct).\n            ("
        }
      ],
      "instrument": "Over the abandoned-lane set, count lanes whose newest assistant record carries a `stop_reason` other than `end_turn`. Counting rule: one observation per lane, taken from that lane's newest assistant record only.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "CHANGELOG 0.9.4; AGENTS.md section 1; src/live/children.rs:72-84 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Same subagent-corpus pass as TAIL-004: for each lane take the newest `type:\"assistant\"` record's `stop_reason`, keep the lanes whose newest timestamped record is more than 1 h old, and bucket.",
          "observed": "7,476 dead lanes with at least one assistant record: `tool_use` 3,190, `end_turn` 1,987, null 1,703, `stop_sequence` 596. Non-end_turn = 5,489 = 73.4%. Repeating with csift's own settled boundary (tail older than 300 s) gives 5,519 of 7,506 = 73.5%.",
          "rule": "One observation per lane, taken from that lane's newest assistant record only; a null `stop_reason` counts as non-end_turn (it is exactly the case a stop_reason-only rule would misread).",
          "note": "Reproduces the ledger figure almost exactly (73.4% of 7,476 versus the recorded 73% of 7,320; the population grew with the corpus). The largest single bucket is `tool_use` (3,190 lanes), i.e. lanes that died between a tool call and its result - the shape TAIL-001 covers. The code site src/live/children.rs:72-78 matches verbatim, and the conjunct it guards is what keeps these 73% out of the `generating` state."
        }
      ]
    },
    {
      "id": "TAIL-006",
      "area": "live-tail",
      "behavior": "A MAIN transcript GROWS while its session is idle - queue enqueues, `attachment` records and metadata lines are appended with no model turn - so file mtime or byte growth alone is never evidence of activity.",
      "depends": "csift classifies WHAT landed rather than whether the file moved: `tail_shape` reads record shape and ignores mtime, and `wait` folds each newly appended record through `classify` instead of treating growth as an event; an mtime-based liveness heuristic would call every idle main session running.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "9-10",
          "snippet": "//! F9: growth alone is never activity - a main transcript grows while idle (enqueues,\n//! attachments). This module classifies WHAT the tail is, not whether the file moved."
        },
        {
          "path": "src/live/activity.rs",
          "lines": "23-27",
          "snippet": "    pub(crate) fn fold(&mut self, rec: &Record, lane: &str) {\n        self.records += 1;\n        self.lanes.insert(lane.to_string());\n        // A pulse absorbed mid-turn is a queue enqueue line / a queued_command\n        // attachment, never a labeled record (v0.10.2): count those deliveries too, so"
        }
      ],
      "instrument": "Leave a session idle and enqueue a message, or let another session's background task complete into it: the file grows (`stat` mtime and byte length advance) while `csift status @<session> --format json | jq .verdict` stays idle. Counting rule: compare the byte delta over a fixed idle interval against the verdict, then `csift search '' @<session> --turn -1 --count-by label` to show no new agent records.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13; AGENTS.md section 1; src/live/tail.rs:9-10 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) For each of the 64 transcripts from `find ~/.claude/projects -maxdepth 2 -name '*.jsonl'`, read the last 2 MB, parse the complete lines, and report the type of the final record, the records that follow the last `type:\"assistant\"` record, and the seconds between the last assistant record and the newest trailing timestamped record. (b) The live 0.1 s poller of TAIL-003, classifying every newly appended record by type.",
          "observed": "(a) 63 of 64 main transcripts end on a NON-assistant record: `last-prompt` 40, `system` 8, `permission-mode` 8, `bridge-session` 4, `cost-state` 2, `file-history-snapshot` 1 - only 1 ends on an assistant record. 53 of 64 carry timestamped records after the last assistant record; those trailing records are `attachment` 203, `system` 138, `user` 87, `last-prompt` 57, `file-history-snapshot` 31, `permission-mode` 24, `mode` 23, `ai-title` 22, `queue-operation` 10, `agent-name` 9, `bridge-session` 6, `cost-state` 3, `atis-latch` 3. Time from the last assistant record to the newest trailing record: p25 1 s, p50 184 s, p75 195 s, max 1,381,853 s (16 days). (b) During a 7-minute live poll of two busy main transcripts, 45 `attachment` records plus `last-prompt`, `ai-title`, `agent-name`, `mode`, `permission-mode`, `atis-latch`, `bridge-session`, `queue-operation` and `file-history-snapshot` lines were appended.",
          "rule": "One row per main transcript for the final-record and trailing-record census; one row per appended record for the live poll. 'Growth without a turn' = a record appended after the last assistant record of the file.",
          "note": "Confirmed twice over, statically and live. The static form is the sharper instrument: a main transcript's LAST write is almost never a model turn (63/64), and the file keeps growing for a median of 3 minutes - in one case 16 days - after the model's final record, so mtime or byte length says nothing about whether a turn is running. Incidental: this census surfaces three record types absent from the AGENTS.md 3.2 list - `bridge-session`, `cost-state` and `atis-latch` - all metadata-only and all currently ignored by the scanning surfaces. Both code sites (src/live/tail.rs:9-10, src/live/activity.rs:23-27) match verbatim."
        }
      ]
    },
    {
      "id": "TAIL-007",
      "area": "live-tail",
      "behavior": "A CHILD transcript grows only from the child's own message flow - completion notifications are appended to the MAIN transcript only - so on a child lane record recency IS a real liveness signal, unlike on the main lane.",
      "depends": "csift's child-lane state machine may legitimately use recency (the `generating` state), while the main-lane verdict never does.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "8-10",
          "snippet": "//! (an unreturned tool call) plus recent growth (child transcripts grow only from the\n//! child's own message flow - notifications land in MAIN only, so recency is a real\n//! signal here, unlike the main lane's F9 trap)."
        }
      ],
      "instrument": "While a subagent runs, watch both files: `csift status @<session> --format json | jq '.children[] | {session_id, state, detail}'` reports the child by its own tail age, and the main file receives the `<task-notification>` the child file never carries. Counting rule: notification records per file, main versus each subagent transcript.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "AGENTS.md section 1; src/live/children.rs:1-10 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift's own classifier, per project directory: `csift search '' ~/.claude/projects/<ENCODED> -t harness.notification --count-by session --format json`, splitting the census keys into uuid-shaped (main transcript) and non-uuid (subagent transcript) - run over two unrelated project directories. Cross-checked with a raw byte census of `<task-notification>` over the same directory split by file class, and with a parse of every notification-bearing subagent line asking whether the tag sits at content start or mid-prose.",
          "observed": "Project directory A: 7 transcripts carrying `harness.notification` records, 183 records, all 7 keys uuid-shaped, 0 subagent. Project directory B: 9 transcripts, 816 records, all 9 uuid-shaped, 0 subagent. Raw bytes in directory A: 1,559 `<task-notification>` hits across 22 of 23 main files versus 706 hits across 81 of 391 subagent files - but all 708 parsed subagent occurrences are `embedded-in-prose`, 0 at content start (record shapes: 519 user/user, 188 assistant/assistant, 1 isMeta).",
          "rule": "One census row per transcript; a census key that is not uuid-shaped is a subagent transcript. For the raw cross-check, one observation per line containing the tag, classified content-start versus embedded by testing whether the reconstructed message text begins with the tag.",
          "note": "The classifier is decisive: 999 delivered notification records over two project directories, 100% in main transcripts, 0 in child lanes. The raw byte count looks like a counter-example until it is parsed - every subagent hit is a quotation of the tag inside prose (this corpus contains sessions that discuss the tag), which is exactly the section-boundary distinction the classifier applies. Growth in a child lane therefore still means the child itself wrote something. Code site src/live/children.rs:8-10 matches verbatim."
        }
      ]
    },
    {
      "id": "TAIL-008",
      "area": "live-tail",
      "behavior": "Each workflow run's `journal.jsonl` (one per `wf_*` directory) is written INCREMENTALLY - `{type:\"started\", agentId}` at spawn and `{type:\"result\", agentId, result}` at return - so for a RUNNING run `started` minus `result` is the number of that run's agents in flight (measured live: 57 started, 49 returned, 8 in flight). It is not a liveness test on its own: a killed, failed or abandoned agent never writes a `result`, so the imbalance is permanent - 613 of the 621 unbalanced entries in this corpus belong to journals untouched for 15 h to 77 days. The journal also carries a third type, `failed`, which is neither counted.",
      "depends": "csift's `children_report` sums that difference across a session's journals as `journal_in_flight`; an imbalance is a LIVE signal precisely because a terminal dump would always balance.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "4-7",
          "snippet": "//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each\n//! `wf_*` dir is written INCREMENTALLY (`{type:\"started\",agentId}` at spawn,\n//! `{type:\"result\",agentId}` at return), so `started - result` = workflow agents in\n//! flight right now."
        },
        {
          "path": "src/live/children.rs",
          "lines": "104-105",
          "snippet": "    // Workflow journals: started - result per wf dir (incremental, so an imbalance is a\n    // LIVE signal - a terminal dump would always balance)."
        },
        {
          "path": "src/live/children.rs",
          "lines": "126-130",
          "snippet": "                match v.get(\"type\").and_then(serde_json::Value::as_str) {\n                    Some(\"started\") => started += 1,\n                    Some(\"result\") => resulted += 1,\n                    _ => {}\n                }"
        }
      ],
      "instrument": "`rg -o '\"type\":\"(started|result)\"' <a wf_*/journal.jsonl> | sort | uniq -c`. Counting rule: one event per journal line, per type; started minus result equals the agents in flight for that run.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "`find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat` piped to a python type/key-set census; then per journal `rg -c '\"type\":\"started\"'` minus `rg -c '\"type\":\"result\"'` paired with `stat -f %m` age; then `csift status @<8-char prefix of a long-idle session> --format json`; then the file listing of the one workflow directory whose journal was written in the last hour.",
          "observed": "160 journals, 12,754 lines: `started` 6,687, `result` 6,066, `failed` 1. Exactly two key sets: {agentId, key, type} 6,688 and {agentId, key, result, type} 6,066. Corpus-wide started minus result = 621. 124 journals balance at 0; 36 carry a positive imbalance, and 35 of those 36 - 613 of the 621 - were last written 15 h to 1,843 h ago (77 days). `csift status` on a session whose tail record is 58 d 19 h old prints evidence `children: 3513 lane(s), 359 live (359 workflow agent(s) in flight)` and verdict `waiting-children`. The single journal written within the hour, belonging to a run that was actually executing, reads started 57 / result 49.",
          "rule": "One event per journal line, per `type`; started minus result per journal directory. A journal counts as stale when its file mtime is more than 1 h old.",
          "note": "The mechanism holds and was confirmed live, but the inference 'an imbalance is a LIVE signal precisely because a terminal dump would always balance' is refuted: 98.7% of the corpus-wide imbalance sits in long-dead runs. The consequence is a live csift defect, reproducible in one command - `csift status` on a session whose newest record is 58 days old reports 359 workflow agents in flight and returns the verdict `waiting-children`. src/live/children.rs:104-105 and 126-130 match verbatim; the `_ => {}` arm at 129 is what drops the `failed` type, so a failed agent inflates `journal_in_flight` by one forever. A fix would need to age the journal (or its run) the way child lanes are aged, and to count `failed` as terminal."
        }
      ]
    },
    {
      "id": "TAIL-009",
      "area": "live-tail",
      "behavior": "A subagent's other on-disk companions carry no usable live state: across 7,520 `subagents/*.meta.json` files there are 15 key sets and not one status/state/completion key (a single file carries `stoppedByUser: true`, a terminal flag, and nothing anywhere reports 'running'). A workflow run's result is terminal-only in both places it lands - as a `result` line appended to the journal at return, and as a run-level result document in the session's `workflows/` directory that does not exist at all until the run ends (163 documents, all with a terminal status: completed 154, failed 5, killed 4).",
      "depends": "csift never derives child liveness from a meta or result file - it reads the child's own transcript tail plus the incremental workflow journal.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "3-4",
          "snippet": "//! `subagents/*.meta.json` has NO status field (child liveness can never come from\n//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each"
        }
      ],
      "instrument": "`jq 'keys' <a subagents/*.meta.json>` shows no status key, and a run in flight has no result file yet. Counting rule: one key-set inspection per meta file; presence/absence per run directory.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "SPEC.md section 6.13"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "`find ~/.claude/projects -path '*/subagents/*' -name '*.meta.json' -print0 | xargs -0 -n 200 python3 -c '<key-set census + any key matching status/state/complete>'`; then `find ~/.claude/projects -type d -name 'wf_*' -exec ls {} \\;` normalised to file shapes; then a status census of the run-level result documents under each session's `workflows/` directory; then the directory listing of the one run that was executing.",
          "observed": "7,520 meta files, 15 distinct key sets, and 0 keys anywhere matching status/state/complete. Largest sets: agentType|spawnDepth 4,582, agentType 2,105, agentType|description|toolUseId 294, agentType|description|spawnDepth|toolUseId 198, the 11-key teammate set 155. ONE exception exists: a single meta with keys agentType|description|isFork|name|spawnDepth|stoppedByUser carrying `\"stoppedByUser\": true`. Every `wf_*` directory contains only `agent-<hex>.jsonl` + `agent-<hex>.meta.json` + `journal.jsonl` (6,687 / 6,687 / 160 corpus-wide) - no result file. The run-level result documents live one level up in the session's `workflows/` directory: 163 of them, status `completed` 154, `failed` 5, `killed` 4, i.e. terminal without exception - and the run that was actually executing had none yet (`ABSENT`) while its journal already showed 57 started / 49 returned.",
          "rule": "One key-set observation per meta file; one filename-shape observation per file inside a `wf_*` directory; one status observation per run-level result document; presence/absence checked once for the single in-flight run.",
          "note": "Both clauses hold; two wordings needed correcting. First, 'has no status field at all' is true for 7,519 of 7,520 metas - the exception is a `stoppedByUser` flag, which is still terminal-only and still never says 'running', so it cannot ground liveness either. Second, there is no result FILE inside a `wf_*` directory; the terminal-only artefact is the run-level document one level up, and its absence for the currently-running run is the positive test that it is written at return. Code site src/live/children.rs:3-4 matches verbatim."
        }
      ]
    },
    {
      "id": "TAIL-010",
      "area": "live-tail",
      "behavior": "A pending tool approval leaves NO trace in the transcript: on disk a blocked lane is just the assistant's `tool_use` record (`stop_reason:\"tool_use\"`) sitting as the LAST record with no following `tool_result` for its id, and the block carries only `{type, id, name, input, caller}` - measured as the single key set of all 20,133 tool_use blocks in two project directories, with no `permission`, `isEscalated` or `requires_approval` key anywhere and neither name present in the binary at all. Escalation-blocked, awaiting-execution and wedged therefore share ONE transcript signature. They do not share one machine signature: the session registry row does carry the block - Claude Code sets `{status:\"waiting\", waitingFor:<reason>}` on `~/.claude/sessions/<pid>.json` while a dialog is open (a permission prompt, a plan approval, a question, a sandbox request), a transition write present in every installed version back to 2.1.229.",
      "depends": "csift's `lifecycle` detects the frozen lane from the NEWEST meaningful record and forces `Running` (never `completed`, which the old walk-back to a trailing assistant text produced), classifying `escalation-blocked` only when Claude Code's own dangerous-rm rule would hoist the command and reporting `awaiting-execution` otherwise rather than pretending to tell slow from wedged.",
      "code": [
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "133-137",
          "snippet": "        // Prefer a tool_use CC would hoist (dangerous rm) so classification is escalation-blocked.\n        if command\n            .as_deref()\n            .is_some_and(crate::bash_danger::is_dangerous_rm)\n        {"
        },
        {
          "path": "src/live/tail.rs",
          "lines": "3-7",
          "snippet": "//! Reads the FINAL window of a transcript (bounded, never the whole file), walks it\n//! backward, and reports the liveness-relevant shape: the newest UNRETURNED tool call\n//! (a use whose id has no later result = a tool in flight, or a process dead mid-tool),\n//! the last assistant `stop_reason`, and the last record's instant. A record is only\n//! trusted from a COMPLETE line (torn tails are skipped by the newline framing)."
        }
      ],
      "instrument": "With a permission prompt open in a live session, run `csift status @main` from another shell: the verdict must be the human-in-the-loop one, naming the pending tool. `csift show @main --turn -1 --raw | jq -r '.message.content[] | select(.type==\"tool_use\") | keys | join(\",\")'` must list only those five keys, and `rg -c 'isEscalated|requires_approval' ~/.claude/projects/**/*.jsonl` must be 0. Counting rule: one verdict per status run; raw occurrence count for the grep.",
      "located": {
        "claude_code": null,
        "csift": "0.7.0",
        "source": "AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Key census of every `tool_use` block across two unrelated project directories (`find ~/.claude/projects/<ENCODED> -maxdepth 1 -name '*.jsonl'` piped to a python block walk); a recursive structural walk asking whether `isEscalated` ever appears as a JSON KEY rather than inside message text; `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'isEscalated|requires_approval|\"caller\"|permissionRequest'` and `rg -o '.{0,110}waitingFor.{0,110}'`; the same `waitingFor` count across the five Claude Code versions installed here; and `rg -o 'waitingFor|\"status\":\"[a-z]+\"' ~/.claude/sessions/*.json`.",
          "observed": "20,133 `tool_use` blocks (7,889 + 12,244) and exactly ONE key set: `caller|id|input|name|type`. The structural walk found 0 occurrences of `isEscalated` as a key; all 38 line hits in one project directory are inside message text (3 user, 9 assistant records in the sampled files). Binary 2.1.258: 0 hits for `isEscalated`, 0 for `requires_approval`, 8 for `\"caller\"`, 15 for `permissionRequest` (all hook-decision plumbing). But the same binary carries `function XBe(T){let I=o_o(T);if(I!==void 0)return{status:\"waiting\",waitingFor:I,working:!1};return{status:T.isLoading||T.delegatedActive?\"busy\":\"i` and the registry write `l$e({status:Nne,waitingFor:oUe},tUe)`, with a dialog table of `waitingFor:\"dialog open\"` / `waitingFor:\"input needed\"` / `waitingFor:\"sandbox request\"` entries and `needs:` strings such as `needs:\"choose: allow or deny the computer-use action\"`; the registry reader is `status:Ke(o.status),waitingFor:typeof o.waitingFor===\"string\"?o.waitingFor:void 0`. `waitingFor` is present in all five installed versions (2.1.229, 2.1.241, 2.1.251, 2.1.257, 2.1.258: 7-8 string hits each, 2 `status:\"waiting\"` each). On disk right now the 7 registry rows read `status` busy 2 / idle 4 / shell 1, none `waiting`, none carrying `waitingFor`.",
          "rule": "One observation per `tool_use` block for the key census; one occurrence per structural key for the walk; raw string-hit counts for the binary greps, per version.",
          "note": "The transcript half is confirmed exactly and at scale. The clause 'it is in NEITHER transcript NOR any control file' is wrong for the current binary and, judging by the version sweep, was already wrong when it was written: the registry advertises the block. csift itself already models this - src/live/registry.rs:5-8 documents the closed status set `busy | shell | idle | waiting` with `waiting` explicitly covering a permission prompt, and src/live/verdict.rs:296-298 uses `registry_waiting` as one leg of the `waiting-hitl` verdict - so the ledger sentence contradicts csift's own current model rather than describing it. What a live instrument would still decide: whether a tool-permission prompt specifically maps to the `waitingFor:\"dialog open\"` path (the binary's `needs:` table names computer-use, sandbox and several dialog kinds but no plain tool-permission string). To settle it, open a permission prompt in an interactive session and read that session's `~/.claude/sessions/<pid>.json` for `status` and `waitingFor`. Both code sites (src/subagent/lifecycle.rs:133-137, src/live/tail.rs:3-7) match verbatim."
        }
      ]
    },
    {
      "id": "TAIL-011",
      "area": "live-tail",
      "behavior": "A backgrounded call is never an in-flight call on disk: a `Bash` tool_use with `input.run_in_background:true` is answered by its own `tool_result` (`Command running in background with ID: ...`) as soon as the task is registered - measured at 56 ms on a live launch, and never pending in 3423 backgrounded tool_use records corpus-wide (0 pending, 0 orphan). Record-to-record the pairing reads as a median of 0.46 s for a shell and 0.37 s for an async spawn (main lane, one project dir), 86% of shells within 1 s and a tail out to 9.9 s, because the assistant record's own flush latency is inside that difference. Either way the launch pairs long before the background work ends, so the tail reports no pending call for the whole life of that work, and a main-lane unreturned call only ever covers a synchronous tool.",
      "depends": "this is why csift 0.10.0 added the whole-file background scan and a seventh verdict: before it, a session idling at `end_turn` with a running background build reported `idle-eot` and satisfied `wait --until stop`; waiting children are likewise derived from child transcripts and workflow journals, never from the main tail.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "4-7",
          "snippet": "//! - a backgrounded SHELL: a `Bash` tool_use with `input.run_in_background:true`; its\n//!   tool_result arrives within milliseconds (\"Command running in background with ID:\n//!   <id>. Output is being written to: <path> ...\"), so the tail state machine pairs it\n//!   at once - it is invisible to the unreturned-call logic by construction;"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "358-367",
          "snippet": "    } else if eot_shape && background.open_counted() > 0 {\n        notes.push(\n            \"the turn ended, but background task(s) have not returned - by design (a dev \\\n             server, a watcher) or not, csift cannot tell: a UI stop, a Monitor timeout or \\\n             agent teardown leaves no transcript marker, and Claude Code reconciles only at \\\n             the next session start. `--background-since` / `--ignore-background` narrow \\\n             what counts; kill a dead one with the tool or the shell\"\n                .to_string(),\n        );\n        Verdict::IdleBackgroundOpen"
        }
      ],
      "instrument": "Launch `sleep 600` with `run_in_background`, then run `csift status @<session>`: the tail row reads `no pending call; last stop_reason end_turn` while the verdict is `idle-background-open` with a background shell row. Counting rule: one launch, one status read. The e2e test `an_open_background_shell_is_the_seventh_verdict_with_its_row` in tests/cli/live/background.rs pins it against a fixture.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "dev session 2026-09-02; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) csift search '\"run_in_background\": *true' -t agent.tool.use --count-by pairing   (2) a live launch of `sleep 600` with run_in_background, then csift status @trap:<marker> five seconds later   (3) python3 walk of one project dir under ~/.claude/projects joining every tool_use block whose input.run_in_background is true to the tool_result block carrying the same tool_use_id, differencing the two records' `timestamp` fields   (4) csift status @<id> --no-subagents over all 23 top-level transcripts of that project dir   (5) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Command running in background with ID'",
          "observed": "(1) '3423 paired' and 'csift: 3423 matched record(s) across 1 pairing key(s)' - one pairing key, zero pending, zero orphan.  (2) tool_result text 'Command running in background with ID: bxov2rges. Output is being written to: ...'; tool_use timestamp 09:33:13.389Z, tool_result timestamp 09:33:13.445Z = 56 ms, while the 600 s sleep kept running; the status read five seconds later showed 'bg  shell  bxov2rges  launched ... (5s ago)' and the only unreturned call at the tail was the synchronous csift Bash call then in flight.  (3) 430 jsonl files, 429 backgrounded launches, 0 unpaired. main-lane Bash n=287 min 0.077 s median 0.462 s p90 1.328 s max 9.933 s, 247/287 <= 1 s; main-lane async Agent spawn n=63 min 0.318 s median 0.367 s p90 0.826 s max 4.276 s; subagent-lane Bash n=80 min 0.056 s median 0.390 s p90 1.199 s max 4.818 s.  (4) verdict census 20 idle-eot / 1 idle-background-open / 1 running / 1 unknown; the idle-background-open row reads exactly 'verdict  idle-background-open' + 'tail      no pending call; last stop_reason end_turn  (3747865s ago)' + one open async-agent bg row launched 59 d 22 h earlier.  (5) the binary carries the literal 'Command running in background with ID: ' and its formatter emits four variants keyed on backgroundedByUser / backgroundedToDeliverMessage / timedOutAfterMs / the plain launch.",
          "rule": "A launch = one tool_use block with input.run_in_background == true. It is PAIRED if a tool_result block with the same tool_use_id exists anywhere in the same file; latency = result record timestamp minus use record timestamp. Pairing census counted by csift over the whole corpus; latency distribution counted over one project dir (430 jsonl files, top-level plus subagent transcripts, journal.jsonl excluded). Verdict census = one csift status read per top-level transcript.",
          "note": "The substantive assertion survived every attempt to refute it: no instrument found a backgrounded launch sitting unreturned at the tail, and the seventh verdict reproduced verbatim on a real transcript ('no pending call; last stop_reason end_turn' with an open background row 59 days old). Only the phrase 'within milliseconds' needed correction - it is exact for the tool round trip (56 ms measured live) but not for the on-disk record delta, whose median is about half a second and whose tail reaches ten seconds. Two additions the claim does not mention, both now instrument-backed. First, the binary's result formatter has four shapes, and three of them describe a call that started SYNCHRONOUS and was converted to a background task later: 'Command was manually backgrounded by user with ID: ...', 'Command was moved to the background (ID: ...) so that a message that arrived while it was running can reach you; it was not interrupted', and 'Command did not complete within its <N>s timeout and was moved to the background (ID: ...)'. The third fired in this very verification run, so a synchronous main-lane call can leave the unreturned-call state by conversion rather than by completion - it still pairs, so the claim's conclusion is unaffected, but the tail's pending window for a slow synchronous tool is bounded by that timeout rather than by the tool. Second, the source comment at src/live/background.rs:5 carries the same 'within milliseconds' wording and would read truer as 'as soon as the task is registered (56 ms measured; a sub-second record delta)'. All five cited code sites exist verbatim at the cited paths and line ranges (src/live/background.rs:4-7, src/live/verdict.rs:358-367), so corrections.code is empty."
        }
      ]
    },
    {
      "id": "TAIL-012",
      "area": "live-tail",
      "behavior": "Claude Code appends whole records, so complete-line framing is the only guard a reader needs: no transcript was ever left with an unterminated final line (0 of 430 files) and no line was ever left unparseable (0 malformed in 20677 lines censused by csift stats). A partially written final line is possible rather than routine - records reach 2.6 MB and 142 of them exceed 64 KiB, so an append can in principle be observed mid-write - but it was never caught: 0 torn tails in 18238418 samples covering 190 observed appends.",
      "depends": "csift's tail reader and `wait`'s incremental cursor both stop at the last newline and hold the torn remainder for the next poll; parsing the torn line would surface a spurious malformed-line count on every live read.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "52-63",
          "snippet": "    // Complete lines only: a torn final line (no trailing newline) is held, not parsed.\n    let mut lines: Vec<&[u8]> = Vec::new();\n    let mut pos = 0usize;\n    while pos < window.len() {\n        match memchr::memchr(b'\\n', &window[pos..]) {\n            Some(nl) => {\n                lines.push(&window[pos..pos + nl]);\n                pos += nl + 1;\n            }\n            None => break, // torn tail: skip (the next poll re-reads it complete)\n        }\n    }"
        },
        {
          "path": "src/live/wait.rs",
          "lines": "147-152",
          "snippet": "            while pos < bytes.len() {\n                let Some(nl) = memchr::memchr(b'\\n', &bytes[pos..]) else {\n                    break; // torn tail: hold the cursor here\n                };\n                let line = &bytes[pos..pos + nl];\n                pos += nl + 1;"
        }
      ],
      "instrument": "Poll a live transcript's byte length in a tight loop and check whether the final byte is a newline: a torn tail appears transiently. Counting rule: one sample per poll. The unit test `tail_shape_reads_pairing_stop_reason_and_holds_torn_tails` in src/live/tests/surfaces.rs asserts the torn line is neither parsed nor counted.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live/tail.rs:52 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) three zero-sleep polling loops in python3, each reading os.path.getsize plus the final byte (open, seek(-1,2), read(1)) of live transcripts: 56 files with mtime under 2 h for 40 s; one single live transcript for 100 s; the 6 most recently written transcripts of one project dir for 150 s.  (2) a single pass over every jsonl in that project dir checking whether the final byte is a newline and recording the longest line.  (3) csift stats @<id> @<id> @<id> --format json, summing skipped_lines over the emitted session rows (the whole-file malformed census).",
          "observed": "(1) 641368 + 7092154 + 10504896 = 18238418 samples; 59 + 3 + 128 = 190 samples in which the file had grown since the previous sample; torn_tail_samples 0 in all three runs, examples list empty.  (2) 430 jsonl files scanned, 0 whose final byte is not a newline; longest single line 2609260 bytes; 142 lines larger than 64 KiB.  (3) 10 session rows, 20677 lines, summed skipped_lines 0.",
          "rule": "One sample = one (file, poll) pair that successfully read a size and a final byte; a sample counts as TORN when that final byte is not 0x0A, and counts as an observed append when the size differs from that file's previous sample. Malformed census = sum of skipped_lines over csift stats session rows, which validates every line of every scanned file.",
          "note": "The half of the claim csift actually depends on is confirmed by instrument and is the half that matters: records land whole, so holding the trailing partial line and re-reading it next poll is sufficient, and a reader that parsed it would be inventing malformed-line counts that the corpus does not contain. The half that says a torn tail is 'a normal transient state' is overstated at the observed rate of 0 in 190 appends. That is not a refutation - the polling loop resolves a file about every 14 microseconds when watching one file and about every 3.5 ms when watching 56, so a single-syscall append of a few-KB record can slip entirely between two samples, and on macOS a write(2) to a regular file is serialized against concurrent reads, which would make the window zero for any record the runtime emits in one call. What would decide it: a syscall trace of the harness process (root fs_usage or dtrace on the writing pid, unavailable to this session) showing whether a record over 64 KiB is emitted as one write(2) or several; several write calls per record means the torn state is real and reachable, one write call per record means the guard is defensive rather than routine. Both cited code sites exist verbatim at the cited paths and line ranges (src/live/tail.rs:52-63, src/live/wait.rs:144-149), so corrections.code is empty; the unit test named in the instrument, tail_shape_reads_pairing_stop_reason_and_holds_torn_tails, is at src/live/tests/surfaces.rs:89 and asserts records_seen == 1 for a fixture whose final line is torn and whose window also holds a garbage line."
        }
      ]
    },
    {
      "id": "TAIL-013",
      "area": "live-tail",
      "behavior": "The newest turn-opening record at the tail may be a MACHINE trigger rather than typed prose - an automation pulse whose raw content is `<task-notification>` XML - so the newest 'prompt' in a transcript is not necessarily something a human wrote, and its raw form must never be shown verbatim.",
      "depends": "csift's `last` section renders `automation_label()` first and falls back to `reconstructed_user_text` only for a genuine prompt, so the last-prompt row prints `[background-command <id> completed] ...` rather than raw XML.",
      "code": [
        {
          "path": "src/live/last.rs",
          "lines": "44-53",
          "snippet": "        if out.user.is_none() {\n            // A genuine prompt, or the machine trigger that opened the turn (an\n            // automation pulse renders as its label, never the raw XML).\n            let text = rec.automation_label().or_else(|| {\n                rec.is_genuine_user()\n                    .then(|| rec.reconstructed_user_text(None))\n                    .flatten()\n            });\n            if let Some(t) = text {\n                out.user = Some(excerpt(rec.timestamp.clone(), &t));"
        }
      ],
      "instrument": "`csift status @<session>` on a session whose last turn was opened by a background-command completion: the last-prompt row reads the label form, not XML. Counting rule: one status read per session. The e2e test `the_last_section_prints_with_only_a_prompt_on_disk` in tests/cli/live/background.rs pins the section.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "src/live/last.rs:44-53 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(1) csift status @<id> --no-subagents over all 23 top-level transcripts of one project dir under ~/.claude/projects, keeping the sessions whose `last` prompt row renders a bracketed label.  (2) python3 read of the matching record in one of those transcripts, printing message.role, type, isMeta and the verbatim string content.  (3) python3 census of every main-lane jsonl in that project dir counting type user records whose string content starts with the notification tag, bucketed by the record's own `version` field.  (4) the same census run against a live subagent transcript and against its parent main transcript, after five background commands completed during this verification run.",
          "observed": "(1) three sessions render an automation label as the newest prompt: '[background-command b1irkb1da completed] Background command \"Commit v0.6.10, reinstall, tag, push (full release chain)\" completed (exit code 0)', '[background-command bah3q0myj killed] Background command \"Wait for both suite outputs\" was stopped', and '[subagent stopped] 2 background agents were stopped by the user: \"This is a test of the `csift` CLI tool's `@trap:<m...\", ...'. No raw XML appeared in any last row.  (2) the record behind the first is line 14223, type user, role user, isMeta absent, string content beginning '<task-notification>\\n<task-id>b1irkb1da</task-id>\\n<tool-use-id>toolu_...</tool-use-id>\\n<output-file>...</output-file>\\n<status>completed</status>\\n<summary>Background command \"...\" completed (exit code 0)</summary>'.  (3) 180 such turn-opening records, oldest 2026-06-26T23:57:40Z stamped version 2.1.191, newest 2026-09-02T03:35:06Z stamped version 2.1.258.  (4) 0 in the 126-line live subagent transcript versus 107 in its parent main transcript, newest stamped 2.1.258.",
          "rule": "A turn-opening automation pulse = a record with type user, message.role user, whose message.content is a STRING whose first non-space characters are the notification open tag. A session counts as rendering the label form when the first `last` prompt row printed by csift status begins with a bracketed label instead of the tag. One status read per top-level transcript; the version bucket is the record's own version field, not the installed binary.",
          "note": "Confirmed on the current Claude Code, not merely historically: the newest such record in this project dir is stamped 2.1.258 and is four hours old at verification time, so the pulse is still written as a plain type user record carrying raw XML as its whole string content. Every csift render of one was the label form; no instrument produced raw XML in a last row. One lane fact the claim does not state and that an implementer should know: this is a MAIN-lane shape only. Five background commands completed inside a subagent lane during this run and each notification reached the model in context, yet the subagent transcript ended with 0 such records against 107 in the parent main transcript - so a reader that expects the pulse in a subagent transcript will find nothing, and background completion is only recoverable from disk via the parent. That does not weaken the claim, whose consequence is a rendering rule; it narrows where the rule is exercised. The cited code site exists verbatim at src/live/last.rs:44-53, so corrections.code is empty; the e2e tests named in the instruments are present, the_last_section_prints_with_only_a_prompt_on_disk at tests/cli/live/background.rs:444 and an_open_background_shell_is_the_seventh_verdict_with_its_row at tests/cli/live/background.rs:26."
        }
      ]
    },
    {
      "id": "TAIL-014",
      "area": "live-tail",
      "behavior": "The newest records of a live transcript are the only on-disk evidence of what a session is doing, and the newest record can be ANY type that carries a `timestamp`, not just `user`/`assistant`; several bookkeeping line types carry no `timestamp` at all, so what dates the tail is the newest TIMESTAMPED record in file order.",
      "depends": "A trailing TIMESTAMPED bookkeeping record does advance the tail instant by design (measured 6.651 s ahead of the newest assistant record), so the tail age must never be read as 'time since the model last did something' nor compared against a role-scoped last_utc. The claim's second half held: `stop_reason` is read separately from the newest assistant record, and the settled lane stayed idle-eot rather than being classified generating.",
      "code": [
        {
          "path": "src/live/tail.rs",
          "lines": "27-28",
          "snippet": "    /// The newest record's timestamp (any type that carries one).\n    pub(crate) last_ts_utc: Option<String>,"
        },
        {
          "path": "src/live/tail.rs",
          "lines": "73-75",
          "snippet": "        if shape.last_ts_utc.is_none() {\n            shape.last_ts_utc = rec.timestamp.clone();\n        }"
        }
      ],
      "instrument": "`csift stats`' `last_utc` is NOT the maximum record timestamp over the whole file: stats aggregates only the role-bearing candidate records, so `last_utc` is the maximum over `user`/`assistant` records (measured: it equalled the newest assistant record exactly, 6.651 s behind the file's true newest timestamped record, a `system` line). `line_types` IS a whole-file census. Therefore a status-tail vs stats-last_utc disagreement is normally the DOMAIN difference, not non-monotonicity - in the same corpus the tail-order instant differed from the file maximum in only 1/23 files, by 1 ms. To see non-monotonicity, compare the last timestamped record in file order against max(timestamp) over the same file directly. The jq also needs the null-safe form: `.evidence[]? | select(.surface==\"tail\")` - the literal `.evidence[]` errors on the header line ('Cannot iterate over null').",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/tail.rs:20-31 (the `TailShape` fields)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) python scan of ONE project dir under ~/.claude/projects (23 transcripts): per file, the type of the newest record carrying `timestamp` in FILE ORDER, the per-type census of `timestamp` presence, the count of adjacent backward timestamp steps, and whether the last timestamped record equals the file maximum. (2) On the newest live session: `csift status @<id> --format json | jq -c '.evidence[]? | select(.surface==\"tail\")'` compared against `csift stats @<id> --format json --no-subagents` and against a direct python read of the same transcript.",
          "observed": "One project dir, 23 transcripts: newest TIMESTAMPED record in file order was `system` in 18 files, `user` in 4, `queue-operation` in 1 - 19/23 were NOT user/assistant. The last LINE of all 23 was a non-record bookkeeping type (last-prompt 19, permission-mode 3, queue-operation 1). Eight line types carried a timestamp on 0 of their occurrences: last-prompt 3543, mode 3520, permission-mode 3520, ai-title 3139, agent-name 2687, file-history-snapshot 435, bridge-session 270, atis-latch 269. Timestamps are not monotone in file order: 22/23 files contained at least one backward adjacent step, 8567 backward pairs over 87444 timestamped records (9.8%); despite that, the last timestamped record differed from the file maximum in only 1/23 files, by 1 ms. Live session (159028 lines): the newest timestamped record was a `system` record at 2026-09-02T11:22:40.203Z and `csift status`'s tail row read age_secs 697 against it; `csift stats` last_utc was 2026-09-02T11:22:33.552Z, exactly the newest `assistant` record, 6.651 s older. The verdict stayed idle-eot with tail value 'no pending call; last stop_reason end_turn'.",
          "rule": "One transcript = one observation. (a) type of the newest record bearing a `timestamp` key, scanning lines in file order and keeping the last one that has the key. (b) per line `type`, count occurrences with and without a `timestamp` key. (c) count adjacent pairs (i, i+1) over timestamped records in file order where ts[i+1] < ts[i]. (d) one status-vs-stats comparison per session, both read in the same second.",
          "note": "Behavior confirmed on all three points, and more strongly than stated: on this corpus the newest timestamped record is usually NOT user/assistant (19/23 files), and the last physical LINE is almost always a type carrying no timestamp at all. Two line types seen bearing no timestamp are not in the csift docs' enumeration - `bridge-session` (270) and `atis-latch` (269) - which is exactly the tolerance the model is built for, but worth a separate claim. Code sites verified verbatim: src/live/tail.rs:27-28 and 73-75 both match the claimed snippets byte for byte, and `parse::parse_line` (src/parse/lines.rs:113) is type-agnostic, so the reverse walk really does take the instant from any type that carries one."
        }
      ]
    },
    {
      "id": "TAIL-015",
      "area": "live-tail",
      "behavior": "The written payload of a file tool lives under a DIFFERENT `tool_use` input key per tool - `Write.content`, `Edit.new_string`, `NotebookEdit.new_source` - and `MultiEdit` nests its payload one level down as `edits[].new_string`; there is no common content key, just as the path splits `file_path` versus `NotebookEdit.notebook_path`.",
      "depends": "`csift wait --until 'write:<path-regex>[:<line-regex>]'` reads the path from `file_path` or `notebook_path` and scans exactly the string-valued input keys `content`, `new_string` and `new_source`, so a MultiEdit's per-hunk `edits[].new_string` is unreachable to the line half of the condition: such a write can satisfy the path half alone and never a line regex.",
      "code": [
        {
          "path": "src/live/conditions.rs",
          "lines": "24-29",
          "snippet": "    /// A Write/Edit/MultiEdit/NotebookEdit whose path matches (and whose written content\n    /// contains a line matching, when given).\n    Write {\n        path_re: regex::Regex,\n        line_re: Option<regex::Regex>,\n    },"
        },
        {
          "path": "src/live/conditions.rs",
          "lines": "172-182",
          "snippet": "                } if matches!(n.as_str(), \"Write\" | \"Edit\" | \"MultiEdit\" | \"NotebookEdit\") => {\n                    let path = input\n                        .get(\"file_path\")\n                        .or_else(|| input.get(\"notebook_path\"))\n                        .and_then(serde_json::Value::as_str)\n                        .unwrap_or_default();\n                    if !path_re.is_match(path) {\n                        return false;\n                    }\n                    line_re.as_ref().is_none_or(|re| {\n                        [\"content\", \"new_string\", \"new_source\"].iter().any(|k| {"
        }
      ],
      "instrument": "The unit test at src/live/tests/conditions.rs:143-157 fixtures a `NotebookEdit` tool_use carrying `notebook_path` + `new_source` and asserts the path source is consulted and that a path miss short-circuits before the line regex. Live check: `csift wait @<id> --timeout 60 --until 'write:parse\\.rs:fn main'` is satisfied by an `Edit` of that file and never by a `MultiEdit` of it. Counting rule: one condition evaluation per appended record; the condition is satisfied by the FIRST matching record.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/conditions.rs:24-29 doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(1) `strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc.strings` then `rg -o` for each tool's input schema. (2) Per-project-dir tool census: `csift search '' @<encoded-dir> --no-subagents -t agent.tool.use --count-by tool`, run once per project dir. (3) Python key census over ONE project dir (23 transcripts) of every Write/Edit/MultiEdit/NotebookEdit tool_use `input` key. (4) Four live `csift wait` runs against a synthetic fixture home: `csift wait --claude-home $FIX @<fixture-uuid> --timeout 6 --until '<cond>'`, with a matching tool_use record appended 2 s after the readiness line.",
          "observed": "Binary 2.1.258 schema strings, quoted: Write - `file_path:i().describe(\"The absolute path to the file to write (must be absolute, not relative)\"),content:i().describe(\"The content to write to the file\")`; Edit - `file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i().describe(\"The text to replace\"),new_string:i().describe(\"The text to replace it with (must be different from old_string)\")`; NotebookEdit - `notebook_path:i().describe(\"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\")` and `new_source:i().describe(\"The new source for the cell\")`; MultiEdit nesting - `{file_path:e,edits:n.map(({old_string:f,new_string:_,replace_all:v})=>...)}`. The path-key split is stated outright in the tool table: `Write:{input:\"file_path\",...},Edit:{input:\"file_path\",...},MultiEdit:{input:\"file_path\",...},NotebookEdit:{input:\"notebook_path\",...}`, and all four are in the live tool registry line `[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",...]`. Corpus: 1722 Edit tool_use records, all 1722 carrying exactly {file_path, old_string, new_string, replace_all}; 145 Write records, all 145 carrying exactly {file_path, content}; 0 MultiEdit and 0 NotebookEdit records in any of the 15 project dirs. Live wait runs: Edit + `write:target\\.rs:NEEDLE_ALPHA` fired (exit 0); MultiEdit + `write:target\\.rs` (path half alone) fired (exit 0); MultiEdit + `write:target\\.rs:NEEDLE_ALPHA`, with NEEDLE_ALPHA present in edits[0].new_string, did NOT fire - 'fired timeout', exit 124.",
          "rule": "One tool_use block = one record for the key census; a record counts under a tool only if block.type=='tool_use' and block.name is that tool. For the wait runs, one condition evaluation per appended record and the first matching record wins; the pass/fail signal is the exit code (0 = a condition fired, 124 = timeout) plus the `fired` field.",
          "note": "The blind spot reproduces exactly as claimed, and the three-run design isolates it: the same needle in a top-level `new_string` fires while the same needle one level down in `edits[].new_string` does not, and the MultiEdit record is not being ignored wholesale because its path half fires on its own. Two side observations. First, MultiEdit and NotebookEdit have zero on-disk instances across all 15 project dirs here, so the blind spot is real at the schema level but has never been exercised by this corpus - the reproduction above is a synthetic fixture, not a found record. Second, `csift wait --help` describes the condition as 'a Write/Edit whose path matches', narrower than the code, which matches Write|Edit|MultiEdit|NotebookEdit (src/live/conditions.rs:161); a reader of the help would not expect a MultiEdit to satisfy the path half at all. Code sites verified verbatim at src/live/conditions.rs:24-29 and 161-171."
        }
      ]
    },
    {
      "id": "TAIL-016",
      "area": "live-tail",
      "behavior": "The set of transcript files that constitute one session is NOT fixed at its start: a subagent lane is born mid-session - its `subagents/agent-<hex>.jsonl` exists only from the moment the lane is spawned - so a watcher that enumerated the file set once would never see a lane spawned after it started.",
      "depends": "`csift wait` re-enumerates a session's subagent transcripts on EVERY poll and joins a newly discovered lane at offset 0, which is exact because a file born after start is wholly post-start; that is what keeps its strict post-start baseline honest for lanes it did not see at start.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "98-112",
          "snippet": "        // ── New-child discovery: a lane spawned after start joins the watch set with\n        //    baseline 0 (its whole content is post-start). ──\n        if args.want_subagents() && !is_subagent_target {\n            for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {\n                if !cursors.iter().any(|c| c.path == sub) {\n                    let lane = crate::subagent::session_id_from_path(&sub);\n                    cursors.push(Cursor {\n                        path: sub,\n                        offset: 0,\n                        is_main: false,\n                        lane,\n                    });\n                }\n            }\n        }"
        }
      ],
      "instrument": "Start `csift wait @<id> --timeout 120 --format json` against a session that then spawns a subagent: the activity census must list the new lane. Counting rule: one lane per discovered subagent transcript path in the census; a lane joined mid-wait contributes its whole file, every byte of which is post-start by construction.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/wait.rs:95-109"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(1) Python stat walk scoped to ONE project dir under ~/.claude/projects: for every `<session>/subagents/**/agent-*.jsonl`, its filesystem birthtime minus the parent transcript's FIRST timestamped record. (2) Live `csift wait --claude-home $FIX @<fixture-uuid> --timeout 7 --until 'tool:Bash' --format json` against a fixture session that had NO subagents directory at start, with `subagents/agent-<hex>.jsonl` created 2 s after the readiness line; plus the same run with `--no-subagents` as a negative control.",
          "observed": "Scoped project dir: 5497 subagent transcripts compared; 5497/5497 were born strictly after their parent session's first timestamped record, 0 at or before it; gap seconds min 633.6, median 1981488, max 3350868. Live run: stderr readiness line said `csift: watching 1 file(s) from byte offsets; conditions: tool:Bash; timeout 7s` - the lane did not exist at baseline - and after the lane was created mid-wait the run exited 0 with `{\"fired\":\"tool:Bash\",\"activity\":{\"lanes\":1,\"records\":1,\"tools\":{\"Bash\":1},...}}`. Negative control with `--no-subagents`, identical lane birth: 'fired timeout', exit 124.",
          "rule": "One subagent transcript = one observation; born-after is birthtime > parse(parent's first record timestamp). For the live run, the readiness line's file count is the baseline watch-set size and the `activity.lanes` count is the number of distinct lanes that contributed post-baseline records; exit 0 with a `fired` condition means the newly discovered lane's record was actually read.",
          "note": "Both halves are instrument-confirmed. The negative control matters: with `--no-subagents` the identical lane birth produced a timeout, so the exit-0 in the positive run is attributable to the per-poll re-enumeration and not to anything landing in the main transcript. The baseline-exactness argument also checks out in code: initial cursors are seeded from `std::fs::metadata(...).map(|m| m.len()).unwrap_or(0)` (src/live/wait.rs:45-70) while a lane discovered inside the loop joins at offset 0, and a file that did not exist at baseline cannot hold pre-baseline bytes. Code site verified verbatim at src/live/wait.rs:95-109."
        }
      ]
    },
    {
      "id": "TAIL-017",
      "area": "live-tail",
      "behavior": "The sidecar FILE is born at the first pending ask; the DIRECTORY usually is not. `<session-uuid>/` is the session's shared sidecar dir - the same dir that holds `subagents/` - so Claude Code creates it on the first subagent spawn, independent of any elicitation. Measured: of 20 sidecar files, 13 sat in a dir that predated them by more than 1 s (median 118410 s, about 1.4 days; max 775906 s, about 9 days), and 11 of those dirs also held `subagents/`; only 7 had a dir born with the file. Since `elicitation::sidecar_path` resolves on `dir.is_dir()` alone (src/subagent/discover.rs:9-14), the path can therefore resolve long before any ask has occurred, at which point the sidecar file simply does not exist yet. Absence at start remains the normal state and never an error, but the correct statement is: the path may or may not resolve at start, and the FILE appears at the first pending ask.",
      "depends": "`csift wait` re-attempts sidecar path resolution on every poll instead of resolving once at start, and joins the file at offset 0 when it appears (wholly post-start by construction); a one-shot resolution would miss every elicitation that began after the watch, which is exactly the class of event a wait exists to catch.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "113-129",
          "snippet": "        // ── Sidecar discovery: the sidecar DIR is typically born mid-wait (the first\n        //    pending ask creates it), and its path resolves only once the dir exists -\n        //    so re-attempt each poll. A file born after start is wholly post-start, so\n        //    baseline 0 is exact. ──\n        if !is_subagent_target {\n            if let Some(sc) = crate::elicitation::sidecar_path(&main) {\n                if !cursors.iter().any(|c| c.path == sc) {\n                    let lane = crate::subagent::session_id_from_path(&main);\n                    cursors.push(Cursor {\n                        path: sc,\n                        offset: 0,\n                        is_main: true,\n                        lane,\n                    });\n                }\n            }\n        }"
        }
      ],
      "instrument": "With the elicitation hook recipe installed, watch a session with `csift wait @<id> --timeout 120 --format json` and let it raise an AskUserQuestion: the sidecar file beside the session's `subagents/` directory does not exist before that ask and does from it, and the pending marker lands in the wait census. Counting rule: one observation per elicitation - the sidecar file's existence before versus after the first pending ask; a session that never elicits never grows one.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/wait.rs:110-126"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) Python walk over the session sidecar directories under ~/.claude/projects: how many `<session>/` dirs exist, how many hold `subagents/`, how many hold `elicitations.jsonl`, and for each sidecar file its birthtime minus its containing dir's birthtime. (2) `python3 -c` read of ~/.claude/settings.json to confirm the recording hook is installed. (3) Live `csift wait --claude-home $FIX @<fixture-uuid> --timeout 7 --until 'auq' --format json` against a fixture session whose `<uuid>/` dir did NOT exist at start, with the dir and a `csiftPhase:\"pending\"` AskUserQuestion marker created 2 s after the readiness line.",
          "observed": "Hook is installed: settings.json hook events are ['Elicitation','ElicitationResult','PostToolUse','PostToolUseFailure','PreToolUse','SessionStart','Stop','UserPromptSubmit'] and the hook config mentions the elicitation sidecar. Corpus: 65 session sidecar dirs, 27 holding `subagents/`, 20 holding `elicitations.jsonl`. Of those 20 sidecar files, only 7 sat in a dir born within 1 s of the file; 13 sat in a dir strictly older by more than 1 s (gap seconds min 183, median 118410, max 775906), and 11 of those 13 dirs also held `subagents/`. Live run: the fixture's `<uuid>/` dir did not exist at start, stderr said `csift: watching 1 file(s) from byte offsets; conditions: auq; timeout 7s`, and after the dir plus sidecar file were created mid-wait the run exited 0 with `activity.tools {\"AskUserQuestion\":1}` and 1 record.",
          "rule": "One sidecar file = one observation; 'dir created by the first ask' counted as (sidecar file birthtime - containing dir birthtime) <= 1 s. For the live run, the readiness line's file count is the baseline watch-set size (1 = the main transcript only, no sidecar cursor) and exit 0 with a fired condition means the sidecar was discovered and read on a later poll.",
          "note": "The depends half is instrument-confirmed and unaffected by the correction: the fixture had neither dir nor file at baseline, wait watched 1 file, and the condition still fired at exit 0 - a one-shot resolution would have missed it. The offset-0 baseline stays exact under the correction, because a pre-existing dir means the sidecar path resolves at seed time and `seed` snapshots the real file length (src/live/wait.rs:45-70), while a path that only resolves later implies the file was created after the baseline. The 'when the recording hook is installed' qualifier is load-bearing and worth keeping prominent: the sidecar is written by a hook, not by Claude Code, so on a machine without it no sidecar ever appears and `--until auq` degrades to native AskUserQuestion tool_use blocks only (src/live/conditions.rs:127-139). Code site verified verbatim at src/live/wait.rs:110-126."
        }
      ]
    },
    {
      "id": "MISC-001",
      "area": "misc",
      "behavior": "Hook-injected conversation context lands in the transcript as a `type:\"attachment\"` record whose payload is `{\"type\":\"hook_additional_context\",\"content\":[...]}` - the single attachment payload shape that carries injected context. `content` is a string ARRAY in real data (one element per injected block; a bare string is tolerated), holding the text a SessionStart / UserPromptSubmit / ... hook injected.",
      "depends": "csift joins the array with a newline in the one extractor `Record::hook_additional_context_text`, classifies the record `harness.meta.hook`, and scans it only under `search --additional-context` (or the `--attachments` superset) with the candidate needle `&&`-gated so a default scan never parses attachment lines; an explicit `show --line`/`--uuid` address renders it flag-free under the refetch law. A bare-string-only reader returns nothing on real data.",
      "code": [
        {
          "path": "src/cli/search_args.rs",
          "lines": "493",
          "snippet": "    /// Also scan hook-injected `additionalContext`: the `attachment` records a SessionStart /"
        }
      ],
      "instrument": "`rg -m1 'hook_additional_context' <transcript> | jq '.attachment.content | type'` prints `array`; then `csift search '<a needle only in injected hook context>' --additional-context @<id>` must find it while the same search without the flag must not. Counting rule: one payload per raw attachment line.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.7.6",
        "source": "SPEC.md section 4.8; SPEC.md section 6 v0.7.6 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -NI '\"hook_additional_context\"' ~/.claude/projects/<one project dir> -g '*.jsonl' | jq -r '.attachment.content|if type==\"array\" then \"array-len-\\(length)\" else \"SCALAR-\\(type)\" end' | sort | uniq -c   ;   csift search 'project-memory-context' @<id> --count-by label   (then the same with --additional-context, then with --attachments)   ;   csift show @<id> --line 8",
          "observed": "Largest such project dir: 25666/25666 attachment lines carry content as a JSON ARRAY, zero scalars; lengths 25541 x len-1 plus len-5, 21, 22, 23, 24, 25, 27, 28, 29 (so multi-block injections are real, not hypothetical). A second project dir: 9459/9459 array. Payload type on the sampled record: {\"type\":\"attachment\",\"att_type\":\"hook_additional_context\",\"content_type\":\"array\"}. Gate, on one needle that lives on exactly 2 attachment lines of one transcript (one hook_success payload, one hook_additional_context payload): default scan = 0 records; --additional-context = 1 record, sole label key harness.meta.hook; --attachments = 2 records, keys harness.meta.hook + harness.meta.attachment (a strict superset). csift show --line 8 rendered the record flag-free as '(gear) harness.meta.hook  L8  <project-memory-context> ... </project-memory-context>'.",
          "rule": "One payload per raw attachment line; the shape census counts lines, the gate census (--count-by label) counts records, so a two-section record cannot inflate it.",
          "note": "Both halves measured. The array shape is universal in the sampled corpus (zero bare strings), so the tolerated bare-string arm is defensive only, and a bare-string-only reader would indeed return nothing. Code sites at src/search/scan.rs:306-309 and src/model/predicates.rs:329-333 are verbatim; the third moved down 5 lines."
        }
      ]
    },
    {
      "id": "MISC-002",
      "area": "misc",
      "behavior": "Large tool outputs are EXTERNALISED to a sibling `tool-results/<id>.txt`, leaving an inline `<persisted-output>` pointer whose body carries the line `Full output saved to: <ABSOLUTE_PATH>` plus a short preview; the same path is also exposed structurally on `toolUseResult.persistedOutputPath`.",
      "depends": "`search --resolve-persisted` replaces the pointer with the file's content BEFORE matching, preferring the structured field (exact, no regex) and falling back to scraping the inline marker line; a read failure is non-fatal and appends an explicit note rather than silently matching nothing.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "306-307",
          "snippet": "    #[serde(rename = \"persistedOutputPath\")]\n    pub(crate) persisted_output_path: Option<serde_json::Value>,"
        }
      ],
      "instrument": "`rg -c 'persistedOutputPath' ~/.claude/projects/*/*.jsonl` for the population and `ls ~/.claude/projects/*/*/tool-results/ | head` for the store; then `csift search '<a phrase only in the external file>' @<session> --resolve-persisted -c` versus the same without the flag. Counting rule: one pointer per externalised tool result; matched records with and without resolution.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 4.6 as consumed by section 6.2; src/model/exchange.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -NI -m1 'persistedOutputPath' ~/.claude/projects/<dir>/<session>.jsonl | jq -c '.toolUseResult|keys'   ;   rg -NIo 'Full output saved to: [^\"\\\\]*' <same transcript>   ;   find ~/.claude/projects -maxdepth 4 -path '*/tool-results/*.txt' | wc -l   ;   csift search '<a phrase present only in the external .txt>' @<id> -c   (then the same with --resolve-persisted)",
          "observed": "Structured field present on a real record: toolUseResult keys [\"interrupted\",\"isImage\",\"noOutputExpected\",\"persistedOutputPath\",\"persistedOutputSize\",\"stderr\",\"stdout\"]. Inline marker present on the same session: 'Full output saved to: ~/.claude/projects/<dir>/<uuid>/tool-results/<id>.txt', with 10 marker lines and 7 <persisted-output> blocks in one transcript. Store: 1891 externalised .txt files corpus-wide, 12 tool-results directories in one project dir. Functional gate: a 35-character phrase that rg finds in the .txt file and NOT in the .jsonl matched 0 exchanges without the flag and 1 exchange with --resolve-persisted. Read-failure arm, against a fixture transcript under --claude-home whose pointer names a path that does not exist: output kept the inline text and appended '[csift: could not resolve persisted output /nonexistent/dir/gone.txt: No such file or directory (os error 2)]', exit 0.",
          "rule": "One pointer per externalised tool result; the gate counts matched exchanges for one fixed needle with and without resolution (0 vs 1).",
          "note": "Both carriers (structured field and inline marker) coexist on the same session, so the 'prefer structured, fall back to scraping' order is exercisable. Code sites at src/model/grouping.rs:249-253 and src/model/exchange.rs:313-316 are verbatim; the record.rs field moved down 6 lines."
        }
      ]
    },
    {
      "id": "MISC-003",
      "area": "misc",
      "behavior": "Claude Code injects a fixed continuation prompt as a type:\"user\", isMeta record whose message.content is a single-element block array [{\"type\":\"text\",\"text\":\"Continue from where you left off.\"}] - not a bare content string. The '522 occurrences' figure does not reproduce: the live corpus carries 6 such records across 4 sessions.",
      "depends": "csift classifies it `harness.schedule.continuation`; being `isMeta` it is excluded from turn opening, so it never counts as an operator message.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "190",
          "snippet": "pub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
        }
      ],
      "instrument": "Count the leaf, not the phrase: `csift search '' -t harness.schedule.continuation --count-by label`. The claim's clause 'hits must key under harness.schedule.continuation only' is wrong - the sentence is ordinary prose that any transcript can quote, and a text search for it keys under 9 labels.",
      "located": {
        "claude_code": null,
        "csift": "0.2.0",
        "source": "src/model/markers.rs SCHEDULE_CONTINUATION_MARKER doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'Continue from where you left off\\.' --count-by label   ;   csift search '' -t harness.schedule.continuation --count-by label   ;   csift search '' -t harness.schedule.continuation --count-by session   ;   csift search '' -t harness.schedule.continuation --raw | jq -c '{type, isMeta, role: .message.role, content: .message.content}'",
          "observed": "Leaf census over the live corpus (7608 sessions in scope): 6 records under harness.schedule.continuation, across 4 distinct sessions. All 6 are byte-identical in shape: {\"type\":\"user\",\"isMeta\":true,\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"Continue from where you left off.\"}]}. A plain TEXT search for the phrase matches 409 records across 9 label keys: agent.tool.result 324, agent.tool.use 46, agent.message 13, agent.thinking 8, agent.communication.inbox 7, harness.schedule.continuation 6, harness.compaction.summary 3, agent.communication.sent 2, harness.meta.attachment 2 - and ZERO under user.message.",
          "rule": "One record per label key. The leaf count uses an empty pattern with -t <leaf> so prose quoting the phrase cannot contribute; the text search uses the phrase itself.",
          "note": "The behavioral core holds under an instrument that ran: the record exists, is type:\"user\" + isMeta, carries exactly that text, and never classifies user.message (0 of 409 phrase matches), so it never counts as an operator message. Only the population number and the phrase-based instrument needed correcting. src/model/markers.rs:177 is verbatim; the 522 figure is also written into that constant's doc comment two lines above, so a doc fix belongs there too."
        }
      ]
    },
    {
      "id": "MISC-004",
      "area": "misc",
      "behavior": "The two input forms and the absence of any task id both reproduce exactly, and '562 uses across 3 files' is exact by block count (561 by distinct tool_use id). The return shape needs splitting: the ARM form returns exactly {clampedDelaySeconds, scheduledFor, wasClamped}; the STOP form returns a 5-key superset adding {stopped, cancelledWakeups}. Neither form carries an id of any kind.",
      "depends": "csift classifies the fired prompt `harness.schedule.wakeup` off that sentinel rather than any task id, and a ScheduleWakeup can never appear in `status`'s background section because there is no id to join on.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "192-193",
          "snippet": "/// The `ScheduleWakeup` TIMER's fired-prompt sentinel (GOLD §5) - `harness.schedule.wakeup`.\n/// When a `ScheduleWakeup` tool fires, the harness injects its `prompt`; this fixed sentinel is"
        }
      ],
      "instrument": "`csift search 'ScheduleWakeup' . -t agent.tool.use --raw | jq -c '.message.content[]?|select(.name==\"ScheduleWakeup\")|.input|keys' | sort | uniq -c`, then `csift search '' . --count-by label | grep harness.schedule`. Counting rule: one row per ScheduleWakeup tool_use block.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'ScheduleWakeup' -t agent.tool.use --raw | jq -c 'select(.message.content!=null) | .message.content[]? | select(.type==\"tool_use\" and .name==\"ScheduleWakeup\") | .input | keys' | sort | uniq -c   ;   a python pass over the 3 transcripts that carry those blocks, joining each tool_use id to its tool_result carrier and tallying toolUseResult key sets   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'clampedDelaySeconds|wasClamped|delaySeconds'   ;   csift search '' -t harness.schedule.wakeup --count-by label",
          "observed": "562 ScheduleWakeup tool_use blocks (561 distinct tool_use ids - one id appears on two records, the per-block flush) across exactly 3 files, per-file 556/4/2. Inputs: 561 x [\"delaySeconds\",\"prompt\",\"reason\"], 1 x [\"stop\"]. Joined results: 561 x exactly (\"clampedDelaySeconds\",\"scheduledFor\",\"wasClamped\"); the single stop call returns (\"cancelledWakeups\",\"clampedDelaySeconds\",\"scheduledFor\",\"stopped\",\"wasClamped\"). Union of every ScheduleWakeup result key = ['cancelledWakeups','clampedDelaySeconds','scheduledFor','stopped','wasClamped']; keys matching /id|task/i: none. Binary corroboration, verbatim: 'True if the requested delaySeconds was outside [60, 3600]', 'clampedDelaySeconds', 'wasClamped', and '`delaySeconds` and `reason` are required when `stop` is not true.' Result text sample: 'Next wakeup scheduled for HH:MM:SS (in Ns). Nothing more to do this turn - the harness re-invokes you when the wakeup fires or a task-notification arrives.' harness.schedule.wakeup leaf: 1 record corpus-wide.",
          "rule": "One row per ScheduleWakeup tool_use BLOCK (562); ids deduped separately (561). Result keys read off the toolUseResult object of the tool_result carrier whose tool_use_id joins the block.",
          "note": "The binary's own required-field sentence independently confirms the {delaySeconds, prompt, reason} vs {stop:true} disjunction. The 'can never appear in status's background section' consequence follows from the measured key union carrying no id, which is the join key that section needs. src/model/markers.rs:179-180 is verbatim."
        }
      ]
    },
    {
      "id": "MISC-005",
      "area": "misc",
      "behavior": "Both directory forms and the string-id/open-status set reproduce exactly, and the merge is proven on a session that owns both. The per-file key set is NOT fixed: activeForm is optional (82 of 402 files lack it) and two further keys occur in real data - owner (36 files) and metadata (26 files).",
      "depends": "`csift status` merges both directory forms into one tasks section and renders anything that is not `completed` as an open row with its verbatim status; resolving only one form silently reports no tasks for half the sessions on disk.",
      "code": [
        {
          "path": "src/live/tasks.rs",
          "lines": "1-8",
          "snippet": "//! The harness task list: `<claude-home>/tasks/<owner>/*.json`, read point-in-time.\n//!\n//! Claude Code's TaskCreate/TaskUpdate tools persist one JSON file per task under a\n//! per-session directory. Two directory-name forms exist on real disks (both verified):\n//! the full session uuid, and the newer `session-<first 8 uuid chars>` form. Each file\n//! carries `{id, subject, description, activeForm, status, blocks, blockedBy}` with\n//! string ids. The set of `status` values is OPEN (pending / in_progress / completed\n//! observed); anything that is not `completed` renders as an open row with its verbatim"
        }
      ],
      "instrument": "`ls ~/.claude/tasks | head` shows both directory forms; then `csift status @<id>` for a session using each. Counting rule: one file per task, directories merged per owner.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6.13"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "ls ~/.claude/tasks | awk '{ if ($0 ~ /^session-[0-9a-f]{8}$/) f=\"session-<8hex>\"; else if ($0 ~ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) f=\"full-uuid\"; else f=\"other\"; print f }' | sort | uniq -c   ;   find ~/.claude/tasks -name '*.json' -print0 | xargs -0 -n50 jq -sc '.[]|keys' | sort | uniq -c   ;   jq -sr '[.[]|.status]|group_by(.)|map({(.[0]):length})|add' over both owner dirs of one session   ;   csift status @<that session>",
          "observed": "177 owner directories: 161 in the session-<first 8 uuid chars> form, 16 in the full-uuid form - both forms live on this disk. 402 task files. Key-set census: 258 x [activeForm,blockedBy,blocks,description,id,status,subject]; 82 x the same MINUS activeForm; 32 x + owner; 26 x + metadata; 4 x [blockedBy,blocks,description,id,owner,status,subject]. Types: id string, status string, blocks array. status values across all 402 files: completed 271, pending 99, in_progress 32 - exactly the three the claim names, nothing else. Merge proof: ONE session owns BOTH forms - 7 files under <full-uuid>/ (6 completed + 1 in_progress) and 21 under session-<8hex>/; combined 28 = 18 completed + 1 in_progress + 9 pending. csift status on that session printed 'tasks 10 open ; 18 completed' (10 = 1 in_progress + 9 pending) with each open row carrying its verbatim status word.",
          "rule": "One file per task; owner directories merged per session; open = status != completed. The merge is decided by a session whose task files are split across BOTH directory forms, so reading either form alone gives a strictly smaller number.",
          "note": "Reading only the full-uuid form for that session would have reported 7 tasks instead of 28, and only the short form 21 - the silent under-report the claim's depends clause predicts. src/live/tasks.rs:1-8 is verbatim; its key list should note that activeForm is optional and that owner/metadata also appear."
        }
      ]
    },
    {
      "id": "MISC-006",
      "area": "misc",
      "behavior": "A conversation FORK, rewind, retry or parallel lane leaves exactly one plain DAG fact: some record has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is live is NOT computable from the jsonl - parallel tool results share a parent BY CONSTRUCTION, so `user` records carrying a `tool_result` block are not conversation children.",
      "depends": "`show --branch-points` reports the facts and ranks them by widest inter-child time gap but never classifies which side is live: a live/abandoned classifier was prototyped and REFUTED against real corpora, because parallel tool fan-out false-positives as abandoned branches on most sessions.",
      "code": [
        {
          "path": "src/show/branch.rs",
          "lines": "3-7",
          "snippet": "//! A Claude Code rewind, retry, or parallel lane leaves one plain DAG fact: some record\n//! has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is\n//! \"live\" is NOT computable from the jsonl: a live/abandoned classifier was prototyped\n//! and refuted against real corpora (parallel tool fan-out makes sibling leaves that\n//! false-positive as abandoned branches on most sessions). So csift reports the facts"
        }
      ],
      "instrument": "`csift show @<session> --branch-points --format json`. Counting rule: records with 2+ conversation children, where a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/show/branch.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift show @<id> --branch-points   and   csift show @<id> --branch-points --format json | jq -c '{kind}' | sort | uniq -c   ;   an independent python recount over the same jsonl implementing the stated rule, and a second recount with tool_result carriers ADMITTED as children",
          "observed": "csift header: '13596 conversation record(s) - 220 branch point(s)'; JSON stream = 1 header + 220 kind:\"branch-point\" rows + 1 summary. Independent python recount over the same 91975-line transcript: conversation records 13596, branch points 220 - exact match on both numbers. Same recount with tool_result carriers admitted as conversation children: 434 branch points (1.97x), confirming that parallel tool fan-out shares a parent by construction and would false-positive as forks. Top-ranked fork: 4 children, widest inter-child gap 13h35m58s. No live/abandoned field appears in the text output or in any JSON row.",
          "rule": "A conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary; a branch point is a parentUuid with 2 or more such children. Both numbers are reproducible from the raw jsonl without csift.",
          "note": "This is the strongest form available: an outside recount of the same file reproduces csift's two headline numbers exactly, and the naive variant reproduces the false-positive the claim says refuted a classifier. src/show/branch.rs:3-7 is verbatim."
        }
      ]
    },
    {
      "id": "MISC-007",
      "area": "misc",
      "behavior": "All five doc strings and the display-only virtual concept reproduce verbatim in 2.1.258. Two wording fixes: the binary spells the field snake_case as `is_virtual`, so the claim's `\"isVirtual\":` probe alone cannot decide the absence - both spellings must be checked, and both measure 0. And the 'measured over 24 non-record types' figure is a corpus-wide tally, not a per-file one: a single 91975-line transcript yields 22 line types, 20 of them non-record, all at zero message{}.",
      "depends": "csift uses these to justify marking the promoted non-record leaves `llm_visible = false` with the careful wording 'not in the surviving conversation' rather than the stronger, false claim that the model never saw them - the load-bearing instrument being that ZERO of them carries a `message{}` field.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "172-178",
          "snippet": "    /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n    /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n    /// over every non-record line type in the corpus; every user/assistant record\n    /// does), and Claude Code's\n    /// own source labels them REPL-render internals. DAG threading is NOT the\n    /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n    /// (chain continuity), and `preservedMessages` lists these uuids at the same rate"
        }
      ],
      "instrument": "`strings -n 6` over the installed Claude Code binary filtered for `turn_duration|away_summary|scheduled_task_fire|isVirtual`, reading the surrounding doc strings (counting rule: one per extracted string); then `rg -c '\"isVirtual\":' ~/.claude/projects -g '*.jsonl'` must be 0 (counting rule: one per matching line).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc_strings.txt; then rg -No '@internal[^\\n]{0,220}(turn_duration|away_summary|scheduled_task_fire|agents_killed|api_error)[^\\n]{0,60}' cc_strings.txt and rg -No 'is_virtual:x\\(!0\\)\\.optional\\(\\)\\.describe\\(\"@internal[^\"]{0,200}' cc_strings.txt   ;   per-project-dir rg -cNI '\"isVirtual\"' and '\"is_virtual\"' over the 5 largest project dirs   ;   a python census over one 91975-line transcript tallying, per top-level type (system split by subtype), how many records carry a message{} object",
          "observed": "Binary doc strings, verbatim: turn_duration - '@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the \\'Done in Ns\\' / \\'Waiting for N agents\\' line. From internal SystemMessage \\'turn_duration\\'.'; away_summary - '@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage \\'away_summary\\'.' plus, separately, 'the session recap (shown when you return after being away for 5+ minutes) is disabled'; scheduled_task_fire - '@internal Emitted when a scheduled task (cron) fires. content is the render text. From internal SystemMessage \\'scheduled_task_fire\\'.'; agents_killed - '@internal Emitted when background agents are terminated (e.g. on interrupt). REPL renders an \\'agents killed\\' banner. From internal SystemMessage \\'agents_killed\\'.'; api_error - '@internal Retryable-API-error frame carrying the plain-data error snapshot and retry counters. REPL renders the retry banner from this. Wire twin is SDKAPIRetryMessage (\\'api_retry\\').'; virtual - 'is_virtual:x(!0).optional().describe(\"@internal Display-only: rendered in the UI but filtered before API send.' ON DISK: 0 occurrences of BOTH \"isVirtual\" and \"is_virtual\" across the 5 largest project dirs (7612 jsonl files). message{} census, one transcript, 22 distinct line types: assistant 12149/12149 and user 7494/7494 carry message{}; the other 20 types carry it 0 times - attachment 49902, last-prompt 3701, permission-mode 3698, mode 3697, ai-title 3697, agent-name 3624, file-history-snapshot 1180, system/stop_hook_summary 974, queue-operation 751, system/turn_duration 408, file-history-delta 248, system/away_summary 225, bridge-session 85, atis-latch 83, system/compact_boundary 33, system/model_refusal_fallback 8, cost-state 8, system/local_command 7, system/agents_killed 2, system/informational 1.",
          "rule": "One per extracted binary string; one per matching LINE for the on-disk key probe; one per parsed record for the message{} census, keyed by top-level type with system split by subtype.",
          "note": "The load-bearing instrument survives refutation cleanly: the message{} split is perfect (2 types at 100%, 20 types at 0%) in a single-file census a stranger can rerun, which is what licenses the careful 'not in the surviving conversation' wording. The absolute-path and away-threshold strings were read in full context to make sure neither contradicts the claim."
        }
      ]
    },
    {
      "id": "MISC-008",
      "area": "misc",
      "behavior": "The load-bearing assertion holds and is what matters: NEITHER phrase ever lands on a `system` record (0 of 62 and 0 of 68). The population numbers have grown since they were taken and should be dropped or re-dated: 'Cogitated for' is now 62 lines in 12 files (not 11 in 2), the background-agents phrase 68 lines in 17 files (not 20). The last-prompt bucket reproduces exactly at 3. Two carriers absent from the original bucket list now appear for both phrases: `attachment` and `queue-operation`.",
      "depends": "csift must not source turn-timing or pending-agent facts from those phrases; the only structural carriers are `turn_duration.durationMs` and `turn_duration.pendingBackgroundAgentCount`, which its promoted-leaf render reads by key.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "197-204",
          "snippet": "            for (key, v) in [\n                (\"messageCount\", rec.message_count.as_ref()),\n                (\n                    \"pendingBackgroundAgentCount\",\n                    rec.pending_background_agent_count.as_ref(),\n                ),\n                (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n            ] {"
        }
      ],
      "instrument": "`rg -cNI 'Cogitated for' ~/.claude/projects -g '*.jsonl'`, then re-parse each hit line and bucket by top-level `type`; expect zero on `system`. Counting rule: one per matching LINE.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project dir: rg -lNI 'Cogitated for' <dir> -g '*.jsonl'  (collecting the file list), then a python pass re-parsing every matching line and bucketing it by top-level `type` with system split by subtype; the same for the regex 'background agent\\(?s?\\)? to finish'   ;   rg -No '[^\\n]{0,50}Waiting for[^\\n]{0,90}' cc_strings.txt | rg -i agent   ;   rg -NI '\"turn_duration\"' <one transcript> | jq -s to tally durationMs / pendingBackgroundAgentCount presence",
          "observed": "'Cogitated for': 62 matching lines across 12 files - assistant 32, user 25, attachment 4, queue-operation 1, system 0. Background-agents phrase: 68 matching lines across 17 files - user 29, assistant 28, attachment 5, queue-operation 3, last-prompt 3, system 0. Binary: the only agent-bearing 'Waiting for' render wording is inside the turn_duration doc string, verbatim \"REPL renders the 'Done in Ns' / 'Waiting for N agents' line\"; 'Cogitated'/'Cogitating' appear only inside the spinner-verb word list. Structural carriers, one transcript: 408 system/turn_duration records, durationMs present on 408/408, pendingBackgroundAgentCount present on 5/408 (emitted only when non-zero); csift renders both by key.",
          "rule": "One per matching LINE, bucketed by the parsed top-level `type` (system records split by subtype). File counts are distinct files containing at least one matching line.",
          "note": "The counts are inherently unstable - both phrases are ordinary English that any transcript discussing the harness will quote, which is exactly why they are unusable as a data source and why the structural carriers are. pendingBackgroundAgentCount being present on only 5 of 408 turn_duration records is worth recording: a reader must treat its absence as zero, not as missing data."
        }
      ]
    },
    {
      "id": "MISC-009",
      "area": "misc",
      "behavior": "Claude Code exports `CLAUDE_CODE_SESSION_ID` into every Bash tool environment and its value is exactly the calling session's jsonl basename - but it names the TOP-LEVEL session in EVERY lane, including inside an in-process subagent, whose own id is withheld from the Bash env and handed only to hooks.",
      "depends": "`csift whoami` uses this variable and nothing else (never a process-tree walk, never most-recent-mtime, which is a false-positive trap under concurrent sessions), refuses to guess when it is absent, reports `is_subagent`/`parent_session_id`/`depth` as null in the env form, and prints a lane note on every `@main` resolution; a subagent must use `@trap:<marker>` to name itself.",
      "code": [
        {
          "path": "src/whoami.rs",
          "lines": "3-7",
          "snippet": "//! ## Detection (verified empirically inside a live Claude Code Bash tool, 2026-06-07)\n//!\n//! Claude Code exports `CLAUDE_CODE_SESSION_ID` into its Bash tool environment.\n//! It was confirmed to equal exactly the session's own jsonl filename:\n//!"
        },
        {
          "path": "src/whoami.rs",
          "lines": "33",
          "snippet": "const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";"
        }
      ],
      "instrument": "In a Bash call run `echo $CLAUDE_CODE_SESSION_ID` and `ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl` - expect exactly one file; `csift whoami` with the variable unset must error with guidance rather than pick a session. From a SUBAGENT's Bash call the same echo prints the parent uuid, while `csift whoami @trap:<a fresh marker>` prints the subagent's own ancestry chain - only a live subagent can show the divergence.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "SPEC.md sections 6.3 and 6.3a; AGENTS.md section 3.8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Run from inside a live SUBAGENT's Bash call: echo ${CLAUDE_CODE_SESSION_ID:+yes}   ;   ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl | wc -l   ;   ls ~/.claude/projects/*/*/subagents/**/*$CLAUDE_CODE_SESSION_ID* | wc -l   ;   env | rg -c '<this subagent's own hex id>'   ;   csift whoami --format json   ;   csift whoami @trap:QuietAmberFalcon7412   ;   env -u CLAUDE_CODE_SESSION_ID -u CODEX_COMPANION_SESSION_ID csift whoami",
          "observed": "The variable was SET inside a subagent lane and its value resolved to exactly 1 top-level ~/.claude/projects/<dir>/<uuid>.jsonl and 0 files under any subagents/ directory - i.e. it named the TOP-LEVEL session, not the running lane. No environment variable in that same Bash environment contained the subagent's own id (grep count 0 over the full env). csift whoami --format json returned is_subagent null, parent_session_id null, depth null, and printed on stderr: 'csift: note: resolved via env ($CLAUDE_CODE_SESSION_ID), which names the TOP-LEVEL session in every lane - from a subagent this is the PARENT's id, not yours. Lane fields are null under env-only resolution'. The text form printed 'lane  unknown from env alone (a subagent sees its parent's id here)'. csift whoami @trap:<a fresh 3-CamelCase-word + 4-digit marker> resolved on the FIRST try to the subagent's own transcript under <parent-uuid>/subagents/workflows/wf_*/agent-<hex>.jsonl and printed the ancestry chain 'subagent <hex> <- you (subagent, depth 0)' over 'session <parent uuid> ^ top-level root'. With both variables unset, whoami exits with 'cannot identify the calling session: CLAUDE_CODE_SESSION_ID is not set (old Claude Code build, or running outside Claude Code). Do NOT trust most-recent-mtime - many sessions may be live at once. Pass an explicit `@<uuid>` target'.",
          "rule": "One Bash call executed in a subagent lane. The env value is resolved against the projects tree by basename, expecting exactly one top-level file and zero subagent files; the divergence is established by @trap resolving to a DIFFERENT transcript in the same call.",
          "note": "This is the live-subagent instrument the claim names, and it ran: the divergence between the env value (top-level uuid) and the true lane (a subagent transcript) was observed in a single Bash invocation, and the subagent's own id is provably absent from its environment. Not instrumented: the 'handed only to hooks' half - no hook fired during this run, so that clause rests on the code path alone. src/whoami.rs:3-7 and :33 are verbatim."
        }
      ]
    },
    {
      "id": "MISC-010",
      "area": "misc",
      "behavior": "Claude Code's config home is `CLAUDE_CONFIG_DIR ?? <os home> + \"/.claude\"`, NFC-normalized: when the variable is set, every path that would be `~/.claude/...` lives under that directory instead. Because the coalescing operator only tests for null/undefined, an EMPTY-STRING `CLAUDE_CONFIG_DIR` is taken as set.",
      "depends": "csift resolves its data root with the precedence `--claude-home` > `$CLAUDE_CONFIG_DIR` (non-empty) > the OS home's `.claude`; the empty-string case is a deliberate documented divergence, and any other drift makes every subcommand read a different corpus than the harness writes.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "60-63",
          "snippet": "/// Claude Code's own config-dir relocation env var. When set, \"every `~/.claude` path\n/// lives under that directory instead\", so csift - which reads Claude Code's data - must\n/// honor it to keep pointing at the same files.\npub const CLAUDE_CONFIG_DIR_ENV: &str = \"CLAUDE_CONFIG_DIR\";"
        },
        {
          "path": "src/path/home.rs",
          "lines": "76-83",
          "snippet": "pub(crate) fn resolve_claude_home(\n    flag_override: Option<&Path>,\n    config_dir_env: Option<&OsStr>,\n    home: &Path,\n) -> PathBuf {\n    if let Some(p) = flag_override {\n        return p.to_path_buf();\n    }"
        }
      ],
      "instrument": "Set `CLAUDE_CONFIG_DIR` to a fixture tree containing `projects/<encoded>/<uuid>.jsonl` and run `csift list`; expect only the fixture rows. The pure precedence function `resolve_claude_home(flag, env, home)` in `src/path/home.rs` is unit-tested without touching the process-global override. For the harness side, `strings` over the installed Claude Code binary filtered for `CLAUDE_CONFIG_DIR` shows one reader coalesced against a home-dir join followed by an NFC normalize. Counting rule: one config-home resolver per match.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.1.0",
        "source": "AGENTS.md section 7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -a -o -N '.{200}CLAUDE_CONFIG_DIR.{260}' ~/.local/share/claude/versions/2.1.258   ;   CLAUDE_CONFIG_DIR=<fixture tree> csift list --format json | jq -rc 'select(.kind==\"session\")|.session_id'   ;   CLAUDE_CONFIG_DIR=$HOME/.claude csift --claude-home <fixture tree> list --format json | jq -rc 'select(.kind==\"session\")|.session_id'   ;   CLAUDE_CONFIG_DIR=\"\" csift list --max-count 1 --format json | jq -rc 'select(.kind==\"header\")|.sessions_in_scope'",
          "observed": "The resolver, read verbatim out of the 2.1.258 binary: `function s(){return process.env.CLAUDE_CONFIG_DIR}` followed by `var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s);` - a nullish-coalescing `??` against a homedir join, then .normalize(\"NFC\"), exactly as the claim states. Because `??` tests only null/undefined, an empty string IS taken as set. A DIFFERENT, legacy path uses the OR form instead (`process.env.CLAUDE_CONFIG_DIR||z()`), and the string 'the configuration home (CLAUDE_CONFIG_DIR) is not an absolute path' was read in context: it is a bail reason inside one feature's placement check, not a gate on the resolver. csift side: with only the env set, list emitted exactly the 1 fixture session and nothing else; with --claude-home on the fixture and the env pointed at the real home, list again emitted exactly that 1 session (flag wins); with CLAUDE_CONFIG_DIR=\"\" csift fell back to the OS home and reported sessions_in_scope 7608 - the documented divergence from the harness's `??`.",
          "rule": "One config-home resolver per binary match; one emitted session row per fixture transcript (the fixture holds exactly 1, so any leakage from the real home is immediately visible as extra rows).",
          "note": "The resolver was found as a single expression rather than inferred, so the NFC normalize and the empty-string consequence are both direct reads rather than deductions. Worth noting for future drift: the binary now also carries an absolute-path validation message and a 'CLAUDE_CONFIG_DIR no longer names ...' mid-run-change warning, neither of which gates the resolver in 2.1.258 - if either is ever hoisted into `Se()`, the empty-string arm changes and this claim would need re-checking. src/path/home.rs:60-63 and :76-83 are verbatim."
        }
      ]
    },
    {
      "id": "MISC-011",
      "area": "misc",
      "behavior": "Add: the cutoff test is per FILE on stat().mtime; deleting a transcript also unlinks two siblings <uuid>.ccr-tip.json and <uuid>.precompact.json; a transcript past the cutoff can still be SPARED by a desktop retention exemption (counted as transcriptsExemptedDesktop, skipped under hipaa/zdr modes); the .jsonl unlinks are deferred to the end of the project directory and the whole batch is abandoned if the error counter rose during that directory; cleanupPeriodDays has a minimum of 1 and 0 is rejected outright.",
      "depends": "csift can only read what survives retention, so its root help tells the operator to check and raise the setting; `--resolve-persisted`, `image --out`, `recover --list-backups` and every subagent-spanning surface read files under that sidecar tree, and any corpus-wide absence claim from `search` is bounded by whatever retention already removed.",
      "code": [
        {
          "path": "src/cli/root.rs",
          "lines": "154-156",
          "snippet": "          Claude Code deletes transcripts older than `cleanupPeriodDays` (default 30!).\\n  \\\n        Check `jq '.cleanupPeriodDays // 30' ~/.claude/settings.json` and consider raising\\n  \\\n        it; csift can only read what survives.\\n\\n\\"
        },
        {
          "path": "src/model/record.rs",
          "lines": "138-141",
          "snippet": "    /// (a coverage annotation). `backupFileName` is usually present (measured 83-98%\n    /// across real corpora), but the store it names is PRUNED and its content has no\n    /// transcript anchor, so it is never used to fabricate content; `recover\n    /// --list-backups` lists the store itself. Additive + tolerant."
        }
      ],
      "instrument": "The stated expectation 'expect no orphaned <uuid>/ sidecar directories' is wrong: 5 of 47 uuid-shaped sidecar dirs on this machine have no sibling .jsonl. The sweep only rm -r's a sidecar when IT deletes the matching transcript; a transcript removed any other way leaves the sidecar behind, and the sweep's orphan branch then prunes only subagents/ workflows/ remote-agents/ contents past the cutoff and rmdir's what becomes empty.",
      "located": {
        "claude_code": "2.1.233",
        "csift": "0.7.6",
        "source": "SPEC.md section 6 v0.7.6 ledger; CHANGELOG 0.7.6; dev session 2026-08-16"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Number of days to retain chat transcripts|cleanupPeriodDays must be at least 1'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function ehr\\(\\).{0,4200}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function x\\(.{0,700}'; jq '.cleanupPeriodDays // 30' ~/.claude/settings.json; python3 census of uuid-shaped sidecar dirs under ~/.claude/projects (a uuid-shaped subdirectory of a project dir; orphan = no sibling <uuid>.jsonl)",
          "observed": "Binary 2.1.258: \"Number of days to retain chat transcripts before automatic cleanup (default: 30). Minimum 1. Use a large value for long retention; use --no-session-persistence to disable transcript writes entirely.\" and \"cleanupPeriodDays must be at least 1. To keep transcripts for a long time, set a large number (e.g. 3650 for ~10 years).\" The per-file cutoff test is MTIME: `let p=await n.stat(e); if(!(p.mtime<r))return a.filesRetainedFresh++,!1; ... await n.unlink(e)`. The sidecar removal, guarded on a uuid-shaped stem: `if(k.name.endsWith(\".jsonl\")){r.transcripts++;let j=k.name.slice(0,-6);if(ge(j)){await a.unlink(d(R,`${j}.ccr-tip.json`)).catch(()=>{}),await a.unlink(d(R,`${j}.precompact.json`)).catch(()=>{}),C.push(re(T)),await a.rm(d(R,j),{recursive:!0,force:!0}).catch(()=>{r.errors++})` - a recursive rm of the whole sibling <uuid>/ tree. Sweep telemetry schema: tengu_retention_sweep {phase, skipped, skipReason, usedDefault, periodDays, transcriptsDeleted, transcriptsExemptedDesktop, sessionFilesDeleted, artifactsDeleted, filesRetainedFresh, filesPastCutoff, historyEntriesPruned, errors}. On disk: cleanupPeriodDays = 730 here, oldest surviving transcript mtime 2026-06-13 (81 days), newest 2026-09-02; 47 uuid-shaped sidecar dirs, of which 5 have NO sibling <uuid>.jsonl, each holding only tool-results/ with 1 file, newest child 2026-06-01..2026-06-28.",
          "rule": "One session per <uuid>.jsonl; one sidecar per uuid-shaped sibling directory; orphan = a uuid-shaped directory with no <uuid>.jsonl beside it. Retention age = now minus the file's mtime (stat().mtime, so reads never renew it).",
          "note": "Retention deletion is UNEXERCISED on this machine (cleanupPeriodDays = 730, oldest file 81 days old), so the deletion path is read out of the 2.1.258 binary rather than observed running. The default-30 half is a binary string, the mtime half and the recursive sidecar rm are binary code, and the orphan census is on-disk."
        }
      ]
    },
    {
      "id": "MISC-012",
      "area": "misc",
      "behavior": "The cap is counted in JavaScript String.length, i.e. UTF-16 code units, not Unicode scalars. Identical for BMP text (all CJK the claim cares about), but an astral character (emoji, CJK ext-B) counts 2. The 'per hook' framing is right for SessionStart/Setup; on PreToolUse/UserPromptSubmit/PostToolUse/Stop/SubagentStop/SubagentStart/PostToolBatch/PostToolUseFailure/UserPromptExpansion an earlier sanitizer truncates additionalContext at 8000 characters or 200 lines, so those events never reach the 10000 threshold.",
      "depends": "`csift verbatim --slice N --window N` packs the reconstruction into character-counted chunks (the unit the cap counts, so a CJK-heavy document is not 3x over-counted the way a byte budget would be) and concatenating the slices reproduces the text exactly, so one SessionStart hook per slice keeps every injected chunk under the wall.",
      "code": [
        {
          "path": "src/cli/verbatim_whoami_args.rs",
          "lines": "285-288",
          "snippet": "    /// fanning a >10K reconstruction across several SessionStart hooks: Claude Code caps EACH\n    /// hook's `additionalContext` at 10,000 CHARACTERS (over-cap is replaced by a file-path +\n    /// short preview, i.e. the body is effectively LOST to the model), so one hook per slice\n    /// keeps every injected chunk under the wall. Slicing is DETERMINISTIC (same session +"
        },
        {
          "path": "src/turns/render.rs",
          "lines": "290-292",
          "snippet": "/// Greedily pack a document's LINES into chunks of at most `window` CHARACTERS (Unicode\n/// scalars - the unit Claude Code's 10,000-char additionalContext cap counts, so a CJK-heavy\n/// document is NOT 3× over-counted the way a byte budget would). A line longer than the"
        }
      ],
      "instrument": "Install a SessionStart hook emitting more than 10,000 characters of additionalContext, then read the resulting `hook_additional_context` attachment with `csift search '' @<id> --additional-context` - it holds a path and a preview, not the body; inject a CJK block of 9,999 scalars to show the unit is scalars, not bytes. Counting rule: Unicode scalars in the injected string versus the cap. Only a live session with a hook can show it.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.3.0",
        "source": "SPEC.md section 6.8; src/turns/render.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'async function ipe\\(.{0,900}'; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'NQn=1e4' with 250 bytes of surrounding context; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Ujr=\\{reason.{0,300}'; python3 census counting Unicode scalars in every ~/.claude/projects/*/*/tool-results/hook-*.txt",
          "observed": "Constant: `var aJ=50000,WNe=500000;var QSt=4,LQn=400000,MQn=200000,$w=50,NQn=1e4,$7e=1e4,FQn=1e5;` - NQn = 1e4 = 10000. Use: `async function ipe(e,n,r,{threshold:o=NQn,storageV5:d}={}){if(e.length<=o)return e;let f=await $3(e,`hook-${n}-${r}`,kS(),d); ...}` reached from `if(Rn.additionalContext) ... yield{additionalContexts:[await ipe(Rn.additionalContext,`${o}-${Bn}`,\"additionalContext\",{storageV5:we})]}`. The over-cap replacement is built by a helper emitting `Output too large (${size}). Full output saved to: ${n.filepath}` then `Preview (first ${...}):` then the preview - a path plus a short preview, exactly as claimed; if the persist itself fails the fallback is `${e.slice(0,o)}` + `[Hook ${r} truncated at ${o} chars - persist-to-disk failed: ...]`. On disk: 166 persisted hook-output files, MINIMUM length 10029 Unicode scalars, maximum 54900 - nothing at or below 10000 was ever persisted. A separate, smaller sanitizer exists for the events routed through hookSpecificOutput validation: `var Ujr={reason:2000,stopReason:2000,systemMessage:4000,additionalContext:8000,permissionDecisionReason:2000},u4t=200` - 8000 chars / 200 lines; its event switch covers PreToolUse, UserPromptSubmit, UserPromptExpansion, PostToolUse, PostToolUseFailure, PostToolBatch, Stop, SubagentStop, SubagentStart, PermissionDenied - SessionStart and Setup are NOT in it, so for the SessionStart recipe the 10000-char persist threshold is the operative wall.",
          "rule": "Count Unicode scalars of every file matching ~/.claude/projects/*/*/tool-results/hook-*.txt; the MINIMUM bounds the persist threshold from above (10029 > 10000). The constant and its comparison bound it from below.",
          "note": "csift's own doc comments (src/cli/verbatim_whoami_args.rs:285-288 and src/turns/render.rs:290-292) say 'CHARACTERS (Unicode scalars)'. Correct for BMP text; the exact unit is UTF-16 code units. Both comments exist verbatim in the current files."
        }
      ]
    },
    {
      "id": "MISC-013",
      "area": "misc",
      "behavior": "Claude Code runs same-event hooks CONCURRENTLY and concatenates their outputs in COMPLETION order, so N hooks registered on one event inject in a nondeterministic sequence.",
      "depends": "The documented SessionStart(compact) recipe uses a PPID-namespaced done-flag barrier to force slice order and a 9,000-character window to stay under the injection cap; csift ignores the resulting attachment record by default, so re-firing the hook creates no feedback loop.",
      "code": [
        {
          "path": "src/turns/render.rs",
          "lines": "34",
          "snippet": "    // inject it under the 10,000-char additionalContext cap. Two sub-modes:"
        }
      ],
      "instrument": "Register the slice hook script N times, trigger a compaction, then `csift search '<the slice header>' @<uuid> --additional-context --count-by label` - N hits under `harness.meta.hook`, in slice order only when the barrier is present. Counting rule: one attachment record per registered hook.",
      "located": {
        "claude_code": null,
        "csift": "0.3.0",
        "source": "SKILL.md Hook 1 (race fix and the injection cap)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 over the 5 transcripts that carry a multi-hook SessionStart injection: collect every attachment record with type=='hook_success' and hookEvent=='SessionStart' whose stdout carries the marker, in file-line order, group into bursts (consecutive records within 8 lines) and compare emission orders. Plus jq '.hooks | to_entries[] | ...' ~/.claude/settings.json for the declared hook counts.",
          "observed": "Declared config: SessionStart has 34 hooks over 5 matcher groups (one group of 30 identically-shaped hooks in a fixed declared order). 16 marker-bearing hook_success records, 5 bursts with >=2 records. Emission orders: ('HEADER','file','file') x3, ('HEADER','file','file','file') x1, ('file','file','HEADER') x1 - the header-emitting hook came first in 4 of 5 bursts and LAST in 1, from an unchanged declared order. The hook_additional_context attachment's `content` is an ARRAY whose entry order matches the burst order (a 3-entry record with the header at index 2 was observed alongside three with it at index 0). Binary 2.1.258 builds one async generator per hook - `let yn,nt=Ge.map(async function*({hook:bn,pluginRoot:Rn,pluginId:Dn,...},Rr){...})` - and drains them all through a single merge combinator, `for await(let bn of AOe(nt))`, accumulating each yielded additionalContext into one list.",
          "rule": "One burst = the run of consecutive hook_success attachment records for hookEvent SessionStart carrying the marker, within 8 lines of one another; the burst's emission order is the file-line order of those records. Nondeterminism = two or more distinct orders across bursts under one unchanged declared order.",
          "note": "n = 5 bursts, so the observable (a nondeterministic sequence under a fixed declaration) is established but thinly sampled. The MECHANISM ('concurrently', 'completion order') is inferred from the per-hook async generators plus the merge drain; the combinator itself could not be resolved in the minified bundle. Do NOT cite the binary's `--hook-concurrency <n>` flag as evidence - it belongs to the self-hosted spawn-runner orchestrator, not to session lifecycle hooks."
        }
      ]
    },
    {
      "id": "MISC-014",
      "area": "misc",
      "behavior": "Claude Code's own wire format for transcript lines is COMPACT JSON with no whitespace around the `\"role\":` colon, but a hand-authored or reserialized line (a default JSON dump) is valid JSON carrying `\"role\": \"user\"` and is the SAME record.",
      "depends": "csift's stage-1 candidate keeps use the serialization-tolerant matchers `line_has_role_marker` / `line_has_user_role_marker`; the old exact compact byte needle silently DROPPED a reserialized line one layer BEFORE any malformed counter could see it - not skipped, not counted, simply invisible on every surface: no preview, no count, no match, zero disclosure.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "42-47",
          "snippet": "/// Serialization-tolerant role-marker test - THE stage-1 candidate needle for\n/// message records (R13). CC's own wire format is compact JSON, but a hand-authored\n/// or reserialized line may carry whitespace around the colon (`\"role\": \"user\"`) -\n/// valid JSON, the same record. The old exact-byte needles (`\"role\":\"user\"`)\n/// silently DROPPED such lines one layer BEFORE any malformed counter could see\n/// them: not skipped, not counted, simply invisible on every surface. One `memmem`"
        },
        {
          "path": "src/parse/lines.rs",
          "lines": "87-98",
          "snippet": "/// `\"role\"` is `\"user\"` OR `\"assistant\"` (any JSON whitespace around the colon) -\n/// the candidate test for `search`/`show`/`verbatim`/`list`/`stats` stage-1 filters.\npub fn line_has_role_marker(line: &[u8]) -> bool {\n    line_role_value_matches(line, true, true)\n}\n\n/// `\"role\"` is `\"user\"` only - the genuine-user/carrier hook `files`/`recover` use\n/// (their assistant-side coverage rides tool-name needles, so admitting every\n/// assistant text record here would repeal their §7 prefilter).\npub fn line_has_user_role_marker(line: &[u8]) -> bool {\n    line_role_value_matches(line, true, false)\n}"
        }
      ],
      "instrument": "Write a fixture transcript with a default JSON dump (which emits `\"role\": \"user\"`) under a `--claude-home` tree and confirm `csift search` finds its content. Counting rule: matched records for one known needle with and without the space, expected 1 in both.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 7d; src/parse/lines.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "On one transcript: rg -c '\"role\":\"user\"' FILE; rg -c '\"role\": \"user\"' FILE (and the assistant pair). Then a fixture under --claude-home: write one record with python json.dumps default separators (emits '\"role\": \"user\"'), run `csift --claude-home <tmp>/.claude search 'zebrafish marker' -c`; rewrite the same record with separators=(',',':') and re-run.",
          "observed": "One 48460-line transcript: 3694 lines carry `\"role\":\"user\"` and 0 carry `\"role\": \"user\"`; 7223 carry `\"role\":\"assistant\"` and 0 carry `\"role\": \"assistant\"` - the wire format is compact, with no whitespace around the colon. Fixture: the reserialized (spaced) line returns 1; the same record recompacted returns 1.",
          "rule": "Matched records for one known needle, with and without the space around the role colon; expected 1 in both.",
          "note": "Both csift code sites are present verbatim: src/parse/lines.rs:42-49 (the R13 doc comment) and src/parse/lines.rs:87-98 (line_has_role_marker / line_has_user_role_marker)."
        }
      ]
    },
    {
      "id": "MISC-015",
      "area": "misc",
      "behavior": "Because Claude Code JSON-encodes string content in the raw line (`\"` becomes `\\\"`, every control character below 0x20 and DEL becomes `\\uXXXX` / `\\n`), a rendered text can differ byte-for-byte from the raw line wherever whitespace or an escapable character sits.",
      "depends": "csift derives search prefilter needles NECESSITY-ONLY and applies a per-needle safety predicate (whitespace-free, JSON-escape-free, at least 3 bytes), plus a marker list for the small closed set of render paths that fabricate text not present verbatim in the raw line; a needle failing the predicate would silently drop real matches, which is the one place the no-silent-truncation contract actually bites.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "493",
          "snippet": "// The agents-stopped kill notice renders a fabricated `[subagent stopped]` head."
        }
      ],
      "instrument": "`csift search 'hello world' @<a session containing that phrase across a newline>` must still match; the prefilter unit tests in `src/search/tests/matcher.rs` pin the predicate. Counting rule: matched records with and without the whitespace in the pattern.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "SPEC.md section 7d"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Fixture under --claude-home with two assistant records, text 'alpha\\nbravo' (a real newline) and 'he said \"charlie delta\" loud'; then `csift --claude-home <tmp>/.claude search 'alpha\\s+bravo' -c`, `... search 'said \"charlie' -c`, `... search 'charlie' -c`. Plus rg -c on one real transcript for JSON-escaped newlines and escaped quotes.",
          "observed": "The raw line is `...\"type\":\"text\",\"text\":\"alpha\\nbravo\"}]}}` - the rendered text differs byte-for-byte from the raw line at the whitespace. All three searches return 1: a pattern whose match spans the escaped newline (1), a pattern containing a character that is escaped in the raw line (1), and the plain-literal control (1). On one real 48460-line transcript, 2216 lines carry a JSON-escaped newline and 4765 carry an escaped quote.",
          "rule": "Matched records for a needle whose rendered form contains whitespace or a JSON-escapable character, versus the plain-literal control; expected 1 for each.",
          "note": "The claim's own instrument sentence points at src/search/tests/matcher.rs for the predicate unit tests; the fixture run above is the black-box equivalent and is what was executed. code-site note: src/search/matcher.rs: the `let mut verifiable: Vec<&[u8]> = vec![...]` block with the five markers (<task-notification>, \"answers\", User has answered your questions, Your questions have been answered, stopped by the user) is at lines 487-494, not 485-492; the text is otherwise verbatim."
        }
      ]
    },
    {
      "id": "MISC-016",
      "area": "misc",
      "behavior": "Corpus geometry at 2026-09-02: 7789 jsonl files (was 2437); largest file 687.5 MB across 91975 lines (was 225 MB / 115879); ladder min 82 B, p50 805 B, p90 3.5 KB, p99 50.0 KB, max 18.0 MB for a single line. The min (82 B) and the p50 order of magnitude are unchanged; the tail is what moved - a single line is now up to ~18 MB, 45x the claimed 400 KB maximum.",
      "depends": "The tail reader does NOT step 64 KiB to 256 KiB. TAIL_CHUNK = 64 * 1024 (src/parse/lines.rs:8) and RevLines::fill DOUBLES the window without a ceiling until it contains a newline (`take = take.saturating_mul(2)`, src/parse/readers.rs:63-66) - which is exactly what makes an 18 MB line readable. The in-code comment on that loop still says 'guards the 400 KB single-line max' and is itself stale.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "30-38",
          "snippet": "    // SAFETY: read-only mmap of a file we just opened. The documented hazard is a\n    // concurrent truncation by another writer; we never write through the map and\n    // treat its length as fixed-at-open, which is the SPEC's accepted contract\n    // (§7a). The crate lints `unsafe_code = \"deny\"` (unsafe forbidden crate-wide);\n    // mmap is irreducibly unsafe and SPEC-mandated, so this is the single audited\n    // call site that explicitly allows it.\n    #[allow(unsafe_code)]\n    let mmap =\n        unsafe { Mmap::map(&file) }.with_context(|| format!(\"cannot mmap {}\", path.display()))?;"
        }
      ],
      "instrument": "`find ~/.claude/projects -name '*.jsonl' | wc -l` for the file count, `wc -lc` on the largest for lines and bytes, `awk '{print length}' <largest> | sort -n` for the percentile ladder. Counting rule: bytes per line excluding the trailing newline; percentiles over all lines of one file.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "find ~/.claude/projects -name '*.jsonl' -type f | wc -l; find ~/.claude/projects -name '*.jsonl' -type f -exec stat -f '%z %N' {} ';' | sort -rn | head -3; wc -lc < LARGEST; python3 ladder over LARGEST reading len(line.rstrip(b'\\n')) per line, sorting, and indexing int(q*n)",
          "observed": "7789 jsonl files under ~/.claude/projects. Largest: 687.5 MB, 91975 lines. Ladder for that file: min 82 B, p50 805 B, p90 3536 B (3.5 KB), p99 51250 B (50.0 KB), max 18031027 B (17.2 MB). The previously-cited landmark file now measures 396.7 MB / 123044 lines with min 82 B, p50 774 B, p90 4535 B, p99 34126 B, max 13333694 B (12.7 MB).",
          "rule": "File count = every *.jsonl under ~/.claude/projects (subagent transcripts and workflow journals included). Line length = bytes excluding the trailing newline; percentiles over all lines of ONE file, index = int(q*n) into the sorted list.",
          "note": "The parse-timing figures in the claim (1.39 s parse-every-line vs 0.65 s prefilter-then-parse over a 0.28 s memchr floor) were NOT re-measured here; they were attached to a file that has since grown, so they should be re-benchmarked before being restated."
        }
      ]
    },
    {
      "id": "MISC-017",
      "area": "misc",
      "behavior": "The shrink is a RENAME over the path (compact temp -> transcript), not an in-place truncate; a reader that already mmapped the old inode keeps reading it intact, which is why the fixed-at-open contract survives compaction.",
      "depends": "csift skips a BLANK final fragment uncounted (line_payload rejects an all-whitespace remainder, skipped_lines stays 0), but it does NOT skip a torn one: a `{`-framed partial record is counted as ONE malformed line (skipped_lines 1) - disclosed rather than hidden, and stable across repeated runs at exit 0. That is the correct behaviour under the no-silent-truncation contract, but it is counting, not skipping.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "101-109",
          "snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n    let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n    let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n    if line.iter().all(u8::is_ascii_whitespace) {\n        None\n    } else {\n        Some(line)\n    }\n}"
        }
      ],
      "instrument": "Run `csift search '' @main` repeatedly against the currently-writing session while it works; every run must exit 0 with a stable `skipped_lines` count. Counting rule: `skipped_lines` from the JSON summary across repeated runs.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 7a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Three live probes plus one fixture. (a) python3: sample every main transcript touched in the last hour every 50 ms for 280 s, read only the appended bytes, count reads whose tail lacks a newline. (b) python3: sample ONE live subagent transcript's size and final byte every 1 ms for 45 s. (c) python3: sample the newest-mtime transcript every 100 ms for 40 s. (d) fixture under --claude-home: append a half-written record with no trailing newline, then run `csift --claude-home <tmp>/.claude search 'torncheck' --format json` three times; repeat with a blank (whitespace-only) tail. Plus `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'performCompactTranscript\\(.{0,2400}'`.",
          "observed": "(a) 51 records appended across 9 live main transcripts in 280 s; torn-tail reads 0. (b) 31824 samples: grew 0, shrank 0, torn 0. (c) 400 samples: torn 0. So growth is routine and a torn final fragment was NOT observed in ~38000 samples. Shrink, from the binary: `async performCompactTranscript(e,n,r,o){... let f=`${e}.compact.tmp.${Its(4).toString(\"hex\")}` ... await Ti(f,e) ... s(\"tengu_transcript_compact\",{bytesBefore:C.size,bytesAfter:ke})}` - the compacted temp is RENAMED over the live transcript path, so the file at the path shrinks. (d) csift with a torn `{`-framed tail: matched=1 skipped_lines=1, exit 0, identical on 3 consecutive runs. With a blank tail: matched=1 skipped_lines=0.",
          "rule": "Sample the file's size and final byte; torn tail = final byte != '\\n'. For csift, read `matched` and `skipped_lines` from the JSON summary's last line across repeated runs and compare.",
          "note": "A torn final fragment was never caught in ~38000 samples across three probes (down to a 1 ms sampler), so on this machine the appends look atomic at the available resolution. The claim's torn-tail hazard is therefore supported only by csift's tested handling of a synthetic torn tail, not by an observed one. A writer-side probe (a hook or an fs-event watcher sampling inside the write) would decide whether CC ever leaves a partial line visible."
        }
      ]
    },
    {
      "id": "MISC-018",
      "area": "misc",
      "behavior": "The measured band for the record @trap needs - an assistant record carrying a tool_use - is 0.11 s to 3.11 s (n=6), not '~1 to 3.4 s'. The upper bound is right; the lower bound is much looser, so a main-lane FIRST try can occasionally win the race rather than 'normally missing'. Text-only assistant records lag further still (2.32-6.40 s). The contrast that proves the asynchrony is structural: user, attachment and queue-operation records land within 0.01-0.19 s of their own timestamp while assistant records take seconds.",
      "depends": "csift's `@trap:<marker>` resolves first-try from a subagent but normally loses the race from the main thread, so the no-match error routes `@main` first and a re-run of the SAME marker second; a hook asking for the PREVIOUS prompt must exclude hits younger than now-3s and take the newest survivor.",
      "code": [
        {
          "path": "src/path/trap.rs",
          "lines": "14-19",
          "snippet": "/// very command that launched this run. Mechanism + TIMING (subagent verified live 2026-07-12;\n/// main-lane mechanism re-measured 2026-08-29): a SUBAGENT's transcript flushes per content\n/// block as it closes, so its launching tool_use is on disk at dispatch and a first try\n/// resolves. The MAIN conversation writes an async flush of the COMPLETED assistant message\n/// that lands ~1-3.4s AFTER the tool was dispatched - a RACE, not a wait (a 263s command was\n/// observed with its unpaired tool_use already on disk 39s in). csift finishes well inside"
        }
      ],
      "instrument": "From a live main session run `csift whoami @trap:<a fresh 3-word-plus-4-digit marker>` - it misses; a second, SEPARATE Bash invocation with the same marker resolves. Counting rule: two invocations, two outcomes. Only a live session can time the flush.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.8.2",
        "source": "SPEC.md section 6.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "`csift whoami @trap:<a fresh 3-CamelCase-word + 4-digit marker>` run ONCE from inside a live subagent's Bash call. Then python3: sample every main transcript touched in the last hour every 50 ms for 280 s, parse each appended line, and record lag = sample wall clock minus the record's own ISO `timestamp`, bucketed by record type (assistant records carrying a tool_use split out).",
          "observed": "The trap resolved on the FIRST invocation, printing `subagent <id>  <- you (subagent, depth 0)` with the session root and the transcript path - the launching tool_use was already on disk at dispatch. Flush lag over 280 s across 9 live main transcripts: assistant records CARRYING a tool_use n=6 min 0.11 s p50 0.65 s p90/max 3.11 s; assistant text-only records n=7 min 2.32 s p50 3.67 s max 6.40 s; user records (tool_result carriers) n=7 min 0.05 s max 0.19 s; attachment records n=27 min 0.02 s max 0.19 s; queue-operation n=4 max 0.01 s.",
          "rule": "One trap invocation, one outcome. For the lag: sample each live main transcript's size at 50 ms, read only appended bytes, lag = sample time minus the record's own ISO timestamp; bucket by record type. The 0.01 s floor on queue-operation records bounds clock skew and sampler granularity.",
          "note": "The main-lane half of the claim (a first `@trap` try from the top-level thread misses, a second resolves) could NOT be executed - this lane is a subagent, and the marker must be embedded in the very command that launches the run. What would decide it: a top-level session running `csift whoami @trap:<fresh marker>` in one Bash call and the SAME marker again in a second, separate Bash call, recording both outcomes. The evidence here is the flush-lag distribution that governs that race, plus the first-try subagent resolution."
        }
      ]
    },
    {
      "id": "MISC-019",
      "area": "misc",
      "behavior": "A background task can be designed never to return (a dev server, a file watcher), so there is no bound after which an open background task must have closed.",
      "depends": "csift makes `--timeout` REQUIRED on `wait` and exits 124 on the bound - the GNU timeout convention and the single documented exception to the crate's 0-vs-non-zero exit law - rather than blocking forever on `--until stop`.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "10-12",
          "snippet": "/// The GNU `timeout` convention - the ONE documented exception to the crate's\n/// 0-vs-non-zero exit law (a monitor's timeout is a normal outcome scripts branch on).\nconst TIMEOUT_EXIT: u8 = 124;"
        },
        {
          "path": "src/live/wait.rs",
          "lines": "36-42",
          "snippet": "    // The timeout is REQUIRED (v0.10.0): a background task may never return by design\n    // (a dev server, a watcher), so an unbounded wait on `stop` is a bug, not a wait.\n    let Some(timeout_secs) = args.timeout else {\n        bail!(\n            \"wait needs --timeout <SECS>: a background task can be designed never to return \\\n             (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, \\\n             branch on exit 124, and read the at-exit report; narrow what counts with \\"
        }
      ],
      "instrument": "Of the two e2e tests named, `p6_history_never_fires_and_timeout_is_124` is at tests/cli/live/wait.rs:45 but `wait_demands_a_timeout_and_reports_what_it_saw` lives at tests/cli/live/background.rs:198, not in wait.rs.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "CHANGELOG 0.10.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "`csift wait @<uuid> --until stop` (no --timeout), then `csift wait @<uuid> --until stop --timeout 2` against a live session; echo $? after each. Plus `csift status`-style background rows in the timeout report, and grep for the four named e2e tests under tests/.",
          "observed": "Without --timeout: exit 1 with `csift: error: wait needs --timeout <SECS>: a background task can be designed never to return (a dev server, a watcher), so a wait without a bound never ends. Pick a bound, branch on exit 124, and read the at-exit report; ...`. With --timeout 2: exit 124, stdout first line `fired    timeout`, stderr readiness line `csift: watching 114 file(s) from byte offsets; conditions: stop; timeout 2s`. The at-exit report listed two still-open background rows on that session - an async agent launched 2h 15m earlier and a shell launched 2h 16m earlier with 'output 0 B, last write 2h 16m ago' - i.e. background work that had not returned after two hours, which is the premise the required timeout exists for.",
          "rule": "Two invocations, two exit codes: reject (non-zero, message names the reason) versus timeout (exactly 124).",
          "note": "The Claude Code half of the claim - that a background task can be designed never to return - is a design statement rather than a falsifiable behaviour; the closest instrument reading is the pair of background rows still open after 2h+ in the at-exit report above. code-site note: `const TIMEOUT_EXIT: u8 = 124;` is at src/live/wait.rs:12 (its two-line doc comment at :10-11), and the exit is taken at src/live/wait.rs:215 via `std::process::exit(i32::from(TIMEOUT_EXIT))`. The bail block is at src/live/wait.rs:36-42, verbatim as quoted."
        }
      ]
    },
    {
      "id": "MISC-020",
      "area": "misc",
      "behavior": "Transcript growth is the only event stream a watcher gets, and content appended before the watch started is history the harness will never re-emit.",
      "depends": "csift seeds a byte-offset baseline per watched file at start and prints a readiness line on stderr so a scripted caller can order its own trigger AFTER the snapshot; without that line an append racing the snapshot is correctly treated as history and the wait hangs to its timeout.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "48-56",
          "snippet": "    // ── Baselines: snapshot every currently-watched file's length. Only bytes appended\n    //    AFTER these offsets are events; history is `search`'s job. ──\n    let mut cursors: Vec<Cursor> = Vec::new();\n    let seed = |path: PathBuf, is_main: bool, cursors: &mut Vec<Cursor>| {\n        if cursors.iter().any(|c| c.path == path) {\n            return;\n        }\n        let offset = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);\n        let lane = crate::subagent::session_id_from_path(&path);"
        },
        {
          "path": "src/live/wait.rs",
          "lines": "80-83",
          "snippet": "    eprintln!(\n        \"csift: watching {} file(s) from byte offsets; conditions: {}; timeout {timeout_secs}s{}\",\n        cursors.len(),\n        args.until.join(\" | \"),"
        }
      ],
      "instrument": "Append a matching record to a transcript BEFORE launching `csift wait` on it: the wait must not fire and must exit 124. Counting rule: one pre-append, one wait. The e2e tests `p5_wait_fires_only_on_post_start_events` and `p11_history_in_every_lane_never_fires` in tests/cli/live/wait.rs pin the baseline semantics in every lane.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live/wait.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Fixture under --claude-home: write a transcript, APPEND a tool_use record for Grep BEFORE starting the wait, then `csift --claude-home <tmp>/.claude wait @<uuid> --until tool:Grep --timeout 3`; echo $?. Then, on the same file, start `... --until tool:Grep --timeout 12` while a background subshell appends an equivalent record 2 s after launch; echo $?.",
          "observed": "Pre-append (the condition already satisfied by history): exit 124, stdout `fired    timeout`. Post-baseline append: exit 0, stdout `fired    tool:Grep` and `verdict  running`. Both runs printed the readiness line on stderr before polling: `csift: watching 1 file(s) from byte offsets; conditions: tool:Grep; timeout <N>s`.",
          "rule": "One pre-append then one wait, one exit code (expect 124); then one post-baseline append then one wait, one exit code (expect 0 naming the condition). The pair isolates the baseline: identical file, identical condition, only the append's timing relative to the snapshot differs.",
          "note": "Both cited code sites are verbatim in the current file: the baseline snapshot block at src/live/wait.rs:45-53 and the readiness eprintln at src/live/wait.rs:77-80. Both named e2e tests exist (tests/cli/live/wait.rs:7 and :206)."
        }
      ]
    },
    {
      "id": "MISC-021",
      "area": "misc",
      "behavior": "A subagent lane can be spawned at any moment during a turn, so the set of transcripts belonging to one session is not fixed at the start of an observation.",
      "depends": "csift's `wait` re-discovers child transcripts on every poll and seeds a newly appeared lane at baseline offset 0, since a file born after start is entirely post-start; discovering only at start would miss every event in a lane spawned mid-wait.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "98-112",
          "snippet": "        // ── New-child discovery: a lane spawned after start joins the watch set with\n        //    baseline 0 (its whole content is post-start). ──\n        if args.want_subagents() && !is_subagent_target {\n            for sub in crate::subagent::subagent_transcript_files(&main).unwrap_or_default() {\n                if !cursors.iter().any(|c| c.path == sub) {\n                    let lane = crate::subagent::session_id_from_path(&sub);\n                    cursors.push(Cursor {\n                        path: sub,\n                        offset: 0,\n                        is_main: false,\n                        lane,\n                    });\n                }\n            }\n        }"
        }
      ],
      "instrument": "Start `csift wait @<uuid> --until tool:Read --timeout 60`, then spawn a subagent that calls Read: the wait fires on the child lane. Counting rule: one wait, one mid-wait spawn. The e2e test `p9_wait_discovers_children_spawned_mid_wait` in tests/cli/live/wait.rs pins it.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live/wait.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift agents @<session> --format json  AND  csift search '' @<session> -t user.message --no-subagents --format json --max-count 0, joined in python (each lane's trigger_utc bisected into the sorted list of main-lane user.message ts_utc); then a live mid-wait probe on a synthetic home: csift --claude-home <TMP>/midwait/.claude wait @<fixture-uuid> --until tool:Read --timeout 30 --format json, with a helper process creating <fixture-session>/subagents/agent-<hex>.jsonl 6 s after the wait began",
          "observed": "Live session (one busy session in the corpus): 79 user.message turn openers on the main lane, 109 subagent lanes carrying a trigger_utc. 109 of 109 lanes were triggered STRICTLY after the opening user record of their containing turn; 0 landed exactly at an opener and 0 before the first opener. The busiest single turn spawned 31 lanes at 26 distinct instants, from T0+321 s to T0+3095 s (a 46-minute spread inside ONE turn); 19 of the 79 turns spawned at least one lane. Mid-wait probe: at baseline csift printed 'csift: watching 1 file(s) from byte offsets; conditions: tool:Read; timeout 30s' (only the main transcript existed - the subagents directory had not been created); the child file was written at +6 s; the wait exited 0 with fired='tool:Read', waited_secs=7, activity={'lanes':1,'records':1,'tools':{'Read':1},'thinking':0,'agent_messages':0}, and an evidence row 'children: 1 lane(s), 1 live'.",
          "rule": "One session for the corpus half: a lane counts as mid-turn when its trigger_utc is strictly greater than the ts_utc of the nearest preceding main-lane user.message record. One wait for the second half: exactly one child transcript created after the readiness line printed, and the awaited tool name occurs ONLY inside that child file, so a fire cannot come from baseline history.",
          "note": "Both halves decided by instruments. The code site is verbatim in the current file at src/live/wait.rs:95-109 (unchanged line numbers), and the named e2e test p9_wait_discovers_children_spawned_mid_wait exists at tests/cli/live/wait.rs:140. The 109/109 count is the strongest form of the claim: in this session no subagent lane at all existed when its turn opened, so a watch set fixed at observation start would have been empty for every one of them."
        }
      ]
    },
    {
      "id": "MISC-022",
      "area": "misc",
      "behavior": "Claude Code exposes NO single field that answers 'has this session stopped': the answer must be joined from the harness session registry status, the transcript tail, the owner process, the child lanes, the elicitation sidecar and the background launches, and those surfaces disagree in a bounded number of ways.",
      "depends": "csift's `assess_full` ranks them in one fixed precedence - dead process > blocked on human > tool in flight > live children > open background task under the lens > clean end of turn > unknown - and emits an `unknown` verdict with the disagreement in the evidence rows rather than guessing.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "85-89",
          "snippet": "/// Join the surfaces into one verdict. Precedence (each step names its evidence):\n/// dead process > blocked-on-human > tool-in-flight > live children > open background\n/// task under the lens > clean EoT > unknown. Never guesses: a shape the rules cannot\n/// rank is `unknown` with the disagreement in the evidence rows.\npub(crate) fn assess_full("
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "370-380",
          "snippet": "    } else if main_tail.records_seen == 0 {\n        notes.push(\"no records readable at the tail\".to_string());\n        Verdict::Unknown\n    } else {\n        notes.push(format!(\n            \"tail shape unranked: no pending call, last stop_reason {} - not an end_turn, \\\n             not provably running\",\n            main_tail.last_stop_reason.as_deref().unwrap_or(\"(none)\")\n        ));\n        Verdict::Unknown\n    }"
        }
      ],
      "instrument": "`csift status @<uuid> --format json | jq '.verdict, .evidence'` on a session in each state; the unit test `verdict_slugs_are_the_closed_wire_set` in src/live/tests/conditions.rs pins the wire slugs. Counting rule: one verdict per invocation; the evidence array must name every surface that answered.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "src/live/verdict.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python over ~/.claude/sessions/*.json reading status/statusUpdatedAt/updatedAt/pid plus ps -p PID -o lstart= per row; csift status @<session> --format json per row reading .verdict and .evidence[]; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c for endedAt/exitedAt/stoppedAt/finishedAt/SessionEnd and | rg -o for the registry status enum and the SessionEnd hook descriptor; csift stats @<session> --no-subagents --format json for the whole-file line-type census",
          "observed": "Registry status is a closed 4-value set with no terminal member - the binary carries `var je=[\"busy\",\"shell\",\"idle\",\"waiting\"];function Ke(e){return je.includes(e)?e:void 0}` and the row parser applies it as `status:Ke(o.status)`. None of the 7 on-disk registry rows carried endedAt/exitedAt/stoppedAt/finishedAt; binary counts: exitedAt 0, stoppedAt 0, finishedAt 2, endedAt 10 (all in unrelated snapshot/daemon-upgrade code). SessionEnd exists only as a HOOK event descriptor - `SessionEnd:{summary:\"When a session is ending\",description:`Input to command is JSON with session end reason.` - i.e. a user-configured command, not a persisted field. All 7 pids were alive, yet statusUpdatedAt ages spanned 76 s to 772594 s (8.9 days) - the field is transition-written, so its age is not a liveness signal. Joined verdicts over the same 7 rows: running x3, idle-eot x3, idle-background-open x1, with 3 to 5 evidence rows each. Disagreements: 2 of 7 rows say registry 'busy' while the tail says 'no pending call; last stop_reason end_turn' (the two fields imply opposite answers), and 1 of 7 says registry 'idle' (age 772594 s) yet resolves idle-background-open because background says '1 open'. A whole-file line-type census of one transcript returned 13 distinct types (ai-title, assistant, atis-latch, attachment, bridge-session, file-history-delta, file-history-snapshot, last-prompt, mode, permission-mode, queue-operation, system, user) - none of them a session-end marker.",
          "rule": "One registry row per file in ~/.claude/sessions; one csift status invocation per row. A 'disagreement' counts when the registry status field and the transcript tail imply opposite answers to 'has this session stopped' (busy/shell vs a clean end_turn with no pending call, or idle vs an unreturned background launch). Surfaces counted from the evidence array of each invocation.",
          "note": "The refutation attempt (find one field that answers it) failed on every leg: the registry status enum has no terminal value and is stale by up to 8.9 days on a live pid, no ended/exited/stopped timestamp exists in the row, SessionEnd is a hook event rather than a persisted fact (and would not fire for a killed process), and the transcript itself has no end-of-session line type. Claude Code's own reader performs the same kind of join for a different purpose and can also answer 'unknown' - the binary carries `isSameProcess(d.pid, procStartFt ?? procStart)` feeding `{verdict:\"live\"|\"none\"|\"unknown\", ...unproven}`. Both code snippets are verbatim in the current file at src/live/verdict.rs:85-89 and 370-380, the six surface names (registry, pid, tail, children, sidecar, background) exist in the current code, and the unit test verdict_slugs_are_the_closed_wire_set exists at src/live/tests/conditions.rs:39 pinning 7 slugs. Limits of this run: 4 of the 7 wire verdicts were observed live (running, idle-eot, idle-background-open, plus waiting-children on the synthetic fixture of MISC-021); unknown, waiting-hitl and stale-dead were not reproduced and remain pinned only by tests, and the sidecar surface emitted no row because no probed session had an unanswered elicitation."
        }
      ]
    },
    {
      "id": "MISC-023",
      "area": "misc",
      "behavior": "On Unix, `ps -p PID -o lstart=` is not universally available: a busybox `ps` rejects the flags outright and returns failure even for a LIVE pid, while `/proc/<pid>` answers liveness directly on Linux and does not exist on macOS.",
      "depends": "csift falls back to a `/proc/<pid>` directory test before declaring a pid dead and discloses the reuse guard as skipped rather than silently trusting a pid-only probe - a wrong `Dead` here turns every verdict into `stale-dead`.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "213-223",
          "snippet": "    if !out.status.success() || text.is_empty() {\n        // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n        // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n        // present = alive with the start time unknown (the reuse-guard skip is\n        // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n        // reliable) = the no-such-process verdict stands.\n        if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n            return PsProbe::Alive(None);\n        }\n        return PsProbe::NoProcess;\n    }"
        }
      ],
      "instrument": "Run `ps -p $$ -o lstart=` on a busybox userland (empty output, non-zero status) versus on macOS or glibc Linux (a local-zone date). Counting rule: one probe per platform. The unit tests `probe_pid_own_process_guard_states` and `probe_pid_reports_a_reaped_pid_dead` in src/live/tests/surfaces.rs pin the alive and dead arms; only a real run on a busybox host can confirm the fallback path.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/registry.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "docker run --rm busybox sh -c 'ps -p 1 -o lstart=; echo \"exit=$?\"; test -d /proc/1 && echo PROC_1_EXISTS || echo NO_PROC_1; ps --help'  AND on the macOS host: ps -p $$ -o lstart=; echo exit=$?; test -d /proc/$$ ; ls -d /proc ; ps -p 999999 -o lstart=",
          "observed": "BusyBox v1.38.0 userland: `ps: invalid option -- 'p'` on stderr followed by its usage line `Usage: ps [-o COL1,COL2=HEADER] [-T]`, STDOUT EMPTY, exit=1 - and the pid probed was 1, the container's live init process. `/proc/1` present in the same container (PROC_1_EXISTS). macOS (Darwin 24.6.0): `ps -p <own pid> -o lstart=` exits 0 with `Wed  2 Sep 20:08:25 2026`; `/proc` does not exist (`ls: /proc: No such file or directory`); a nonexistent pid gives `ps: process id too large: 999999` with exit=1.",
          "rule": "One probe per userland. The busybox arm counts as confirmed only when the probed pid is provably alive (pid 1 of a running container) AND the probe still returns non-zero with empty stdout - that is exactly the false-dead shape the claim describes. The macOS arm counts as confirmed when /proc is absent and the ps form succeeds for a live pid and fails for a dead one.",
          "note": "Code verbatim in the current file at src/live/registry.rs:209-219. Two details worth recording: busybox rejects `-p` during option parsing, before `-o lstart` is ever considered, and it writes the complaint to stderr - so the arm of csift's guard that actually fires is `text.is_empty()` on stdout, not only the exit status; and `/proc/<pid>` was confirmed present in the busybox container, so the fallback returns Alive(None) there rather than a wrong Dead. The csift binary itself is a macOS build and was not executed inside the container, so the branch is decided by the two measured preconditions rather than by observing csift's own output on that userland; running a Linux build of csift inside a busybox container and reading its pid evidence row would close that last gap."
        }
      ]
    },
    {
      "id": "MISC-024",
      "area": "misc",
      "behavior": "`ps -o lstart=` renders its date in at least two field orders - `Sun Aug 16 09:04:23 2026` and `Sun 16 Aug 09:04:23 2026` - and the order is chosen by the LC_TIME locale of the process that runs ps, not by the host family: one macOS host produced the first order under LC_ALL=C and en_US.UTF-8 and the second under en_GB.UTF-8 and en_AU.UTF-8. Claude Code avoids the ambiguity for its own registry field by pinning the environment (`LC_ALL=C TZ=UTC ps -o lstart= -p <pid>`), so procStart is always the C-locale order in UTC; a probe that inherits the caller's locale, as csift's does, sees whichever order that locale selects.",
      "depends": "csift tries both strftime patterns before giving up and degrading to a disclosed pid-only probe; matching only one order silently disables the pid-reuse guard on the other family of hosts.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "224-234",
          "snippet": "    let local = crate::timez::local_tz();\n    for fmt in [\"%a %b %e %H:%M:%S %Y\", \"%a %e %b %H:%M:%S %Y\"] {\n        if let Ok(bd) = jiff::fmt::strtime::parse(fmt, &text) {\n            if let Ok(dt) = bd.to_datetime() {\n                if let Ok(z) = dt.to_zoned(local.clone()) {\n                    return PsProbe::Alive(Some(z.timestamp()));\n                }\n            }\n        }\n    }\n    PsProbe::Alive(None)"
        }
      ],
      "instrument": "`for L in C en_US.UTF-8 en_GB.UTF-8 en_AU.UTF-8; do LC_ALL=$L ps -p <pid> -o lstart=; done` on a single host, then the same four locales around `csift status @<id> --format json | jq '.evidence[]|select(.surface==\"pid\").value'`. Counting rule: one rendering per locale; every order must yield `alive (start-time guard matched)`, because a strftime parse that falls through both patterns degrades to `alive (pid only)` and silently disables the pid-reuse guard.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/registry.rs"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for L in C en_US.UTF-8 en_GB.UTF-8 en_AU.UTF-8; do LC_ALL=$L ps -p <live-pid> -o lstart=; done  on ONE macOS host; then the same four locales wrapped around csift status @<session> --format json, reading the value of the evidence row whose surface is 'pid'; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'LC_ALL=C TZ=UTC ps -o lstart= -p '",
          "observed": "Same host, same pid, four locales: LC_ALL=C -> `Tue Sep  1 01:02:24 2026`; en_US.UTF-8 -> `Tue Sep  1 01:02:24 2026`; en_GB.UTF-8 -> `Tue  1 Sep 01:02:24 2026`; en_AU.UTF-8 -> `Tue  1 Sep 01:02:24 2026`. So BOTH claimed field orders came out of one machine - the discriminator is LC_TIME, not the host family. csift's pid evidence row read `alive (start-time guard matched)` under all four locales; a parse falling through both patterns would instead read `alive (pid only)` (src/live/verdict.rs:192 vs 196), so the second pattern is load-bearing on this host's default locale. Claude Code 2.1.258 pins the environment for its own copy of this probe - the binary contains the literal `LC_ALL=C TZ=UTC ps -o lstart= -p ` - and the matching on-disk registry procStart for that pid reads `Mon Aug 31 15:02:24 2026`, the C-locale order rendered in UTC, ten hours behind the local ps rendering.",
          "rule": "One rendering per locale on one host, plus one csift status invocation per locale. Both orders count as tolerated only if the pid evidence row says `start-time guard matched` under each, since a failed strftime parse silently degrades to `alive (pid only)`.",
          "note": "The two-order fact holds and both patterns are exercised in practice; only the causal attribution needed fixing - it is a locale property, reproducible on one machine, not a macOS-versus-Linux property, so the claim's original instrument (compare macOS against a Linux host) could have shown one order twice and wrongly concluded the second pattern was dead code. Code verbatim in the current file at src/live/registry.rs:220-230. Follow-up worth considering for csift: pinning `LC_ALL=C TZ=UTC` on its own ps call, the way Claude Code does, would make the probe locale-independent and reduce the tolerated set to one pattern; today the guard depends on the caller's locale landing in the two-pattern set, and a locale rendering a third order (a non-English month abbreviation, for instance) would silently skip the reuse guard."
        }
      ]
    },
    {
      "id": "MISC-025",
      "area": "misc",
      "behavior": "Claude Code 2.1.258 carries ONE bundled array literal of 183 comma-separated quoted tool names, opening [\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",\"Glob\",\"Grep\",\"LS\",\"TodoWrite\",\"TaskCreate\",\"TaskGet\",\"TaskList\",\"TaskUpdate\",\"TaskStop\",\"TaskOutput\",\"LSP\",...] and continuing through \"Snip\", \"WebFetch\", \"WebSearch\", \"WebBrowser\", \"Agent\", \"Task\", \"Workflow\", \"Skill\", \"ScheduleWakeup\", \"SendMessage\", \"SendUserMessage\", \"SendFile\", \"Artifact\" and 105 mcp__-prefixed connector names. It is NOT a tool registry: it is the `disallowed_tools` list of a remote pull-request-review job config, and it omits first-party tool names that a separate 22-element tool array does carry (JavaScript, AskUserQuestion, ToolSearch). No single array in the binary enumerates every first-party tool; tool-name arrays are per-purpose (allow-lists, deny-lists, read-only sets).",
      "depends": "csift never enumerates the registry: every scanning surface keys on a hand-picked SUBSET of tool names as byte needles (subagent spawn linkage matches exactly `Agent`/`Task`/`Workflow`, `files` admits on six needles, `recover` on eleven), so a registry member csift does not name is invisible to `files`, `recover`, `agents` spawn linkage and the `status` background section without any counter registering the loss.",
      "code": [
        {
          "path": "src/subagent/spawn.rs",
          "lines": "250-251",
          "snippet": "pub(crate) fn is_spawn_tool(name: &str) -> bool {\n    matches!(name, \"Agent\" | \"Task\" | \"Workflow\")"
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "295",
          "snippet": "    static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> ="
        }
      ],
      "instrument": "`strings -n 30` over the installed Claude Code binary, filtered for the array literal opening [\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\", parsed as JSON: 183 elements, of which 105 are mcp__-prefixed. A second `rg` for the surrounding bytes recovers its binding (`var ymo=`) and its use site (`disallowed_tools:[...ymo]`).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 30 \"$CC\" | rg -o '\\[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\"[^\\]]*\\]' | sort -u  # then json.loads(...) for len(); plus: strings -n 30 \"$CC\" | rg -o 'var ymo=\\[\"Bash\".{0,60}' and rg -o 'mcp__claude-code-remote\"\\].{0,400}' and rg -o '\\[[^\\[\\]]{20,3000}\"TodoWrite\"[^\\[\\]]{0,3000}\\]'",
          "observed": "exactly 1 distinct array literal matched; json.loads gives 183 elements and the raw text carries 182 commas (183 = commas+1); first 23 elements are Bash, BashOutput, KillShell, PowerShell, Tmux, Monitor, REPL, Read, Edit, MultiEdit, Write, NotebookEdit, Glob, Grep, LS, TodoWrite, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskStop, TaskOutput, LSP - matching the claim's opening verbatim. 105 of the 183 are mcp__-prefixed connector tool names, 78 are not. The literal is bound as `var ymo=[...]` and its single observed use is `disallowed_tools:[...ymo]` inside a job_config for a remote pull-request-review job, paired with a two-element `allowed_tools` array. A SEPARATE 22-element array literal exists: [\"Bash\",\"Read\",\"Write\",\"Edit\",\"Glob\",\"Grep\",\"NotebookEdit\",\"WebFetch\",\"WebSearch\",\"Task\",\"TodoWrite\",\"TaskCreate\",\"TaskUpdate\",\"TaskGet\",\"TaskList\",\"TaskStop\",\"Skill\",\"REPL\",\"JavaScript\",\"AskUserQuestion\",\"ToolSearch\",\"SendUserMessage\"], and 3 of its names (JavaScript, AskUserQuestion, ToolSearch) are ABSENT from the 183-element array.",
          "rule": "One element per comma-separated quoted name inside the single matched array literal (verified two ways: JSON array length, and comma count + 1). Distinct-literal count after `sort -u`. Membership test: exact string equality against the parsed 183-element list.",
          "note": "The 183 count and the quoted names hold verbatim, so the depends-clause consequence (csift names only a hand-picked subset of tool names as byte needles, and an unnamed tool is invisible to files/recover/agents-spawn-linkage with no counter registering the loss) is unaffected - but the population should not be called a registry. csift's code sites verify verbatim at the claimed lines: src/subagent/spawn.rs:242-243 is `pub(crate) fn is_spawn_tool(name: &str) -> bool { matches!(name, \"Agent\" | \"Task\" | \"Workflow\")`, and src/recover/scan.rs:295 is `static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> =`."
        }
      ]
    },
    {
      "id": "MISC-026",
      "area": "misc",
      "behavior": "A transcript's filename basename equals the record-level `sessionId` on every observed file: over the live corpus's 67 top-level transcripts, all 67 carried at least one `sessionId` value, 0 carried a value differing from the basename, and 0 carried more than one distinct value.",
      "depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback when the filename yields nothing; the harness session-registry join in `status` matches a row by `sessionId`, so the two agreeing is what makes that join work at all.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "82-87",
          "snippet": "    // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n    let session_id = if session_id.is_empty() {\n        data_session_id.unwrap_or_default()\n    } else {\n        session_id\n    };"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "57-59",
          "snippet": "        if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n            continue;\n        }"
        }
      ],
      "instrument": "Same procedure; the corpus size is a moving number (66 when first measured, 67 one day later - it grows by one per new session), so the reproducible part is the two zeros, not the denominator.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every ~/.claude/projects/<encoded>/<uuid>.jsonl at depth 1: collect set(re.finditer(rb'\"sessionId\"\\s*:\\s*\"([^\"]*)\"', bytes)) per file and compare against basename[:-6]; separately python3 over ~/.claude/sessions/*.json collecting each row's sessionId and testing membership in the set of corpus transcript basenames.",
          "observed": "top-level transcripts: 67. Files carrying >=1 sessionId: 67. Files with none: 0. Files whose basename is absent from their own sessionId value set: 0. Files holding more than one distinct sessionId value: 0. Harness registry: 7 .json rows, 7 carrying a non-empty sessionId, 7 of 7 resolving to a transcript basename present in the corpus.",
          "rule": "One observation per top-level transcript file (a project dir's direct *.jsonl children only - subagent transcripts live one level down under <uuid>/subagents/ and are excluded). The regex is unescaped-JSON-key only: an escaped \\\"sessionId\\\" quoted inside prose does not match, so a value set of size 1 means the key never appears with a foreign value at any nesting depth. One observation per harness registry row for the join test.",
          "note": "Number corrected 66 -> 67; the invariant itself is unchanged and the two failure counts are still 0. The depends-clause was instrumented directly rather than inferred: every harness registry row's sessionId resolved to a corpus transcript basename (7/7), which is exactly the join src/live/registry.rs:57-59 performs. Both csift code sites verify verbatim at the claimed lines - src/session/summarize.rs:82-87 (`// Prefer the filename-derived id; cross-check with the data id (SS2.4 spirit).` followed by the `if session_id.is_empty()` fallback) and src/live/registry.rs:57-59 (`if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) { continue; }`)."
        }
      ]
    },
    {
      "id": "MISC-027",
      "area": "misc",
      "behavior": "Claude Code's `SessionStart` hook matcher accepts exactly five source values - the array literal `[\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]` - so `resume` and `fork` are first-class session-start causes distinct from `compact`, and the firing source is handed to the hook as `.source` on its stdin payload.",
      "depends": "csift's #1 hook recipe registers `SessionStart` with matcher `compact` and re-checks `.source` inside the script before injecting the verbatim turns a compaction clipped, so it fires on a compaction only; the same script under matcher `resume` would fire on a resume, and the separate `fork` source is what distinguishes a fork from a resume at hook time - a distinction the transcript itself does not carry.",
      "code": [
        {
          "path": "SKILL.md",
          "lines": "526",
          "snippet": "in=$(cat); [ \"$(jq -r '.source//empty' <<<\"$in\")\" = compact ] || exit 0"
        },
        {
          "path": "SKILL.md",
          "lines": "534",
          "snippet": "Register N times: `{\"matcher\":\"compact\",\"hooks\":[{\"type\":\"command\",\"command\":\"/ABS/csift-turns-slice.sh i\"}]}`, i=1..N. Absolute path (a relative command resolves against the hook child's cwd, not the skill's install dir; `$CLAUDE_PROJECT_DIR` is set for every hook child, global registrations included, so it is not the reason); `--window 9000` stays under the 10K additionalContext cap."
        }
      ],
      "instrument": "`strings -n 20` over the installed Claude Code binary, filtered for an array literal starting `[\"startup\",\"resume\",\"clear\",\"compact\"` - one distinct occurrence, `[\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]`. Counting rule: exact array literal, deduplicated.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 20 \"$CC\" | rg -o '\\[\"startup\",\"resume\",\"clear\",\"compact\"[^\\]]*\\]' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,150}\"SessionStart\".{0,150}' | sort -u",
          "observed": "Exactly one distinct array literal: [\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]. Its schema context is `hook_event_name:x(\"SessionStart\"),source:ee([\"startup\",\"resume\",\"clear\",\"compact\",\"fork\"]),agent_type:i().optional(),model:i().optional(),session_title:i().optional(),seconds_since_l...`. The payload builder reads `j={...,hook_event_name:\"SessionStart\",source:n,agent_type:d,model:f,session_title:o??fu(U.id),...I}` and passes the same value as `matchQuery:n` to the hook runner, so the matcher matches on the source value and the same value is handed to the hook as `source`.",
          "rule": "Exact array literal, deduplicated with `sort -u`: one distinct occurrence. Payload-field presence read from the one matched builder expression.",
          "note": "Confirmed in both directions: the five-value enum is the schema for the `source` field AND the value is passed as `matchQuery`, which is what makes `{\"matcher\":\"compact\"}` select on it. The builder also carries `agent_type`, `model` and `session_title` alongside `source` - fields the claim does not mention but which do not affect it. csift's SKILL.md sites verify verbatim at the claimed lines: line 526 is `in=$(cat); [ \"$(jq -r '.source//empty' <<<\"$in\")\" = compact ] || exit 0` and line 534 is the `Register N times: {\"matcher\":\"compact\",...}` sentence."
        }
      ]
    },
    {
      "id": "MISC-028",
      "area": "misc",
      "behavior": "Claude Code accepts a `--fork-session` flag (registered help: `When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)`; guidance string: `Add --fork-session to branch off a copy instead.`). It does NOT write a `forkSession` record field: the sentence `forkSession follows across the compaction break. Distinct from the session-file chain parent (which is the post-compact summary). Absent from older producers.` is the `.describe()` text of a schema field named `logical_parent_uuid` - i.e. it documents the backpointer the fork OPERATION follows, and `forkSession` there is the operation's name. The fork validators require `sessionId` to be a UUID and, when supplied, `upToMessageId` to be a UUID (optional). 0 of the live corpus's 67 top-level transcripts carry the key `forkSession` - consistent with no such record field existing. A fork does leave an on-disk trace, in the ORIGIN file: a `{\"type\":\"continued-in\",\"sessionId\":<origin>,\"continuedInSessionId\":<successor>}` record naming the successor session.",
      "depends": "csift models the compaction-fork CLONE (a byte-copied head whose first timestamped record is a `compact_boundary`, joined to its origin by that boundary uuid) but not `forkSession`; a forked session's relationship to its origin is therefore not surfaced by `list`, and a consumer cannot distinguish an ordinary resume from a fork.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "186-189",
          "snippet": "pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n    let dir = path.parent()?;\n    let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());\n    let entries = std::fs::read_dir(dir).ok()?;"
        }
      ],
      "instrument": "`strings -n 20` over the installed binary for the describe() text, the flag registration and the two validator messages; then a byte scan of every top-level transcript for `forkSession` (0 of 67) and an rg for `{\"type\":\"continued-in\"` (1 of 67, its successor id resolving to a sibling transcript).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "CC=~/.local/share/claude/versions/2.1.258; strings -n 20 \"$CC\" | rg -o '.{0,200}forkSession follows across.{0,200}' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,80}--fork-session.{0,120}' | sort -u; strings -n 20 \"$CC\" | rg -o '.{0,120}upToMessageId.{0,160}' | sort -u; python3 byte-scan of every top-level ~/.claude/projects/<encoded>/<uuid>.jsonl for the key \"forkSession\"; rg -o '\\{\"type\":\"continued-in\"[^}]*\\}' --glob '*.jsonl' over ~/.claude/projects.",
          "observed": "(a) The sentence is the `.describe()` argument of a schema field literally named `logical_parent_uuid`: `logical_parent_uuid:Y().nullable().optional().describe(\"@internal uuid of the last pre-compact message - the backpointer \"+\"forkSession follows across the compaction break. Distinct from the session-file chain parent (which is the post-compact summary). Absent from older producers.\")`. (b) The flag is registered as `.option(\"--fork-session\",\"When resuming, create a new session ID instead of reusing the original (use with --resume or --continue)\",()=>!0)`, and the guidance string `Add --fork-session to branch off a copy instead.` is present. (c) Validators: `if(!Wf(r))throw new k(...,\"forkSession: invalid sessionId (must be a UUID)\");if(n.upToMessageId&&!Wf(n.upToMessageId))throw new k(...,\"forkSession: invalid upToMessageId (must be a UUID)\")` - upToMessageId is guarded by a truthiness test, so it is optional. (d) 0 of 67 top-level transcripts contain the key \"forkSession\". (e) 1 file in the whole corpus carries a record of type `continued-in`, shaped `{\"type\":\"continued-in\",\"timestamp\":\"2026-09-02T09:54:22.256Z\",\"sessionId\":<origin>,\"continuedInSessionId\":<successor>}`; the successor id resolves to a sibling transcript file in the same project dir. The binary declares the same shape as a schema (`type:x(\"continued-in\"),continuedInSessionId:i()`) and lists it in a merge-policy table as `\"continued-in\":\"always\"`.",
          "rule": "One observation per extracted string on the binary half. One observation per file on the corpus half: 67 top-level transcripts scanned, 0 containing the byte sequence \"forkSession\", 1 containing a `continued-in` record. Successor-link resolution counted as a filename-basename match anywhere under ~/.claude/projects.",
          "note": "Two corrections. First, `forkSession` is an operation/option name, not a record key - the described field is `logicalParentUuid`, which csift already models and renders on compact_boundary records, so the ledger's framing overstated the gap. Second, the depends-clause line 'a consumer cannot distinguish an ordinary resume from a fork' is partly refuted: an ordinary resume reuses the session id and appends to the same file (no successor to point at), whereas the observed fork minted a new id and wrote a `continued-in` successor link into the origin. That link is a real, unmodeled csift gap - `list` does not surface it, and csift has no `continued-in` leaf. Caveat on strength: the single corpus instance came from a fork probe already present in the corpus, not from a fork I re-ran, so the trigger is inferred from the record's own shape and its schema name rather than observed at dispatch; running `claude --resume <id> --fork-session` and re-scanning the origin would settle whether `continued-in` is fork-specific or is also written on a background handoff. src/session/summarize.rs:186-189 verifies verbatim (`pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String>` and the three lines under it)."
        }
      ]
    },
    {
      "id": "MISC-029",
      "area": "misc",
      "behavior": "A tool-call record's only STRUCTURAL appearance of the tool NAME on the raw jsonl line is the `name` field of its `tool_use` block: measured over one project dir, 7764 of 7974 single-tool_use assistant records (97.4%) contain the name exactly once, the remainder being incidental content matches. The name is NOT mirrored on the result side - only 678 of 7986 single-tool_result carriers (8.5%) contain the tool name at all, so a result carrier is reachable only via the tolerant user-role matcher, not via a name needle. A tool whose name shares no substring with a scanner's needle set therefore leaves its tool_use line unreachable before any parse happens.",
      "depends": "`csift files`' pre-JSON prefilter admits a line on exactly six needles - `Edit`, `Write`, `Bash`, `filePath`, `file-history-snapshot`, `is_error` - plus the tolerant user-role matcher; a renamed or new mutating tool whose name contains none of those and whose input key is neither `filePath` nor an error carrier is rejected pre-parse, so its mutations never reach the extraction step and never reach the malformed or dropped counters either.",
      "code": [
        {
          "path": "src/files/run.rs",
          "lines": "175-180",
          "snippet": "pub(crate) fn line_is_files_candidate(line: &[u8]) -> bool {\n    // R13: the genuine-user hook is serialization-tolerant (user-only - assistant\n    // coverage rides the tool-name needles below, so admitting every assistant\n    // text record here would repeal this prefilter). Finders built ONCE (per-line\n    // hot path - the stateless form rebuilt its searcher every call).\n    static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 6]> ="
        },
        {
          "path": "src/files/run.rs",
          "lines": "183-188",
          "snippet": "                memmem::Finder::new(b\"Edit\"),\n                memmem::Finder::new(b\"Write\"),\n                memmem::Finder::new(b\"Bash\"),\n                memmem::Finder::new(b\"filePath\"),\n                // Settings-family external writes read the snapshot version sequence.\n                memmem::Finder::new(b\"file-history-snapshot\"),"
        }
      ],
      "instrument": "Read `line_is_files_candidate` in src/files/run.rs and count the memmem::Finder::new entries inside its LazyLock array (6), then test each name in a tool-name array for case-sensitive containment of one of those needles. Reported against two populations because no single binary array enumerates every first-party tool: the 183-element remote-job disallow list (176 blind) and a 22-element first-party tool array (17 blind).",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/files/run.rs line_is_files_candidate"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "sed -n '173,196p' src/files/run.rs (count the memmem::Finder::new entries in the LazyLock array); then python3 containment test of every name in each extracted tool-name array against those 6 needles; then python3 over one project dir's 23 *.jsonl under ~/.claude/projects: for each assistant record holding exactly one tool_use block, count occurrences of the block's `name` value in the raw line; and the same over single-tool_result user records joined to the name by tool_use_id.",
          "observed": "The array is declared `[memmem::Finder<'static>; 6]` with elements Edit, Write, Bash, filePath, file-history-snapshot, is_error, and the function ends `crate::parse::line_has_user_role_marker(line) || NEEDLES.iter().any(|f| f.find(line).is_some())`. Containment: of the 183-element array, 7 names contain one of the six needles (Bash, BashOutput, Edit, MultiEdit, Write, NotebookEdit, TodoWrite) and 176 do not; of the separate 22-element first-party tool array, 5 hit and 17 do not (Read, Glob, Grep, WebFetch, WebSearch, Task, TaskCreate, TaskUpdate, TaskGet, TaskList, TaskStop, Skill, REPL, JavaScript, AskUserQuestion, ToolSearch, SendUserMessage). Name-occurrence census over one project dir: 7974 single-tool_use assistant records examined, 7764 (97.4%) carry the tool name exactly once in the raw line; the 210 with more than one are content coincidences concentrated in Bash 125, Edit 45, Write 25, Agent 10, SendMessage 4, AskUserQuestion 1. Result side: of 7986 single-tool_result user records with a resolvable tool name, only 678 (8.5%) contain that name anywhere in the raw line.",
          "rule": "One needle per memmem::Finder::new call inside the LazyLock array (6). One containment test per array name, case-sensitive Python `in`. Name-occurrence census: one observation per assistant record whose message.content holds exactly ONE tool_use block; occurrences counted as raw-byte substring hits of the name value. Result-side census: one observation per user record whose content holds exactly ONE tool_result block whose tool_use_id joins to a name harvested from the same project dir.",
          "note": "The 6 needles and both code snippets verify verbatim at the claimed lines (src/files/run.rs:175-180 and :183-188). Two wording corrections. The parenthetical 'mirrored in the toolUseResult spelling' is not supported by measurement - the result carrier usually does not carry the name, which is precisely why the tolerant user-role matcher is load-bearing there. And the population called 'the 183-entry registry array' is a remote-job disallow list (see MISC-025), so the blind-spot count is reported against both arrays. The substance - a new or renamed mutating tool whose name misses all six needles is rejected pre-parse and reaches no counter - is unchanged and is the sharper number on the first-party array: 17 of 22 named first-party tools contain none of the six needles."
        }
      ]
    },
    {
      "id": "MISC-030",
      "area": "misc",
      "behavior": "Claude Code writes the artifacts a file-recovery scan needs - `Read` calls, `tool_result` bodies, integrity errors, `edited_text_file` attachments and `file-history-snapshot` lines - under distinct field spellings rather than one common key, so a recovery scanner needs one needle per spelling instead of a single structural hook.",
      "depends": "`csift recover` admits a line only when one of eleven literal byte needles is present (`toolUseResult`, `Edit`, `Write`, `Read`, `Bash`, `PowerShell`, `filePath`, `file_path`, `file-history-snapshot`, `edited_text_file`, `tool_use_error`) plus the tolerant user-role matcher, and builds its per-window opaque accounting from what survives; a mutating tool outside that set never enters the disclosure, so the opaque count UNDER-reports rather than over-reports - the one direction the no-silent-loss contract forbids.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "291-295",
          "snippet": "pub(crate) fn line_is_recover_candidate(line: &[u8]) -> bool {\n    // R13: the genuine-user hook is serialization-tolerant (user-only, like files').\n    // Finders built ONCE (per-line hot path - the stateless form rebuilt its searcher\n    // every call).\n    static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> ="
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "298-311",
          "snippet": "                memmem::Finder::new(b\"toolUseResult\"),\n                memmem::Finder::new(b\"Edit\"),\n                memmem::Finder::new(b\"Write\"),\n                memmem::Finder::new(b\"Read\"),\n                memmem::Finder::new(b\"Bash\"),\n                // The opaque accounting counts PowerShell tool calls; an assistant\n                // record carrying one matches no other needle, so without this the\n                // P count silently missed the assistant-side records.\n                memmem::Finder::new(b\"PowerShell\"),\n                memmem::Finder::new(b\"filePath\"),\n                memmem::Finder::new(b\"file_path\"),\n                memmem::Finder::new(b\"file-history-snapshot\"),\n                memmem::Finder::new(b\"edited_text_file\"),\n                memmem::Finder::new(b\"tool_use_error\"),"
        }
      ],
      "instrument": "Read `line_is_recover_candidate` in `src/recover/scan.rs`: the array is declared `[memmem::Finder<'static>; 11]` and its elements are the eleven literals. Counting rule: one needle per `memmem::Finder::new` call inside the `LazyLock` array.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/recover/scan.rs line_is_recover_candidate"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "sed -n '288,315p' src/recover/scan.rs; then, scoped to one project dir under ~/.claude/projects: for n in '\"toolUseResult\"' '\"filePath\"' '\"file_path\"' '\"file-history-snapshot\"' 'edited_text_file' 'tool_use_error' '\"is_error\"' 'staleReadFileStateHint' 'staleRecovered'; do rg -c --no-filename -F \"$n\" *.jsonl | paste -sd+ - | bc; done",
          "observed": "The array is declared `static NEEDLES: std::sync::LazyLock<[memmem::Finder<'static>; 11]> =` at line 295 with exactly the eleven literals toolUseResult, Edit, Write, Read, Bash, PowerShell, filePath, file_path, file-history-snapshot, edited_text_file, tool_use_error, and the function ends `crate::parse::line_has_user_role_marker(line) || NEEDLES.iter().any(|f| f.find(line).is_some())`. Corpus line counts in that one project dir (23 transcripts): \"toolUseResult\" 7986, \"filePath\" 2727, \"file_path\" 2734, \"file-history-snapshot\" 435, edited_text_file 295, tool_use_error 119, \"is_error\" 4739, staleReadFileStateHint 205, staleRecovered 112.",
          "rule": "One needle per memmem::Finder::new call inside the LazyLock array (11). Corpus half: `rg -c -F` counts matching LINES per file, summed across the 23 transcripts of one project dir; a fixed-string search, so no regex interpretation.",
          "note": "Both the code snippet and the eleven literals verify verbatim at the claimed lines (src/recover/scan.rs:291-295 and :298-311). The behavior claim - that the artifacts a recovery scan needs are written under distinct spellings rather than one common key - is confirmed on disk rather than only from the code: `filePath` (2727 lines) and `file_path` (2734 lines) are two genuinely distinct live spellings in the same project dir, and the freshness signals the claim's depends-clause names appear under their own third and fourth spellings (staleReadFileStateHint 205 lines, staleRecovered 112 lines). The under-report direction of the depends-clause follows from the prefilter shape and is not separately measurable without a mutating tool outside the eleven needles."
        }
      ]
    },
    {
      "id": "NAR-001",
      "area": "narration",
      "behavior": "Since at least Claude Code 2.1.170 (records on disk from 2026-06-10) the API can return a SECOND `thinking` block inside one assistant message: a short summary of the reasoning beside it, in the conversation's language. It is identical in shape to a reasoning block - measured over 91,904 real signatures, both kinds carry exactly the block key-set {type, thinking, signature} with zero variation - so nothing on the wire separates the two except that signature's contents. Presence is NOT monotone in client version: it is a server-side behavior, and long runs of later clients wrote none. Clients from 2.1.241 carry the classifier and render the block under the hint word `summarized`.",
      "depends": "csift splits the leaf `agent.thinking.narration` off `agent.thinking`; without the split an API-issued summary is counted as the model's own reasoning in every `search -t`, `--count-by label` and `stats` figure, and `verbatim` would replay it as reasoning. It is also the first leaf whose dotted path another leaf prefixes, which is why `-t` selection matches by dot SEGMENT (`-t agent.thinking` selects both leaves).",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "3-8",
          "snippet": "//! Since at least CC 2.1.170 (2026-06-10) the API can return a SECOND `thinking` block\n//! in an assistant message: a one-sentence, user-language summary of the reasoning\n//! beside it (clients >= 2.1.241 render it dim under the hint word `summarized`).\n//! Nothing distinguishes it on the wire except a tag encoded inside the base64\n//! `signature`: protobuf field path 2 -> 1 -> 8, taking the LAST length-delimited field\n//! at each level, yielding a UTF-8 string reading `narration` vs `thinking`."
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "66-70",
          "snippet": "    /// `agent.thinking.narration` - a narration-tagged thinking block: an API-issued\n    /// one-sentence summary of the reasoning beside it, NOT the reasoning itself. The\n    /// tag hides inside the base64 `signature` (see `model/narration.rs`); the first\n    /// leaf whose path is prefixed by another leaf (`-t agent.thinking` selects both).\n    AgentThinkingNarration,"
        }
      ],
      "instrument": "`csift stats <target> --format json | tail -1 | jq '{narration_blocks, unknown_thinking_tags}'` counts narration BLOCKS per model; cross-check the leaf split with `csift search '' @<session> --count-by label | rg 'agent\\.thinking'`, which on one measured session read 609 `agent.thinking` and 313 `agent.thinking.narration`.",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) for v in 2.1.229 2.1.234 2.1.241 2.1.251 2.1.257 2.1.258; do strings -n 6 ~/.local/share/claude/versions/$v | rg -c 'isNarrationTaggedBlock'; done   (b) strings -n 6 <2.1.258 binary> | rg 'I3n=\"narration\",scn=\"summarized\"'   (c) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally bucketing each block's sorted key-set and its record's `version` field   (d) csift search '' @<session> --count-by label --no-subagents",
          "observed": "(a) isNarrationTaggedBlock absent in 2.1.229 and 2.1.234, present in 2.1.241, 2.1.251, 2.1.257 and 2.1.258 - the client-side narration classifier is introduced at 2.1.241. (b) 2.1.258 carries `var o=2,A=1,a=8,I3n=\"narration\",scn=\"summarized\"` and the render site `e(ns,{hint:scn,children:a})`; 2.1.241 carries the same pair as `ation\",Iol=\"(summarized)\"`. The 2.1.258 classifier reads `function i(r){try{if(r.type!==\"thinking\"||!r.signature)return!1; ... return n===I3n}catch(n){if(rc().claim(\"narration_classifier_error\"))h(n);return!1}}` exported as `isNarrationTaggedBlock`. (c) on disk both kinds carry EXACTLY the same block key-set {signature, thinking, type}: 3,103 narration blocks and 88,966 reasoning blocks, zero variation; narration text is short (median 99 chars, max 626) against reasoning (median 519, max 96,225); the earliest narration-tagged block was written by CC 2.1.170 at 2026-06-10T02:10:12.644Z, and the corpus contains older transcripts (2.1.150, 2.1.156, 2.1.159 - 2,874 signatures, 0 narration). (d) one session: 609 agent.thinking + 313 agent.thinking.narration; `-t agent.thinking` selects both leaves (census under that selector prints both rows).",
          "rule": "One count per thinking BLOCK, keyed by the tag its own signature decodes to; the census counts one per RECORD per surviving leaf. Client-version boundary = the oldest installed binary whose strings contain `isNarrationTaggedBlock`. Corpus is live, so block totals drift upward between runs; the narration figures were stable across two passes 15 minutes apart.",
          "note": "Holds, with three wording corrections. (1) `written in the user's language` is too strong: inside ONE session whose human writes Chinese, narration blocks appeared in both Chinese and English, so the block tracks the conversation rather than a fixed user language. (2) `render it dim` - the hint WORD `summarized` and its render site are provable from the binary (`hint:scn`), but the dim styling is not decidable from strings alone; running 2.1.258 with a narration-bearing turn and inspecting the rendered attributes would decide it. (3) `Since at least 2.1.170` reads as a client-version gate; the per-writing-version census refutes that reading - 2.1.170 wrote 61 narration blocks out of 213 signatures, then 2.1.177 through 2.1.193 wrote 14,110 signatures with ZERO narration, and 2.1.196 resumed. Not drifted: CC 2.1.258 both carries the classifier and wrote 166 narration blocks in this corpus, the newest today."
        }
      ]
    },
    {
      "id": "NAR-002",
      "area": "narration",
      "behavior": "The tag separating the two thinking-block kinds is encoded inside the base64 `signature` at protobuf field path 2 -> 1 -> 8, taking the LAST length-delimited (wire type 2) field at each level; the payload decodes to the UTF-8 string `narration` or `thinking`.",
      "depends": "`thinking_signature_tag` mirrors exactly that walk and `thinking_block_class` maps a decoded `narration` to `agent.thinking.narration` and every other outcome to `agent.thinking`. The LAST-field rule matches the client byte for byte, but on this corpus it is not yet load-bearing: a FIRST-field walk returned the identical tag on all 91,904 signatures (0 divergences), so today the two rules are indistinguishable from the data and only the client decoder decides which is correct. A shifted field PATH does break the decode. Every failure path - absent or empty signature, bad base64, truncated varint, unknown wire type, non-UTF-8 - degrades to untagged rather than erroring, in both the client (`default:return!1`, `catch{return}`) and csift.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "26-35",
          "snippet": "pub(crate) fn thinking_signature_tag(signature: Option<&str>) -> Option<String> {\n    let sig = signature?;\n    if sig.is_empty() {\n        return None;\n    }\n    let bytes = crate::image::decode_base64(sig)?;\n    let f2 = last_len_delimited(&bytes, 2)?;\n    let f1 = last_len_delimited(f2, 1)?;\n    let f8 = last_len_delimited(f1, 8)?;\n    std::str::from_utf8(f8).ok().map(str::to_string)"
        },
        {
          "path": "src/model/narration.rs",
          "lines": "88-90",
          "snippet": "                if field == want {\n                    found = Some(&bytes[pos..end]);\n                }"
        }
      ],
      "instrument": "Take a signature from any thinking block (`csift show @<session> --line <n> --raw | jq -r '.message.content[] | select(.type==\"thinking\") | .signature'`), base64-decode it, then walk field 2, then field 1, then field 8, taking the LAST length-delimited field at each level; the payload is the UTF-8 string `narration` or `thinking`. Counting rule: one decode per thinking block, tag read from that block's own signature. The csift unit tests under src/model/tests pin the LAST-field rule and the full degradation matrix with hand-emitted protobuf fixtures.",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger; AGENTS.md section 3.3a; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'function GFt|I3n=\"narration\"'   (b) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, run twice per signature - once taking the LAST wire-type-2 field at each of the three levels and once taking the FIRST - and comparing the two decoded tags",
          "observed": "(a) the 2.1.258 binary contains the decoder verbatim: `var o=2,A=1,a=8,I3n=\"narration\",scn=\"summarized\";function P3n(d){let n;try{n=atob(d)}catch{return}let e=new Uint8Array(n.length);for(let t=0;t<n.length;t++)e[t]=n.charCodeAt(t);let r=GFt(e,o);if(r===void 0)return;let i=GFt(r,A);if(i===void 0)return;return O3n(i,a)}` with the field getter `function GFt(n,r){let e;return acn(n,{onBytes(u,a){if(u===r)e=a}})?e:void 0}` - the callback ASSIGNS on every matching field, so the LAST wire-type-2 field wins - and `function O3n(n,r){let e=GFt(n,r);return e===void 0?void 0:l.decode(e)}` (UTF-8). The scanner `acn` skips wire 0/1/5 and `default:return!1`, i.e. an unknown wire type or any truncation aborts to undefined. It is called as `P3n(r.signature)`. (b) 91,904 real signatures decode under the 2 -> 1 -> 8 LAST-field walk to exactly two UTF-8 strings, `thinking` (88,801) and `narration` (3,103), with zero base64 errors, zero malformed protobuf and zero non-UTF-8 payloads.",
          "rule": "One decode per thinking block, tag read from that block's own signature. Divergence rule: count a signature as divergent when the FIRST-field walk and the LAST-field walk return different strings.",
          "note": "The behavior claim holds and is now confirmed at the strongest available level: the field path, the LAST-field selection rule and the two tag strings are all readable in the 2.1.258 binary itself, not merely inferred from data. Refined only because the `depends` clause asserts that a FIRST-field walk `silently collapses narration back into agent.thinking` - measured, that never happens on any of the 91,904 signatures in this corpus. The claim's protection is against a future signature that carries repeated fields, not against today's data."
        }
      ]
    },
    {
      "id": "NAR-003",
      "area": "narration",
      "behavior": "The signature's outer format-version varint is not a discriminator: the values 2, 4 and absent all appear in real data carrying both tags.",
      "depends": "csift never consults the version varint and reads the tag only from the 2 -> 1 -> 8 payload; keying on the version would misclassify blocks in all three version populations at once.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "16-17",
          "snippet": "//! `narration` classifies as plain thinking. The outer format-version varint is NEVER\n//! consulted (values 2, 4 and absent all carry both tags in real data)."
        }
      ],
      "instrument": "Group real signatures by their outer format-version varint and cross-tabulate against the decoded 2 -> 1 -> 8 tag: each of the values 2, 4 and absent occurs with BOTH tags, so the version alone predicts nothing. Counting rule: one signature per thinking block.",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording every TOP-LEVEL wire-type-0 (varint) field of the decoded signature and cross-tabulating it against the decoded 2 -> 1 -> 8 tag",
          "observed": "Top-level varint field 1 takes exactly two values plus absence, and every one of the three populations carries BOTH tags: field1=2 -> 50,105 thinking / 2,875 narration; field1=4 -> 1,055 thinking / 220 narration; field1 absent -> 37,641 thinking / 8 narration. (Top-level varint field 3 is 1 on every signature.) Total 91,904 signatures.",
          "rule": "One signature per thinking block; a signature is binned by the set of its top-level varint fields and cross-tabbed against its decoded tag. The claim is refuted if any bin is single-tag.",
          "note": "Holds corpus-wide, but the margin is thin in one cell and the claim is scope-sensitive: the field1-absent population holds only 8 narration signatures against 37,641 thinking ones, and restricted to a SINGLE project directory (443 files, 12,873 signatures) that cell was 5,251 thinking / 0 narration - i.e. inside one project the version varint DOES look like a perfect discriminator. A verifier who scopes too narrowly will wrongly conclude the varint is a discriminator, which is exactly the mistake the claim exists to prevent. csift's independence from the varint is confirmed in code: `thinking_signature_tag` reads only the wire-type-2 chain, and `last_len_delimited` skips wire-0 fields without inspecting their values."
        }
      ]
    },
    {
      "id": "NAR-004",
      "area": "narration",
      "behavior": "Token usage is reported once per API message and covers the reasoning block and its narration sibling together, so the transcript carries no per-block token split between reasoning and its summary.",
      "depends": "`stats` reports `narration_blocks` as a BLOCK count per model and says the token split is unavailable; deriving a per-kind token figure would be a fabrication, so every narration quantity keeps the blocks-not-messages counting rule.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "53-57",
          "snippet": "    /// Narration-tagged thinking blocks per model (`agent.thinking.narration`: an\n    /// API-issued summary of the reasoning beside it). A BLOCK count only - the token\n    /// split is not derivable from the jsonl (usage is per MESSAGE and covers the\n    /// reasoning block and its narration sibling together).\n    narration_blocks: BTreeMap<String, usize>,"
        }
      ],
      "instrument": "`csift stats <target> --format json | tail -1 | jq .narration_blocks` gives blocks per model; compare with the same scope's token map, which is keyed per API message id and deduped per message. Counting rule: narration is counted in BLOCKS; usage is counted once per message and cannot be attributed to one of the two thinking blocks it covers.",
      "located": {
        "claude_code": "2.1.241",
        "csift": "0.9.2",
        "source": "SPEC.md section 6 v0.9.2 ledger item 1; src/stats.rs field docs"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 narr_usage.py ~/.claude/projects  # groups every thinking-bearing record by message.id within one transcript, decodes each block's tag, and compares json.dumps(message.usage, sort_keys=True) across the per-block records of one message; also scans every thinking block's key-set for any key containing 'token' or 'usage'",
          "observed": "3,103 assistant messages contain a narration block. 2,927 of them (94.3%) are written as exactly 2 per-block records - one carrying the reasoning block, one carrying the narration block - and 2,926 of those 2,927 carry a BYTE-IDENTICAL `message.usage` object on both records. The single exception is a streaming partial, not a per-block split: its first record reads output_tokens=3 and its last reads output_tokens=922 with `output_tokens_details.thinking_tokens: 426` - one message-level thinking total, not two block-level ones. Zero of the 91,904 thinking blocks carry any key containing 'token' or 'usage'. The remaining 176 narration messages are single-record.",
          "rule": "One group per (transcript, message.id). Usage is compared as the canonical JSON of message.usage. A per-block split would require either differing usage objects that sum to the message total, or a token key on the block itself; neither occurs.",
          "note": "Holds. The instrument also shows WHY the split is unavailable rather than merely absent: Claude Code writes one jsonl record per content block and repeats the identical per-MESSAGE usage object on each, so a naive per-record sum double-counts a reasoning+narration message and still yields no split. `output_tokens_details.thinking_tokens` is the closest thing to a thinking figure and it too is one number per message covering both blocks."
        }
      ]
    },
    {
      "id": "NAR-005",
      "area": "narration",
      "behavior": "The tag universe is open in principle and measured closed at exactly two values - `thinking` and `narration` - over 91,904 real signatures; a third value would be indistinguishable from reasoning to any consumer that hard-codes the pair.",
      "depends": "csift treats any decoded value other than `narration` as plain `agent.thinking` (never an error) and counts the residue in `stats`' `unknown_thinking_tags`, so a new API tag value surfaces as a non-zero number without needing a csift release.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "15-16",
          "snippet": "//! `agent.thinking`), never an error. The tag set is open: any value other than\n//! `narration` classifies as plain thinking. The outer format-version varint is NEVER"
        },
        {
          "path": "src/stats.rs",
          "lines": "58-60",
          "snippet": "    /// Thinking-signature tags that decoded to something OTHER than thinking or\n    /// narration - a new tag value surfaces here without a csift release.\n    unknown_thinking_tags: usize,"
        },
        {
          "path": "src/stats.rs",
          "lines": "300-301",
          "snippet": "                        Some(\"thinking\") | None => {}\n                        Some(_) => out.unknown_thinking_tags += 1,"
        }
      ],
      "instrument": "`csift stats <target> --format json | tail -1 | jq '{narration_blocks, unknown_thinking_tags}'` over a corpus spanning 2026-06-10 onward; a non-zero `unknown_thinking_tags` means the tag universe moved and the decoder's assumptions need re-reading. Counting rule: one per thinking block whose decoded tag is neither `thinking` nor `narration`; measured 0 over 91,904 real signatures.",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md v0.9.2 ledger; CHANGELOG 0.9.2; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, tallying the decoded tag string   (b) csift stats ~/.claude/projects/<one project dir> --format json | tail -1 | jq '{unknown_thinking_tags, narration_blocks}'   (c) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'isNarrationTaggedBlock'",
          "observed": "(a) exactly two decoded values over 91,904 signatures: `thinking` 88,801 and `narration` 3,103; zero base64 failures, zero malformed protobuf, zero non-UTF-8, zero third value. (b) csift reports unknown_thinking_tags 0 and 996 narration blocks over one project directory - matching the independent Python decode of the same directory block for block (996). (c) the client's own test is a strict equality `return n===I3n` against the constant `\"narration\"`, so any third value falls to plain thinking there too.",
          "rule": "One count per thinking block whose decoded tag is neither `thinking` nor `narration`; measured 0 over 91,904 real signatures under the projects root.",
          "note": "Holds; only the sample size needed correcting - the ledger says 13,957 signatures, the current corpus carries 91,904 (the corpus is live and grows). The conclusion strengthens: a 6.6x larger sample still yields exactly two values. The claim's own escape hatch was also exercised end to end - csift's counter reads 0 and its narration total reproduces the independent decode exactly."
        }
      ]
    },
    {
      "id": "NAR-006",
      "area": "narration",
      "behavior": "Thinking-block `signature` strings reach 223,160 base64 characters (167,368 decoded bytes) in real data, and the WHOLE string must be decoded to read the tag: the protobuf scanner aborts as soon as a length-delimited field's declared length overruns the buffer, so every prefix of a signature - even 99% of it - reports untagged. The tag itself does NOT sit at the end of the byte stream (measured median 21.6%, max 42.4% of the decoded length); `LAST field` is a protobuf field-ordering rule at each nesting level, not a byte position.",
      "depends": "csift decodes the WHOLE signature string; any windowed decode under-reports `agent.thinking.narration` with no error and no dropped-count disclosure - a failure a narration figure cannot detect from its own output.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "11-13",
          "snippet": "//! entirely (~4% measured), so adjacency is never a shortcut. Signatures reach 200K+\n//! base64 chars; the WHOLE string is decoded (a prefix decode silently reports every\n//! long signature untagged). Every failure path - absent/empty signature, bad base64,"
        }
      ],
      "instrument": "`rg -o '\"signature\":\"[^\"]*\"' <transcript> | awk '{print length-14}' | sort -n | tail -1` for the maximum base64 length (the 14 subtracts the `\"signature\":\"` key and the closing quote); measured 223,160 characters. Counting rule: one measurement per thinking-block signature string. Re-run any narration count after changing the decode window, since the difference shows up only as a lower narration total.",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) rg -o '\"signature\":\"[^\"]*\"' <one transcript> | awk '{print length}' | sort -n | tail -1   (b) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording, for every narration signature, the byte offset of the tag payload inside the DECODED buffer as a fraction of the decoded length   (c) the same decoder run on character prefixes (10/25/50/75/90/99/100%, truncated to a multiple of 4) of the longest narration signature",
          "observed": "(a) the longest `\"signature\":\"...\"` match in the one file holding it is 223,174 characters; minus the 14-character `\"signature\":\"` + closing-quote wrapper that is 223,160 base64 characters (167,368 decoded bytes). That maximal signature is `thinking`-tagged; the longest NARRATION signature in the whole corpus is only 3,536 characters. (b) over 3,103 narration signatures the tag payload's byte offset inside the decoded buffer is min 0.57%, median 21.6%, max 42.4% of the decoded length - it is never in the last 10%. (c) prefixes of the longest narration signature at 10, 25, 50, 75, 90 and 99% of its characters ALL decode to no tag; only the full 3,536-character string decodes to `narration`.",
          "rule": "One measurement per thinking-block signature string. Offset rule: index of the tag bytes in the base64-decoded buffer divided by that buffer's length. Prefix rule: truncate the base64 string to floor(n*frac/4)*4 characters and re-run the identical 2 -> 1 -> 8 walk.",
          "note": "The operational conclusion holds and was tested directly - a windowed decode silently reports untagged with no error - but both the number and the stated mechanism needed correcting. The number is stale (200,356 -> 223,160; the corpus is live). The mechanism sentence `the tag sits at the END of the decoded structure` is refutable: the tag is at roughly the first quarter of the decoded bytes and never in the last tenth. The real reason a prefix decode fails is that the scanner treats any truncation as malformed and aborts to untagged, which is what the prefix ladder shows. Note also that the 200K-class signatures are reasoning blocks, not narration ones, so the cost the alignment gate avoids is dominated by signatures that can never be narration."
        }
      ]
    },
    {
      "id": "NAR-007",
      "area": "narration",
      "behavior": "The base64 alignment of the tag bytes MIXES inside one transcript: the token `narration` appears at all three stream offsets (0, 1, 2 mod 3), encoded as `bmFycmF0aW9u`, `5hcnJhdGlv` or `uYXJyYXRp`, so no single-alignment substring test is sound.",
      "depends": "csift gates the expensive full decode behind all three alignment needles - a microsecond substring check instead of decoding a 200 KB signature on every thinking block of every candidate record; the ungated form cost about 20% more wall time on a 685 MB transcript, and dropping any one needle loses tagged blocks.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "38-46",
          "snippet": "/// The three base64 alignments of the tag bytes (`narration` at stream offset 0/1/2\n/// mod 3). A signature containing NONE of them cannot decode to `narration`, so the\n/// hot path skips the full decode - a µs substring check instead of decoding a 200KB\n/// signature (classify runs on every thinking block of every candidate record; the\n/// ungated form cost ~+20% wall on a 685MB transcript). Verified sound on 13,957 real\n/// signatures (alignment MIXES within one file - all three needles are required).\n/// Whitespace inside the base64 (tolerated by the decoder, never observed in real\n/// signatures) would break needle adjacency, so it conservatively re-opens the gate.\nconst NARRATION_B64_ALIGNMENTS: [&str; 3] = [\"bmFycmF0aW9u\", \"5hcnJhdGlv\", \"uYXJyYXRp\"];"
        },
        {
          "path": "src/model/narration.rs",
          "lines": "51-56",
          "snippet": "pub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n    let Some(sig) = signature else {\n        return Class::AgentThinking;\n    };\n    let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n        || sig.bytes().any(|b| b.is_ascii_whitespace());"
        }
      ],
      "instrument": "Count each needle separately over one transcript (`rg -c 'bmFycmF0aW9u' <transcript>` and the same for `5hcnJhdGlv` and `uYXJyYXRp`): more than one alignment fires inside a single file. Soundness check: for every real signature run both the full 2 -> 1 -> 8 decode and the three-needle gate and assert the gate never closes on a signature the decode tags `narration` - asserted over 13,957 real signatures. Counting rule: one classification per signature. Removing the gate must leave output byte-identical while costing roughly 20% more wall time on a 685 MB transcript, same query and warm cache.",
      "located": {
        "claude_code": "2.1.241",
        "csift": "0.9.2",
        "source": "SPEC.md v0.9.2 ledger; AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs alignment comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) for n in bmFycmF0aW9u 5hcnJhdGlv uYXJyYXRp; do rg -c \"$n\" <one transcript>; done   (b) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally recording which of the three needles each signature contains, and asserting that no signature the decode tags `narration` lacks all three",
          "observed": "(a) inside ONE 88,275,010-byte transcript all three alignments occur: bmFycmF0aW9u on 179 lines, 5hcnJhdGlv on 13, uYXJyYXRp on 340. (b) corpus-wide the three needles fire 2,664 (uYXJyYXRp) + 287 (bmFycmF0aW9u) + 152 (5hcnJhdGlv) = 3,103 times, exactly the narration total; 10 files carry more than one alignment and 2 files carry all three. Soundness: 0 of 3,103 narration-tagged signatures lack all three needles, so the gate never closes on a tagged block. Precision is also perfect here: 0 of the 88,801 `thinking`-tagged signatures contain any needle.",
          "rule": "One classification per signature. Gate-soundness rule: count signatures whose full 2 -> 1 -> 8 decode reads `narration` while none of the three needles is a substring of the base64 - the claim is refuted by any non-zero count. Mixing rule: a file counts as mixed when two or more distinct needles occur in it.",
          "note": "The behavior claim - all three alignments occur, and they MIX inside one transcript - is confirmed with the claim's own per-needle rg instrument, so no single-alignment substring test is sound. Two caveats on the `depends` clause, which I did not re-measure. First, the `~+20% wall on a 685MB transcript` figure was not reproduced: it needs an A/B of two release builds differing only in the gate, warm cache, same query, and the largest single transcript on this machine is now 720,932,882 bytes, so the original file has grown and the number would not be comparable anyway. Second, the gate's PRECISION (zero false opens today) means the measured saving is close to the maximum achievable, which is consistent with the claim's direction."
        }
      ]
    },
    {
      "id": "NAR-008",
      "area": "narration",
      "behavior": "Real signatures carry no embedded whitespace inside the base64, though the decoder accepts it; whitespace would split the tag bytes and break the adjacency the alignment needles depend on.",
      "depends": "csift's gate opens on any needle hit OR on any ASCII whitespace byte in the signature, so a hypothetical whitespace-bearing signature falls through to the full decode instead of being silently skipped - the gate is conservative in the direction that cannot lose a tagged block.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "44-46",
          "snippet": "/// Whitespace inside the base64 (tolerated by the decoder, never observed in real\n/// signatures) would break needle adjacency, so it conservatively re-opens the gate.\nconst NARRATION_B64_ALIGNMENTS: [&str; 3] = [\"bmFycmF0aW9u\", \"5hcnJhdGlv\", \"uYXJyYXRp\"];"
        },
        {
          "path": "src/model/narration.rs",
          "lines": "55-56",
          "snippet": "    let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n        || sig.bytes().any(|b| b.is_ascii_whitespace());"
        }
      ],
      "instrument": "Extract every signature string from a transcript and scan each for an ASCII whitespace byte: zero in the measured corpus. Counting rule: one per signature string. The gate's whitespace arm is the fallback that keeps the needle check sound if that ever changes.",
      "located": {
        "claude_code": "2.1.241",
        "csift": "0.9.2",
        "source": "AGENTS.md section 3.3a; CHANGELOG 0.9.2; src/model/narration.rs alignment comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally testing `any(c.isspace() for c in signature)` on every extracted signature string",
          "observed": "0 of 91,904 signature strings contain an ASCII whitespace character. Block shapes are uniform: 0 thinking blocks have an absent `signature` key and 35 have an empty-string one, and every non-empty signature is unbroken base64.",
          "rule": "One test per signature string, over every thinking block under ~/.claude/projects; the claim is refuted by any signature containing a whitespace byte.",
          "note": "Holds. The conservative arm is present in the code as claimed (`|| sig.bytes().any(|b| b.is_ascii_whitespace())` re-opens the gate), so a whitespace-bearing signature would fall through to the full decode instead of being skipped. Worth recording that the arm is currently dead in the measured sense - it never fires - which is the point: it is insurance against a serializer change, not a live path. A transcript rewritten by a JSON pretty-printer would be the case that exercises it."
        }
      ]
    },
    {
      "id": "NAR-009",
      "area": "narration",
      "behavior": "About 5.7% of narration blocks (176 of 3,103 measured) have no reasoning sibling in the same assistant message at all, so a narration block cannot be identified by adjacency to a reasoning block. Adjacency is worse than the figure suggests, because Claude Code writes one jsonl record per content block: measured per RECORD rather than per message id, 100% of narration blocks look sibling-less.",
      "depends": "csift classifies from the block's OWN signature and never from position; an adjacency heuristic mislabels the sibling-less population, and a record holding both kinds splits per BLOCK rather than per record.",
      "code": [
        {
          "path": "src/model/narration.rs",
          "lines": "10-11",
          "snippet": "//! Classification is by SIGNATURE ONLY - a narration block can lack a reasoning sibling\n//! entirely (~4% measured), so adjacency is never a shortcut. Signatures reach 200K+"
        },
        {
          "path": "src/search/hits.rs",
          "lines": "373-375",
          "snippet": "                    // Signature-only split (narration vs reasoning); adjacency is never\n                    // consulted, and a mixed multi-block record splits per BLOCK.\n                    let class = crate::model::thinking_block_class(signature.as_deref());"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "206-208",
          "snippet": "                Block::Thinking { signature, .. } => {\n                    push_unique(out, thinking_block_class(signature.as_deref()));\n                }"
        }
      ],
      "instrument": "`csift search '' @<session> -t agent.thinking.narration --format json | jq -r .line` names each narration record; for each, `csift show @<session> --line <n> --raw | jq '[.message.content[].type]'` shows the blocks on THAT RECORD, which is not the same question - group records by `message.id` first. Counting rule: narration blocks whose assistant message carries no thinking block of any other kind, over all narration blocks; measured 5.67% (176/3,103).",
      "located": {
        "claude_code": "2.1.170",
        "csift": "0.9.2",
        "source": "SPEC.md section 5.1; SPEC.md section 6 v0.9.2 ledger item 1; AGENTS.md section 3.3a; src/model/narration.rs module doc"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 narr_usage.py ~/.claude/projects  # groups every thinking-bearing record by message.id within one transcript, decodes each block's tag, and counts narration messages whose block set contains no `thinking`-tagged block, no unsigned thinking block and no redacted_thinking block",
          "observed": "176 of 3,103 narration blocks (5.67%) sit in an assistant message with NO other thinking block of any kind - not ~4%. The other 2,927 (94.3%) share a message with exactly one reasoning block, and that message is written as 2 separate jsonl records. Because of that per-block record split, a per-RECORD adjacency test is not merely unreliable but usually wrong: run per record instead of per message.id over one project directory, it reported 996 of 996 narration blocks (100%) as sibling-less.",
          "rule": "Group by (transcript, message.id); a narration block counts as sibling-less when its message's thinking-block set contains no block that decodes to `thinking`, no thinking block with an empty signature, and no redacted_thinking block. Denominator = all narration blocks.",
          "note": "Holds in substance - adjacency is never a shortcut - but the number and the unit both needed fixing. 4% -> 5.67%, and `in the same assistant message` has to be spelled out as `grouped by message.id`, because the obvious reading (same jsonl record) gives 100% and would make the claim look far stronger than it is. The corrected figure is the one that stresses csift's design: even at the message level, one narration block in eighteen has nothing to be adjacent to."
        }
      ]
    },
    {
      "id": "NAR-010",
      "area": "narration",
      "behavior": "Because the tag is base64-hidden inside `signature`, a narration record's raw jsonl bytes never spell `narration` as a consequence of BEING narration - the tag appears only as one of three base64 alignments. The record's own thinking text can still contain the words incidentally: measured 20 of 3,103 narration records (0.64%) carry `narration` or `summary` somewhere outside the signature. In a transcript holding 313 narration records and no such prose, `rg -c narration` returns 0.",
      "depends": "csift renders `[narration summary]` in the display LABEL zone only, never as matchable text - matchable text that is not a byte substring of the raw line breaks the search prefilter laws, so `csift search narration -t agent.thinking.narration` would return zero while rows visibly rendered the marker. The same display-only status is why the fixed `--siblings` policy caps narration at 1 per turn (one summary suffices).",
      "code": [
        {
          "path": "src/search/render.rs",
          "lines": "42-45",
          "snippet": "    // A narration record is an API summary, never the model's reasoning - the marker\n    // rides the LABEL zone (display-only, like [error]); matchable text stays verbatim.\n    if h.class == Class::AgentThinkingNarration {\n        return format!(\"{} [narration summary]\", h.class.path());"
        },
        {
          "path": "src/search/turns_match.rs",
          "lines": "239-241",
          "snippet": "    if class == Class::AgentThinkingNarration {\n        return Some(1);\n    }"
        }
      ],
      "instrument": "`rg -cNI 'narration' ~/.claude/projects -g '*.jsonl'` finds only prose mentions, never a signature-derived one, while `csift search '' <project-dir> --count-by label | rg narration` reports the real leaf count; the two numbers are unrelated by construction. Counting rule: lines for the `rg`, records per leaf for the census.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.2",
        "source": "AGENTS.md section 3.3a; dev session measurement 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) rg -c 'narration' <one transcript holding 313 narration records>   (b) csift search 'narration' @<session> -t agent.thinking.narration --no-subagents -c; csift search '\\[narration summary\\]' @<session> --no-subagents -c; csift search 'narration' @<session> --no-subagents -c   (c) csift search '' @<session> -t agent.thinking.narration --no-subagents --max-count 2   (d) python3 narr_scan.py ~/.claude/projects  # ~120-line script: for every *.jsonl under the projects root, json-parse each line containing the byte string '\"signature\"', take every message.content block with type==\"thinking\", base64-decode its signature, then protobuf-scan the bytes taking the LAST wire-type-2 field numbered 2, then 1, then 8 inside that, and UTF-8-decode the payload, additionally testing each narration record's RAW line for the byte strings narration / Narration / summary / Summary",
          "observed": "(a) 0 lines match in a transcript that carries 313 narration records. (b) all three searches return 0, including the literal marker text. (c) every rendered hit prints `agent.thinking.narration [narration summary]` in the label zone, so the marker is visible while matching nothing. (d) corpus-wide, however, 20 of 3,103 narration records (0.64%) DO contain one of those words in the raw line - `summary` 10, `Summary` 1, `narration` 9 - always in the record's own prose, never from the signature (the tag's only byte form in the line is one of the three base64 alignments).",
          "rule": "Lines for the rg; matched exchanges for `csift search -c`; records for the label census. Word rule: case-sensitive byte search of the whole raw jsonl line of each narration record, with the `\"signature\":\"...\"` values blanked out first to prove the hit is not signature-derived.",
          "note": "The operational point holds exactly as designed and was verified three ways: the rendered `[narration summary]` marker matches nothing, so the display-only rule is real and the search prefilter laws are intact. Refined because the absolute wording - `no substring of the line spells narration or summary` - is refutable at 0.64% of narration records, and the counterexamples come from sessions that discuss the feature (this repository's own dev transcripts). The correct statement is about provenance: nothing signature-derived is matchable; incidental prose is."
        }
      ]
    },
    {
      "id": "NAR-011",
      "area": "narration",
      "behavior": "Narration-tagged thinking blocks do occur in subagent transcripts, but are orders of magnitude rarer there than in top-level ones: in one measured corpus, 24 narration records across 5 of ~7,543 subagent transcripts (0.07% of files) versus 3,079 across 22 of 66 top-level transcripts (33% of files). Occurrence is bursty and not lane- or model-gated - all five carrying transcripts belong to a single parent session, whose 10 other subagent transcripts run the same model id and carry none. Both built-in and teammate subagent shapes are attested.",
      "depends": "csift runs the identical signature classifier on every transcript it reads, subagent files included, so the rarity is a property of the data rather than a scope gap: the subagent-resident narration records are counted with no code change, and on any spanning surface the spanned-minus-restricted difference is exactly the subagent-resident count.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "289-291",
          "snippet": "                if let Block::Thinking { signature, .. } = b {\n                    match crate::model::thinking_signature_tag(signature.as_deref()).as_deref() {\n                        Some(crate::model::NARRATION_TAG) => {"
        },
        {
          "path": "src/model/narration.rs",
          "lines": "48-59",
          "snippet": "/// The leaf for a thinking block: `agent.thinking.narration` iff the signature tag reads\n/// exactly `narration`; every other outcome (tag `thinking`, an unknown tag, no tag)\n/// stays `agent.thinking`.\npub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n    let Some(sig) = signature else {\n        return Class::AgentThinking;\n    };\n    let gate_open = NARRATION_B64_ALIGNMENTS.iter().any(|n| sig.contains(n))\n        || sig.bytes().any(|b| b.is_ascii_whitespace());\n    if !gate_open {\n        return Class::AgentThinking;\n    }"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "206-208",
          "snippet": "                Block::Thinking { signature, .. } => {\n                    push_unique(out, thinking_block_class(signature.as_deref()));\n                }"
        }
      ],
      "instrument": "`csift search '' -t agent.thinking.narration --count-by label` run with and again with `--no-subagents`: the difference is the subagent-resident narration record count (24 here, not 0). Cross-check per transcript with `--format json` and a count of hits whose `is_subagent` is true, and independently of csift by base64-decoding each thinking block's signature and reading protobuf path 2 -> 1 -> 8. Counting rule: one per record per leaf; subagent transcripts are the files under `<uuid>/subagents/` inside each project directory, excluding journal.jsonl.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.2",
        "source": "dev session measurement 2026-09-01"
      },
      "first_seen_claude_code": "2.1.251 (the client version stamped on all 24 attested subagent narration records)",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "1) corpus census, subagents spanned vs restricted:\n   csift search '' -t agent.thinking.narration --count-by label\n   csift search '' -t agent.thinking.narration --count-by label --no-subagents\n2) per-transcript split of the same scan:\n   csift search '' -t agent.thinking.narration --format json \\\n     | python3 -c \"import sys,json;h=[x for l in sys.stdin for x in json.loads(l).get('hits',[])];\\\nprint(len(h), sum(1 for x in h if x['is_subagent']), len({x['session_id'] for x in h if x['is_subagent']}))\"\n3) independent signature decode (does not use csift): a 40-line python protobuf reader\n   walking base64(signature) -> last length-delimited field 2 -> 1 -> 8 -> utf-8, run over\n   the subagent transcripts of the one session that has any:\n   python3 nardec.py ~/.claude/projects/<project-dir>/<session-uuid>/subagents/agent-*.jsonl\n4) independent byte prefilter on the same files (the three base64 alignments of the tag):\n   rg -c 'bmFycmF0aW9u|5hcnJhdGlv|uYXJyYXRp' <each subagent transcript>\n5) the code site's own counter:\n   csift stats @<session-prefix> --format json   (and again with --no-subagents)\n6) corpus shape:\n   find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' ! -name journal.jsonl | wc -l\n   find ~/.claude/projects -maxdepth 2 -name '*.jsonl' | wc -l\n7) code site check:\n   awk 'NR>=289 && NR<=291' src/stats.rs",
          "observed": "(1) 3103 records under agent.thinking.narration with subagents spanned; 3079 with --no-subagents. Difference = 24 narration records living in subagent transcripts, so the two figures are NOT equal. (2) 3103 hits total, 24 with is_subagent true, spread over 5 distinct subagent transcripts under a single parent session; the remaining 3079 sit in 22 top-level transcripts. Hit timestamps on the subagent side run 2026-09-01T01:07 to 2026-09-01T18:24 local. (3) independent decoder over the 15 subagent transcripts of that session: tag census {'thinking': 187, 'narration': 24}; every narration-tagged record carries version 2.1.251. Per file: 2, 2, 6, 1 and 13 narration blocks in five transcripts, zero in the other ten. Four of the five are bare built-in subagent ids, one is a name-embedded teammate id. (4) rg needle counts on those same five files: 2, 2, 6, 1, 13 = 24, matching the decoder line for line. (5) csift stats on that parent session: narration_blocks total 30 with subagents spanned (16 sessions in scope) vs 6 with --no-subagents (1 session) -> 24 in subagent transcripts, matching (1) and (3). (6) 7543 subagent transcripts and 66 top-level transcripts in the corpus at measurement time (the live corpus grew by 2 subagent transcripts during the ten minutes of this verification, so treat 7.5k as the order of magnitude, not a fixed constant). (7) src/stats.rs lines 289-291 still read verbatim: 'if let Block::Thinking { signature, .. } = b {' / 'match crate::model::thinking_signature_tag(signature.as_deref()).as_deref() {' / 'Some(crate::model::NARRATION_TAG) => {'. Binary corroboration: strings -n 6 on Claude Code 2.1.258 yields the standalone symbols 'narrationBlockCount', 'narration_generate' and 'narration_classifier_error', i.e. narration is first-class machinery in the current client.",
          "rule": "A subagent transcript is a *.jsonl under <session-uuid>/subagents/ inside a project directory, excluding journal.jsonl (the claim's own rule). A narration record is one transcript record carrying at least one thinking block whose signature decodes, via protobuf field path 2 -> 1 -> 8 taking the LAST length-delimited field at each level, to the exact string 'narration'. csift search --count-by label counts one per record per leaf; csift stats and the independent decoder count one per BLOCK - the two agree here at 24 because every narration record in this set carries exactly one narration block. The subagent figure is derived as (spanned count) - (--no-subagents count) and cross-checked directly by counting hits whose is_subagent field is true; both give 24.",
          "note": "REFUTED, and the refutation is triple-instrumented. Claude Code does write narration-tagged thinking blocks into subagent transcripts: 24 of them sit in 5 subagent transcripts of the live corpus, and the spanned-versus-restricted census that the claim cites as proof of equality now differs by exactly that 24 (3103 vs 3079). The blocks are genuine, not a csift artifact - a protobuf decoder written independently of csift finds the same 24 signatures tagged 'narration' in the same five files, and a raw byte grep for the three base64 alignments of the tag returns the same 2/2/6/1/13 per-file split.\n\nNew behavior as measured: narration in subagent lanes is real but sporadic and bursty. It is concentrated - all 5 carrying transcripts belong to ONE parent session, while 10 sibling subagent transcripts of that same session, running the same single model id, carry only 'thinking'-tagged signatures. So it is not gated on the subagent lane and not gated on the model; within one session some subagent turns get a narration block and others do not. Both on-disk shapes are represented: four bare built-in subagent transcripts and one name-embedded teammate transcript. Base rates for a stranger: 24 narration records across ~7.5k subagent transcripts (5 transcripts, 0.07% of files) versus 3079 across 66 top-level transcripts (22 transcripts, 33% of files) - three orders of magnitude rarer per file, which is a plausible reason the original scan read as a clean zero.\n\nHonesty about WHY it drifted: every one of the 24 records carries client version 2.1.251, not 2.1.258, and all five files were fully written on 2026-09-01 - the same date the claim records as its measurement. So these records were already on disk when the claim was made, and this is most likely a scope gap in the original measurement rather than a behavior change between 2.1.251 and 2.1.258. Either reading leaves the claim's assertion false against the current corpus, which is why the verdict is drifted rather than refined: the claim's entire content is an absence, and the absence does not hold.\n\nWhat survives intact is the claim's 'depends' clause, and it is now positively demonstrated rather than merely argued: csift runs the identical signature classifier on subagent files, so the subagent narration was counted with no code change - src/stats.rs:289-291 is unmoved and its counter reports 30 blocks spanned vs 6 restricted on the affected session. The ledger entry should be rewritten from an absence claim into a rarity claim with the per-file base rates above."
        }
      ]
    },
    {
      "id": "PATH-001",
      "area": "path-encoding",
      "behavior": "Claude Code derives the `<ENCODED>` project-directory basename from the session's canonical start cwd by NFC-normalizing it and replacing every UTF-16 CODE UNIT outside [A-Za-z0-9] with exactly one `-`, position-preserving: no collapsing of consecutive dashes and no case folding, so `.`, `/`, `_` and space each become one dash, an NFD-spelled accent recomposes to a single unit (one dash), and an astral character (two surrogate units) yields TWO dashes. Since 2.1.234 this is the DEFAULT rather than the only path: the basename is `CLAUDE_CODE_PROJECT_DIR_NAME ?? sanitize(cwd)`, and the override is honored only when `CLAUDE_CONFIG_DIR` is also set and the value matches /^[A-Za-z0-9_-]{1,64}$/ and is not a Windows reserved device name.",
      "depends": "`path::encode_cwd` reproduces the transform unit-wise to locate the project directory; a char-wise or byte-wise replacement diverges on any non-ASCII cwd, so every subcommand that takes a real path or `.` as its target silently scans the wrong project or none.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "5-13",
          "snippet": "/// Encode an absolute cwd to its Claude Code project-dir basename - the EXACT transform\n/// CC applies (extracted verbatim from the 2.1.228 binary): the cwd is first\n/// NFC-normalized (every CC path rides `.normalize(\"NFC\")` - a macOS filesystem hands out\n/// NFD, so an accented path is re-composed before encoding), then the JS regex\n/// `replace(/[^a-zA-Z0-9]/g,\"-\")` runs per UTF-16 CODE UNIT: every unit outside ASCII\n/// alphanumerics becomes ONE `-` - so an astral char (two surrogate units) yields TWO\n/// dashes, and a Windows `C:\\Users\\x` yields `C--Users-x` (`:` and `\\` are one unit\n/// each). No dash collapsing, no case folding. A char-wise or byte-wise replacement\n/// DIVERGES from CC on any non-ASCII cwd and resolves the wrong dir."
        },
        {
          "path": "src/path/home.rs",
          "lines": "15-27",
          "snippet": "pub fn encode_cwd(cwd: &Path) -> String {\n    use unicode_normalization::UnicodeNormalization;\n    let s = cwd.to_string_lossy();\n    let s: String = s.nfc().collect();\n    let mut out = String::with_capacity(s.len());\n    for unit in s.encode_utf16() {\n        match u8::try_from(unit) {\n            Ok(b) if b.is_ascii_alphanumeric() => out.push(char::from(b)),\n            _ => out.push('-'),\n        }\n    }\n    out\n}"
        },
        {
          "path": "src/path/tests/encode.rs",
          "lines": "97-103",
          "snippet": "    // An astral char is TWO surrogate units → TWO dashes (the JS regex's view).\n    assert_eq!(encode_cwd(Path::new(\"/tmp/x\\u{1D11E}y\")), \"-tmp-x--y\");\n    // A Windows cwd: the drive colon and each backslash are one dash → `C--…`.\n    assert_eq!(\n        encode_cwd(Path::new(r\"C:\\Users\\dev\\proj\")),\n        \"C--Users-dev-proj\"\n    );"
        }
      ],
      "instrument": "Pick any directory under `~/.claude/projects/`, read the `cwd` of the first record of one contained `*.jsonl` (`head -1 <file> | jq -r .cwd`) and apply the transform by hand: the result must equal the directory basename character for character. Counting rule: one output dash per UTF-16 code unit that is not ASCII-alphanumeric, so an astral character contributes two dashes and an NFC-composed accent one. The unit test `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` pins NFC/NFD parity, the astral double dash and the Windows shape; to re-derive from the harness, run `strings` over the shipped Claude Code binary and grep for the literal replacement regex `[^a-zA-Z0-9]` and for `normalize(\"NFC\")`, expecting one sanitizer and no case fold.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.3",
        "source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 3.1; CHANGELOG 0.7.3; src/path/home.rs encode_cwd doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{60}CLAUDE_CODE_PROJECT_DIR_NAME.{700}' ; node -e on the extracted regex; python3 pass over ~/.claude/projects comparing each directory basename against the encoding of the first cwd found in its top-level transcripts",
          "observed": "Binary carries the sanitizer verbatim: `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}` and `function KA(e){let n=k(e);if(n.length<=IL)return n;return `${n.slice(0,IL)}-${be(e)}`}`. But the project key is now `function Em(e){return Apr()??KA(e)}` with `var Apr=Zo(()=>s()?sLn(_()):void 0,I)`, `function _(){return process.env.CLAUDE_CODE_PROJECT_DIR_NAME}`, `function s(){return process.env.CLAUDE_CONFIG_DIR}`, validator `D=/^[A-Za-z0-9_-]{1,64}$/` plus reserved-name reject `C=/^(?:con|prn|aux|nul|com[0-9]|lpt[0-9])$/i`. The binary's own changelog text reads: '## 2.1.234 - Added the optional `CLAUDE_CODE_PROJECT_DIR_NAME` environment variable: hosts that give each session its own config directory can choose a short name for the per-project transcript directory'. Executing the extracted regex: '/tmp/x\\u{1D11E}y' => '-tmp-x--y' (astral char = TWO dashes); NFD '/tmp/cafe\\u0301' => '-tmp-cafe-' (10 chars) vs NFC '/tmp/caf\\u00e9' => '-tmp-caf-' (9 chars), equal only after .normalize('NFC'). Live corpus: 12 of 12 project directories holding top-level transcripts had at least one transcript whose first recorded cwd encodes byte-for-byte to the basename; 0 exceptions.",
          "rule": "One output dash per UTF-16 code unit outside [A-Za-z0-9], position-preserving (no collapsing, no case fold). A directory counts as confirming iff some top-level transcript's first recorded cwd, NFC-normalized and transformed unit-wise, equals the basename character for character. Denominator = project directories holding at least one top-level *.jsonl.",
          "note": "The transform itself is unchanged and confirmed both in the binary and by executing the extracted regex. The refinement is an added escape hatch introduced in 2.1.234, after this claim's 2.1.228 baseline: when a host sets CLAUDE_CONFIG_DIR, it may also set CLAUDE_CODE_PROJECT_DIR_NAME to a short literal basename, and the cwd is then not encoded at all. csift's encode_cwd has no counterpart, so under both env vars every path-taking target would resolve to a directory that does not exist while the real transcripts sit under the literal name. Not reachable on this machine (neither variable is set here); the gap is a resolution miss, not a wrong-directory read, because resolution is fail-closed."
        }
      ]
    },
    {
      "id": "PATH-002",
      "area": "path-encoding",
      "behavior": "Claude Code encodes the CANONICAL form of the cwd - symlinks resolved, then Unicode-NFC - rather than the raw string it was given, so two spellings of the same directory (a symlinked path and its real path, an NFD and an NFC spelling) converge on ONE projects directory.",
      "depends": "csift's `absolutize` canonicalizes and `encode_cwd` applies NFC before encoding; without both, a symlinked or NFD-spelled target argument encodes to a directory name that does not exist on disk and resolution fails. csift canonicalizes realpath only, so a non-ASCII NFC-vs-NFD mismatch between a STORED `cwd` string and the target is the one accepted comparison edge.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "6-8",
          "snippet": "/// CC applies (extracted verbatim from the 2.1.228 binary): the cwd is first\n/// NFC-normalized (every CC path rides `.normalize(\"NFC\")` - a macOS filesystem hands out\n/// NFD, so an accented path is re-composed before encoding), then the JS regex"
        },
        {
          "path": "src/path/home.rs",
          "lines": "182-189",
          "snippet": "/// Absolutize a real filesystem path WITHOUT requiring it to exist (the project\n/// the cwd points at may have been deleted while its transcripts remain). We\n/// canonicalize when possible to resolve symlinks/`..`, else fall back to\n/// joining with the current dir + lexical normalization.\npub fn absolutize(p: &Path) -> Result<PathBuf> {\n    if let Ok(c) = std::fs::canonicalize(p) {\n        return Ok(c);\n    }"
        },
        {
          "path": "src/path/project_dirs.rs",
          "lines": "138-141",
          "snippet": "/// Whether a stored `cwd` string denotes the same directory as `want` (the canonical\n/// target). Trailing-slash tolerant. Exact for the ASCII paths that dominate; a non-ASCII\n/// NFC-vs-NFD mismatch is the one accepted edge (CC stores realpath+NFC, csift realpath only).\npub(crate) fn cwd_equivalent(stored: &str, want: &Path) -> bool {"
        }
      ],
      "instrument": "On a filesystem that hands out NFD (macOS), create a directory whose name carries a decomposed accent, start a session in it, then compare a listing of `~/.claude/projects` against `csift list <that path>` given each spelling: exactly one directory must exist and both spellings must resolve to it. Counting rule: one directory per distinct canonical cwd.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.3",
        "source": "SPEC.md section 2.1; src/path/home.rs absolutize doc; src/path/project_dirs.rs cwd_equivalent doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]{0,120}\\}from\"fs\";import\\{cwd as c\\}from\"process\";function o\\(n\\)\\{return n\\.normalize\\(\"NFC\"\\)\\}function uLn' ; csift --claude-home $FIX list <real dir> vs list <symlink to it> --no-subagents --format json",
          "observed": "Binary: `import{realpathSync as r}from\"fs\";import{cwd as c}from\"process\";function o(n){return n.normalize(\"NFC\")}function uLn(){let n=\"\";if(typeof process<\"u\"&&typeof process.cwd===\"function\"&&typeof r===\"function\")try{let e=c();try{n=o(r(e))}catch{n=o(e)}}catch{}return n}` followed by `var Lpr=uLn()` and `function dn(e=uLn()){return v7t({...project:{originalCwd:e,projectRoot:e,cwd:e}})}`. So the recorded start cwd is NFC(realpathSync(process.cwd())), with NFC(process.cwd()) as the throw-fallback. Fixture: the symlink spelling and the real spelling encode to DIFFERENT strings (equality check printed False), yet `csift list` returned 1 session row for each, from the one directory named after the real path's encoding. Executing the extracted regex, an NFD and an NFC spelling of the same accented directory converge to the identical basename after .normalize('NFC') (both '-tmp-caf-').",
          "rule": "One project directory per distinct canonical cwd. Both spellings of one directory must return the same non-zero row count and resolve to the same on-disk basename; row count = lines whose envelope kind is 'session'.",
          "note": "Both halves confirmed: symlinks are resolved by realpathSync and the result is NFC-normalized before it is ever encoded, so a symlinked spelling and an NFD spelling converge on one directory. The one nuance worth carrying: realpath resolution is best-effort - if realpathSync throws, Claude Code falls back to the raw process.cwd() under NFC only, so a cwd that cannot be resolved is stored un-canonicalized. The live corpus cannot discriminate on its own (27 of 27 still-existing recorded cwd values already equal their realpath and 31 of 31 are already NFC, with zero non-ASCII and zero symlinked cwds), which is why the binary and the fixture carry this verdict."
        }
      ]
    },
    {
      "id": "PATH-003",
      "area": "path-encoding",
      "behavior": "Because the sanitizer does no slash, colon or case pre-processing, a Windows cwd encodes to a LETTER-led directory name - `C:\\Users\\x` becomes `C--Users-x` (the drive letter passes through, the `:` and each `\\` are one unit each) - while a UNC root `\\\\server\\share` becomes the double-dash-led `--server-share`; a Unix cwd is always single-dash-led.",
      "depends": "csift's `@`-token grammar admits both encoded shapes: a leading `-` token and the `<letter>--...` drive shape are project-dir tokens (a drive-shaped token that matches no directory falls through to real-path resolution, since it can also be a relative path), and a UNC-encoded directory is targeted as `@--server-...` because a bare `--`-leading token is reserved for the mistyped-flag guard; without the drive-shape arm a Windows project directory is unreachable.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "135-145",
          "snippet": "/// True iff `token` is a plausible pre-encoded projects-dir basename, per §2.3 step 1:\n/// only `[A-Za-z0-9-]` (so no `/`), and one of the two shapes CC's encoder can emit for\n/// an absolute path - a Unix cwd leads with `-` (the leading `/` encodes to `-`; a UNC\n/// `\\\\server\\…` leads with `--`), a WINDOWS cwd leads with `<drive-letter>--` (the `:`\n/// and `\\` of `C:\\` each encode to `-`, verbatim from the 2.1.228 binary's sanitizer).\npub(crate) fn looks_like_encoded_token(token: &str) -> bool {\n    if !token.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {\n        return false;\n    }\n    token.starts_with('-') || is_drive_encoded_token(token)\n}"
        },
        {
          "path": "src/path/home.rs",
          "lines": "147-154",
          "snippet": "/// The Windows drive-letter encoded shape: `<letter>--…` (`C:\\Users\\x` → `C--Users-x`).\n/// Distinct from every other token class: a Unix encoded dir leads with `-`, an id is\n/// hex/uuid-shaped, and a real RELATIVE path named like this is disambiguated by the\n/// caller (encoded-dir lookup first, real-path fallthrough on a miss).\npub(crate) fn is_drive_encoded_token(token: &str) -> bool {\n    let b = token.as_bytes();\n    b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b'-' && b[2] == b'-'\n}"
        },
        {
          "path": "src/path/tests/encode.rs",
          "lines": "40-113",
          "snippet": "#[test]\nfn encoded_token_shapes_unix_windows_unc() {\n    assert!(looks_like_encoded_token(\"-Users-dev-example-project\"));\n    assert!(looks_like_encoded_token(\"C--Users-dev-proj\")); // Windows drive shape\n    assert!(looks_like_encoded_token(\"--server-share-proj\")); // UNC (leads with --)\n    assert!(!looks_like_encoded_token(\"Users-dev\")); // no encoded lead-in\n    assert!(!looks_like_encoded_token(\"C--Users/dev\")); // a separator disqualifies\n    assert!(!looks_like_encoded_token(\"C-Users-dev\")); // one dash after the drive ≠ `C:\\`"
        }
      ],
      "instrument": "The unit test `encoded_token_shapes_unix_windows_unc` pins all three lead-in shapes, and `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` pins the drive encoding itself; end-to-end, `csift list '@C--Users-x'` against a fixture tree built with `--claude-home` must resolve the token as an encoded project dir rather than reject it as a mistyped flag. Counting rule: one dash per non-alphanumeric UTF-16 unit, so `C:\\` yields `C--`. Standing caveat: a scan of the development corpus found ZERO drive-letter-led project directories, so the on-disk shape is derived from the sanitizer, not observed - only a real Windows session settles it.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.3",
        "source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 3.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "node on the sanitizer extracted verbatim from the binary (`const k=(e)=>e.replace(/[^a-zA-Z0-9]/g,\"-\")`) applied to a Windows drive path, a UNC root and a Unix path; csift --claude-home $FIX list '@C--Users-x' / '@--server-share-proj' / bare '--server-share-proj'; ls ~/.claude/projects | awk '{print substr($0,1,1)}' | sort | uniq -c",
          "observed": "Sanitizer output: 'C:\\\\Users\\\\x' => 'C--Users-x' (len 10, letter-led); '\\\\\\\\server\\\\share\\\\proj' => '--server-share-proj' (len 19, double-dash-led); '/Users/dev/proj' => '-Users-dev-proj' (single-dash-led). The binary applies KA/k with no platform branch (the only platform switch nearby belongs to the cache-directory helper, not to the projects key). csift: '@C--Users-x' -> 1 session row; '@--server-share-proj' -> 1 session row; bare unprefixed 'C--Users-x' -> 1 session row; bare '--server-share-proj' -> clap usage error (mistyped-flag guard). Live corpus: 15 of 15 project directory basenames lead with '-'; 0 drive-shaped (letter followed by '--') and 0 UNC-shaped.",
          "rule": "One dash per non-alphanumeric UTF-16 code unit, so 'C:\\\\' yields 'C--'. Lead-in class counted as the first character of each basename under ~/.claude/projects. csift row count = envelope lines whose kind is 'session'.",
          "note": "The encoding shapes are no longer derived-only: running Claude Code's own extracted sanitizer produces the letter-led drive form and the double-dash-led UNC form directly, and csift admits both token shapes while still rejecting a bare '--'-leading token as a mistyped flag. The ledger's standing caveat survives unchanged for the on-disk half: this corpus still contains zero drive-letter-led project directories (15/15 lead with '-'), so the claim that a real Windows session actually lands in such a directory is confirmed only through the sanitizer, not by observation. A single Claude Code session run on Windows, then listing the basenames under its projects root, would settle it."
        }
      ]
    },
    {
      "id": "PATH-004",
      "area": "path-encoding",
      "behavior": "Claude Code caps the encoded project-directory basename at 200 characters (`MAX_SANITIZED_LENGTH`) and stores anything longer as `<first-200>-<hash>`, so for a deeply-nested project the full encoding of the cwd does not exist on disk at all.",
      "depends": "csift compares the encoded length against the same 200-character cap before falling back to the prefix scan; a wrong cap leaves every deeply-nested project unresolvable by `csift list .` and by every other path-taking target.",
      "code": [
        {
          "path": "src/path/project_dirs.rs",
          "lines": "73",
          "snippet": "pub(crate) const MAX_SANITIZED_LENGTH: usize = 200;"
        },
        {
          "path": "src/path/project_dirs.rs",
          "lines": "52-53",
          "snippet": "    if encoded.len() > MAX_SANITIZED_LENGTH {\n        let prefix = format!(\"{}-\", &encoded[..MAX_SANITIZED_LENGTH]);"
        }
      ],
      "instrument": "List the basenames under `~/.claude/projects` with their lengths (`ls ~/.claude/projects | awk '{ print length($0), $0 }' | sort -rn | head`): any basename longer than 200 must be exactly 200 encoded characters, then a `-`, then a hash token. Counting rule: characters of the directory basename, not bytes of the cwd.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.2.0",
        "source": "SPEC.md section 2.1; AGENTS.md section 3.1; src/path/project_dirs.rs MAX_SANITIZED_LENGTH"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{100}MAX_SANITIZED_LENGTH.{100}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; ls ~/.claude/projects | awk '{print length($0)}' | sort -rn | head -1 ; fixture: build a directory whose encoding exceeds 200 chars, mint the directory name with the binary's own formula, then csift --claude-home $FIX list <that path>",
          "observed": "Binary exports the constant: `IL as MAX_SANITIZED_LENGTH` and defines `var IL=200` immediately before `function be(e){return Math.abs(zq(e)).toString(36)}function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}function KA(e){let n=k(e);if(n.length<=IL)return n;return `${n.slice(0,IL)}-${be(e)}`}`. A second, independent sanitizer for the cache directory carries the same literal: `var v=m(\"claude-cli\"),C=200`. Live corpus: longest project-directory basename is 117 characters; 0 basenames exceed 200, so the cap is not exercised here. Fixture: a cwd encoding to 236 characters produced a key of 207 = 200-character prefix + '-' + 6-character suffix, the full 236-character encoding did not exist on disk, and csift resolved the real path to the capped directory (1 session row).",
          "rule": "Characters of the directory basename, not bytes of the cwd. A capped name must be exactly 200 encoded characters, then one '-', then a non-empty suffix.",
          "note": "The 200 cap holds and is now pinned to a named export rather than an inferred symbol. Worth recording that the corpus cannot exercise it: the deepest project here encodes to 117 characters, so csift's long-path branch is reached only under a synthetic fixture on this machine. code-site note: src/path/project_dirs.rs:72 dates the cap to a sanitizer symbol that is not present in 2.1.258; in 2.1.258 the cap is `var IL=200`, re-exported as `IL as MAX_SANITIZED_LENGTH`, and the sanitizer is the pair `k` (the regex) and `KA` (the cap plus hash suffix). The value 200 is unchanged; only the symbol name and verification date in that comment are stale."
        }
      ]
    },
    {
      "id": "PATH-005",
      "area": "path-encoding",
      "behavior": "For a cwd whose encoding exceeds 200 characters Claude Code stores the directory as `<first-200>-<hash>`, and its own project-directory lookup PREFIX-SCANS the projects root for `<first-200>-` rather than recomputing the hash to address the directory. In 2.1.258 that suffix is a single deterministic function of the original cwd string - a 31-multiplier int32 hash, absolute-valued, base-36 - so it IS reconstructible by a reader who holds the binary and the exact cwd; the reason to prefix-scan is that the harness itself prefix-scans and that the suffix input is the pre-sanitized cwd, not the recoverable directory name.",
      "depends": "csift mirrors the prefix scan (`find_dir_by_prefix`) and, among multiple matches, prefers the directory whose first session's recorded `cwd` equals the target, falling back to the sole or first match; recomputing a digest instead would resolve nothing for a deeply-nested project.",
      "code": [
        {
          "path": "src/path/project_dirs.rs",
          "lines": "75-80",
          "snippet": "/// Resolve a >200-char encoded path to its on-disk dir by prefix-scanning the projects\n/// root for `<first-200>-<hash>` (the hash is not reconstructible - see [`resolve_target`]).\n/// Among multiple matches (two paths identical for the first 200 encoded chars - vanishingly\n/// rare), prefer the dir whose first session's recorded `cwd` equals the target; otherwise\n/// fall back to the sole / first match. Returns `None` when nothing matches.\npub(crate) fn find_dir_by_prefix(root: &Path, prefix: &str, abs: &Path) -> Result<Option<PathBuf>> {"
        },
        {
          "path": "src/path/project_dirs.rs",
          "lines": "52-59",
          "snippet": "    if encoded.len() > MAX_SANITIZED_LENGTH {\n        let prefix = format!(\"{}-\", &encoded[..MAX_SANITIZED_LENGTH]);\n        if let Some(found) = find_dir_by_prefix(&root, &prefix, &abs)? {\n            return Ok(ProjectDir {\n                dir: found,\n                target_cwd: Some(abs.clone()),\n            });\n        }"
        }
      ],
      "instrument": "Create a directory whose absolute path encodes to more than 200 characters, open a session in it, then run `csift list <that path>`: it must return rows even though the directory named by the full encoding does not exist. Counting rule: one resolved directory per target; with more than one prefix match, disambiguation is by the in-record `cwd` of each candidate's first session.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.2.0",
        "source": "SPEC.md section 2.1; AGENTS.md section 3.1; src/path/project_dirs.rs find_dir_by_prefix doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{500}function KA\\(e\\)\\{let n=k\\(e\\).{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}Ctt\\(.{200}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{150}Bun\\.hash.{150}' ; fixture: reimplement the binary's suffix formula in python, mint the directory, then csift --claude-home $FIX list <deep path>",
          "observed": "The suffix formula is fully present in the binary: `function zq(t){let e=0;for(let r=0;r<t.length;r++)e=(e<<5)-e+t.charCodeAt(r)|0;return e}` and `function be(e){return Math.abs(zq(e)).toString(36)}`, consumed by `KA` as `${n.slice(0,IL)}-${be(e)}`. That is a 31-multiplier int32 hash over the ORIGINAL cwd string, absolute-valued and base-36 encoded - deterministic and reconstructible. `Bun.hash` does occur in the binary but in unrelated paths (skill content hashes, message hashing, and an xxHash64 backup-file namer); it does not appear in KA. Reconstructing be() from these bytes produced a 6-character suffix that csift's prefix scan then resolved (1 session row). The prefix scan itself is confirmed on the harness side: `let i=KA(e);if(i.length<=IL)return t;let o=ia(),...,s=u(i.slice(0,IL)+\"-\"),p=u(r);try{for(let l of <readdir>){if(!l.isDirectory()||!u(l.name).startsWith(s))continue;...}}` - Claude Code enumerates the projects root and keeps names starting with the 200-character prefix plus '-' rather than recomputing the hash to address the directory.",
          "rule": "One resolved directory per target. The suffix is 'reconstructible' iff a reader holding only the binary and the exact cwd string can compute the on-disk basename; tested by minting the directory from the reconstructed formula and requiring the consumer's prefix scan to return a non-zero row count.",
          "note": "The behavior csift depends on is intact and was confirmed twice over: the harness prefix-scans, and csift's mirror of it resolves a capped directory the full encoding does not name. What failed refutation-testing is the stated reason. Keeping the prefix scan is still correct - it is what the harness does, it costs one readdir, and it stays right if a second producer ever writes a different suffix - but the code comment should no longer claim the suffix is uncomputable, because this verification computed it. code-site note: src/path/project_dirs.rs:49-51 states 'The suffix is NOT reconstructible (the CLI uses Bun.hash, the SDK djb2 - different digests for the same path)'. Against 2.1.258 that is inaccurate on both counts: there is one digest in the shipped project-key path and it is not Bun.hash. The operative sentence that follows - that Claude Code prefix-scans rather than recomputing, and that csift mirrors it - is confirmed and should be kept as the whole justification."
        }
      ]
    },
    {
      "id": "PATH-006",
      "area": "path-encoding",
      "behavior": "Claude Code resolves its config home as `CLAUDE_CONFIG_DIR` when that variable is set, else the OS home directory joined with `.claude`, and NFC-normalizes the result; when the variable is set, every path that would be `~/.claude/...` - the `projects/` tree included - lives under that directory instead.",
      "depends": "csift resolves its data root with the precedence `--claude-home` flag > `$CLAUDE_CONFIG_DIR` (when non-empty) > the OS home's `.claude`, and every subcommand reaches the data through that one resolver; ignoring the variable would make csift read a different corpus than the harness writes.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "60-63",
          "snippet": "/// Claude Code's own config-dir relocation env var. When set, \"every `~/.claude` path\n/// lives under that directory instead\", so csift - which reads Claude Code's data - must\n/// honor it to keep pointing at the same files.\npub const CLAUDE_CONFIG_DIR_ENV: &str = \"CLAUDE_CONFIG_DIR\";"
        },
        {
          "path": "src/path/home.rs",
          "lines": "72-75",
          "snippet": "/// Pure precedence resolver for the Claude config dir, factored out so the ordering is\n/// unit-testable without touching process-global env / `OnceLock` state. Order:\n/// 1. explicit `--claude-home` flag, 2. `$CLAUDE_CONFIG_DIR` (when non-empty),\n/// 3. `$HOME/.claude`."
        },
        {
          "path": "src/path/home.rs",
          "lines": "84-90",
          "snippet": "    if let Some(d) = config_dir_env {\n        if !d.is_empty() {\n            return PathBuf::from(d);\n        }\n    }\n    home.join(\".claude\")\n}"
        }
      ],
      "instrument": "Point `CLAUDE_CONFIG_DIR` at a fixture tree holding `projects/<ENCODED>/<uuid>.jsonl` and run `csift list`: only the fixture rows may appear. The pure resolver `resolve_claude_home(flag, env, home)` is unit-testable without touching the process-global override, and the e2e case `custom_claude_home_via_env_var_and_flag` exercises the flag before, after and instead of the env var. To re-derive the harness side, run `strings` over the shipped Claude Code binary and grep for `CLAUDE_CONFIG_DIR`. Counting rule: one config-home resolver reached by every subcommand.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.2.0",
        "source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 7; src/path/home.rs CLAUDE_CONFIG_DIR_ENV doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{250}\"projects\".{250}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]*homedir[^}]*\\}from\"os\".{0,80}' ; env -u CLAUDE_CONFIG_DIR HOME=$FIX/home csift list --no-subagents --format json ; env HOME=$FIX/home CLAUDE_CONFIG_DIR=$FIX/alt csift list ... ; env HOME=$FIX/home CLAUDE_CONFIG_DIR=$FIX/alt csift --claude-home $FIX/home/.claude list ...",
          "observed": "Binary: `function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s)` with `import{homedir as R}from\"os\"` in the same chunk, and the projects tree hangs off it as `function ia(){return S(Se(),\"projects\")}`. Every other namespace in the path builder is likewise rooted at that configHome (transcripts, sidecars, tasks, sessions, file-history). csift precedence, three runs against two fixture trees: HOME only -> 1 row, the home-tree session; HOME plus CLAUDE_CONFIG_DIR -> 1 row and the recorded cwd printed belongs to the alt tree only; --claude-home plus CLAUDE_CONFIG_DIR -> the home-tree cwd only.",
          "rule": "One config-home resolver reached by every subcommand. Each run must surface sessions from exactly one fixture tree; identified by the distinct cwd marker string each tree's single session carries.",
          "note": "Confirmed on both sides, including the detail that the harness NFC-normalizes the resolved config home itself, not just the paths beneath it. csift's documented divergence on an EMPTY value is untouched by this claim and is the separate PATH-011 question; the binary's `??` here is the same coalescing operator that claim turns on, so an exported-but-empty variable would resolve the harness to the empty root."
        }
      ]
    },
    {
      "id": "PATH-007",
      "area": "path-encoding",
      "behavior": "Claude Code does not implement a per-platform home branch itself: it resolves `.claude` as join(os.homedir(), '.claude') and NFC-normalizes the result, delegating the platform split to the runtime's os.homedir(), which reads $HOME on Unix and %USERPROFILE% on Windows. The consequence the claim rests on is unchanged - nothing in the harness's config-home path consults HOME on Windows, so a Git-Bash or MSYS shell's exported POSIX-style HOME cannot steer it.",
      "depends": "csift's `home_dir()` is cfg-split the same way, so a stray POSIX `HOME` inside a Windows shell does not point csift at a `.claude` directory Claude Code never writes; the conventional variable is read first so a test harness can relocate home per subprocess.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "31-35",
          "snippet": "/// load-bearing on Windows - CC never consults `HOME` there, but Git-Bash/MSYS shells\n/// export one (often a POSIX-style `/c/Users/...` a native process cannot use), and\n/// honoring it would point csift at a `.claude` dir CC never writes. The conventional env\n/// var is read first so a test harness can relocate home per-subprocess; `std::env::home_dir`\n/// (un-deprecated, Windows-correct since Rust 1.85 - MSRV is above both) is the fallback."
        },
        {
          "path": "src/path/home.rs",
          "lines": "36-48",
          "snippet": "pub(crate) fn home_dir() -> Result<PathBuf> {\n    #[cfg(not(windows))]\n    if let Some(h) = std::env::var_os(\"HOME\") {\n        if !h.is_empty() {\n            return Ok(PathBuf::from(h));\n        }\n    }\n    #[cfg(windows)]\n    if let Some(h) = std::env::var_os(\"USERPROFILE\") {\n        if !h.is_empty() {\n            return Ok(PathBuf::from(h));\n        }\n    }"
        }
      ],
      "instrument": "On Windows, run `csift list` from a Git-Bash shell that exports a POSIX-style `HOME` and confirm the rows still come from the profile directory's `.claude\\projects`; print both `HOME` and `%USERPROFILE%` in that shell and compare against where `projects/` actually exists. On Unix the equivalent check is the e2e case `custom_claude_home_via_env_var_and_flag`, which spawns the binary with `HOME` set to a temp fixture. Counting rule: one home resolution per process.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.1",
        "source": "SPEC.md section 2.1; SPEC.md section 6 v0.7.3 ledger; AGENTS.md section 7; CHANGELOG 0.7.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'import\\{[^}]*homedir[^}]*\\}from\"os\".{0,80}' ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}USERPROFILE.{200}' ; env -u CLAUDE_CONFIG_DIR HOME=$FIX/home csift list --no-subagents --format json",
          "observed": "Claude Code does not branch on platform in this path at all: the config home is `(process.env.CLAUDE_CONFIG_DIR ?? i(R(),\".claude\")).normalize(\"NFC\")` where R is `import{homedir as R}from\"os\"`, so the per-platform split lives inside the runtime's os.homedir() rather than in harness code. Consistently, the 26 USERPROFILE occurrences in the binary sit in unrelated features - IDE discovery, a PowerShell executable probe, desktop-path resolution, a separate ANTHROPIC_CONFIG_DIR resolver - and none of them participate in resolving `.claude`. No HOME branch appears in the config-home path either. csift Unix half: with CLAUDE_CONFIG_DIR unset and HOME pointed at a fixture tree, csift listed 1 session row, the fixture's own.",
          "rule": "One home resolution per process; the run must surface exactly the fixture tree's session count and no others. Occurrence counting for USERPROFILE is literal matches in the extracted strings, classified by the enclosing function.",
          "note": "The Unix half is verified here and the 'no HOME branch on the Windows path' half is supported by the binary, which names HOME nowhere in the config-home resolution. What this machine cannot decide is the last link: that os.homedir() under the runtime Claude Code actually ships reads %USERPROFILE% and ignores a POSIX HOME on Windows. That is a property of the runtime, not of the harness. Deciding it needs a Windows session: run Claude Code from a Git-Bash shell exporting a POSIX-style HOME, print HOME and %USERPROFILE%, and check which of the two directories gained a .claude\\projects tree. This corpus offers no proxy - 15 of 15 project directories are Unix-shaped."
        }
      ]
    },
    {
      "id": "PATH-008",
      "area": "path-encoding",
      "behavior": "The cwd-to-directory map is many-to-one and Claude Code does not disambiguate collisions for any cwd of 200 encoded characters or fewer: two different cwds that encode identically share ONE projects directory, distinguishable only by the session uuid and by each record's own `cwd` field.",
      "depends": "csift uses the encoded directory as the lookup key exactly as the harness does, and intends to keep, for a REAL-path target, only the files whose recorded cwd IS that path while an EXPLICIT encoded-dir token skips the filter. As of Claude Code 2.1.258 that filter no longer fires on real data: the harness now writes a session-state cache line as physical line 1 of every transcript, and read_first_cwd inspects only line 1, so the stored cwd reads as absent and every candidate is admitted under the absent-cwd keep. The mechanism is intact and testable only where line 1 carries cwd.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "126-132",
          "snippet": "    /// The canonical cwd of a REAL-path target - `Some` when the user passed an actual\n    /// filesystem path, `None` for a pre-encoded `<ENCODED>` dir token (where the user\n    /// explicitly named the dir) or an all-projects scan. When `Some`, session enumeration\n    /// filters this dir's files to those whose recorded `cwd` IS this path, so a lossy-\n    /// encoding COLLISION (a different cwd that encodes to the same dir, §2.1) never leaks a\n    /// sibling's sessions - or their subagents - into the result.\n    pub target_cwd: Option<PathBuf>,"
        },
        {
          "path": "src/path/resolver.rs",
          "lines": "319-324",
          "snippet": "/// Admit one candidate `<stem>.jsonl`: the cwd COLLISION GUARD (SPEC 2.1 - a dir resolved\n/// from a REAL path may be shared by a DIFFERENT cwd under the lossy encoding, so keep only\n/// files whose recorded `cwd` IS this target; a file whose `cwd` is absent is kept), then the\n/// UNION-DOMAIN prefix collection (a prefix may name a subagent of a session that itself does\n/// NOT match - cost paid only on a prefix-targeted invocation), then the exact-id / prefix\n/// keep decision."
        },
        {
          "path": "src/path/resolver.rs",
          "lines": "341-347",
          "snippet": "    if let Some(want) = &pd.target_cwd {\n        if let Some(stored) = read_first_cwd(&p) {\n            if !cwd_equivalent(&stored, want) {\n                return;\n            }\n        }\n    }"
        }
      ],
      "instrument": "`csift list <ENCODED-dir> --format json | jq -r 'select(.kind==\"session\") | .cwd' | sort -u` - more than one distinct value proves a collision inside that directory, and the same query against the REAL path must return exactly one. Counting rule: distinct `cwd` strings across the directory's top-level session rows.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 2.1; src/path/resolver.rs admit_entry doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 comparing the encodings of two constructed sibling cwds; csift --claude-home $FIX list '@<ENCODED>' vs list <real path A> vs list <real path B> --no-subagents --format json; python3 census of the first physical line of every top-level *.jsonl under ~/.claude/projects; csift list '@<ENCODED>' vs list <real path> against one live project directory",
          "observed": "Many-to-one confirmed constructively: two distinct cwds differing only in one separator character encode to the identical 112-character basename (equality check printed True) and share one directory. Guard behaves as documented WHEN the cwd is on line 1: the encoded-dir token returned 2 session rows (no filter, the user named the directory) while each real path returned 1. But the guard is inert against real transcripts: 0 of 66 top-level transcripts in the live corpus carry a cwd field on physical line 1 - line 1 is a session-state cache line in every case (62 'last-prompt', 3 'ai-title', 1 'queue-operation') - and csift's read_first_cwd (src/path/project_dirs.rs:109) reads ONLY the first line, so it returns None and the file is kept unconditionally. Demonstrated on a live directory holding 7 top-level transcripts of which 3 record a cwd that does not encode to the basename: both the encoded-dir token and the real path returned 7 rows; the real path should have returned 4.",
          "rule": "A collision is two DISTINCT cwd strings whose encodings are equal - not merely two distinct cwd values in one directory, which the deep-cd case also produces. Guard efficacy = rows returned for a real-path target divided by rows returned for the encoded-dir token over the same directory; 1.0 means the guard never fired. Line-1 census counts every top-level *.jsonl once, keyed on presence of a top-level cwd field in the first physical line.",
          "note": "The claim's statement about Claude Code holds - the map is many-to-one and the harness does not disambiguate below the cap. The csift half has drifted out from under it, and this is the most actionable finding in the area. The stated reason for reading only line 1 is a performance one - 'the cwd field sits in the first record's first ~200 bytes' - and that premise is now false for 100% of the corpus. A fix should scan the first few lines, or the first line carrying a message field, rather than exactly one; the bounded 64 KiB head read already covers the extra lines. Note also that the ledger's own instrument for this claim is unsound as written: distinct cwd values inside one directory prove nothing about collisions, because the deep-cd case in PATH-009 produces them too - the encodings must be compared, not the cwd strings."
        }
      ]
    },
    {
      "id": "PATH-009",
      "area": "path-encoding",
      "behavior": "A projects directory is keyed by the session's START cwd alone: a deep in-session `cd` and a subagent's cwd appear in the record data but never mint a directory of their own, so a directory implies the start cwd of its files while the reverse does not hold - an observed `cwd` value need not have a directory named after its encoding.",
      "depends": "csift never enumerates one directory per observed cwd; `list` reads a session's `cwd` from its FIRST record deliberately (the per-record cwd follows the tracked shell cwd, so a last-seen value can legitimately be a subdirectory), and a scoped command that assumed a directory exists for every seen cwd would report empty scopes.",
      "code": [
        {
          "path": "src/session/rows.rs",
          "lines": "29-33",
          "snippet": "    /// Decoded human-readable cwd (read from the data, §2.4), if present. FIRST-seen\n    /// deliberately: the record cwd follows the tracked shell cwd (SPEC 4.9), so the\n    /// last-seen value can legitimately be a subdirectory; the session's home is the\n    /// opening value. Asymmetric with version/git_branch below on purpose.\n    pub cwd: Option<String>,"
        },
        {
          "path": "src/session/summarize.rs",
          "lines": "28-33",
          "snippet": "            if let Some(text) = preview_text(rec) {\n                // Capture identity off the first user record (it carries cwd / version /\n                // gitBranch / sessionId in real data).\n                cwd = rec.cwd.clone();\n                version = rec.version.clone();\n                git_branch = rec.git_branch.clone();"
        }
      ],
      "instrument": "Collect the distinct `cwd` values across one directory's top-level transcripts (`rg -o --no-filename '\"cwd\":\"[^\"]+\"' ~/.claude/projects/<ENCODED>/*.jsonl | sort -u`) and compare that count against the number of directories whose basename equals the encoding of each value: the former can exceed the latter. Counting rule: distinct `cwd` string values across one directory's top-level transcripts.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 1; src/session/rows.rs cwd field doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{250}originalCwd:.{150}' ; python3 collecting every distinct cwd value across all top-level transcripts under ~/.claude/projects and testing each against the set of directory basenames",
          "observed": "The project directory is built from originalCwd, not from the tracked per-record cwd: `function RS(e){return he(Bu(e.root.project.originalCwd),e.root.id,Bge)}` with `function Bu(e){return S(ia(),Em(e))}`, and originalCwd is seeded once at startup by `function dn(e=uLn()){return v7t({...project:{originalCwd:e,projectRoot:e,cwd:e}})}` while in-session movement updates only the cwd slot: `setCwd(c){S.update({project:{cwd:c}})}`. Live corpus: 31 distinct cwd values observed across top-level transcripts, against 15 project directories; only 13 of the 31 have a directory named after their encoding, and 18 have none.",
          "rule": "Distinct cwd string values across one corpus's top-level transcripts, compared against the set of directory basenames under ~/.claude/projects; a value 'has a directory' iff its NFC+unit-wise encoding is exactly some basename. The claim requires observed-cwd count to be able to exceed directory count and requires at least one observed cwd with no directory.",
          "note": "Confirmed on both sides, and the binary supplies the mechanism the claim only asserted: the directory is keyed by originalCwd, which is written once at session start and never re-derived, while setCwd moves only the tracked cwd. 18 of 31 observed cwd values have no directory of their own - a comfortable margin, so the asymmetry is not an artifact of a small corpus. csift's choice in src/session/rows.rs to report the FIRST-seen cwd rather than the last is the matching read: first-seen is the value the directory key was derived from."
        }
      ]
    },
    {
      "id": "PATH-010",
      "area": "path-encoding",
      "behavior": "Some <ENCODED> project directories hold NO top-level *.jsonl at all. On this corpus the observed instance is a directory carrying only a memory/ directory (2 of 15). The variant in which such a directory carries only a nested <session-uuid>/ sidecar tree of subagent transcripts was not observed here and remains asserted rather than measured.",
      "depends": "csift's top-level session enumeration is non-recursive and tolerates a jsonl-less (or vanished) project directory by returning an empty result for it; a recursive walk would mis-list a subagent transcript as a session (its bare-hex id is not a re-feedable target) and a hard error on a childless directory would break an all-projects `list`.",
      "code": [
        {
          "path": "src/path/resolver.rs",
          "lines": "527-536",
          "snippet": "/// The top-level `<uuid>.jsonl` session files directly in `dir` (non-recursive). Tolerates an\n/// unreadable/vanished dir (empty result).\npub(crate) fn top_level_jsonls(dir: &Path) -> Vec<PathBuf> {\n    let mut out = Vec::new();\n    if let Ok(read) = std::fs::read_dir(dir) {\n        for entry in read.flatten() {\n            let p = entry.path();\n            if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)\n                && p.extension().is_some_and(|e| e == \"jsonl\")\n            {"
        },
        {
          "path": "src/subagent/ids.rs",
          "lines": "24-30",
          "snippet": "pub fn session_id_from_path(path: &Path) -> String {\n    path.file_stem()\n        .and_then(|s| s.to_str())\n        .map(bare_agent_id)\n        .map(str::to_string)\n        .unwrap_or_default()\n}"
        }
      ],
      "instrument": "List the project directories holding no top-level jsonl (`for d in ~/.claude/projects/*/; do [ -z \"$(ls \"$d\"*.jsonl 2>/dev/null)\" ] && echo \"$d\"; done`), then run `csift list`: it must complete without error and print no bare-hex session id. Counting rule: one directory per line printed.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 1; src/path/resolver.rs top_level_jsonls doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for d in ~/.claude/projects/*/; do [ -z \"$(ls \"$d\"*.jsonl 2>/dev/null)\" ] && echo \"$d\"; done ; csift list --max-count 0 --format json, then counting session rows and testing each top-level row's session_id against ^[0-9a-f]{12,}$",
          "observed": "2 of 15 project directories hold no top-level *.jsonl. Both contain a memory directory and nothing else transcript-shaped (the only other entry seen across the two is a .DS_Store, an operating-system artifact). No directory in this corpus is sidecar-only - that is, none holds a nested <session-uuid>/ subagent tree without also holding top-level transcripts. csift list over the whole corpus exited 0, emitted 7600 session rows of which 7534 are subagent rows, and 0 top-level rows carried a bare-hex session id.",
          "rule": "One directory per line printed by the jsonl-less loop; denominator = directories under ~/.claude/projects. csift must exit 0 and emit zero top-level rows whose session_id matches ^[0-9a-f]{12,}$ (the bare-hex shape that is not a re-feedable target).",
          "note": "The consequence csift depends on is fully confirmed: the enumeration is non-recursive and tolerant, an all-projects list completes cleanly across 15 directories including the 2 empty ones, and no subagent transcript is ever mis-listed as a session - 0 of 66 top-level rows carry a bare-hex id. Only the enumeration of WHICH empty shapes occur needed narrowing. A sidecar-only directory would arise if every top-level transcript in a project were deleted while its per-session subagent subdirectory survived; deleting the top-level jsonl from a project that has subagents would produce it on demand."
        }
      ]
    },
    {
      "id": "PATH-011",
      "area": "path-encoding",
      "behavior": "An EMPTY-STRING `CLAUDE_CONFIG_DIR` is still taken as SET by Claude Code, because the null-coalescing operator guarding the read tests only for null/undefined - so the harness resolves its config home to the empty string rather than falling back to the OS home, and every path derived from it becomes RELATIVE to the process cwd (a one-shot run in an empty directory mints `projects/`, `sessions/` and `backups/` there, transcript included). The coalescing read is NOT the only way the harness reads this variable: the global `.claude.json` path and the runner config path read it with a truthiness fallback instead, so an empty value falls back to the OS home on those sites and the harness ends up split between two roots in one process.",
      "depends": "csift treats an empty `$CLAUDE_CONFIG_DIR` as UNSET and falls through to the OS home's `.claude` - a deliberate, documented divergence (honoring the empty value would resolve the projects root to the relative path `projects`, i.e. whatever happens to sit under the invoking cwd, and normally a hard `cannot read projects root` error rather than the corpus); the same non-empty test also gates the lazy home lookup, so a relocated root still works when `$HOME` is unset.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "72-75",
          "snippet": "/// Pure precedence resolver for the Claude config dir, factored out so the ordering is\n/// unit-testable without touching process-global env / `OnceLock` state. Order:\n/// 1. explicit `--claude-home` flag, 2. `$CLAUDE_CONFIG_DIR` (when non-empty),\n/// 3. `$HOME/.claude`."
        },
        {
          "path": "src/path/home.rs",
          "lines": "84-88",
          "snippet": "    if let Some(d) = config_dir_env {\n        if !d.is_empty() {\n            return PathBuf::from(d);\n        }\n    }"
        },
        {
          "path": "src/path/home.rs",
          "lines": "100-107",
          "snippet": "    // `$HOME` feeds only the default branch; resolve it lazily so a relocated config dir\n    // (flag or env) still works when `$HOME` is unset.\n    let have_higher = flag.is_some() || env.as_deref().is_some_and(|d| !d.is_empty());\n    let home = if have_higher {\n        PathBuf::new()\n    } else {\n        home_dir()?\n    };"
        }
      ],
      "instrument": "Harness side: `cd \"$(mktemp -d)\" && CLAUDE_CONFIG_DIR= claude --print 'Reply with exactly: ok'; find . -maxdepth 2` - the run must create `projects/`, `sessions/` and `backups/` in that cwd, and no directory named after that cwd's encoding may appear under ~/.claude/projects. Reading the operator directly: `strings -n 6 <the shipped Claude Code binary> | rg -o '.{0,120}CLAUDE_CONFIG_DIR.{0,220}'` and read the coalescing expression on the config-home resolver (`??`), noting that other read sites of the same variable use a truthiness fallback. csift side: `CLAUDE_CONFIG_DIR= csift list --max-count 1 --format json | head -1` must print the same `sessions_in_scope` as the same command with the variable unset. Counting rule: one config-home resolver per process.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.2.0",
        "source": "SPEC.md section 2.1; AGENTS.md section 7; src/path/home.rs resolve_claude_home doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}CLAUDE_CONFIG_DIR.{0,220}'   (2) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}\\(Se\\(\\),\"projects\"\\).{0,160}'   (3) cd \"$(mktemp -d)\" && CLAUDE_CONFIG_DIR= claude --print 'Reply with exactly: ok'; find . -maxdepth 2   (4) ls ~/.claude/projects | grep -c <the probe dir's encoded basename>   (5) CLAUDE_CONFIG_DIR= csift list --max-count 1 --format json | head -1; env -u CLAUDE_CONFIG_DIR csift list --max-count 1 --format json | head -1; CLAUDE_CONFIG_DIR=/nonexistent-root-xyz csift list --max-count 1 --format json   (6) awk 'NR>=72 && NR<=107 { printf \"%d\\t%s\\n\", NR, $0 }' src/path/home.rs",
          "observed": "(1) the resolver is present verbatim in 2.1.258: `function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s);` - the guard is `??`, which tests only null/undefined, so an empty string wins the coalesce. (2) the projects root hangs off that same value, in two chunks: `function ia(){return S(Se(),\"projects\")}` and `function Pl(){return o(Se(),\"projects\")}`. (3) one `claude --print` run in an empty scratch directory with CLAUDE_CONFIG_DIR exported EMPTY created 3 top-level directories in the process cwd - `projects/`, `sessions/`, `backups/` - with the transcript at `./projects/<encoded-cwd>/<uuid>.jsonl`; the config home therefore resolved to the empty string and every derived path became RELATIVE to the process cwd. (4) 0 directories matching that cwd's encoding exist under ~/.claude/projects, so nothing fell back to the OS home. (5) csift diverges as documented: empty and unset both report `\"sessions_in_scope\":7597,\"subagent_sessions\":7532,\"top_level_sessions\":65` (byte-identical header lines), while a non-empty bogus value is honored and errors `cannot read projects root /nonexistent-root-xyz/projects`. (6) all three claim snippets are verbatim at the stated lines: 72-75 doc, 84-88 `if let Some(d) = config_dir_env { if !d.is_empty() { return PathBuf::from(d); } }`, 100-107 the lazy-home block.",
          "rule": "Harness side: count the top-level directories a single one-shot `claude --print` run creates in an empty scratch cwd while CLAUDE_CONFIG_DIR is exported empty - 3 (projects, sessions, backups) if the empty value is taken as SET, 0 if the harness falls back to the OS home; cross-check with the count of directories under ~/.claude/projects whose basename equals the encoding of that scratch cwd (0 = no fallback). csift side: compare the `kind:\"header\"` line of `csift list --max-count 1 --format json` under an exported-empty vs an unset CLAUDE_CONFIG_DIR - identical `sessions_in_scope` means empty is treated as unset. One config-home resolution per process.",
          "note": "Holds at 2.1.258, and now with a live receipt rather than a read of the operator alone: the empty value is taken as set and the config home becomes the empty string, so the harness wrote its whole state tree into the invoking directory. Two refinements. (a) The consequence is a RELATIVE root, not an unreadable one - `join(\"\", \"projects\")` is `projects`, so the harness silently starts a second corpus under whatever cwd it was launched from; the ledger's parenthetical that an empty root 'would make every subcommand read nothing' is close but imprecise. (b) The binary is not uniform about this variable: the config home that mints `projects/` coalesces on null/undefined, while the global `.claude.json` path and the self-hosted-runner config path use a truthiness fallback - observed live, since `CLAUDE_CONFIG_DIR= claude mcp list` in the same empty scratch directory still resolved the user-level server list from the OS home while writing nothing to the cwd. One thing to watch in later versions: 2.1.258 carries the string `the configuration home (CLAUDE_CONFIG_DIR) is not an absolute path`, reached from a scoped placement check (`if(!TI(we)) return vv(..., \"placement\")`), so at least one subsystem already rejects a non-absolute config home; that guard does not sit on the resolution path the probe exercised, but a future version hardening it globally would turn this claim's empty-value branch into an error path. csift's divergence is unaffected either way and is the safer behavior. All three cited code sites are verbatim at their stated lines in the current file, so corrections.code is empty."
        }
      ]
    },
    {
      "id": "PATH-012",
      "area": "path-encoding",
      "behavior": "Inside a `<ENCODED>` project directory a transcript's filename basename IS the session id: over the whole live corpus, 66 top-level transcripts carried a top-level `sessionId` on at least one record, 0 files carried a `sessionId` that differed from the basename minus `.jsonl`, and 0 files carried more than one distinct `sessionId` value - so the filename and the in-record id are two spellings of one identity, and the id is recoverable without parsing a single line.",
      "depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback for a file whose name yields nothing; the live session-registry join then matches a registry row by its `sessionId` against that filename-derived id, so the two agreeing is what makes the join work at all. If the harness ever wrote a transcript under a name other than its session id, `list` rows would carry a re-feedable-looking id that addresses a different session.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "82-87",
          "snippet": "    // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n    let session_id = if session_id.is_empty() {\n        data_session_id.unwrap_or_default()\n    } else {\n        session_id\n    };"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "57-59",
          "snippet": "        if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n            continue;\n        }"
        }
      ],
      "instrument": "Python over every `~/.claude/projects/*/*.jsonl`: collect the set of top-level `sessionId` string values per file, compare it against the basename minus `.jsonl`, and report the files whose basename is absent from that set plus the files carrying more than one distinct value. Counting rule: one observation per file that carries at least one `sessionId` (a file with none is not counted either way). Measured 2026-09-02: 66 files observed, 0 mismatches, 0 files with two distinct values.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'  (whole-corpus, one pass)\nimport os,re,json,glob\nroot=os.path.expanduser(\"~/.claude/projects\")\npat=re.compile(rb'\"sessionId\"\\s*:\\s*\"([^\"]*)\"')\nfor f in sorted(glob.glob(os.path.join(root,\"*\",\"*.jsonl\"))):\n    base=os.path.basename(f)[:-6]; vals=set()\n    for raw in open(f,\"rb\"):\n        c=pat.findall(raw)\n        if not c: continue\n        s={x.decode(\"utf-8\",\"replace\") for x in c}\n        if s=={base}: vals.add(base); continue\n        try: o=json.loads(raw)\n        except Exception: continue\n        if isinstance(o,dict) and isinstance(o.get(\"sessionId\"),str): vals.add(o[\"sessionId\"])\n    # tally: base not in vals -> mismatch; len(vals)>1 -> multi\nEOF\n; plus the registry join (python over ~/.claude/sessions/*.json: glob ~/.claude/projects/*/<row sessionId>.jsonl) ; plus a SCOPED subagent counter-check over the single project dir holding the most subagent transcripts ; plus `sed -n '82,87p' src/session/summarize.rs` and `rg -n 'sessionId' -B4 -A4 src/live/registry.rs`",
          "observed": "files_scanned 66; lines_scanned 803946; files_with_sessionId 66; files_without_sessionId 0; basename_absent_from_set 0; files_with_multiple_distinct 0. Registry join: registry_rows_with_sessionId 7; rows_whose_sessionId_names_an_existing_top_level_transcript 7; rows_with_no_matching_transcript_filename 0; a registry row's sessionId is 36 chars and the row key set is [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version]. Scoped subagent counter-check (one project dir, 5497 subagent transcripts present, first 400 checked): sessionId_equals_own_basename 0; sessionId_differs_from_basename 400; no_sessionId 0. Both code sites present verbatim at the claimed lines.",
          "rule": "One observation per TOP-LEVEL transcript file matching ~/.claude/projects/<ENCODED>/<name>.jsonl (subagent transcripts under <uuid>/subagents/ are excluded by the glob depth). Per file, collect the set of TOP-LEVEL sessionId string values: a byte regex over each line is the fast path, and any line whose regex hits are not all equal to the basename is re-parsed with json.loads so that only the top-level key can enter the set (a sessionId quoted inside record content never counts). A file whose set is empty is counted in neither column. mismatch = basename absent from the set; multi = set size > 1. Registry rule: one observation per ~/.claude/sessions/*.json carrying a string sessionId; joined = at least one ~/.claude/projects/*/<sessionId>.jsonl exists.",
          "note": "Reproduced exactly at CC 2.1.258: 66 files, 66 carrying a top-level sessionId, 0 basename mismatches, 0 files with two distinct values. The registry half of the 'depends' was instrumented too and the join is total on this machine (7 of 7 rows name an existing transcript filename), so filename-derived id == registry join key in practice, not just in principle. The claim's restriction to TOP-LEVEL transcripts is load-bearing and was measured: in a scoped check of one project dir, 400 of 400 subagent transcripts carry a sessionId that differs from their own basename (they carry the owning session's uuid), which is exactly why csift derives a subagent row's id from the path rather than from the record. Both code snippets are verbatim at the claimed paths and lines: src/session/summarize.rs:82-87 and src/live/registry.rs:57-59."
        }
      ]
    },
    {
      "id": "PATH-013",
      "area": "path-encoding",
      "behavior": "The transcript writer and the project-directory encoder handle astral-plane characters by OPPOSITE conventions: the writer emits them verbatim as 4-byte UTF-8 and never as an escaped surrogate pair (over the whole live corpus - 66 top-level transcripts, 803,946 lines - 8,633 lines, 1.074%, carry a 4-byte UTF-8 lead byte in 0xF0-0xF4, and 0 lines carry a real `\\udXXX` surrogate escape; exactly 1 line matches a naive surrogate regex, but its backslash run is EVEN, so those six characters are literal text quoted inside a tool result rather than an escape the writer produced), while the cwd encoder counts the same character as TWO UTF-16 code units and emits TWO dashes into the directory name.",
      "depends": "csift must not carry either rule across: the verbatim-UTF-8 transcript convention is what keeps a non-ASCII (emoji, CJK) needle prefilter-eligible against raw line bytes - the needle-safety predicate escapes only `\"`, C0 controls and DEL - while the same emoji sitting in a cwd must be re-encoded unit-wise by `path::encode_cwd` to two dashes or the project directory is not found. Applying the encoder's UTF-16 view to search needles, or the writer's codepoint view to the encoder, breaks one surface silently in each direction.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "538-540",
          "snippet": "pub(crate) fn json_escapes_in_string(c: char) -> bool {\n    c == '\"' || (c as u32) < 0x20 || c == '\\u{7f}'\n}"
        },
        {
          "path": "src/path/home.rs",
          "lines": "15-27",
          "snippet": "pub fn encode_cwd(cwd: &Path) -> String {\n    use unicode_normalization::UnicodeNormalization;\n    let s = cwd.to_string_lossy();\n    let s: String = s.nfc().collect();\n    let mut out = String::with_capacity(s.len());\n    for unit in s.encode_utf16() {\n        match u8::try_from(unit) {\n            Ok(b) if b.is_ascii_alphanumeric() => out.push(char::from(b)),\n            _ => out.push('-'),\n        }\n    }\n    out\n}"
        }
      ],
      "instrument": "Writer side: python over every `~/.claude/projects/*/*.jsonl` - count a line if any of its raw bytes is in 0xF0..=0xF4, and separately if a match of `\\\\u[dD][89abAB][0-9a-fA-F]{2}` is preceded by an ODD run of consecutive backslash bytes (the parity guard is required: without it the corpus returns 1 false positive, a doubled backslash in quoted content). Counting rule: one observation per whole line, not prefix-bounded. Whole-corpus rather than a random sample, so a stranger reruns the identical number. Measured 2026-09-02 at CC 2.1.258: 803,946 lines, 8,633 four-byte-lead lines, 0 real surrogate escapes. Encoder side: `strings -n 6 <cc-binary> | rg -o 'function k\\(e\\)\\{return e\\.replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\)\\}.{0,300}'` recovers the sanitizer and its >200-cap wrapper, the `\"projects\"` path builder shows that wrapper is what names the project directory, a search for a unicode-flag variant (`/gu`, `/ug`, `/gv`) returns zero matches, and evaluating the extracted regex in a JS engine yields two dashes for one astral character against one dash for one BMP character. The csift unit test `encode_matches_cc_exactly_on_non_ascii_and_windows_paths` (src/path/tests/encode.rs) then pins the same law crate-side, but it is a regression guard on csift, not an instrument on Claude Code.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Writer side: python3 over every ~/.claude/projects/*/*.jsonl - per line, count a hit if any raw byte is in 0xF0..=0xF4, and separately run re.compile(rb'\\\\u[dD][89abAB][0-9a-fA-F]{2}') and then walk backwards from the match start counting the consecutive 0x5C bytes, classifying the match as a REAL JSON escape only when that run length is ODD. Encoder side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var v=m\\(\"claude-cli\"\\),C=200;.{0,340}' ; strings -n 6 ... | rg -o 'function k\\(e\\)\\{return e\\.replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\)\\}.{0,300}' ; strings -n 6 ... | rg -o '.{0,160}\"projects\".{0,160}' | rg -i 'join|normali|slice' ; strings -n 6 ... | rg -o '\\[\\^a-zA-Z0-9\\]/(gu|ug|gv)' ; then node -e on the extracted regex: (t)=>t.replace(/[^a-zA-Z0-9]/g,\"-\"). Code sites: sed -n '535,541p' src/search/matcher.rs ; sed -n '15,27p' src/path/home.rs ; rg -n 'encode_matches_cc_exactly_on_non_ascii_and_windows_paths' -A 14 src/path/tests/encode.rs",
          "observed": "Writer side, whole corpus: files 66; lines_total 803946; lines_with_4byte_utf8_lead_F0_F4 8633 (1.074%); files_with_astral 44; lines_matching_naive_surrogate_regex 1; lines_with_REAL_surrogate_escape_odd_backslash_run 0. The single naive hit has consecutive_backslashes_before_u == 2 (even), the line parses as JSON, and the decoded record contains no astral character at all - the six characters are literal text inside a quoted tool result, not an escape the writer emitted. Encoder side, extracted verbatim from the binary: `var v=m(\"claude-cli\"),C=200;function S(t){let e=t.replace(/[^a-zA-Z0-9]/g,\"-\");if(e.length<=C)return e;return`${e.slice(0,C)}-${Math.abs(zq(t)).toString(36)}`}` and `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}function KA(e){let n=k(e);if(n.length<=IL)return n;return`${n.slice(0,IL)}-${be(e)}`}` and the projects-path builder `defaultPath(){let e=HU(),n=to(e,\"projects\"),r=mn(),d=this.canonicalWcRootForProject(r)??Jr(r)??r,f=e===Se()?Em(d):KA(d);return(to(n,f,cue)+Bl).normalize(\"NFC\")}` with `function Em(e){return Apr()??KA(e)}`. A search for a unicode-flag variant `[^a-zA-Z0-9]/gu`, `/ug` or `/gv` returned ZERO matches anywhere in the binary. node evaluating that exact extracted regex: astral_alone_dashes=2, bmp_cjk_char_dashes=1 (one astral char -> two dashes, one BMP CJK char -> one dash).",
          "rule": "Writer side: one observation per whole line (not prefix-bounded) over all 66 top-level transcripts. A line counts as astral-bearing if any of its raw bytes lies in 0xF0..=0xF4 (the 4-byte UTF-8 lead range). A line counts as carrying a REAL surrogate escape only if a match of \\u[dD][89abAB][0-9a-fA-F]{2} is preceded by an ODD number of consecutive backslash bytes; an even run means the backslash is itself escaped, so the sequence is literal content text rather than an escape. Encoder side: the counting rule is the presence or absence of a matched literal in the stripped binary - a matched string is stable evidence, and the zero-match search for a unicode-flag variant is the negative control; the behavioural half is deterministic evaluation of the extracted regex, where dash count for a single character discriminates the UTF-16 code-unit view (2 for astral) from the codepoint view (1).",
          "note": "Both halves of the claim survive, but the wording and the instrument needed correction on three points. (1) The writer-side surrogate count is 0 only under a backslash-parity guard: the naive regex the claim states returns exactly 1 hit over the whole corpus, and that hit is a doubled backslash - literal `\\u`-shaped text inside a quoted tool result, with no astral character in the decoded record. The claim's 'and 0 lines carry a surrogate escape' is true of the mechanism but false of the stated rule as written. (2) The claim's numbers came from a random 14-file, 43,547-line sample (286 hits, 0.66%), which no stranger can rerun to the same number; the whole-corpus census is deterministic and gives 8,633 of 803,946 lines, 1.074%, across 44 of 66 files. (3) The claim's encoder-side instrument was a csift unit test only - csift's own code, which by itself cannot decide how Claude Code behaves. Replaced with a binary extraction: the sanitizer `function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")}` and its cap wrapper `KA` are in the 2.1.258 binary, the projects-path builder resolves to `KA` on both branches (`Em` falls through to `KA`), the regex carries NO unicode flag (a search for /gu, /ug and /gv variants matched nothing in the whole binary), and evaluating that exact regex gives two dashes for one astral character. All three code snippets are verbatim at the claimed paths and lines: src/search/matcher.rs:537-539, src/path/home.rs:15-27, and the named unit test at src/path/tests/encode.rs:91-104 asserting `-tmp-x--y` for an astral char. Two incidental observations, neither a defect: the live corpus offers no discriminating power for the astral rule, because 0 of the 12 project directories whose records carry a cwd have a non-ASCII cwd, so the UTF-16 and codepoint encodings agree on all 12 - the binary and the regex evaluation are the only instruments that can decide it here; and the unit test's comment cites the 2.1.228 minified symbol names, whereas 2.1.258 names the same functions `k`, `KA` and `Em` - the asserted behavior is unchanged."
        }
      ]
    },
    {
      "id": "PLAN-001",
      "area": "plan",
      "behavior": "Entering Plan Mode makes Claude Code write a type:\"attachment\" record whose attachment payload is {\"type\":\"plan_mode\",\"reminderType\":\"full\"|\"sparse\",\"isSubAgent\":<bool>,\"planFilePath\":\"<absolute path>\",\"planExists\":<bool>} plus optional customInstructions / workshopDocPath / workshopOfferDocPath / workshopActiveDocPath. The record also carries the session's top-level slug, and planFilePath is always \"<slug>.md\". This attachment is the AUTHORITATIVE binding and takes precedence, but it is not the ONLY binding: when no plan_mode attachment exists Claude Code falls back to the first-valid-slug law (PLAN-006).",
      "depends": "The byte prefilter token plan_mode also admits the sibling attachment types plan_mode_exit (23 records) and plan_mode_reentry (2), which likewise carry planFilePath; src/plan.rs then drops them with an exact att.get(\"type\")==Some(\"plan_mode\") check, so the binding stays correct. In this corpus no transcript carries an exit/reentry attachment without a plan_mode one (0 of 18), so the extra candidates never change an answer.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "11-13",
          "snippet": "//! {\"type\":\"attachment\",\"attachment\":{\"type\":\"plan_mode\",\n//!    \"planFilePath\":\"/Users/…/.claude/plans/nested-prancing-popcorn.md\",\n//!    \"isSubAgent\":false,\"planExists\":false}, …}"
        },
        {
          "path": "src/plan.rs",
          "lines": "110-118",
          "snippet": "        let Some(att) = rec.attachment_value() else {\n            continue;\n        };\n        if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"plan_mode\") {\n            continue;\n        }\n        let Some(plan_file) = att.get(\"planFilePath\").and_then(serde_json::Value::as_str) else {\n            continue;\n        };"
        },
        {
          "path": "src/plan.rs",
          "lines": "82-90",
          "snippet": "/// Tight byte prefilter for the plan-resolution pre-pass: `plan_mode` is a rare token, so\n/// a giant transcript parses only its handful of attachment lines (the scan still splits\n/// newlines over the whole file, but `serde_json` runs on almost nothing).\nfn line_is_plan_candidate(line: &[u8]) -> bool {\n    // Built ONCE (per-line hot path - the stateless form rebuilt its searcher every call).\n    static PLAN_MODE: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"plan_mode\"));\n    PLAN_MODE.find(line).is_some()\n}"
        }
      ],
      "instrument": "`csift search '' <project dir> --count-by attachment | rg plan_mode` counts the binding records (counting rule: one per attachment record; measured 21 corpus-wide), then `csift plan @<id> --format json | jq '{plan_file, line, binding_source, slug, plan_exists}'` and `csift show @<id> --line <line> --raw | jq .attachment` must show that same payload.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "SPEC.md section 6.7.1; AGENTS.md section 5; src/plan.rs module doc; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' --count-by attachment  (run from ~, whole corpus); then, over every plan_mode payload, rg -N --no-filename -o '\\{\"type\":\"plan_mode\"[^}]*\\}' -g '*.jsonl' . | jq -rc 'keys|join(\",\")' | sort | uniq -c; then per record: jq -r '(.slug) as $s | (.attachment.planFilePath|split(\"/\")|last|sub(\"\\\\.md$\";\"\")) as $b | if $b==$s then \"basename==slug\" else \"MISMATCH\" end'; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{130}plan_mode.{130}'",
          "observed": "Census: plan_mode=21, plan_mode_exit=23, plan_mode_reentry=2, plan_file_reference=151 attachment records. All 21 plan_mode payloads share one key set: isSubAgent,planExists,planFilePath,reminderType,type. 21/21 carry a top-level slug; 21/21 basename(planFilePath)==\"<slug>.md\". Binary emits: C.push({type:\"plan_mode\",reminderType:U,isSubAgent:!!r.agentId,planFilePath:f,planExists:_!==null,customInstructions:r.options.planModeInstructions,workshopDocPath:v,...W&&{workshopOfferDocPath:voe()},...z&&{workshopActiveDocPath:voe()}}). Counter-check: the naive byte pattern '\"type\":\"plan_mode\"' matches 67 records (3.2x over-count) because csift's own docs quote the shape and dev sessions read them back through tool results.",
          "rule": "One row per attachment record, keyed on the PARSED attachment.type field (not raw bytes); key sets compared as the sorted comma-joined key list of the attachment payload object.",
          "note": "Code sites verified verbatim: src/plan.rs 11-13, 107-115, 79-87 all match the current file exactly. The load-bearing half of the claim - that a path-shaped guess would mis-attribute edits to another session's plan - is untouched."
        }
      ]
    },
    {
      "id": "PLAN-002",
      "area": "plan",
      "behavior": "Within one transcript every plan_mode attachment carries the SAME planFilePath (18/18 transcripts, distinct-path cardinality 1). planExists is the field observed to flip false->true once the plan file is first written - observed in 2 of the 3 transcripts that carry more than one plan_mode record. reminderType is a second per-attachment field that is computed fresh on every attachment, so it is not guaranteed constant either; it was \"full\" on all 21 records here because no transcript in this corpus carries enough plan_mode attachments to reach the sparse branch.",
      "depends": "csift takes the LATEST `plan_mode` attachment in file order as the current binding, so a session that entered Plan Mode repeatedly still reports one stable plan path with a current existence flag.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "22-24",
          "snippet": "//! Within one transcript every `plan_mode` attachment carries the same `planFilePath`\n//! (only `planExists` flips `false→true` once the plan is first written); we take the\n//! LATEST occurrence as the current binding."
        },
        {
          "path": "src/plan.rs",
          "lines": "107-109",
          "snippet": "    // File order == line order; overwriting keeps the LATEST plan_mode binding.\n    let mut latest: Option<PlanRef> = None;\n    for (line_no, rec) in &records {"
        }
      ],
      "instrument": "`jq -r 'select(.attachment.type==\"plan_mode\") | [.attachment.planFilePath, (.attachment.planExists|tostring)] | @tsv' <transcript> | sort | uniq -c` - expect one path carrying both flag values. Counting rule: one row per plan_mode attachment record.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 5; src/plan.rs module doc; SPEC.md section 6.7.1; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "For each transcript matching '\"type\":\"plan_mode\"': rg -N --no-filename -o '\\{\"type\":\"plan_mode\"[^}]*\\}' \"$f\" | jq -r .planFilePath | sort -u | wc -l  (distinct paths), and the same piped through jq -r '.planExists|tostring' | sort -u  and  jq -r .reminderType | sort -u",
          "observed": "18 transcripts carry plan_mode attachments, 21 records total (15 transcripts x1 record, 3 transcripts x2 records). distinct planFilePath per transcript = 1 in 18/18. planExists value sets: 15 transcripts {false}, 2 transcripts {false,true} (the flip), 1 transcript {false} across both of its records. reminderType = {full} in 18/18.",
          "rule": "One row per transcript carrying at least one plan_mode attachment; a field is called stable when its sort -u cardinality within that transcript is 1.",
          "note": "The binary computes reminderType as U=(F!==null&&F.workshopDocPath!==v||I%X8n.FULL_REMINDER_EVERY_N_ATTACHMENTS===1)?\"full\":\"sparse\", where I counts plan_mode attachments since the last plan_mode_exit - so \"only planExists flips\" is a corpus observation, not a law. csift reads neither field, so the binding is unaffected. Code sites src/plan.rs 22-24 and 104-106 verified verbatim."
        }
      ]
    },
    {
      "id": "PLAN-003",
      "area": "plan",
      "behavior": "Claude Code writes Plan-Mode plans flat under ~/.claude/plans as <slug>.md, and a subagent's plan takes a -agent-<agentId> suffix (<slug>-agent-<agentId>.md). The name is three words ONLY when the slug was minted without a seed; with a seed it is <slugified-seed>-<adjective>-<noun>, so 4 of the 21 observed plan paths are not three-word. The suffix is the full agentId, which is a+16 hex for a plain subagent but embeds a dashed name for a teammate - not a bare hex in general. The name is not derivable from the session id. The plans directory is flat for Claude Code's own writes but is not exclusively Claude-Code-owned: 26 of 67 entries here are operator-created, including 2 subdirectories.",
      "depends": "Unchanged and reinforced: csift must never derive a plan path from a session id or a directory listing, because the directory also holds operator files and nested directories that no session binds.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "5-8",
          "snippet": "//! Claude Code stores Plan-Mode plans flat under `~/.claude/plans/` with a random\n//! three-word name (`nested-prancing-popcorn.md`); a subagent's plan gets an\n//! `-agent-<hex>` suffix. The name is NOT derivable from the session id - it is bound\n//! to the session by a record the transcript writes on entering Plan Mode:"
        }
      ],
      "instrument": "`ls ~/.claude/plans | sed 's/\\.md$//' | awk -F- '{print NF}' | sort | uniq -c` - 3 fields for a session plan, more for an `-agent-<hex>` one. Counting rule: one row per .md file.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 5; src/plan.rs module doc; SPEC.md section 6.7.1; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "ls -A ~/.claude/plans | rg -c '^[a-z]+-[a-z]+-[a-z]+\\.md$'  and  rg -c '^[a-z]+-[a-z]+-[a-z]+-agent-[0-9a-f]+\\.md$'  and the inverse listing; find ~/.claude/plans -mindepth 2 -name '*.md' | wc -l; dirname/word-field census over all 21 planFilePath values; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}function n_\\(.{300}'",
          "observed": "~/.claude/plans holds 67 top-level entries: 35 match <word>-<word>-<word>.md, 6 match <word>-<word>-<word>-agent-<id>.md, and 26 match neither (operator-parked .md backups, .js files, a .json, and 2 subdirectories holding 43 further .md files). All 21 planFilePath values sit directly in ~/.claude/plans - no subdirectory - so Claude Code's own writes are flat. Word-field counts of the 21 basenames: 17 have 3 fields, 2 have 4, 1 has 6, 1 has 10. Binary: function n_(n){let e=Q(),i=UN(e);if(Lf().markPlanPathServed(e),!n)return d(Ea(),`${i}.md`);return d(Ea(),`${i}-agent-${n}.md`)} - the name is <slug>.md, and the suffix is the agentId verbatim. Observed agentId shapes on disk: a+16hex (17 chars) and a<dashed-name>-<16hex> (31 and 42 chars seen); all 6 suffixed plan files use the 17-char form.",
          "rule": "One row per entry in ~/.claude/plans for the name census; one row per plan_mode attachment record for the directory and word-field census; field count = awk -F- '{print NF}' on the basename with .md stripped.",
          "note": "Code site src/plan.rs 5-8 verified verbatim. None of the 18 plan_mode-bearing transcripts is a subagent transcript (0 under a subagents/ path), so the -agent- suffix is evidenced by the binary and the 6 on-disk files rather than by any attachment record in this corpus."
        }
      ]
    },
    {
      "id": "PLAN-004",
      "area": "plan",
      "behavior": "The three-word plan name is drawn from a CSPRNG: randomBytes(4).readUInt32BE(0) % len indexes an adjective list, a gerund list and a mixed noun/surname list, with no session id anywhere in the draw. That pure three-word draw is the NO-SEED path. When getPlanSlug is called with a seed, the name is instead <slugified-seed>-<adjective>-<noun> (the gerund list unused), where the seed is slugified prompt text - so on that path the name DOES encode the opening words of the session, and 3 of 24 bound slugs here took it. The name is still never derivable from the session id.",
      "depends": "csift never derives or guesses a plan file name; the binding record is the only source, and a derivation from the session id would name a wrong file on every session.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "63",
          "snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
        },
        {
          "path": "src/plan.rs",
          "lines": "5-8",
          "snippet": "//! Claude Code stores Plan-Mode plans flat under `~/.claude/plans/` with a random\n//! three-word name (`nested-prancing-popcorn.md`); a subagent's plan gets an\n//! `-agent-<hex>` suffix. The name is NOT derivable from the session id - it is bound\n//! to the session by a record the transcript writes on entering Plan Mode:"
        }
      ],
      "instrument": "`strings` the installed Claude Code 2.1.258 bundle and grep for the word lists (e.g. `prancing`, `foraging`): the namer draws with randomBytes, not a seeded PRNG, and takes no session-id argument. Counting rule: presence of the three word arrays and absence of any session-id argument in the namer.",
      "located": {
        "claude_code": "2.1.227",
        "csift": "0.6.0",
        "source": "SPEC.md section 6.7.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'randomBytes as .{0,3}[,}].{0,120}'  and  rg -o '.{200}readUInt32BE\\(0\\)%.{60}'  and rg -o '.{0,900}\"babbage\".{0,900}' / '.{0,700}\"floofy\".{0,700}' / '.{150}\"foraging\".{150}'  and  rg -o '.{0,120}getPlanSlug\\(.{0,600}'",
          "observed": "Verbatim: 'randomBytes as o}from\"crypto\";var t=[\"abundant\",\"ancient\",\"bright\",\"calm\",...' then 'function g(e){return o(4).readUInt32BE(0)%e}function a(e){return e[g(e.length)]}function xVt(){let e=a(t),n=a(s),i=a(l);return `${e}-${n}-${i}`}'. Three arrays: t = adjectives (jolly, bubbly, elegant, distributed, ... plus some participles like frolicking, humming), s = gerunds (foraging, prancing, snacking, dancing, wishing, ...), l = concrete nouns then computer-scientist surnames (popcorn, kettle, marshmallow, whistle, ... babbage, backus, pike, shamir, stallman, ...). xVt() takes no argument. But its caller does: getPlanSlug(n,e){...let s=e?O$e(e):\"\";...r=s?`${s}-${PU()}`:xVt()...} with function PU(){let e=a(t),n=a(l);return `${e}-${n}`} and O$e slugifying the first 4 words of the seed, lowercased, non-alnum collapsed to dashes, capped at 40 chars. Corpus: of 24 distinct bound slugs, 21 are strict three-word and 3 are seeded.",
          "rule": "Counting rule as written in the claim - presence of the three word arrays and absence of any session-id argument in the namer - both pass. Slug shape counted as one row per distinct bound slug across the 26 top-level bindings, classified by the regex ^[a-z]+-[a-z]+-[a-z]+$.",
          "note": "The claim's 'carries zero information about the session' and 'unknowable before Plan-Mode entry' are both too strong: the seeded path encodes prompt text, and PLAN-005 shows the slug exists before Plan-Mode entry. The mechanism itself - CSPRNG, the exact modulo formula, three lists, no session id - matched the binary exactly. csift's dependency (never derive or guess a name) is unaffected. Code sites src/plan.rs 61-66 and 5-8 verified verbatim."
        }
      ]
    },
    {
      "id": "PLAN-005",
      "area": "plan",
      "behavior": "slug is one stable value per session (cardinality 1 in every transcript checked), absent from most transcripts (38 of 64 top-level ones carry none) and from metadata-only records, and Claude Code derives the plan file NAME from it (21/21 plan_mode records have basename(planFilePath) == \"<slug>.md\", and the binary's plan-path getter is d(getPlansDirectory(), `${getPlanSlug(session)}.md`)). It is NOT minted at Plan-Mode entry: it is minted lazily on the first request for a plan slug and is already present on records written before the plan_mode attachment in 18 of 18 cases, sometimes by tens of thousands of lines. A compaction is one of the minters (5 of 26 first carriers are system/compact_boundary).",
      "depends": "csift reads the slug off the binding record itself - the plan_mode attachment record always carries it - and its slug-only fallback walks the file from line 1 for the FIRST slug-carrying record, binding <plans-dir>/<slug>.md only when that record's slug passes the harness validity rule; a drift in when the slug is minted changes which record binds and therefore which plan file a session is reported bound to.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "45-50",
          "snippet": "    /// The session's plan slug (one stable value per session, minted BEFORE Plan Mode\n    /// is entered - the first slug-carrying record precedes the plan_mode line in every\n    /// measured session - and absent from every record before the mint and from\n    /// metadata-only records). The harness derives the plan file name from it.\n    #[serde(default)]\n    pub slug: Option<String>,"
        },
        {
          "path": "src/plan.rs",
          "lines": "63",
          "snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
        },
        {
          "path": "src/plan.rs",
          "lines": "63-68",
          "snippet": "    /// The session's plan slug (the harness derives the plan file name from it).\n    /// Read from the binding record itself: the `plan_mode` attachment record always\n    /// carries it. The slug is minted BEFORE Plan-Mode entry (measured at CC 2.1.258:\n    /// the first slug-carrying record precedes the `plan_mode` line in 18 of 18\n    /// transcripts), so earlier records may carry it too - the binding record is\n    /// simply the authoritative carrier. `None` when the record predates the field."
        },
        {
          "path": "src/plan.rs",
          "lines": "143-149",
          "snippet": "    if latest.is_none() {\n        if let Some((line_no, rec)) = first_slug_record(bytes) {\n            if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {\n                // The project root the harness resolves plansDirectory against is the\n                // session's ORIGINAL cwd - the transcript's first recorded cwd - not the\n                // slug record's own cwd, which follows the tracked shell cwd and may\n                // already sit in a subdirectory (section 3.11; v0.10.2)."
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "6-11",
          "snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n    static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));"
        }
      ],
      "instrument": "`csift search '\"slug\"' @<session> --raw | head -1` gives the first slug-carrying line; compare its line number to the `line` reported by `csift plan @<session> --format json`. Counting rule: first slug-carrying line by file order.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "src/plan.rs and src/model/record.rs comments"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "Per plan_mode-bearing transcript: first top-level slug line via jq -rR 'input_line_number as $n | (fromjson? // empty) | select(has(\"slug\")) | $n' \"$f\" | head -1, compared with rg -n -m1 '\"type\":\"plan_mode\"' \"$f\" | cut -d: -f1; first-carrier type census over all 64 top-level transcripts; basename(planFilePath)==\"<slug>.md\" check over all 21 plan_mode records; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}getPlanSlug\\(.{0,600}'",
          "observed": "In 18 of 18 plan_mode-bearing transcripts the first top-level slug record sits STRICTLY BEFORE the plan_mode line. Line pairs (first_slug -> first_plan_mode): 9->30, 10->14, 11->15, 39->49, 172->181, 179->186, 266->273, 268->274, 363->371 (x2), 717->718, 869->876 (x2), 1537->64713, 1549->11222, 1575->1584, 2042->2051, 2134->2141. First-carrier types among those 18: 11 assistant, 4 user, 2 system, 1 attachment - never the plan_mode record itself. Across all 64 top-level transcripts, 26 carry a top-level slug (first carriers: 12 assistant, 5 attachment, 5 system/compact_boundary, 4 user) and 38 carry none, while only 18 carry a plan_mode attachment - so at least 8 slug-bearing sessions never entered Plan Mode. Distinct slug values per transcript = 1 everywhere checked. Binary: getPlanSlug(n,e) mints on first request per session and caches it (i.set(n,r)); the plan-path getter n_() calls it unconditionally, so any code path that needs a plan path mints the slug.",
          "rule": "One row per transcript. First slug carrier = the first line whose PARSED record has a top-level slug key (byte hits were confirmed top-level: 0 of 26 resolved to a nested occurrence). Strictly-before = first_slug_line < first_plan_mode_line.",
          "note": "csift's CODE is unaffected: first_slug_record walks from line 1, so it finds the true first carrier regardless of where the mint happened, and the plan_mode branch reads the slug off the binding record. Only the doc comments misstate the mechanism - and the 'absent from the whole head window' clause is the actively misleading one, since a head-window reader could legitimately find the slug. code-site note: src/plan.rs 61-66 and src/model/record.rs 45-49 are verbatim-present but their prose is wrong. src/plan.rs 61-66 says 'the slug is minted at Plan-Mode entry, so it is absent from every earlier record (including the whole head window), but always present on the plan_mode attachment record'; only the last clause survives (21/21). src/model/record.rs 45-49 says 'minted when the session enters Plan Mode; ABSENT from every record before that entry'; both clauses are contradicted. Suggested replacement for both: 'one stable value per session, minted lazily the first time a plan slug is requested (Plan-Mode entry, a compaction, or any plan-path lookup) and cached for the session's life; absent from sessions that never needed one and from metadata-only records. Claude Code derives the plan file name from it.'"
        }
      ]
    },
    {
      "id": "PLAN-006",
      "area": "plan",
      "behavior": "When no `plan_mode` attachment exists anywhere in a transcript, Claude Code still binds a plan: its own rule takes the FIRST record in the log carrying a valid `slug` and binds `<plans-dir>/<slug>.md`, consulting no attachment.",
      "depends": "csift adopted the same second law with disclosed provenance (`binding_source:\"slug-only\"`), so a session whose plan Claude Code will still inject is no longer answered with \"no plan\".",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "137-142",
          "snippet": "    // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n    // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n    // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n    // forked/background session reaches this state by construction (the clone strips\n    // attachment history), and its slug is often minted by the first own compaction -\n    // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file."
        },
        {
          "path": "src/plan.rs",
          "lines": "145",
          "snippet": "if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "6-13",
          "snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n    static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));\n    let mut line_no = 0usize;\n    for line in bytes.split(|&b| b == b'\\n') {"
        }
      ],
      "instrument": "`csift plan <project> --format json | jq 'select(.binding_source==\"slug-only\")'`, then confirm the named transcript carries zero plan_mode attachments (`csift search 'plan_mode' @<id> --attachments -c` = 0) and that its `plan_file` equals `<plans-dir>/<first valid slug>.md`. Counting rule: one row per resolved session.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; CHANGELOG 0.9.4; src/plan.rs comment; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{900}getSlugFromLog: rejecting.{900}'; then, for each of the 64 top-level transcripts, csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")', cross-checked against rg -c '\"type\":\"plan_mode\"' \"$f\" and the first '\"slug\":\"' line number.",
          "observed": "Binary, verbatim: var ue=/^[a-z0-9][a-z0-9-]{0,119}$/;function j(n){let e=n.messages.find((i)=>i.slug)?.slug;if(e===void 0)return;if(!ue.test(e)){t(`getSlugFromLog: rejecting malformed transcript slug (${e.length} chars)`);return}return e} and its caller: async function Z1e(n,e,i){let r=j(n);if(!r)return!1;let s=e??Q();if(yqt(s,r),i&&g())return pe(i,n,r).catch(...);let o=d(Ea(),`${r}.md`);...} where yqt is setPlanSlug and Ea is getPlansDirectory - the FIRST message carrying a slug, validated, then the plan file is <plans-dir>/<slug>.md, with no attachment consulted. On disk: 26 top-level bindings = 18 plan_mode + 8 slug-only (3 of the 8 minted_at_compaction). All 8 slug-only transcripts contain 0 plan_mode records; csift's reported line equals the file's first '\"slug\":\"' line in 8/8 (804, 1099, 1471, 1663, 2565, 2981, 3343, 4010); basename == \"<first slug>.md\" in 8/8, all under ~/.claude/plans. First carriers of those 8: 3 system/compact_boundary, 4 attachment, 1 assistant.",
          "rule": "One row per top-level transcript resolved by csift plan; a binding counts as slug-only when binding_source==\"slug-only\", and is cross-checked by requiring rg -c '\"type\":\"plan_mode\"' == 0 on the same file.",
          "note": "Both halves confirmed independently: Claude Code's own law from the binary (find-first + regex validation + <plans-dir>/<slug>.md) and csift's adoption of it on every slug-only session in the corpus. Code sites src/plan.rs 134-139, 140-155 and 161-168 verified verbatim."
        }
      ]
    },
    {
      "id": "PLAN-007",
      "area": "plan",
      "behavior": "A `slug` binds only if it passes Claude Code's validity rule: at most 120 characters, a lowercase-alphanumeric head, and a tail of lowercase alphanumerics or dashes.",
      "depends": "csift's slug-only fallback applies the same predicate, so a tolerated `slug` value CC itself would reject never produces a bound plan path.",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "27-37",
          "snippet": "/// Claude Code's slug validity rule (lowercase alnum head, alnum/dash tail, <=120\n/// chars) - a stray tolerated `slug` value that CC itself would reject never binds.\npub(crate) fn slug_is_valid(s: &str) -> bool {\n    let mut chars = s.chars();\n    let Some(head) = chars.next() else {\n        return false;\n    };\n    s.len() <= 120\n        && (head.is_ascii_lowercase() || head.is_ascii_digit())\n        && chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')\n}"
        }
      ],
      "instrument": "Under `--claude-home <fixture>`, a transcript whose only slug fails the rule (uppercase head) must make `csift plan @<id>` report no binding, while the same fixture with a valid slug binds `<plans-dir>/<slug>.md`. Counting rule: one binding decision per transcript.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{900}getSlugFromLog: rejecting.{900}'  for the regex; then five fixtures under csift --claude-home <fixture>/.claude plan @<id> --format json, each a two-line transcript whose only slug-carrying record has the slug under test and which carries no plan_mode attachment.",
          "observed": "Claude Code's rule, verbatim: var ue=/^[a-z0-9][a-z0-9-]{0,119}$/ - a lowercase-alnum head plus at most 119 more characters from [a-z0-9-], i.e. 120 total. Fixture results: 'Jolly-prancing-popcorn' (uppercase head) -> no binding; '-jolly-prancing' (dash head) -> no binding; 'jolly_prancing' (underscore in tail) -> no binding; a 121-character all-'a' slug -> no binding; a 120-character all-'a' slug -> binds <claude-home>/plans/<120 a's>.md. csift's slug_is_valid (head is_ascii_lowercase||is_ascii_digit, tail all lowercase/digit/dash, s.len()<=120) accepts exactly the same language.",
          "rule": "One binding decision per fixture transcript; 'no binding' means csift plan --format json emitted only the header and summary envelope lines and no kind==\"plan\" row.",
          "note": "The byte-length vs code-unit question is moot: the accepted alphabet is ASCII-only, so csift's s.len()<=120 in bytes and the regex's {0,119} in UTF-16 code units agree on every string either one accepts. Code site src/plan.rs 182-192 verified verbatim."
        }
      ]
    },
    {
      "id": "PLAN-008",
      "area": "plan",
      "behavior": "Claude Code resolves `plansDirectory` from the merged settings scopes (user, then the project's `.claude/settings.json`, then `.claude/settings.local.json`, the more specific scope winning), joins a relative value to the process's current project cwd through the same getter that stamps every record's top-level `cwd` (the async-local cwd, else the project cwd, the original cwd only when that getter throws), refuses a result that escapes that cwd, and falls back to `<claude-home>/plans`. The resolved directory is memoized at its first access, so the binding follows the cwd as it stood early in the session, before any tracked-shell `cd` moved the per-record value.",
      "depends": "csift's slug-only binding calls `plans_dir(root)` with `root` = the transcript's FIRST recorded `cwd` (`first_cwd`, the earliest instant csift can observe), reads the same three scopes in the same precedence from that root, joins and containment-checks against it, and falls back to `<claude-home>/plans`. Before v0.10.2 the root was the slug-carrying record's own `cwd`, which can already have drifted into a subdirectory: the plan file was then joined under the subdirectory and a project-scope `plansDirectory` was silently dropped. A session resumed from another directory re-memoizes in the harness, which the first-record instrument cannot see (documented limit).",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "82-96",
          "snippet": "    let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n    candidates.push(root.join(\".claude\").join(\"settings.json\"));\n    candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n    let mut value: Option<String> = None;\n    for p in candidates {\n        let Ok(raw) = std::fs::read_to_string(&p) else {\n            continue;\n        };\n        let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n            continue;\n        };\n        if let Some(d) = v.get(\"plansDirectory\").and_then(serde_json::Value::as_str) {\n            value = Some(d.to_string()); // later (more specific) scopes win\n        }\n    }"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "97-107",
          "snippet": "    let Some(d) = value else {\n        return default;\n    };\n    let joined = crate::path::lexical_normalize(&root.join(d));\n    let root_n = crate::path::lexical_normalize(root);\n    if joined.starts_with(&root_n) {\n        joined\n    } else {\n        default\n    }\n}"
        },
        {
          "path": "src/plan.rs",
          "lines": "144-151",
          "snippet": "        if let Some((line_no, rec)) = first_slug_record(bytes) {\n            if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {\n                // The project root the harness resolves plansDirectory against is the\n                // session's ORIGINAL cwd - the transcript's first recorded cwd - not the\n                // slug record's own cwd, which follows the tracked shell cwd and may\n                // already sit in a subdirectory (section 3.11; v0.10.2).\n                let root_owned = first_cwd(bytes).or_else(|| rec.cwd.clone());\n                let root = root_owned.as_deref().map(Path::new);"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "55-70",
          "snippet": "pub(crate) fn first_cwd(bytes: &[u8]) -> Option<String> {\n    let mut pos = 0usize;\n    let mut seen = 0usize;\n    while pos < bytes.len() && seen < 256 {\n        let end = memchr::memchr(b'\\n', &bytes[pos..]).map_or(bytes.len(), |i| pos + i);\n        let line = &bytes[pos..end];\n        pos = end + 1;\n        seen += 1;\n        if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n            if let Some(c) = rec.cwd.as_deref().filter(|c| !c.is_empty()) {\n                return Some(c.to_string());\n            }\n        }\n    }\n    None\n}"
        }
      ],
      "instrument": "Set `plansDirectory` in a fixture `settings.json` under `--claude-home`, then `csift plan @<id> --format json | jq -r .plan_file` names that directory; with the key unset the same session resolves under `<claude-home>/plans`. Counting rule: one directory lookup per invocation.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.4",
        "source": "SPEC.md section 6 v0.9.4 ledger; src/plan.rs comment; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{600}plansDirectory must be within project root.{600}'  and  rg -o '.{300}plansDirectory.{0,200}'  for the settings-schema description; then a csift fixture: csift --claude-home <fixture>/.claude plan @<id> --format json | jq -r .plan_file with plansDirectory unset, set to the relative value \"elsewhere\", and set to an absolute path.",
          "observed": "Claude Code's settings schema, verbatim: plansDirectory:i().optional().describe(\"Custom directory for plan files, relative to project root. If not set, defaults to ~/.claude/plans/\"). Resolver, verbatim: #n(){let e=Je().plansDirectory;if(e){let i=ne(),r=O(i,e);if(le(r,i))return r;t(`plansDirectory must be within project root: ${e}`,{level:\"error\"})}return F()} with function F(){return d(Se(),\"plans\")} and the containment guard function le(n,e){if(n!==e&&!n.startsWith(e+B))return!1;...let i=NS(e);if(i===null)return!1;let r=n;for(;;){let s=NS(r);if(s!==null)return s===i||s.startsWith(i+B);let o=A(r);if(o===r)return!1;r=o}}. csift fixture: unset -> <claude-home>/plans/<slug>.md; \"elsewhere\" -> <claude-home>/elsewhere/<slug>.md; an absolute path -> that path verbatim.",
          "rule": "One directory lookup per invocation; the fixture compares the basename-stripped directory of the reported plan_file across three settings.json states of one otherwise identical transcript.",
          "note": "This machine has no plansDirectory set in ~/.claude/settings.json, so no transcript in the corpus exercises the setting and the divergence could not be observed on a Claude-Code-written record. It is established from two independent binary strings - the user-facing schema description and the resolver plus its error message - against a csift fixture. A live confirmation would need a session started in a project whose settings set plansDirectory, then comparing the plan_mode attachment's planFilePath against csift plan's answer."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "drifted",
          "instrument": "installed csift 0.10.1 against a synthetic --claude-home tree: a slug-only transcript whose record 1 carries cwd=<root> and record 2 (the slug carrier) cwd=<root>/sub; three runs of `csift plan @<id> --format json` with plansDirectory=myplans in <claude-home>/settings.json, then only in <root>/.claude/settings.json, then only in <root>/sub/.claude/settings.json; the harness mechanism read from the 2.1.258 binary's plans-directory resolver",
          "observed": "run 1: plan_file <root>/sub/myplans/<slug>.md (joined under the drifted cwd); run 2: <claude-home>/plans/<slug>.md (the project-scope setting dropped); run 3: <root>/sub/subplans/<slug>.md (the settings anchor was the slug record's cwd); the binary resolves through the cwd getter that also stamps records and memoizes the directory on first access",
          "rule": "one run = one plan call; a wrong binding = a plan_file not under <root>/myplans; the harness mechanism is read from the shipped binary, not run",
          "note": "csift 0.10.2 binds against the transcript's first recorded cwd (plan/slug.rs first_cwd) and reads the settings scopes from that root; both wrong bindings are pinned by the e2e plan_slug_binding_resolves_plans_directory_against_the_first_recorded_cwd"
        }
      ]
    },
    {
      "id": "PLAN-009",
      "area": "plan",
      "behavior": "The plan approval carrier is a tool_result on a type:\"user\"/role:\"user\" record, with no is_error key and no typed user message. There are two variants: the normal one begins 'User has approved your plan.' and names where the plan was saved, and an empty-plan one reads 'User has approved exiting plan mode. You can now proceed.' and names no path. Both are structurally the harness greenlight, distinct from both rejection shapes.",
      "depends": "csift never treats the approval as a user message or a turn boundary; counting `role:user` carriers instead would fabricate one human turn per approved plan.",
      "code": [
        {
          "path": "src/model/tests/boundaries.rs",
          "lines": "276-342",
          "snippet": "#[test]\nfn plan_approval_is_not_a_boundary() {\n    // The approval path is the harness greenlight (no typed message, no is_error) -\n    // must NOT become a turn boundary.\n    let r = parse(\n        r#\"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"t\",\"content\":\"User has approved your plan. You can now start coding. Your plan has been saved to: /Users/testuser/.claude/plans/elegant-scribbling-dream.md\"}]}}\"#,\n    );\n    assert!(!r.is_plan_rejection_boundary());\n    assert!(!r.is_auq_answer_boundary());\n    assert!(!r.opens_turn());\n}"
        }
      ],
      "instrument": "rg -c 'User has approved your plan' over the corpus does NOT count the carriers - it over-counts 5.3x (64 byte hits vs 12 real carriers) because csift's own specs quote the string and dev sessions read those files back through tool results. Count carriers structurally: select tool_result blocks whose flattened content starts with the phrase.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 4.2.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -c 'User has approved your plan' -g '*.jsonl' . (raw byte count) versus a structural jq census selecting tool_result blocks whose flattened content starts with 'User has approved'; then csift show \"$f\" --line \"$ln\" --format json | jq -rc '[(.labels|join(\"+\")),(.opens_turn|tostring)]' on every carrier; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -n 'User has approved your plan' with the following strings-lines.",
          "observed": "Binary, 2.1.258, two approval results, both {type:\"tool_result\",content:...,tool_use_id:v} with no is_error field: if(!n||n.trim()===\"\")return{type:\"tool_result\",content:\"User has approved exiting plan mode. You can now proceed.\",tool_use_id:v} and otherwise a template beginning 'User has approved your plan. You can now start coding. Start with updating your todo list if applicable' / 'Your plan has been saved to: ' / 'You can refer back to it if needed during implementation.'. On disk: the phrase occurs 64 times across 30 transcripts as raw bytes, but only 12 records carry it as an actual tool_result block's leading text - a 5.3x over-count. All 12 are type:\"user\", role:\"user\", have no is_error key, have string (not array) content, and do name the saved path. Zero records carry the phrase as a whole string message.content. csift labels all 12 agent.tool.result as a single label with opens_turn false.",
          "rule": "One row per tool_result BLOCK whose content, flattened across the string and array forms, starts with 'User has approved' - not one row per raw byte occurrence. The label census is one row per such carrier.",
          "note": "The 2.1.258 wording inserts 'Start with updating your todo list if applicable' after 'You can now start coding.'; the fixture in src/model/tests/boundaries.rs 323-333 uses the older wording. That is inert - csift matches nothing on this string, it simply asserts the record is not a boundary - but the fixture is now a historical rather than a current sample. The test itself is present verbatim at the claimed lines and still passes the behavior it pins: all 12 real carriers classify agent.tool.result and open no turn."
        }
      ]
    },
    {
      "id": "PLAN-010",
      "area": "plan",
      "behavior": "For an ExitPlanMode REJECTION the rejected `tool_use_id` is the id of the ExitPlanMode `tool_use`, whose `input.planFilePath` names the plan under discussion; an AskUserQuestion-clarify rejection carries no plan path.",
      "depends": "csift builds a `PlanIndex` (`tool_use_id` to `planFilePath`) once per session and appends a `[plan: <path>]` pointer to the reconstructed rejection text, so a consuming LLM can go Read the rejected plan; with no index the rejection text is returned alone.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "225-235",
          "snippet": "    /// - an answered AskUserQuestion → the full Q+options+answer unit\n    ///   ([`Record::auq_exchange`]);\n    /// - a tool-use rejection-with-message → the user's typed instruction, optionally\n    ///   suffixed with a `[plan: <path>]` pointer when `plan_index` resolves the rejected\n    ///   `tool_use_id` to an ExitPlanMode plan (§4.2.4). `plan_index` may be `None` (no\n    ///   plan resolution attempted), in which case the rejection text is returned alone.\n    ///\n    /// Returns `None` when this record does not open a turn. CODEPOINT-SAFE throughout\n    /// (delegates to the codepoint-safe accessors).\n    #[must_use]\n    pub fn reconstructed_user_text(&self, plan_index: Option<&PlanIndex>) -> Option<String> {"
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "5-9",
          "snippet": "/// An index of ExitPlanMode `tool_use_id → planFilePath` built from a session's records\n/// (§4.2.4). A tool-use rejection-with-message ([`Record::plan_rejection_message`])\n/// resolves the rejected `tool_use_id` through this index to surface a `[plan: <path>]`\n/// pointer so a consuming LLM can go Read the plan. Built once per session via\n/// [`PlanIndex::from_records`]; cheap (one `BTreeMap` of the few ExitPlanMode calls)."
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "281-286",
          "snippet": "    /// The ExitPlanMode tool_use blocks carried by this (assistant) record, as\n    /// `(tool_use_id, plan_file_path)` pairs - the raw material a [`PlanIndex`] is built\n    /// from. `plan_file_path` prefers `input.planFilePath`; a block with no path yields\n    /// an empty string (still indexed so the id is known to be an ExitPlanMode). Empty\n    /// for any record carrying no ExitPlanMode tool_use.\n    #[must_use]"
        }
      ],
      "instrument": "`csift search 'To tell you how to proceed' @<id>` - an ExitPlanMode rejection renders the typed instruction with a trailing `[plan: <path>]`, an AUQ-clarify rejection renders none. Counting rule: one pointer per rejection whose rejected id is in that session's ExitPlanMode index.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 4.2.4; src/model/exchange.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Structural jq census over every '\"name\":\"ExitPlanMode\"' record for input.planFilePath / input.plan; then, for every tool_result carrying 'To tell you how to proceed' with is_error==true, a per-file join of its tool_use_id against that file's tool_use id->name map; then csift show \"$f\" --line \"$ln\" | rg -c '\\[plan: ' on each; and strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}case kc:\\{let d=n_\\(r\\).{0,180}'",
          "observed": "28 of 28 ExitPlanMode tool_use blocks on disk carry both input.planFilePath and input.plan. Binary, verbatim: case kc:{let d=n_(r);Lp(d);let f=BN(r);return H1(o),f!==null?{...n,plan:f,planFilePath:d}:n} - normalizeToolInput injects the plan path and content together, or neither. Genuine rejections (tool_result with is_error:true carrying the 'To tell you how to proceed, the user said:' tail): 10 total - 3 rejected an ExitPlanMode tool_use id, and all 3 of those tool_uses carry planFilePath; 7 rejected an AskUserQuestion tool_use id, and none of those carries a planFilePath. csift renders a trailing [plan: <path>] pointer on 3 of 3 ExitPlanMode rejections and 0 of 7 AskUserQuestion rejections.",
          "rule": "One row per tool_result block containing the typed-tail phrase AND carrying is_error:true, joined by tool_use_id to the issuing tool_use's name within the same file; the pointer test is one row per such rejection.",
          "note": "Counting-rule trap worth recording: without the is_error:true requirement the same phrase also matches 59 Bash and 80 Read tool_results, which are file contents echoed back rather than rejections - requiring is_error:true is what isolates the real ones. Edge case not observed here (0 of 28): the binary omits BOTH plan and planFilePath from the ExitPlanMode input when the plan content cannot be read, so a rejection of such a call would carry no pointer; csift still indexes the id (an empty path) and returns the rejection text alone, which is the correct degradation. Code sites src/model/exchange.rs 225-235 and 281-286 and src/model/grouping.rs 5-9 verified verbatim."
        }
      ]
    },
    {
      "id": "PLAN-011",
      "area": "plan",
      "behavior": "A session carries exactly one slug value for its whole life: across the corpus 26 top-level transcripts carry a `slug` and zero carry more than one distinct value (counting rule: distinct slug values per file).",
      "depends": "csift does not model slug transitions, and its fallback stops at the FIRST slug-carrying record via an early-exit walk - sound only because the value never changes.",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "6-13",
          "snippet": "/// The FIRST record carrying a `slug` field (Claude Code's binding key), by a\n/// sequential early-exit walk - the fallback runs only when no `plan_mode` exists, and\n/// a slugged session's first carrier normally sits early after the mint point.\npub(crate) fn first_slug_record(bytes: &[u8]) -> Option<(usize, crate::model::Record)> {\n    static SLUG: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"slug\\\"\"));\n    let mut line_no = 0usize;\n    for line in bytes.split(|&b| b == b'\\n') {"
        }
      ],
      "instrument": "`for f in ~/.claude/projects/*/*.jsonl; do jq -r 'select(.slug)|.slug' \"$f\" | sort -u | wc -l; done | sort | uniq -c` - only 0 and 1 appear. Counting rule: distinct slug values per transcript.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "CHANGELOG 0.9.4; SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects && rg -l '\"slug\"' --glob '*.jsonl' --max-depth 2 . > slugfiles.txt; wc -l < slugfiles.txt; for f in $(cat slugfiles.txt); do rg --no-filename '\"slug\"' \"$f\" | jq -r 'select(.slug)|.slug' | sort -u | wc -l; done | sort | uniq -c",
          "observed": "26 top-level transcripts contain the byte sequence \"slug\"; the per-file distinct-top-level-slug histogram is `26 1` - every one of the 26 reports exactly 1 distinct value, none reports 0 or >=2.",
          "rule": "One row per top-level transcript (a `*.jsonl` at depth 2 under ~/.claude/projects, i.e. <encoded-project-dir>/<session>.jsonl; subagent transcripts sit at depth 4 and are excluded). Row value = number of distinct values of the TOP-LEVEL `.slug` field, counted by `jq 'select(.slug)|.slug' | sort -u | wc -l` over the `\"slug\"`-prefiltered lines.",
          "note": "Numbers reproduce the claim exactly (26 transcripts, zero with more than one value). One trap worth recording for anyone rerunning this: the counting rule must be the top-level `.slug` field, not a byte scan. `rg -o '\"slug\"\\s*:\\s*\"[^\"]*\"'` returns TWO distinct values on one of the 26 transcripts - the session's real three-word slug (2624 occurrences) and that same slug prefixed with a progress counter, `483/483 <slug>` (21 occurrences). The second lives inside nested tool-call and attachment payloads, never in the top-level field; re-run through `jq 'select(.slug)'` and that file collapses to 1. The claim's own instrument already uses jq, so the behavior sentence stands as written. Code site src/plan.rs:161-168 confirmed verbatim (the `first_slug_record` early-exit walk over a `\"slug\"` memmem finder)."
        }
      ]
    },
    {
      "id": "PLAN-012",
      "area": "plan",
      "behavior": "csift's `minted_at_compaction` flag means exactly what its code says - the first slug-carrying record is itself a `system`/`compact_boundary` - and it is only ever set on the `slug-only` fallback path. Measured over the 26 slug-bearing top-level transcripts, the first slug carrier is one of FOUR record types: `assistant` 12, `attachment` 5, `system`/`compact_boundary` 5, `user` 4. Exactly 8 transcripts carry no `plan_mode` attachment at all and therefore bind `slug-only`; of those, 3 are boundary-first (`minted_at_compaction:true`), 1 is assistant-first and 4 are attachment-first (`minted_at_compaction:false`). Measured examples: a boundary-first slug-only transcript with 2083 slug-bearing records and one distinct value; an assistant-first transcript with 69933 slug-bearing records and one distinct value.",
      "depends": "csift sets `minted_at_compaction` when the first slug carrier is a compact boundary. It is a positive signal that a slug arrived at a compaction rather than at Plan-Mode entry, but it is NOT a two-way discriminator for whether Plan Mode ran - `binding_source` is what carries that, and a `slug-only` binding with `minted_at_compaction:false` is the common case (5 of 8 measured).",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "76-80",
          "snippet": "    /// True when the first slug-carrying record is a `compact_boundary` itself: the\n    /// slug was MINTED at that compaction (Plan Mode never ran; Claude Code will\n    /// still inject or rebuild the file it names) - the forked-session signature.\n    pub minted_at_compaction: bool,\n}"
        },
        {
          "path": "src/plan.rs",
          "lines": "145",
          "snippet": "if let Some(slug) = rec.slug.as_deref().filter(|s| slug_is_valid(s)) {"
        }
      ],
      "instrument": "For each slug-bearing transcript take the first line containing `\"slug\"` and print its `type`/`subtype`: `system/compact_boundary` marks the minted-at-compaction case (counting rule: one row per slug-bearing transcript, first carrier only). Then `csift plan @<id> --format json | jq '{slug, binding_source, minted_at_compaction}'`.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "CHANGELOG 0.9.4; SPEC.md section 6 v0.9.4 ledger; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && for f in $(cat slugfiles.txt); do first=$(rg --no-filename '\"slug\"' \"$f\" | jq -rc 'select(.slug)|[.type,(.subtype//\"-\")]|@tsv' | head -1); att=$(rg --no-filename 'plan_mode' \"$f\" | jq -r 'select(.attachment.type==\"plan_mode\")|\"x\"' | wc -l); row=$(csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|[.binding_source,(.minted_at_compaction|tostring)]|@tsv' | head -1); printf '%s\\t%s\\t%s\\n' \"$first\" \"$att\" \"$row\"; done | sort | uniq -c",
          "observed": "26 slug-bearing top-level transcripts. First-slug-carrier record type crossed with the csift binding: `assistant` 11 -> plan_mode/false and 1 -> slug-only/false; `attachment` 1 -> plan_mode/false and 4 -> slug-only/false; `system`+`compact_boundary` 2 -> plan_mode/false and 3 -> slug-only/TRUE; `user` 4 -> plan_mode/false. So first-carrier types are assistant 12, attachment 5, system/compact_boundary 5, user 4; `plan_mode` attachment records number 0 in exactly 8 transcripts, and those 8 are precisely the 8 that read `slug-only`. Slug-bearing record counts on the five boundary-first transcripts: 169, 2083, 2670, 31385, 94111. One assistant-first transcript carries 69933 slug-bearing records with one distinct value.",
          "rule": "One row per slug-bearing top-level transcript (the same 26 as PLAN-011). First carrier = the `type`/`subtype` of the FIRST line whose top-level `.slug` is non-null, in file order. `plan_mode` attachment count = records with `attachment.type==\"plan_mode\"`. Binding = the `binding_source`/`minted_at_compaction` of the single `kind:\"plan\"` row csift emits for that file under `--no-subagents`.",
          "note": "The mechanism csift implements is confirmed by instrument (3 transcripts read slug-only with minted_at_compaction:true, and all 3 are boundary-first with zero plan_mode attachments), and the claim's 69,933-record assistant-first transcript still exists with one distinct slug value. What is refuted is the claim's implied biconditional, in both directions: 2 of the 5 boundary-first transcripts DO carry `plan_mode` attachments, so they bind via `plan_mode` and never reach the slug fallback at all; and 5 of the 8 transcripts with no `plan_mode` attachment have a non-boundary first carrier. The `attachment`-first bucket has a clean explanation the instrument surfaced: on one transcript the `plan_mode` attachment record and the first slug carrier are the SAME record (identical timestamp, `type:\"attachment\"`), which is precisely what src/plan.rs:61-66 documents. The claim's 480-slug-record forked transcript is no longer identifiable; boundary-first transcripts now carry 169 / 2083 / 2670 / 31385 / 94111 slug-bearing records. Code sites src/plan.rs:73-77 and src/plan.rs:140-155 confirmed verbatim."
        }
      ]
    },
    {
      "id": "PLAN-013",
      "area": "plan",
      "behavior": "A transcript minted by copying another session at a compaction has NO `plan_mode` attachment of its own - the fork strips the attachment history - so Claude Code's binding for it falls to the first-valid-slug law. The fork strips the SLUG as well: the clone's head `compact_boundary` (its first timestamped record) carries `slug:null`, and its first slug-carrying record arrives 1443 lines and roughly 23 hours later at the clone's OWN next compaction, minting a slug DIFFERENT from the origin's. Claude Code does re-inject a plan into the clone in full, twice over: at the fork instant it re-injects the plan bound to the session it was forked FROM, and after each later compaction it re-injects the clone's own newly-minted plan.",
      "depends": "csift's `slug-only` fallback plus `minted_at_compaction` exists for exactly this shape; binding on the attachment alone answered \"no plan\" for a session whose plan Claude Code re-injects in full.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "70-75",
          "snippet": "    /// How the binding was established: `plan_mode` (the explicit attachment, path\n    /// verbatim) or `slug-only` (no `plan_mode` anywhere; the FIRST slug-carrying\n    /// record binds `<plans-dir>/<slug>.md` - Claude Code's own binding law, which a\n    /// forked/background session reaches because the fork strips its history's\n    /// attachments, and any compaction mints a slug even without Plan Mode).\n    pub binding_source: &'static str,"
        },
        {
          "path": "src/plan.rs",
          "lines": "137-142",
          "snippet": "    // No `plan_mode` anywhere: fall back to Claude Code's ACTUAL binding law - the\n    // FIRST record carrying a `slug` binds `<plans-dir>/<slug>.md` (the harness's\n    // getSlugFromLog takes the first slug in the log, no attachment consulted). A\n    // forked/background session reaches this state by construction (the clone strips\n    // attachment history), and its slug is often minted by the first own compaction -\n    // reporting \"no plan\" there is a WRONG answer: CC will inject/rebuild that file."
        }
      ],
      "instrument": "Pick a session `csift list --format json | jq -r 'select(.clone_of)'` reports as a clone, then `csift plan @<that id> --format json | jq -r '[.binding_source, (.minted_at_compaction|tostring)] | @tsv'` reads `slug-only`. Counting rule: one binding per transcript.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.4",
        "source": "AGENTS.md section 3.5; CHANGELOG 0.9.4; csift GOLD plan v0.9.4 round; dev sessions 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift list --format json --no-subagents --max-count 0 | jq -rc 'select(.clone_of)|[.session_id,.clone_of]|@tsv'  ;  csift plan <clone>.jsonl --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|{binding_source,minted_at_compaction,plan_exists,line}'  ;  rg --no-filename 'plan_mode' <clone>.jsonl | jq -r 'select(.attachment.type==\"plan_mode\")|\"x\"' | wc -l  ;  python3 walk over the clone printing, in file order, every `compact_boundary` (line, slug, trigger, ts) and every `attachment.type==\"plan_file_reference\"` (line, planFilePath basename, len(planContent), ts)",
          "observed": "Exactly 1 clone in the corpus. Clone: 0 `plan_mode` attachment records; csift reports {\"binding_source\":\"slug-only\",\"minted_at_compaction\":true,\"plan_exists\":true,\"line\":1471}. Its origin reports {\"binding_source\":\"plan_mode\",\"minted_at_compaction\":false,\"plan_exists\":true,\"line\":1584}, and the two bind DIFFERENT plan files (different three-word slugs). Clone in file order: L28 BOUNDARY slug=null trigger=auto 2026-08-31T11:41:41Z (its first timestamped record); L43 plan_file_reference naming the ORIGIN's plan file, planContent 521705 chars, 2026-08-31T11:41:39Z; L1471 BOUNDARY slug=<the clone's own> trigger=auto 2026-09-01T10:17:05Z - the first slug-carrying record, 1443 lines and 22h35m after the head boundary; L2175 BOUNDARY manual then L2185 plan_file_reference naming the clone's own plan, 553003 chars; L3254 BOUNDARY manual then L3264 same, 487001 chars; L5251 BOUNDARY auto then L5258 same, 493085 chars. Clone total 5530 lines.",
          "rule": "One binding per transcript (csift's single `kind:\"plan\"` row under `--no-subagents`). Clone set = transcripts for which `csift list --format json` emits a non-null `clone_of`. Re-injection events = records with `attachment.type==\"plan_file_reference\"`, one row each, compared by `planFilePath` against that transcript's own csift-resolved bound plan.",
          "note": "Both halves that matter for csift are confirmed by instrument: the one clone in the corpus has zero `plan_mode` attachments, binds `slug-only` with `minted_at_compaction:true`, and the file its slug names exists on disk. The clause needing correction is 'keeping the slug-carrying records' - the clone kept none of the origin's. The 'will still inject or rebuild the file that slug names' half is now confirmed directly rather than inferred: the clone carries four `plan_file_reference` attachments, each a full `planContent` payload of roughly half a megabyte, three naming its own minted plan and landing 7-10 lines after a `compact_boundary`. The fourth, 15 lines after the head boundary, names the ORIGIN's plan - the one case in the whole corpus where a re-injection does not name the transcript's own csift-resolved binding, and it is exactly the fork instant, before the clone's own slug existed. Code sites src/plan.rs:67-72 and src/plan.rs:134-139 confirmed verbatim."
        }
      ]
    },
    {
      "id": "PLAN-014",
      "area": "plan",
      "behavior": "The plan name is minted and the `plan_mode` attachment written at Plan-Mode ENTRY, while the `.md` file itself lands on disk only when plan content is first written - an arbitrary gap in which a session is bound to a plan file that does not exist (the attachment's own `planExists` is false).",
      "depends": "csift reports `plan_exists:false` (text `[missing]`) as the accurate state of that window rather than erroring, and `recover --file @plan` can still rebuild the content from the transcript alone.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "58-60",
          "snippet": "    /// Whether that plan file currently exists on disk (a recover target need NOT exist -\n    /// recovering a deleted plan from the transcript is the whole point).\n    pub plan_exists: bool,"
        },
        {
          "path": "src/plan.rs",
          "lines": "63",
          "snippet": "/// The session's plan slug (the harness derives the plan file name from it)."
        }
      ],
      "instrument": "Decidable from the corpus alone, no live session required: the `plan_mode` attachment carries its own `planExists` field, so `rg --no-filename 'plan_mode' <transcript> | jq -rc 'select(.attachment.type==\"plan_mode\")|(.attachment.planExists|tostring)'` reads the state of the window retroactively. The first `plan_mode` attachment of a transcript is `planExists:false` in 18 of 18 measured transcripts, and the flip to `true` (seen on 2 transcripts) always comes later in file order.",
      "located": {
        "claude_code": "2.1.227",
        "csift": "0.8.1",
        "source": "SPEC.md section 6.7.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && for f in $(find . -maxdepth 2 -name '*.jsonl'); do rg --no-filename 'plan_mode' \"$f\" | jq -rc 'select(.attachment.type==\"plan_mode\")|(.attachment.planExists|tostring)'; done | sort | uniq -c   [and the same per-file preserving file order, plus `head -1` for the first attachment]   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'planExists|plan_mode|planFilePath|plansDirectory|getSlugFromLog' | sort | uniq -c",
          "observed": "21 `plan_mode` attachment records across 18 top-level transcripts: 19 carry `planExists:false`, 2 carry `true`. Taking only the FIRST `plan_mode` attachment per transcript: 18 of 18 are `false`. Of the 3 transcripts with two or more such attachments, 2 show the ordered sequence `false>true` and 1 shows `false>false`; no transcript shows `true` before `false`. In one transcript the `plan_mode` attachment record and that transcript's first slug-carrying record are the SAME record (identical timestamp 2026-07-28T13:12:49.650Z, `type:\"attachment\"`), carrying `planFilePath`, `planExists:false` and the slug together. Binary 2.1.258 strings: `plan_mode` 77, `planFilePath` 30, `planExists` 9, `plansDirectory` 7, `getSlugFromLog` 2.",
          "rule": "One row per `plan_mode` attachment record (`attachment.type==\"plan_mode\"`) in a top-level transcript, value = `attachment.planExists`; plus one row per transcript taking only the first such record in file order. Binary counts are occurrences of each literal in `strings -n 6` output.",
          "note": "The behavior holds and is now measured rather than asserted. What was wrong is the claim's own instrument line, 'Only a live interactive session can show the window' - the attachment records the window's state itself. The gap is real and it is the default: every transcript that entered Plan Mode was bound to a not-yet-existing plan file at entry, and only 2 of 18 were later observed writing it while still emitting a further attachment. Binary 2.1.258 still ships every string this mechanism is named by, including `plansDirectory` (why csift refuses to guess the plans directory) and `getSlugFromLog` (the first-slug binding law the fallback mirrors). Code sites src/plan.rs:56-58 and src/plan.rs:61-66 confirmed verbatim; the same-record observation directly confirms the 61-66 doc comment's claim that the slug is 'always present on the `plan_mode` attachment record'."
        }
      ]
    },
    {
      "id": "PLAN-015",
      "area": "plan",
      "behavior": "A session may freely `Edit`/`Write` ANOTHER session's plan file; those land as ordinary structured tool calls on a path under the plans directory, indistinguishable by path from the session's own plan.",
      "depends": "csift refuses path heuristics for the binding, and `plan --audit` identifies plan files by a JOIN against the corpus's `plan_mode` bindings (one `plan_mode`-prefiltered scan of every project) rather than a plans-directory guess, warning when the mutating session does not bind the file.",
      "code": [
        {
          "path": "src/plan.rs",
          "lines": "16-20",
          "snippet": "//! This `plan_mode` attachment is the AUTHORITATIVE binding. Crucially it is the *only*\n//! reliable one: a session may freely `Edit`/`Write` OTHER sessions' plan files (they\n//! show up as ordinary tool calls on a `~/.claude/plans/…` path), so \"any plans/ path the\n//! session touched\" is NOT the session's own plan. The bound plan is the one named in the\n//! `plan_mode` attachment, full stop - no path heuristics."
        },
        {
          "path": "src/plan/audit.rs",
          "lines": "3-13",
          "snippet": "//! Why this audit exists: a session may freely `Edit`/`Write` ANOTHER session's plan\n//! file (it is an ordinary tool call on a path), but after a compaction only the\n//! session's OWN bound plan is re-injected in full - content parked in an unbound plan\n//! file does not come back. The audit finds every structured mutation the target scope\n//! made to a file that SOME session binds as its plan, and warns when the mutating\n//! session does not bind that file itself.\n//!\n//! Identification is a JOIN against the corpus's `plan_mode` bindings (one scan of\n//! every project, `plan_mode`-prefiltered so it parses almost nothing), never a plans\n//! directory guess (`plansDirectory` is configurable). Bash-side edits are outside\n//! this audit: structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` only."
        }
      ],
      "instrument": "`csift plan --audit` with NO positional target resolves the CALLING session from the environment and audits only that scope - it is not a corpus audit, and it reports `warnings:0` even when cross-session plan edits exist elsewhere. To get the corpus answer, pass every project directory explicitly as an `@<encoded-dir>` positional: cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit --format json ${=dirs} | jq 'select(.kind==\"plan-edit\" and .bound_by_owner==false)'. In zsh the `${=dirs}` word-split form is required; a bare `$dirs` is passed as one argument and the resolver rejects it.",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "src/plan.rs module doc; src/plan/audit.rs module doc; CHANGELOG 0.8.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit --format json ${=dirs} | jq -rc 'select(.kind==\"plan-edit\")|[.mutations,(.bound_by_owner|tostring)]|@tsv'   [contrast: csift plan --audit --format json with NO target]   ;   csift files --by file --regex '\\.claude/plans/' --format json | jq -rc 'select(.kind==\"file\")|[.path,.write,.edit,.bash,.total]|@tsv'",
          "observed": "With no target: summary {\"bindings\":73,\"plan_files_touched\":1,\"warnings\":0} and a single plan-edit row with `bound_by_owner:true` - zero evidence either way. With all 14 project dirs passed as explicit `@<encoded-dir>` targets: summary {\"bindings\":6004,\"plan_files_touched\":18,\"warnings\":2}, 18 plan-edit rows, of which 2 carry `bound_by_owner:false` with 262 and 31 structured mutations respectively; each of those two plan files ALSO has a `bound_by_owner:true` row from its binder (11 and 380 mutations). Separately `csift files --by file --regex '\\.claude/plans/'` returns 69 rows, and several distinct plan files appear on two rows each with different `session_id` - the same plan file mutated from two different transcripts.",
          "rule": "One plan-edit row per (owning transcript, plan file) pair with at least one structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` mutation to a file that some session binds via `plan_mode`; `bound_by_owner` is false when the mutating transcript does not itself bind that file. Bash-side mutations are outside the audit. The `csift files` rows are one per (transcript, absolute path) pair whose full path matches the regex.",
          "note": "The behavior is confirmed by instrument once the scope is right: 2 of 18 (transcript, plan file) pairs in the corpus are a transcript making structured mutations to a plan file it does not bind, one of them 262 mutations deep. `csift files --by file --regex '\\.claude/plans/'` independently shows plan files are ordinary structured mutation targets - 69 (transcript, path) rows under the plans directory, several plan files carrying rows from two different transcripts. The refinement is entirely about the instrument: as written it audits the calling session only and returns an empty set, which would read as a refutation of the very claim it was meant to support. csift's design choice is separately supported at the binary level - `plansDirectory` appears 7 times in 2.1.258, so the plans directory genuinely is configurable and a path guess would be unsound. Code sites src/plan.rs:16-20 and src/plan/audit.rs:3-13 confirmed verbatim."
        }
      ]
    },
    {
      "id": "PLAN-016",
      "area": "plan",
      "behavior": "After a compaction Claude Code re-injects the session's BOUND plan file in full, as a `type:\"attachment\"` record whose `attachment.type` is `plan_file_reference`, carrying `planFilePath` plus the entire plan text in `planContent` (measured payloads 487k-553k characters). Measured over 151 such records in top-level transcripts, 150 name the transcript's own bound plan and 150 land 4-15 lines after a `compact_boundary`. The single exception is a forked transcript's first re-injection, which names the plan bound to the session it was forked from - so the rule is better stated as 'the plan bound to the conversation being continued', which for every non-forked transcript is its own. Content parked in a plan file the session does not bind is never re-injected.",
      "depends": "`plan --audit` exists to surface that hazard: it warns for every structured mutation the target scope made to a plan file the mutating session does not bind, and names the session that does bind it.",
      "code": [
        {
          "path": "src/plan/audit.rs",
          "lines": "3-13",
          "snippet": "//! Why this audit exists: a session may freely `Edit`/`Write` ANOTHER session's plan\n//! file (it is an ordinary tool call on a path), but after a compaction only the\n//! session's OWN bound plan is re-injected in full - content parked in an unbound plan\n//! file does not come back. The audit finds every structured mutation the target scope\n//! made to a file that SOME session binds as its plan, and warns when the mutating\n//! session does not bind that file itself.\n//!\n//! Identification is a JOIN against the corpus's `plan_mode` bindings (one scan of\n//! every project, `plan_mode`-prefiltered so it parses almost nothing), never a plans\n//! directory guess (`plansDirectory` is configurable). Bash-side edits are outside\n//! this audit: structured `Write`/`Edit`/`MultiEdit`/`NotebookEdit` only."
        }
      ],
      "instrument": "The warning line is longer than previously quoted: `warning: N mutation(s) to <path> by session <id>, which does NOT bind it (bound by <id>, L<line>). Only the BOUND plan is re-injected in full after a compaction.` And the audit must be given explicit project targets to reach the whole corpus (see PLAN-015).",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "CHANGELOG 0.8.1; src/plan/audit.rs module doc"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && dirs=$(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \\; | sed 's/^/@/' | tr '\\n' ' '); csift plan --audit ${=dirs} | rg -i 'warn'   ;   for f in $(rg -l 'plan_file_reference' --glob '*.jsonl' --max-depth 2 .); do bound=$(csift plan \"$f\" --no-subagents --format json | jq -rc 'select(.kind==\"plan\")|.plan_file'); rg --no-filename 'plan_file_reference' \"$f\" | jq -rc 'select(.attachment.type==\"plan_file_reference\")|.attachment.planFilePath'; done   [each compared against $bound]   ;   python3 walk pairing each `plan_file_reference` record with the nearest preceding `compact_boundary` line   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'plan_file_reference|planContent' | sort | uniq -c",
          "observed": "The corpus-scoped audit prints exactly 2 warning lines, matching the JSON summary `warnings:2` and the 2 `bound_by_owner:false` rows: `warning: 31 mutation(s) to ~/.claude/plans/<file> by session <id>, which does NOT bind it (bound by <id>, L1584). Only the BOUND plan is re-injected in full after a compaction.` and the same shape for 262 mutations. Re-injection mechanism: 151 records with `attachment.type==\"plan_file_reference\"` in top-level transcripts, across 13 files / 11 distinct session ids (two sessions each appear under two encoded project dirs). Of those 151, 150 name the transcript's own csift-resolved bound plan and 1 names a foreign plan. 150 of 151 sit 4 to 15 lines after a `compact_boundary` (observed gaps: 4,5,6,7,8,10,15). Each record carries the FULL plan text in `attachment.planContent` - measured payloads of 487001, 493085, 521705 and 553003 characters. Binary 2.1.258 strings: `plan_file_reference` 6, `planContent` 29.",
          "rule": "Warnings: one per (owning transcript, plan file) pair with `bound_by_owner:false`, counted from the corpus-scoped audit's warning lines and cross-checked against the JSON summary `warnings` field. Re-injections: one row per `attachment.type==\"plan_file_reference\"` record in a top-level transcript; 'own' means its `planFilePath` string-equals the `plan_file` csift resolves for that same transcript under `--no-subagents`; adjacency counted as (record line number minus nearest preceding `compact_boundary` line number) <= 20.",
          "note": "Both halves now rest on instruments rather than on the module doc. csift's warning surface behaves exactly as claimed once scoped correctly - 2 warnings, one per `bound_by_owner:false` pair, JSON summary agreeing. Claude Code's re-injection behavior is confirmed directly and the mechanism is now named: the harness writes a `plan_file_reference` attachment carrying the full `planContent`, and 2.1.258 still ships both strings. The 150/151 count is the load-bearing number - re-injection is keyed to the binding, one file per compaction, so an unbound plan file's content does not come back. The one foreign re-injection is not a counterexample to the hazard: it is the fork instant, where the conversation being continued was the origin's, and it is the same event that gives PLAN-013 its shape."
        }
      ]
    },
    {
      "id": "PLAN-017",
      "area": "plan",
      "behavior": "Every structured file-mutation tool_use carries its path key as a QUOTED JSON key in the raw line - `\"file_path\"` for Write/Edit/MultiEdit and `\"notebook_path\"` for NotebookEdit - and any quote inside string CONTENT is JSON-escaped, so those two quoted keys are safe raw-byte needles.",
      "depends": "The two quoted needles prefilter the TARGET SCOPE only, not the whole corpus. plan --audit runs two different prefilters: line_is_structured_mutation_candidate (the two quoted path keys) over the resolved session files, and line_is_plan_candidate (the single needle b\"plan_mode\", src/plan.rs:83-88) over every project when the scope mutated anything. The needle-mutation run separates them observationally: plan-edit rows went 6 -> 0 while bindings stayed 427.",
      "code": [
        {
          "path": "src/plan/audit.rs",
          "lines": "37-40",
          "snippet": "    static FILE_PATH: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"file_path\\\"\"));\n    static NOTEBOOK_PATH: std::sync::LazyLock<memchr::memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memchr::memmem::Finder::new(b\"\\\"notebook_path\\\"\"));"
        }
      ],
      "instrument": "The proposed refutation ('replace the needle with an unquoted file_path to see the row count collapse') cannot work: an unquoted needle is a strict SUPERSET of the quoted one, so the row count can only stay equal or grow. The load-bearing test is to substitute a needle that does NOT occur - changing src/plan/audit.rs line 38 to b\"\\\"filepath\\\"\" collapses plan-edit rows 6 -> 0 while the corpus binding count stays 427, which is exactly the silent-zero failure the claim predicts.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/plan/audit.rs:33-35 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,120}Write:\\{input:\"file_path\".{0,700}'  (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' | sort | uniq -c  (3) python3 scan of every *.jsonl in the csift project's own transcript directory under ~/.claude/projects, counting tool_use blocks named Write/Edit/MultiEdit/NotebookEdit whose raw line contains neither b'\"file_path\"' nor b'\"notebook_path\"'  (4) csift plan --audit --format json . (working-tree build), then the same run after editing src/plan/audit.rs line 38 to the non-occurring needle b\"\\\"filepath\\\"\", cargo build, rerun, git checkout -- src/plan/audit.rs, cargo build, rerun",
          "observed": "Claude Code 2.1.258 carries its own tool->path-input-key table verbatim: 'Read:{input:\"file_path\",...},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]},Glob:{input:\"path\",...},Grep:{input:\"path\",...},LSP:{input:\"filePath\",...},Bash:{responseMembers:[\"rawOutputPath\",\"persistedOutputPath\"]},...}'. The file-mutation tool set is the single literal '[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]' (1 occurrence), bound as 'var opr=[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"],LPt=new Set(opr)'. The only other tool carrying a path-shaped input key that is NOT covered by the two needles is LSP (input key 'filePath'), and LSP declares 'isReadOnly(){return!0}', so it is not a mutator. Corpus scan: 23 transcripts, 104821 lines, Edit uses=1722, Write uses=145, MultiEdit uses=0, NotebookEdit uses=0; lines missing both quoted needles = 0 of 1867. Needle-mutation experiment: baseline plan-edit rows=6, bindings=427, plan_files_touched=6; with the needle changed to \"filepath\" plan-edit rows=0, plan_files_touched=0, bindings UNCHANGED at 427; after revert plan-edit rows=6 again.",
          "rule": "Binary side: exact literal presence, one hit per distinct string-table entry. Corpus side: one count per tool_use BLOCK whose name is in {Write,Edit,MultiEdit,NotebookEdit}; a block counts as 'missing' when the raw jsonl line carrying it contains neither byte string. Audit side: one plan-edit row per (owning parent session id, mutated path) pair, read from --format json rows with kind==\"plan-edit\"; bindings counted from the summary's 'bindings' field.",
          "note": "Behavior holds and is now located in the binary, not just inferred: Claude Code 2.1.258 ships the tool->path-key mapping as a literal table, and the four mutation tools map to exactly the two keys csift prefilters on. The false-positive direction is also safe by JSON construction - content that quotes the key is emitted as \\\"file_path\\\", whose bytes place a backslash where the needle needs its closing quote, so the needle does not match. Two fields needed correction (the refutation instrument, which was backwards, and the depends' claim that the needles prefilter the whole corpus). One standing caveat for a future Claude Code: LSP already uses the camelCase key 'filePath' and is only excluded because it declares isReadOnly; a mutating tool adopting that spelling would fall outside both needles."
        }
      ]
    },
    {
      "id": "PLAN-018",
      "area": "plan",
      "behavior": "A `plansDirectory` value is resolved AGAINST THE PROJECT ROOT (the session's cwd) and must stay contained in it: Claude Code refuses a value that escapes the root (`../x`, an absolute path elsewhere) with the literal message `plansDirectory must be within project root: ` and falls back to `<claude-home>/plans`.",
      "depends": "The harness containment predicate has THREE rejection arms, not two, and csift models only the first: (a) the lexical prefix test n!==e && !n.startsWith(e+separator), which csift mirrors; (b) a filesystem probe over the joined path (an lstat / opendir-nofollow / readlink walker) whose non-undefined result rejects the value outright - unmodeled and not mentioned in the claim; (c) the realpath walk up to the nearest existing ancestor, compared against realpath(root) - the divergence the claim already names. Arms (b) and (c) can only make the harness STRICTER than csift, so csift's lexical mirror is a superset: it can accept a directory the harness refused, never refuse one the harness accepted.",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "39-49",
          "snippet": "/// The plans directory, by Claude Code's own rule (binary 2.1.258, re-verified for\n/// v0.10.1): `plansDirectory` is read from the MERGED settings (`<project>/.claude/\n/// settings.local.json` over `<project>/.claude/settings.json` over\n/// `<claude-home>/settings.json`), resolved AGAINST THE PROJECT ROOT (the session's\n/// cwd), and must stay CONTAINED in that root - a value that escapes it (`../x`, an\n/// absolute path elsewhere) is refused and Claude Code falls back to\n/// `<claude-home>/plans`. csift mirrors the lexical containment check (the harness\n/// additionally walks symlinks of the nearest existing ancestor - unmodeled, a\n/// symlinked plans dir that escapes only after resolution is the one divergence).\n/// The transcript's FIRST recorded `cwd`: the harness's project root for the session -\n/// the value the head records are stamped with before any Bash `cd` moves the"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "100-106",
          "snippet": "    let joined = crate::path::lexical_normalize(&root.join(d));\n    let root_n = crate::path::lexical_normalize(root);\n    if joined.starts_with(&root_n) {\n        joined\n    } else {\n        default\n    }"
        }
      ],
      "instrument": "'exactly two literals' needs a counting rule: strings -n 20 | rg 'plansDirectory' returns 5 LINES, of which 2 are standalone string-table entries ('plansDirectoryResolutions' and 'plansDirectory must be within project root: ') and 3 are occurrences inside larger minified-JavaScript chunks (the export map, the settings-schema describe, and the resolver body). Counting rule for 'exactly two': lines matching ^[^{}();=]*plansDirectory[^{}();=]*$.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "src/plan.rs:198-208 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,45}plansDirectory.{0,60}' | sort | uniq -c  (2) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}plansDirectory must be within project root.{0,300}'  (3) strings -n 20 ~/.local/share/claude/versions/2.1.258 | rg -o 'function le\\(n,e\\)\\{if\\(n!==e.{0,700}'  (4) a temporary fixture home passed via --claude-home, holding one transcript whose first record carries a slug and a cwd equal to the fixture project root, run as csift --claude-home <fixture-home> plan @<prefix> --format json, once per plansDirectory value",
          "observed": "The resolver is present verbatim: 'class Wtr{#e=void 0;directory(){return this.#e??=this.#n(),this.#e}#n(){let e=Je().plansDirectory;if(e){let i=ne(),r=O(i,e);if(le(r,i))return r;t(`plansDirectory must be within project root: ${e}`,{level:\"error\"})}return F()}reset(){this.#e=void 0}}'. The settings-schema entry states the rule in prose: 'plansDirectory:i().optional().describe(\"Custom directory for plan files, relative to project root. If not set, defaults to ~/.claude/plans/\")'. The containment predicate is 'function le(n,e){if(n!==e&&!n.startsWith(e+B))return!1;if(Cm(ce(),n)!==void 0)return!1;let i=NS(e);if(i===null)return!1;let r=n;for(;;){let s=NS(r);if(s!==null)return s===i||s.startsWith(i+B);let o=A(r);if(o===r)return!1;r=o}}', with 'function NS(t){try{return LW(Dt.native(t))}catch{return null}}'. Fixture results, reading the kind==\"plan\" row's plan_file: no plansDirectory -> <claude-home>/plans/<slug>.md; \"docs/plans\" -> <project-root>/docs/plans/<slug>.md; \"../outside\" -> <claude-home>/plans/<slug>.md; \"/tmp/elsewhere\" -> <claude-home>/plans/<slug>.md; settings.local.json \"local-plans\" over settings.json \"docs/plans\" -> <project-root>/local-plans/<slug>.md; value set only in <claude-home>/settings.json -> still resolved against the PROJECT root; project settings.json over <claude-home>/settings.json -> project value wins.",
          "rule": "Binary side: exact literal presence, one line per distinct string-table entry. Fixture side: one observation per plansDirectory value; the observable is the single kind==\"plan\" JSON row's plan_file field; 'contained' means the emitted path is under the fixture project root, 'fell back' means it is under <claude-home>/plans.",
          "note": "Behavior confirmed twice over: the harness's own resolver and error string were read out of the binary, and a fixture reproduced all four outcomes (default, contained, dot-dot escape, absolute elsewhere) plus the settings-layer precedence. One fixture result is worth recording because it is not in the claim: a plansDirectory set in <claude-home>/settings.json is still resolved against the PROJECT root, not the config home, so the same user-level value yields a different directory per project. Three fields needed correction: the instrument's literal count, the unreleased status of the csift code under test, and the number of unmodeled arms in the harness's containment check."
        }
      ]
    },
    {
      "id": "PLAN-019",
      "area": "plan",
      "behavior": "The five layers and the high-to-low order policySettings > flagSettings > localSettings > projectSettings > userSettings are correct, but the supporting literal is not the merge order. The authoritative literal is the LOW-to-HIGH list [\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"] (bound as Bs and returned by nn()), which the binary itself annotates 'Ordered low-to-high priority - later entries override earlier ones.' plansDirectory is read off the merged result of that list.",
      "depends": "The gap is wider than 'three of five'. Beyond the missing policy and flag layers, the two layers csift does read can resolve to a DIFFERENT DIRECTORY than csift assumes: projectSettings resolves to the record's cwd (which csift matches), but localSettings resolves via eW(cwd, canonicalGitRoot), which prefers the canonical GIT ROOT when it differs from the cwd, is not the home directory, and passes an ownership probe. So for a session whose cwd is a subdirectory of its git repository, the harness reads <git-root>/.claude/settings.local.json while csift reads <cwd>/.claude/settings.local.json.",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "82-85",
          "snippet": "    let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n    candidates.push(root.join(\".claude\").join(\"settings.json\"));\n    candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n    let mut value: Option<String> = None;"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "93-95",
          "snippet": "        if let Some(d) = v.get(\"plansDirectory\").and_then(serde_json::Value::as_str) {\n            value = Some(d.to_string()); // later (more specific) scopes win\n        }"
        }
      ],
      "instrument": "The cited literal ['policySettings','flagSettings',...['localSettings','projectSettings']...] exists, but it is a first-wins precedence walk for a DIFFERENT settings key (an auto-memory directory lookup: 'for(let r of n){let o=be(r)?.autoMemoryDirectory;if(o!=null)return{dir:o,source:r}}'), and its middle pair is behind a runtime conditional. Five distinct policySettings-led arrays exist, two of them in orders that contradict each other, so no single bracketed literal settles precedence. Grep for the low-to-high five-element list and its describe string instead.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE '\"(userSettings|projectSettings|localSettings|policySettings|managedSettings|flagSettings)\"' | sort | uniq -c | sort -rn  (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | grep -oE '\\[\"policySettings\",\"[^]]{0,160}\\]' | sort -u  (3) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,200}\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\".{0,400}'  (4) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,300}case\"localSettings\".{0,300}'  (5) strings -n 8 ~/.local/share/claude/versions/2.1.258 | rg -o 'managed-settings\\.json|settings\\.local\\.json' | sort | uniq -c",
          "observed": "Layer-literal counts reproduce the claim exactly: userSettings 267, policySettings 209, localSettings 191, projectSettings 148, flagSettings 114, managedSettings 4. The canonical layer list is 'var Bs=[\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"]', repeated as 'function nn(){return[\"userSettings\",\"projectSettings\",\"localSettings\",\"flagSettings\",\"policySettings\"]}', and the binary states its own ordering: the same five-element enum is described as 'Ordered low-to-high priority \\u2014 later entries override earlier ones.' Corroborated by the reverse walk 'function nE(e){let n=Ci();for(let r=n.length-1;r>=0;r--){let o=n[r];if(be(o)?.[e]!==void 0)return o}return null}'. plansDirectory is read from the MERGED object: 'function Je(){return Rb().settings||{}}' and the resolver's 'let e=Je().plansDirectory'. Human names: 'case\"userSettings\":return\"user settings\";case\"projectSettings\":return\"shared project settings\";case\"localSettings\":return\"project local settings\";case\"flagSettings\":return\"command line arguments\";case\"policySettings\":return\"enterprise managed settings\"'. Filenames: settings.local.json 144 occurrences, managed-settings.json 36. Per-layer directory resolution: 'function WCt(e,n){switch(e){case\"userSettings\":return ue(Se());case\"policySettings\":case\"projectSettings\":return ue(n.cwd);case\"localSettings\":return eW(n.cwd,n.canonicalGitRoot);case\"flagSettings\":return n.flagPath?hg(ue(n.flagPath)):ue(n.cwd)}}'. The four precedence arrays found by (2) are ['policySettings','flagSettings',...conditional ['localSettings','projectSettings']...,'userSettings'], ['policySettings','flagSettings','userSettings'], ['policySettings','flagSettings'], ['policySettings','projectSettings','localSettings'] and ['policySettings','userSettings','flagSettings'] - five distinct literals, not one.",
          "rule": "One occurrence per quoted literal in the binary's string table, counted with grep -oE over strings -n 12 output and tallied by uniq -c. A 'precedence array' is any bracketed literal whose first element is \"policySettings\".",
          "note": "Substance holds - five layers, that precedence, localSettings as .claude/settings.local.json, the managed layer as managed-settings.json - and every count in the claim's instrument reproduced to the digit. Three fields needed correction: the cited array is a different key's lookup rather than the merge order, the real ordering literal is the low-to-high list that documents itself, and the depends understates the gap by omitting the git-root resolution of localSettings. Note also that managedSettings (4 hits) is not a sixth layer: the managed layer is named policySettings and only its FILE is called managed-settings.json."
        }
      ]
    },
    {
      "id": "PLAN-020",
      "area": "plan",
      "behavior": "'--settings' is one of THREE settings-affecting launch flags, all read in the same eager-load sequence: --settings supplies the flagSettings layer, --managed-settings supplies the policy layer's file, and --setting-sources selects which sources load at all (with --restricted equivalent to an empty source list). Precedence of the flagSettings layer is as claimed - above localSettings, projectSettings and userSettings - but BELOW policySettings, per the low-to-high layer list.",
      "depends": "Confirmed and widened. csift's blindness is not only to --settings: --managed-settings and --setting-sources/--restricted change which settings apply too, and --setting-sources can DISABLE a source csift does read (the harness carries the message 'source is disabled for this session (--setting-sources)'), so csift can be wrong by reading a file the session ignored as well as by missing one it honored. The blindness is now instrument-backed rather than assumed: the session registry files under ~/.claude/sessions record no argv - their key union across 7 files contains nothing settings- or argument-related - and no transcript record type carries launch flags either.",
      "code": [
        {
          "path": "src/plan/slug.rs",
          "lines": "86-92",
          "snippet": "    for p in candidates {\n        let Ok(raw) = std::fs::read_to_string(&p) else {\n            continue;\n        };\n        let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {\n            continue;\n        };"
        },
        {
          "path": "src/plan/slug.rs",
          "lines": "82-85",
          "snippet": "    let mut candidates: Vec<PathBuf> = vec![home.join(\"settings.json\")];\n    candidates.push(root.join(\".claude\").join(\"settings.json\"));\n    candidates.push(root.join(\".claude\").join(\"settings.local.json\"));\n    let mut value: Option<String> = None;"
        }
      ],
      "instrument": "sort -u over the extracted spellings yields 13 distinct hits at CC 2.1.258, not the 4 listed. The two the claim names as error strings are both present. For a stable rerun, prefer the argv-table literal '\"--settings\":(e)=>{t.settings=e}' and the flagSettings->'--settings' mapping, which pin the flag's identity rather than counting prose.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -n 10 ~/.local/share/claude/versions/2.1.258 | grep -oE '\\-\\-settings[ <]?[a-z-]*' | sort -u  (2) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}\"--settings.{0,300}'  (3) strings -n 12 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,90}--settings cannot.{0,160}'  (4) python3 over every ~/.claude/sessions/*.json, printing the union of top-level keys",
          "observed": "The flag is parsed with a value: the argv table contains 'a={\"--cwd\":(e)=>{r=e},\"--settings\":(e)=>{t.settings=e},\"--add-dir\":(e)=>t.addDir.push(e),...}', and the eager settings load reads three settings flags in sequence: 'OVt(\"--settings\");if(e)Ypr(e);let a=OVt(\"--managed-settings\");if(a)c(a);let n=OVt(\"--setting-sources\");if(n!==void 0)g(n);if(LR(\"--restricted\")||PW())g(\"\"),gnt(!0);'. The flagSettings layer is mapped to the flag by name in a permission-warning path: 'nt.source===\"flagSettings\"&&!Ho(\"flagSettings\")?\"--settings\":nt.source===\"policySettings\"?\"managed policy settings\":...', and flagSettings' human name is 'command line arguments'. Error strings present verbatim: 'hooks given with --settings cannot be told apart from ones added at run time' and 'All hooks are switched off in your user settings or --settings file (disableAllHooks)'. Also present: 'source is disabled for this session (--setting-sources)'. Distinct --settings spellings found by (1): 13, not 4 - --settings, '--settings ', '--settings and', '--settings cannot', '--settings file', '--settings files', '--settings flag', '--settings or', '--settings sources', '--settings still', '--settings to', '--settings value', '--settings-enabled'. Session registry: 7 files parsed under ~/.claude/sessions, union of top-level keys = [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version] - no argv, args, flags, settings or setting-source key of any kind.",
          "rule": "Binary side: distinct string-table hits containing the flag spelling, deduplicated with sort -u, one row per distinct extracted substring. Registry side: one file per *.json under ~/.claude/sessions (the paired *.key files are not JSON and are excluded); the observable is the UNION of top-level keys across all parsed files.",
          "note": "The flag exists, takes a value, is the flagSettings layer, and outranks the project, local and user layers - all confirmed from the binary. Two fields needed correction: the string set is three times larger than listed, and the flag is one of three settings-affecting launch flags rather than the only one. The depends is now positively verified rather than inferred, because the session registry was inspected and demonstrably records no launch arguments; that also means csift cannot even DISCLOSE the uncertainty per session, since there is no on-disk signal that a settings flag was used."
        }
      ]
    },
    {
      "id": "QT-001",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes a `type:\"queue-operation\"` line for every input-queue state change. The line carries four to six top-level keys - always `type`, `operation`, `timestamp` and `sessionId`, optionally `content` and `reason` - and no `uuid`, no `parentUuid`, no `message`, no `promptId` and no queue id. The measured `operation` vocabulary is `enqueue`, `dequeue`, `remove` and `popAll`, treated as an open set.",
      "depends": "csift promotes the line to the LLM-invisible leaf `user.queued` and carries `operation` and `reason` verbatim into the label zone and the JSON hit; with no `uuid` on the line the record is addressable only by `show --line`, and with no `message{}` a role-based byte prefilter never keeps it, which is why the leaf needs its own explicit gate.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "155-159",
          "snippet": "    /// `queue-operation` (v0.10.0): the queue event - `enqueue` | `dequeue` | `remove` |\n    /// `popAll` (open set; measured those four). The human-typed (or automation) text\n    /// rides top-level `content` on every operation except `dequeue`.\n    #[serde(default)]\n    pub operation: Option<String>,"
        }
      ],
      "instrument": "`rg -NI '\"type\":\"queue-operation\"' ~/.claude/projects -g '*.jsonl'` piped through a JSON reader that unions all top-level keys; counting rule = one observation per physical line, reported as the distinct sorted key sets and their frequencies (measured 13,412 lines, exactly three key sets). `csift search '' <project-dir> -t user.queued --count-by label` gives the labelled subset.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 5.1; src/model/record.rs:148-152 comment; CHANGELOG 0.10.0 (queue facts, measured); SPEC.md section 6 v0.10.0 ledger item 7; SPEC.md v0.10.0 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'type:\"queue-operation\"[^}]{0,300}'  AND  for each of the 15 project directories separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (json.loads each line, union of sorted top-level key sets)",
          "observed": "Binary writer, verbatim: `function o(Br,br,Ts){let qi={type:\"queue-operation\",operation:Br,timestamp:new Date().toISOString(),sessionId:Q(),...br!==void 0&&{content:br},...Ts!==void 0&&{reason:Ts}};n(qi)}`. Corpus: 13,512 queue-operation lines, EXACTLY three distinct key sets - 9,130 x [content, operation, sessionId, timestamp, type]; 4,237 x [operation, sessionId, timestamp, type]; 145 x [content, operation, reason, sessionId, timestamp, type]. No uuid, parentUuid, message, promptId or queue id in any set. operation values on disk: enqueue 6,760 / dequeue 3,441 / remove 3,203 / popAll 108. Binary call sites of the same recorder: o(\"enqueue\"), o(\"dequeue\"), o(\"remove\"), o(\"popAll\",typeof ...), and o(\"popOne\",typeof Eo.value===\"string\"?Eo.value:void 0).",
          "rule": "One observation per physical jsonl line whose parsed top-level type == 'queue-operation'; key sets are the sorted tuple of top-level keys; rg is run once per project directory and the per-directory results are summed (15 directories, 5.2 GB total). Binary side: one observation per matched string literal.",
          "note": "Behavior statement confirmed twice over: the binary writer literally builds the 4-to-6-key object, and the corpus shows exactly the three key sets that writer can produce. Only the instrument's line count and the operation vocabulary needed updating."
        }
      ]
    },
    {
      "id": "QT-002",
      "area": "queue-and-telemetry",
      "behavior": "Whether a `queue-operation` line carries the queued text at all is OPERATION-DEPENDENT: top-level `content` is present on `enqueue` 6,710/6,710 (100%), `popAll` 108/108 (100%), `remove` 2,372/3,168 (75%) and `dequeue` 0/3,426 (never). A `dequeue` therefore carries nothing to search.",
      "depends": "csift assigns no label to a content-less queue line and renders the label zone as `[enqueue]` or `[remove - <reason>]`; a `dequeue` can be paired with its `enqueue` only by ordering and text, never by an id.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "38-42",
          "snippet": "    fn queued_class(&self) -> Option<Class> {\n        let text = self.content_str()?;\n        if text.trim().is_empty() {\n            return None;\n        }"
        }
      ],
      "instrument": "Extract every `queue-operation` line and cross-tabulate `operation` against the presence of `content` and of `reason`; counting rule = one row per physical queue line, grouped by operation value. Expect 100% content on enqueue and popAll, 0% on dequeue.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7; src/model/record.rs:148-152 comment; CHANGELOG 0.10.0 (queue facts, measured)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (cross-tabulate parsed `operation` against presence of top-level `content` and `reason`)",
          "observed": "enqueue: content 6,760/6,760 (100%) - popAll: content 108/108 (100%) - remove: content 2,407/3,203 (75.1%) - dequeue: content 0/3,441 (0%). Rendered form checked with `csift show @<SESSION> --line <N>`: a content-bearing remove renders `user.queued [remove - absorbed_mid_turn]`; a content-bearing enqueue renders `user.queued [enqueue]`. A content-less or rider queue line is not addressable at all: `csift show --line` on one returns `csift: error: no such record(s): L<N>` and names which line kinds an explicit address does render.",
          "rule": "One row per physical queue-operation line, grouped by the `operation` value; 'content present' = the top-level key exists (an empty string would still count, none were observed).",
          "note": "The operation-dependence is exact: every enqueue and every popAll carried text, no dequeue ever did. The 75% figure for remove reproduced to within 0.1 percentage point."
        }
      ]
    },
    {
      "id": "QT-003",
      "area": "queue-and-telemetry",
      "behavior": "A `reason` field occurs only on a `remove` queue line and takes exactly two measured values, `absorbed_mid_turn` (99 occurrences) and `delivered_to_agent` (11) - both structural evidence that the queued text was consumed. The field is absent on every other operation and every other line type.",
      "depends": "A queued hit renders `[remove - <reason>]` in the label zone and carries JSON `queue_reason`, kept verbatim as an open set; the two values are the only consumption evidence the format offers, and csift reports them rather than deriving a dispatch verdict from them.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "161-165",
          "snippet": "    /// `queue-operation` `remove` reason - measured values `absorbed_mid_turn` and\n    /// `delivered_to_agent`, both STRUCTURAL evidence that the queued text was\n    /// consumed. Absent on every other line. Open set, kept verbatim.\n    #[serde(default)]\n    pub reason: Option<String>,"
        }
      ],
      "instrument": "`csift search '' <target> -t user.queued --format json | jq -r '.queue_operation + \"/\" + (.queue_reason // \"-\")' | sort | uniq -c`; counting rule = one row per content-bearing queue line. Cross-check the raw side with `rg -o '\"reason\":\"[a-z_]+\"' over the same files.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "src/model/record.rs:154-158 comment; dev session 2026-09-02; SPEC.md section 5.1; CHANGELOG 0.10.0 (the queue line has no join key); SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"reason\":' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (keep lines whose PARSED object has a top-level `reason`, bucket by (type, operation) and by value); plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'consume\\([A-Za-z0-9_.]+,\\{reason:\"[a-z_]+\"\\}' and rg -oN '.{200}o\\(\"remove\".{200}'",
          "observed": "Corpus: top-level `reason` occurs on 145 lines, all of them type=queue-operation with operation=remove; values absorbed_mid_turn 133, delivered_to_agent 12. In a second, larger project directory 786 lines contain the substring '\"reason\":' and ZERO of them carry it as a top-level key (all nested). Binary: the remove helper is `function Dr(Br,br,Ts){...o(\"remove\",typeof _o.value===\"string\"?_o.value:void 0,Ts)}` - i.e. the third argument is the recorded reason - and the reason literals reaching it include `absorbed_mid_turn`, `delivered_to_agent`, `delivered_as_tool_result`, `dropped_by_hook` and `cleared_on_cancel`.",
          "rule": "One observation per physical line whose parsed object has `reason` as a TOP-LEVEL key (nested `reason` keys inside toolUseResult and message payloads are excluded); values counted verbatim.",
          "note": "The 'open set, kept verbatim' discipline is vindicated - and the binary shows three unobserved values, two of which mean the OPPOSITE of consumption. That asymmetry is the sharpest thing this check found and deserves its own claim."
        }
      ]
    },
    {
      "id": "QT-004",
      "area": "queue-and-telemetry",
      "behavior": "A `queue-operation` line carries NO join key to the user record its text eventually becomes: the measured 4-6 keys include no `promptId`, no `uuid` and no queue id. Dispatch is therefore unprovable from the queue side - the delivered record can be reached only by ordering and by the text itself.",
      "depends": "csift renders the verbatim `operation` and `reason` facts and NEVER asserts a `dispatched` flag; inventing a join would fabricate a link between a queued draft and a delivered turn that the format does not carry.",
      "code": [
        {
          "path": "src/search/hits.rs",
          "lines": "189-199",
          "snippet": "    // v0.10.0: the queue facts ride only a queue-operation record (None elsewhere).\n    let queue_operation = if rec.is_type(\"queue-operation\") {\n        rec.operation.clone()\n    } else {\n        None\n    };\n    let queue_reason = if rec.is_type(\"queue-operation\") {\n        rec.reason.clone()\n    } else {\n        None\n    };"
        },
        {
          "path": "src/search/render.rs",
          "lines": "47-55",
          "snippet": "    // v0.10.0: a queued line names its queue event (and a remove's reason) in the label\n    // zone - display-only; the matchable text stays the verbatim queued content.\n    if h.class == Class::UserQueued {\n        let op = h.queue_operation.as_deref().unwrap_or(\"queued\");\n        return match h.queue_reason.as_deref() {\n            Some(reason) => format!(\"{} [{op} · {reason}]\", h.class.path()),\n            None => format!(\"{} [{op}]\", h.class.path()),\n        };\n    }"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "55-60",
          "snippet": "    /// (no `message{}`); the dispatched twin, when there is one, is the later\n    /// `user.message` record. Automation riders (a `<task-notification>` / peer\n    /// message queued by the harness) are NOT the human and carry no label here;\n    /// content-less `dequeue` lines carry nothing to search. The queue line has no\n    /// join key (measured: 4-6 keys, no promptId/uuid), so `dispatched` is never\n    /// asserted - only the verbatim `operation` + `reason` facts ride the hit."
        }
      ],
      "instrument": "`csift show <target> --line <a queue-operation line> --raw | jq 'keys'`: no promptId, uuid or queue-id field appears. Counting rule = the key set of one queue line, repeated over the corpus-wide union (three distinct key sets, none with a join field).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "CHANGELOG 0.10.0 (the queue line has no join key); SPEC.md section 5.1; SPEC.md section 6 v0.10.0 ledger item 7; src/model/record.rs:154-158 comment; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift show @<SESSION> --line <N> --raw | jq -c 'keys'  (run on two queue-operation lines: one remove-with-reason, one enqueue); plus the corpus-wide key-set union from QT-001; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN 'type:\"queue-operation\"[^}]{0,300}'",
          "observed": "Raw keys of a remove line: [\"content\",\"operation\",\"reason\",\"sessionId\",\"timestamp\",\"type\"]. Raw keys of an enqueue line: [\"content\",\"operation\",\"sessionId\",\"timestamp\",\"type\"]. Corpus-wide union over 13,512 lines: exactly three key sets, none containing promptId, uuid, parentUuid, messageId or any queue/task id. Binary writer builds the object from exactly {type, operation, timestamp, sessionId, content?, reason?} - there is no join field to omit.",
          "rule": "The key set of one queue line via `jq keys`, generalised by the corpus-wide union of parsed top-level key sets (one observation per physical line); a 'join key' means any field naming the eventual user record (promptId, uuid, messageId) or the queue entry (a queue/entry id).",
          "note": "Confirmed at the source: the writer in the shipped binary has no join field to write, so dispatch is unprovable from the queue side by construction and not merely unobserved. csift refusing to assert `dispatched` is correct. Only the record.rs line numbers moved."
        }
      ]
    },
    {
      "id": "QT-005",
      "area": "queue-and-telemetry",
      "behavior": "The input queue is dominated by automation rather than human text: of 9,190 content-bearing `queue-operation` lines the content-leading shape was `<task-notification>` 7,996, plain prose 1,141, a slash command 24 and `<agent-message from=\"...\">` about 29 - roughly 87% harness RIDERS, whose delivered twin already classifies as a harness notification or an inbound peer message.",
      "depends": "csift labels only a human-typed queue line `user.queued`, refusing a rider by the same content-shape law that reparents the delivered user record; labelling every content-bearing queue line would inflate the apparent human queue traffic roughly sevenfold and duplicate records already labelled elsewhere.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "32-48",
          "snippet": "    /// `user.queued` iff the queue line carries the HUMAN's text. The queue also\n    /// carries harness riders - a `<task-notification>` pulse or a peer message - whose\n    /// delivered twin already classifies `harness.notification.*` / `agent.communication\n    /// .inbox`; the same content-shape law that reparents those user records applies,\n    /// so a rider is never the human here. A content-less line (`dequeue`) has nothing\n    /// to search and carries no label.\n    fn queued_class(&self) -> Option<Class> {\n        let text = self.content_str()?;\n        if text.trim().is_empty() {\n            return None;\n        }\n        let at_boundary = text.trim_start();\n        if at_boundary.starts_with(TASK_NOTIFICATION_PREFIX) || is_peer_message(text) {\n            return None;\n        }\n        Some(Class::UserQueued)\n    }"
        }
      ],
      "instrument": "Extract content-bearing `queue-operation` lines and bucket them by the first non-whitespace token of `content`; counting rule = one bucket observation per content-bearing line (riders = a boundary-anchored notification or peer tag). Then `csift search '' <target> -t user.queued -c` must be far smaller than that denominator.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; AGENTS.md section 3.3a; SPEC.md section 6 v0.10.0 ledger item 7; AGENTS.md section 3.3a (queued rider law); CHANGELOG 0.10.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"type\":\"queue-operation\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (bucket by the first non-whitespace token of `content`); then csift search '' -t user.queued --count-by label --max-count 0 (whole corpus, csift 0.10.0)",
          "observed": "9,275 content-bearing queue lines: <task-notification> 8,075 (87.0%), plain prose 1,146 (12.4%), <agent-message ...> 30, a bare slash command 24. Riders (task-notification + agent-message + teammate-message) = 8,105 = 87.4%. csift's corpus-wide `user.queued` census = 1,170 records, which equals 1,146 + 24 exactly - i.e. csift labels every non-rider content-bearing line and no rider. In one project directory taken alone the split was 685 riders / 62 human-shaped and csift's census for that directory was 62.",
          "rule": "One bucket observation per content-bearing queue line, bucketed by the first non-whitespace token; 'rider' = content begins with <task-notification, <agent-message or <teammate-message. csift census counts one record per labelled record.",
          "note": "The rider law is exact, not approximate: csift's independent census (1,170) equals the hand-computed non-rider count (1,170) to the record. 'roughly sevenfold' measures 7.9x."
        }
      ]
    },
    {
      "id": "QT-006",
      "area": "queue-and-telemetry",
      "behavior": "The human-versus-automation discriminator lives on the DELIVERED twin, never on the queue line: the `queued_command` attachment carries `commandMode` in {`task-notification` 2,106, `prompt` 226}, and the delivered user record carries `origin.kind` in {`task-notification` 2,804, `human` 2,482, `peer` 57, `coordinator` 49} plus `promptSource` in {`system` 2,861, `typed` 2,564, `queued` 104}. `promptSource` alone is NOT a draft discriminator: measured drafts were 315 `typed` against 2 `queued` while finals were 317/317 `typed`.",
      "depends": "csift does not model `origin` or `promptSource` today and classifies by content shape instead; these are the structural fields any future authorship or dispatch assertion would have to key on, and the draft census is why `promptSource` cannot stand in for the superseded-draft rule.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "32-35",
          "snippet": "    /// `user.queued` iff the queue line carries the HUMAN's text. The queue also\n    /// carries harness riders - a `<task-notification>` pulse or a peer message - whose\n    /// delivered twin already classifies `harness.notification.*` / `agent.communication\n    /// .inbox`; the same content-shape law that reparents those user records applies,"
        }
      ],
      "instrument": "`rg -oNI '\"promptSource\":\"[a-z]+\"' ~/.claude/projects -g '*.jsonl' | sort | uniq -c`, and the same for `\"commandMode\":\"...\"` and for `\"kind\":\"...\"` inside an `origin` object; counting rule = one observation per match. `promptSource:\"queued\"` was observed spanning Claude Code 2.1.177 to 2.1.252.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 7"
      },
      "first_seen_claude_code": "2.1.177",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"promptSource\"' / 'commandMode' / '\"origin\":{\"kind\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (parse each line, census the parsed values by carrier); draft side: csift search '' -t user.unsent --raw --max-count 0 (whole corpus) then census `promptSource` on those records, and join their `parentUuid` back to non-draft string-content user records via rg -F -f <uuid list> per project directory; binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,120}promptSource:.{0,200}' and rg -oN 'function lle\\(.{0,900}'",
          "observed": "promptSource on the corpus has FIVE values, not three: system 2,874, typed 2,607, queued 105, suggestion_accepted 1, sdk 1 - and the binary's own switch enumerates exactly those five (`case\"sdk\" ... case\"system\" ... case\"typed\":case\"queued\":case\"suggestion_accepted\"`). commandMode inside a queued_command attachment: task-notification 2,131, prompt 228 (two values). origin.kind on the delivered type:\"user\" record: task-notification 2,819, human 2,388, peer 51, coordinator 49; origin also rides inside the queued_command attachment (human 140, peer 6). The binary's origin classifier switches on ELEVEN kinds: human, plugin, channel, task-notification, peer, coordinator, unclassified, observer, auto-continuation, observer-activity, slack-ping. promptSource:\"queued\" spans 2.1.177 through 2.1.258. Draft census: 827 superseded-draft records - typed 616, queued 13, field absent 198. Their surviving siblings (non-draft string-content user records sharing a draft parentUuid): 446 records - typed 321, system 11, queued 0, field absent 114.",
          "rule": "One observation per parsed line for each field; draft set = every record csift labels `user.unsent`; 'final' = a non-draft `type:\"user\"` record with STRING message.content whose parentUuid is shared with at least one draft. Binary vocabularies = one observation per switch-case literal.",
          "note": "The core assertion - the discriminator lives on the delivered twin, and promptSource alone cannot separate draft from final - survived every instrument. Both enumerations needed widening, and the binary now supplies the closed sets rather than leaving them as corpus observations."
        }
      ]
    },
    {
      "id": "QT-007",
      "area": "queue-and-telemetry",
      "behavior": "Human prose enqueued into the input queue does not reliably become a user record: counting enqueue lines only, over 6 sessions (n=782 texts, whitespace-normalized), 72% match a later user record exactly and 81% match by prefix - so 19-28% of typed queue text never becomes a user record at all. An earlier ~61%-never figure counted every queue operation, remove lines included, over 3 sessions and is superseded by the narrower count.",
      "depends": "the gap is why `user.queued` exists as its own searchable leaf: for that 19-28% the queue line is the only on-disk copy of what the operator typed, and a `-t user.message` search over it would report a definitive absence.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "41-47",
          "snippet": "            return None;\n        }\n        let at_boundary = text.trim_start();\n        if at_boundary.starts_with(TASK_NOTIFICATION_PREFIX) || is_peer_message(text) {\n            return None;\n        }\n        Some(Class::UserQueued)"
        }
      ],
      "instrument": "For one session extract every `enqueue` line and test its `content` against `csift search -F <text> @<id> -t user.message`; counting rule = enqueue lines only (never remove or dequeue), one comparison per enqueued text, with exact-string and 40-char-prefix matches counted separately.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 7; CHANGELOG 0.10.0 (queue facts, measured)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over the SIX session files carrying the most enqueue lines corpus-wide (found with rg -c --no-heading '\"operation\":\"enqueue\"' -g '*.jsonl' ~/.claude/projects/<PROJ> per directory): read each file once, collect every enqueue `content` and every `type:\"user\"` record text (string content and text blocks), whitespace-normalize both, then test exact membership, 40-character prefix membership and substring membership",
          "observed": "6 session files; 5,721 enqueue lines total, of which 777 are human-shaped (non-rider). Human-shaped: exact match to a later user text 565 (73%), 40-char-prefix match 593 (76%), substring of some user text 638 (82%). So 18-24% of human-typed enqueued text never becomes a user record. Rider enqueues for contrast: 4,944 lines, 51% exact, 63% prefix40.",
          "rule": "One comparison per enqueue line (never a remove or dequeue), restricted to human-shaped content (not <task-notification / <agent-message / <teammate-message). Texts whitespace-normalized (runs of whitespace collapsed to one space, trimmed) on both sides. 'exact' = normalized equality; 'prefix40' = some user text starts with the enqueue's first 40 characters; 'substring' = the enqueue text occurs inside the newline-joined user texts.",
          "note": "The gap is real and reproduced closely (73% vs the claimed 72% exact; the 19-28%-never band brackets the measured 18-24%). n came out 777 against the claimed 782 on a differently-selected session set, which is close enough to be the same effect. The one thing that needs fixing is naming the match rule."
        }
      ]
    },
    {
      "id": "QT-008",
      "area": "queue-and-telemetry",
      "behavior": "A background task completion notification is delivered on THREE carriers: a `queue-operation` line with `operation:\"enqueue\"`, an `attachment` whose payload `type` is `queued_command` (carrying `commandMode:\"task-notification\"`), and - only when the session was idle - a `type:\"user\"` string record. 1,000 of 2,502 returned main-lane shells (40%) never produce a user record at all, so for those the queue line and the attachment are the only trace.",
      "depends": "the csift background scanner reads all three carrier shapes; restricting it to `type:\"user\"` records would miss 40% of shell completions and leave them permanently `open`, inflating the idle-background-open verdict.",
      "code": [
        {
          "path": "src/live/background_scan.rs",
          "lines": "225-236",
          "snippet": "pub(crate) fn carrier_text(rec: &Record) -> Option<String> {\n    if rec.is_type(\"queue-operation\") {\n        rec.content_str().map(str::to_string)\n    } else if rec.attachment_type().as_deref() == Some(\"queued_command\") {\n        rec.attachment_value()\n            .and_then(|v| v.get(\"prompt\")?.as_str().map(str::to_string))\n    } else if let Some(Content::Text(s)) = rec.message.as_ref().and_then(|m| m.content.as_ref()) {\n        Some(s.clone())\n    } else {\n        None\n    }\n}"
        },
        {
          "path": "src/live/background.rs",
          "lines": "22-27",
          "snippet": "//! It rides THREE carriers: a `type:\"user\"` string record when the session was idle,\n//! or (40% of returned shells) a `queue-operation` enqueue + remove and a\n//! `queued_command` attachment when it landed mid-turn - never a user record. A shell\n//! launched from a SUBAGENT lane is completed in the PARENT main transcript (607/618\n//! measured; zero notifications exist in any subagent transcript), so this scan reads\n//! launches from every lane and completions from the main file."
        }
      ],
      "instrument": "Take a known background task id from a launch result and `rg -n '<its id>' <the main transcript>`, then classify each matching line by its `type` field; counting rule = one carrier observation per matching line, bucketed into `queue-operation` / `attachment` / `user`. A launch with a queue row but no user row is a mid-turn delivery.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg --no-heading -H -N -e run_in_background -e tool-use-id -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 - collect main-lane assistant tool_use blocks (Bash/PowerShell) with input.run_in_background == true as LAUNCHES, then join every <tool-use-id> found in a queue-operation `content`, a `queued_command` attachment `prompt`, or a `type:\"user\"` string content back to that launch id",
          "observed": "2,534 main-lane background shell launches; 2,520 have at least one completion carrier. Carrier-set distribution: 1,193 {queue-operation, user(string), +hook_success attachment}, 1,015 {queue-operation, attachment:queued_command}, 311 {queue-operation, user(string)}, 1 {queue-operation}. Returned shells with NO user-record carrier: 1,016 = 40%. The queue-operation carrier is present on all 2,520. The `queued_command` attachment appears on exactly the 1,015 launches that have no user record - i.e. mid-turn delivery. No <task-notification> element was found in any subagent transcript; the 9 subagent user-string records matching <tool-use-id> all begin `[SYSTEM NOTIFICATION - NOT USE...` and the 171 other subagent occurrences are inside tool_result payloads.",
          "rule": "One launch observation per assistant tool_use block with run_in_background true in a non-subagent transcript; one carrier observation per (launch id, carrier kind) pair, joined on the exact <tool-use-id> string. 'returned' = at least one carrier; 'no user record' = no carrier of kind type:\"user\" with string content.",
          "note": "The strongest result in this batch: the three-carrier model is not just observed, it partitions cleanly - a queue-operation on 100% of returned shells, a queued_command attachment on exactly the 1,015 with no user record, a user string record on the other 1,504. Restricting the scan to user records would indeed lose 40% of shell completions."
        }
      ]
    },
    {
      "id": "QT-009",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes a `type:\"system\"` / `subtype:\"turn_duration\"` record per assistant turn, not per user record (4,580 such records against roughly 40,000 user records in the same corpus). Its payload is integers only - `durationMs` (median 134,688 ms) and `messageCount` (a running total) - and unlike the metadata cache lines it DOES carry `uuid`, `timestamp` and `parentUuid` with `isMeta:false`, plus `slug` on about 93% of records and `sessionKind` rarely.",
      "depends": "csift promotes it to the gated leaf `harness.meta.turn-duration`, renders a FABRICATED `[turn duration: ...]` key=value excerpt from exactly these fields, and - because the record carries a uuid and a timestamp - lets `show --uuid` and `show --line` address it flag-free.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "191-196",
          "snippet": "        Class::MetaTurnDuration => {\n            let mut fields: Vec<String> = Vec::new();\n            let ms = Record::u64_field(rec.duration_ms.as_ref());\n            if let Some(ms) = ms {\n                fields.push(format!(\"{} · durationMs={ms}\", fmt_ms(ms)));\n            }"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "506-508",
          "snippet": "    if args.reaches_gated(Class::MetaTurnDuration) {\n        verifiable.push(b\"turn_duration\");\n    }"
        },
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "19",
          "snippet": "\"stop_hook_summary\" => Some(Class::MetaStopHooks),"
        }
      ],
      "instrument": "`rg -NI '\"subtype\":\"turn_duration\"' ~/.claude/projects -g '*.jsonl'` into a JSON reader; counting rule = one observation per physical line, unioning keys and computing a per-key presence rate. Then `csift search '' <project-dir> -t harness.meta.turn-duration --count-by label` must equal the line count, and `csift show <target> --uuid <a turn_duration uuid>` must render the record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"subtype\":\"turn_duration\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (per-key presence rate, distinct key sets, durationMs median); denominators via rg -c --no-heading '\"type\":\"user\"' / '\"type\":\"assistant\"' / '\"stop_reason\":\"end_turn\"' per directory, split main-lane vs subagent by whether the path contains /subagents/; csift side: csift search '' @<PROJ> -t harness.meta.turn-duration --count-by label and csift show @<SESSION> --uuid <uuid>; binary side: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,200}\"turn_duration\".{0,400}'",
          "observed": "4,633 turn_duration records, ALL type=system, ALL isMeta=false. Key presence: parentUuid/uuid/timestamp/durationMs/messageCount/type/subtype/sessionId/version/cwd/gitBranch/userType/entrypoint/isSidechain 100%, slug 93.2%, pendingWorkflowCount 5.2%, pendingBackgroundAgentCount 2.9%, sessionKind 0.8%; 9 distinct key sets. durationMs median 145,510 ms (min 663, max 984,080,246). All 4,633 are in MAIN transcripts; zero in any subagent transcript. Denominators: main-lane type:\"user\" 67,885, main-lane type:\"assistant\" 133,993, main-lane \"stop_reason\":\"end_turn\" 8,913 (subagent lanes add 161,885 / 298,142 / 13,864). csift census on one project directory returned 287, equal to the raw count of 287 turn_duration lines in that directory. `csift show @<SESSION> --uuid <uuid>` rendered `harness.meta.turn-duration  L577  [turn duration: 12m 45s - durationMs=764630 messageCount=487]`. Binary writer: `function B_t(e,n,r,o,d){return{type:\"system\",subtype:\"turn_duration\",durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,pendingBackgroundAgentCount:o,pendingWorkflowCount:d,timestamp:new Date().toISOString(),uuid:oT(),isMeta:!1}}`, described in the shipped schema as '@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the Done in Ns / Waiting for N agents line.'",
          "rule": "One observation per physical line whose parsed subtype == 'turn_duration'; presence rate = lines carrying the key over all such lines; median over the 4,633 integer durationMs values. Denominator counts are per-line rg counts of the literal type marker, bucketed main vs subagent by path.",
          "note": "Everything structural held: uuid + timestamp + parentUuid + isMeta:false on 100% of records, slug at 93%, sessionKind rare, integers only, and csift's gated leaf addresses it by both --line and --uuid. Only the median and the user-record denominator needed correcting."
        }
      ]
    },
    {
      "id": "QT-010",
      "area": "queue-and-telemetry",
      "behavior": "Two OPTIONAL integer fields ride a `turn_duration` record: `pendingWorkflowCount` (236 of 4,581 records = 5.2%, values 1-3, observed from Claude Code 2.1.156) and `pendingBackgroundAgentCount` (133 of 4,581 = 2.9%, values 1-7, observed from 2.1.159). Across 4,587 such records corpus-wide the only optional keys of any kind are `slug`, `sessionKind` and those two - no background-shell count field exists anywhere.",
      "depends": "the fabricated `harness.meta.turn-duration` excerpt prints only the fields actually present, so a missing optional field must read as absent rather than as zero; the absence of a shell count is why csift must scan the whole main transcript for open background work instead of reading this record.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "177-180",
          "snippet": "    /// `turn_duration`: background agents still running at turn end (the REPL's\n    /// \"Waiting for N agents\" line). Optional; measured on ~3% of records.\n    #[serde(default, rename = \"pendingBackgroundAgentCount\")]\n    pub pending_background_agent_count: Option<serde_json::Value>,"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "124-203",
          "snippet": "            }\n            for (key, v) in [\n                (\"messageCount\", rec.message_count.as_ref()),\n                (\n                    \"pendingBackgroundAgentCount\",\n                    rec.pending_background_agent_count.as_ref(),\n                ),\n                (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),"
        }
      ],
      "instrument": "`rg -c '\"subtype\":\"turn_duration\"' <transcript>` for the denominator and `rg -c 'pendingBackgroundAgentCount' <transcript>` for the numerator; counting rule = turn_duration lines carrying the key over all turn_duration lines. For the closed optional set, run a key-set census over every matched line (one key-set observation per record) and report the distinct sorted key sets - expect zero keys containing `Shell`.",
      "located": {
        "claude_code": "2.1.159",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; src/model/record.rs:160-177 comment; CHANGELOG 0.10.0 (five promoted leaves); src/model/record.rs turn_duration fields"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each project directory separately: rg -NI --no-heading '\"subtype\":\"turn_duration\"' -g '*.jsonl' ~/.claude/projects/<PROJ> | python3 (distinct sorted key sets, value ranges, first version carrying each optional key); plus rg -cNI --no-heading 'budgetTokens' / 'budgetLimit' / 'budgetNudges' / 'briefHiddenCount' per directory; plus strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '.{0,200}\"turn_duration\".{0,400}'",
          "observed": "Over 4,633 turn_duration records: pendingWorkflowCount on 241 (5.2%), values {1,2,3}, earliest version 2.1.156; pendingBackgroundAgentCount on 136 (2.9%), values {1..13}, earliest version 2.1.159. Exactly 9 distinct key sets; the only keys that vary across them are slug, sessionKind, pendingWorkflowCount and pendingBackgroundAgentCount. No key anywhere in the 4,633 records contains the substring 'hell' (no shell count field). budgetTokens/budgetLimit/budgetNudges/briefHiddenCount appear 40/32/9/6 times corpus-wide but NONE of them as a top-level key of a turn_duration record - every occurrence is inside a type:\"user\" or type:\"assistant\" message payload (prose). The 2.1.258 writer nevertheless emits them: `...durationMs:e,budgetTokens:n?.tokens,budgetLimit:n?.limit,budgetNudges:n?.nudges,messageCount:r,...`, and the shipped schema documents them as 'Output tokens spent this turn toward the token budget', 'Turn token-budget ceiling', 'Budget-nudge count this turn'.",
          "rule": "One key-set observation per turn_duration record; presence rate = records carrying the key over 4,633; value range = the distinct integers seen; 'earliest version' = the minimum `version` string on a record carrying the key. Corpus check for a budget field = a top-level key on a parsed turn_duration record, not a substring anywhere in the file.",
          "note": "Two corrections matter. The observed pendingBackgroundAgentCount ceiling is 13, not 7 - a stated range invites a reader to treat it as a bound. And the 'only optional keys' inventory is a corpus fact that the shipped schema already exceeds by three budget fields; csift would render them as absent rather than reporting an unknown key."
        }
      ]
    },
    {
      "id": "QT-011",
      "area": "queue-and-telemetry",
      "behavior": "`pendingBackgroundAgentCount` is never emitted as the literal 0 - absence of the key means zero, not unknown (136 numeric occurrences corpus-wide, values 1 through 13, zero occurrences of the value 0). The field's relation to a transcript-replayed set of open async agents is looser than a match: over 3,316 turn_duration observations on four specimen transcripts the two agreed 2,292 times, and among the 59 records where the field is present 20 agreed exactly while 28 were off by exactly +1 on the replay side - so the field is best read as the harness's own count at that instant, not as something a replay can reconstruct.",
      "depends": "any consumer reading the field must treat an absent key as 0; the residual under-count comes from agents that notified and were then resumed (a notification is not terminal) and from agents spawned in a child lane, so the field is a lower bound on open async work.",
      "code": [
        {
          "path": "src/live/background.rs",
          "lines": "63-74",
          "snippet": "pub(crate) enum BgState {\n    /// Launched, no completion carrier names it yet.\n    Open,\n    Completed,\n    Failed,\n    Killed,\n    /// Claude Code's own orphan reconciliation at the next session start, or an\n    /// explicit `stopped` status.\n    Stopped,\n    /// A Monitor whose timeout fired (the `[Monitor timed out …]` event).\n    TimedOut,\n}"
        }
      ],
      "instrument": "`rg -c '\"pendingBackgroundAgentCount\":\\s*0' ~/.claude/projects -g '*.jsonl'` returns no matches while `rg -c '\"pendingBackgroundAgentCount\"'` returns many; counting rule = one observation per matching line. For the agreement figure, replay a transcript maintaining the set of launched-but-unreturned async agents and compare the set size against the field at each turn_duration record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for d in ~/.claude/projects/*/; do rg -oNI '\"pendingBackgroundAgentCount\"\\s*:\\s*[0-9]+' -g '*.jsonl' \"$d\"; done | sort | uniq -c | sort -rn   # plus, for the agreement half, a four-specimen replay: open an async agent on a tool_result line carrying \"status\":\"async_launched\" (keyed by its \"tool_use_id\"), close it at the first later line naming that id in <tool-use-id>...</tool-use-id>, and compare the open-set size against pendingBackgroundAgentCount at every \"subtype\":\"turn_duration\" line",
          "observed": "zero-probe: 136 lines corpus-wide carry the field as an actual number; the value histogram is 1:59, 2:32, 3:17, 4:9, 5:5, 7:4, 9:2, 8:2, 6:2, 13:1, 12:1, 11:1, 10:1 - the literal 0 never appears. (398 lines contain the bare key STRING; the 262-line difference is escaped prose quoting the field name, not a field.) Replay: 3316 turn_duration observations, replayed open-agent set equal to the field on 2292; among the 59 records where the field is actually present, 20 exact, 28 off by exactly +1 on the replay side, 8 off the other way; 985 records with the field ABSENT had >=1 open agent under the replay rule.",
          "rule": "zero-probe: one observation per physical line matching the unescaped numeric form, corpus-wide, scoped one project directory per rg invocation. Replay: one observation per turn_duration record; `open at that line` = an async_launched tool_result earlier in the file whose tool_use id has not yet appeared inside a <tool-use-id> tag; an absent field counts as 0.",
          "note": "First half CONFIRMED with an exact instrument: the value 0 is never serialized, and the value range is 1-13 (wider than the 1-7 the sibling claim records). Second half NOT reproduced: the stated 608-of-628 agreement, and its claim that every mismatch is an under-count on the transcript side, both fail under the async-agent join csift itself documents (src/live/background.rs lines 8-9 and 19-21) - my replay errs the OTHER way, over-counting on 1,013 observations against 11 under-counts. The residual is dominated by launches whose completion carrier never names the tool_use id (only 5 of 208 launches were still open at end of file, yet a single such leak keeps the set +1 for the rest of the transcript - exactly the +1 offset seen on 28 of the 59 field-bearing records). The original counting rule for `launched-but-unreturned async agent` is not recoverable from the claim text, so the agreement figure should be dropped rather than restated. Code site verified verbatim: src/live/background.rs:63-74 is the BgState enum exactly as quoted."
        }
      ]
    },
    {
      "id": "QT-012",
      "area": "queue-and-telemetry",
      "behavior": "A `turn_duration` record IS written while BACKGROUND work is still open: of 3,314 such records on four specimen transcripts, 2,739 (82.6%) were emitted with at least one background shell open, and on 59 the harness's own `pendingBackgroundAgentCount` was >= 1 (190 more carried a non-zero `pendingWorkflowCount`). An end-of-turn telemetry record therefore says the turn ended, never that the session finished its work.",
      "depends": "this is the exact hole the `idle-background-open` verdict fills: csift must join launches to completions itself rather than reading a turn-end record as a stop signal.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "11-14",
          "snippet": "    /// The turn ended (a clean end_turn) but N background task(s) the lens counts have\n    /// not returned - neither running nor stopped: by design (a dev server, a watcher)\n    /// or not, csift cannot tell. Never satisfies `--until stop`.\n    IdleBackgroundOpen,"
        }
      ],
      "instrument": "Replay a transcript maintaining the set of `run_in_background` launches with no `<task-notification>` naming them yet; at each `subtype:\"turn_duration\"` line record that set size and the line key set. Counting rule = one observation per turn_duration record; `open at that instant` = launched earlier in the file with no completion carrier seen.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python replay over the four specimen transcripts: record every `\"subtype\":\"turn_duration\"` line; open a background shell at the first line matching `running in background with ID: ([A-Za-z0-9_-]+)` and close it at the first later line naming that id inside <task-id>...</task-id>; count open shells at each turn_duration line, and read pendingBackgroundAgentCount / pendingWorkflowCount off the record itself",
          "observed": "3,314 turn_duration records; 1,872 background-shell launches; 2,739 of 3,314 records (82.6%) were emitted with at least one background shell open. The harness's own pending counters were non-zero on the same records: pendingBackgroundAgentCount present on 59 records (values 1-9) and pendingWorkflowCount present on 190. Per specimen the open-shell counts were 1639/1771, 604/1005, 427/430 and 69/108.",
          "rule": "one observation per turn_duration record; `open at that instant` = launched earlier in the same file with no completion carrier naming its id seen yet. The pending-counter figures are one observation per turn_duration record, an absent key counted as zero.",
          "note": "The law reproduces and is far stronger than stated - open background work at a turn-end record is the common case (83%), not the 16% the claim implies. The async-agent half is restated using the harness's own field rather than a replayed set, because the replay rule over-counts (see QT-011). Code site verified verbatim: src/live/verdict.rs:11-14 is the IdleBackgroundOpen doc comment and variant exactly as quoted."
        }
      ]
    },
    {
      "id": "QT-013",
      "area": "queue-and-telemetry",
      "behavior": "A `turn_duration` record emitted while a background SHELL is still running carries no field describing it - 2,739 of 2,739 measured records with an open shell had no shell-related key of any kind, and the key union over all 3,314 measured records is 18 keys whose only optional members are `slug`, `pendingWorkflowCount` and `pendingBackgroundAgentCount`. The pending counts cover background agents and workflows only, so the harness writes nothing on disk about a still-running shell at end of turn.",
      "depends": "csift cannot read open-shell state from end-of-turn telemetry, so `status` joins the session registry, the transcript tail, child lanes, the task list and a process probe, and its background section - a whole-transcript scan for launches and completions - is the only instrument for a running shell.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "124-203",
          "snippet": "            }\n            for (key, v) in [\n                (\"messageCount\", rec.message_count.as_ref()),\n                (\n                    \"pendingBackgroundAgentCount\",\n                    rec.pending_background_agent_count.as_ref(),\n                ),\n                (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),"
        }
      ],
      "instrument": "Launch a `run_in_background` shell, let the turn end, then read the newest end-of-turn record with `csift search '' <target> -t harness.meta.turn-duration --max-count -1 --no-truncate`: durationMs and messageCount only, no shell field. Counting rule = the key set enumerated per record over records known to have had an open shell.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; CHANGELOG 0.10.0 (measured on 100 turn_duration records emitted with an open shell); SPEC.md section 6 v0.10.0 ledger item 1; SPEC.md section 5.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "same four-specimen replay as QT-012, then a key-set census restricted to the turn_duration records that had >=1 open background shell: collect sorted(json.loads(line).keys()) per record and count distinct key sets, plus a scan of the key union for any key containing 'shell', 'bash' or 'command'",
          "observed": "2,739 turn_duration records with an open shell fall into 5 distinct key sets. The union of ALL keys over all 3,314 records is exactly 18: cwd, durationMs, entrypoint, gitBranch, isMeta, isSidechain, messageCount, parentUuid, pendingBackgroundAgentCount, pendingWorkflowCount, sessionId, slug, subtype, timestamp, type, userType, uuid, version. No key contains 'shell', 'bash' or 'command'. The only optional keys are slug (absent on 20 records), pendingWorkflowCount (190) and pendingBackgroundAgentCount (59).",
          "rule": "one key-set observation per turn_duration record known to have had an open shell at that line; the substring test for a shell-related key is run over the key union, not over values.",
          "note": "Holds, at 27x the sample: the shell-invisibility is total. Two of the three code sites are verbatim at the cited lines (src/live/background.rs:4-7, src/cli/live_args.rs:43-48); the record_text.rs block sits one line lower than cited."
        }
      ]
    },
    {
      "id": "QT-014",
      "area": "queue-and-telemetry",
      "behavior": "A `turn_duration` record is NEVER written while a synchronous tool call is in flight: across 628 such records on four specimen transcripts the count of unreturned `tool_use` ids at that line was 0 in 628 of 628.",
      "depends": "the record is therefore usable as proof that the synchronous lane was clear at that instant, but not as a turn-end detector, because its presence is not guaranteed at every clean end.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "19",
          "snippet": "\"stop_hook_summary\" => Some(Class::MetaStopHooks),"
        }
      ],
      "instrument": "Replay one transcript line by line maintaining a running set of `tool_use` ids minus the seen `tool_result` `tool_use_id`s; at every `subtype:\"turn_duration\"` line record the set size. Counting rule = one observation per turn_duration record; the expected census is 0 open ids in every observation.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "two-pass python replay over the four specimen transcripts: pass 1 records, for every tool_use id (regex `\"id\":\"(toolu_[^\"]+)\"` on `\"type\":\"assistant\"` lines), its open line and the line of its `\"tool_use_id\"` result on a `\"type\":\"user\"` line; pass 2 counts, at every `\"subtype\":\"turn_duration\"` line L, the ids with open_line < L < result_line",
          "observed": "3,314 turn_duration records; the in-flight count was 0 in 3,314 of 3,314 (per specimen 1771/1771, 1005/1005, 430/430, 108/108). 28,948 tool_use ids were opened across the four files and every single one has a matching tool_result (0 never-returned); only 2 pairs are written out of file order (the result line precedes the tool_use line), and a naive single-pass open-set that cannot see those 2 pairs is what makes a same-file replay report a phantom 1-2 open calls.",
          "rule": "one observation per turn_duration record; a tool call is `in flight` at that line iff its tool_use record is earlier in the file and its tool_result record is strictly later. Ids never returned anywhere in the file are excluded (there were none).",
          "note": "Reproduced exactly at 5.3x the sample, and the mechanism behind the only plausible counter-example is now pinned: 2 of 28,948 tool_use/tool_result pairs land out of file order, so an interval test (open < L < result) rather than a running open-set is the counting rule a stranger must use. Code site verified verbatim: src/model/classify_promoted.rs:16-21 is the `\"system\" => match self.subtype` arm exactly as quoted."
        }
      ]
    },
    {
      "id": "QT-015",
      "area": "queue-and-telemetry",
      "behavior": "A `turn_duration` record fires almost exclusively at a clean `end_turn` boundary: the preceding assistant `stop_reason` was `end_turn` in 3,267 of 3,314 records, `stop_sequence` in 45 and `tool_use` in 2. It is rare but NOT impossible after an interrupted or rejected turn end: 2 of 174 interrupt-marked gaps and 2 of 41 rejection-marked gaps carried one. Read it as a strong clean-boundary hint, never as a guarantee, and never as a segmentation input.",
      "depends": "csift may read the record as a clean-boundary marker but never as a segmentation input; turn segmentation stays on the four turn-opening record shapes.",
      "code": [
        {
          "path": "src/live/verdict.rs",
          "lines": "309-314",
          "snippet": "    let eot_shape = main_tail.unreturned_use.is_none()\n        && (main_tail\n            .last_stop_reason\n            .as_deref()\n            .is_some_and(|s| s == \"end_turn\")\n            || registry_shell);"
        }
      ],
      "instrument": "Segment a transcript into turn-end gaps (the records between the last assistant record and the next turn opener) and bucket each gap by the previous assistant `stop_reason` and by whether the gap contains a `turn_duration`. Counting rule = one gap per turn end; interrupt-shaped = a user record arriving while the previous assistant `stop_reason` is `tool_use`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 6.13"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python replay over the four specimen transcripts: group assistant records by message id, take each message's last non-null `\"stop_reason\"` and its last record line; for every `\"subtype\":\"turn_duration\"` line take the stop_reason of the nearest preceding assistant message; separately, for every user record containing `[Request interrupted by user` or `The user doesn't want to proceed with this tool use`, test whether a turn_duration line falls between it and the first record of the next assistant message",
          "observed": "3,314 turn_duration records: preceding assistant stop_reason was end_turn on 3,267 (98.6%), stop_sequence on 45, tool_use on 2. Interrupt markers: 174 total, 2 of them followed by a turn_duration before the next assistant record (both in one specimen). Tool/plan rejection markers: 41 total, 2 followed by a turn_duration (same specimen; 10 lines carry both markers).",
          "rule": "one observation per turn_duration record for the stop_reason join; one observation per marker record for the gap tests, where the gap runs from the marker line to the first record of the next assistant message.",
          "note": "The direction holds at 99%, but the two absolute zeros in the claim do not survive a 6x larger sample: interrupt-shaped and rejection-shaped gaps each produced 2 turn_duration records, and the preceding stop_reason vocabulary includes `stop_sequence` (45 records), which the claim does not mention. Code site verified verbatim: src/live/verdict.rs:309-314 is the `eot_shape` binding exactly as quoted."
        }
      ]
    },
    {
      "id": "QT-016",
      "area": "queue-and-telemetry",
      "behavior": "A `turn_duration` record is usually but not always written at a clean `end_turn`: it was present in 3,267 of 3,554 measured clean turn-end gaps and absent from 287. Absence is concentrated in a few Claude Code versions (2.1.217 wrote one in 130 gaps, 2.1.219 ten in 77, 2.1.233 fifty-seven in 130) while eleven other versions in the sample, including 2.1.258, are at or above 98.8%. Its absence is therefore still no evidence that a turn is running - but its presence is close to routine on current versions.",
      "depends": "csift never reads the presence or absence of an end-of-turn telemetry record as proof that a session finished; `status` derives its verdict from the tail state machine, the registry and the background scan instead.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "178-184",
          "snippet": "/// fields only, stable order - so the leaf is both matchable and legible:\n/// - turn-duration: `[turn duration: 1m 5s · durationMs=64911 messageCount=908\n///   pendingBackgroundAgentCount=2]`;\n/// - stop-hooks: `[stop hooks: count=N errors=M prevented=false]` + one `command (Nms)`\n///   line per hook (the commands are the record's only text);\n/// - snapshot: `[file-history snapshot at <ts>: <path>@vN, …]` (paths sorted) or\n///   `[file-history delta at <ts>: <path>@vN backup=<name>]` (the `backup=` suffix"
        }
      ],
      "instrument": "`csift search '' <target> -t harness.meta.turn-duration --count-by turn` against `csift search '' <target> -t user.message -c`; expected: strictly fewer turn_duration records than closed turns. Counting rule = one record per emitting turn, one gap per turn end.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 6.13; dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python replay over the four specimen transcripts: group assistant records by message id; for each message whose last stop_reason is `end_turn`, the gap runs from its last record line to the first record line of the next assistant message; test whether any `\"subtype\":\"turn_duration\"` line falls in that gap; bucket every gap by the `version` string of the assistant message that opened it",
          "observed": "3,554 clean end_turn gaps, 3,267 carry a turn_duration record (91.9%), 287 do not. Per specimen: 1746/1757, 989/996, 425/621, 107/180. Per Claude Code version the presence rate is >= 0.988 at 11 of the 15 versions represented (2.1.150 24/24, 2.1.156 82/82, 2.1.159 621/623, 2.1.177 443/448, 2.1.186 35/35, 2.1.191 541/545, 2.1.218 11/11, 2.1.220 916/918, 2.1.227 31/31, 2.1.228 323/327, 2.1.251 153/154, 2.1.258 19/19) and collapses at three: 2.1.217 1/130, 2.1.219 10/77, 2.1.233 57/130.",
          "rule": "one gap per assistant message whose last stop_reason is end_turn; `present` = at least one turn_duration line strictly inside the gap. Grouping assistant records by message id is load-bearing: treating each assistant RECORD as a turn end (a multi-block message emits several) roughly halves the measured rate and is what produces a ~48% figure.",
          "note": "The law survives (287 clean ends carry no record, so absence proves nothing) but the ratio in the claim is wrong by a factor of two: 91.9% present, not 47.6%. The likely cause of the low original figure is segmenting on assistant RECORDS instead of assistant MESSAGES; the version breakdown also shows the gap is a per-version behaviour, not a steady coin flip. The second code site (src/live/verdict.rs:309-314) is verbatim at its cited lines."
        }
      ]
    },
    {
      "id": "QT-017",
      "area": "queue-and-telemetry",
      "behavior": "The rendered end-of-turn lines the operator sees never land on disk - the `turn_duration` record is their structured body, and the shipped 2.1.258 binary says so in one string: `REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.` Corpus-wide the phrase `Cogitated for` appears on 56 lines (27 assistant, 24 user, 4 attachment, 1 queue-operation) and the wording `waiting for N background agents to finish` on 10 lines (4 user, 3 last-prompt, 1 assistant, 2 attachment) - zero on any `system` record in both cases.",
      "depends": "csift must source turn timing and pending-agent facts from `durationMs` and `pendingBackgroundAgentCount` only; searching the transcript for the rendered phrases finds quoted prose, never telemetry.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "197-204",
          "snippet": "            for (key, v) in [\n                (\"messageCount\", rec.message_count.as_ref()),\n                (\n                    \"pendingBackgroundAgentCount\",\n                    rec.pending_background_agent_count.as_ref(),\n                ),\n                (\"pendingWorkflowCount\", rec.pending_workflow_count.as_ref()),\n            ] {"
        }
      ],
      "instrument": "`rg -cNI 'Cogitated for' ~/.claude/projects -g '*.jsonl'`, then re-parse each hit line and bucket it by top-level `type`; counting rule = one observation per matching LINE. Expect zero on `system`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; src/model/record.rs:160-177 comment; CHANGELOG 0.10.0 (five promoted leaves); src/model/record.rs turn_duration fields"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg \"Waiting for .{0,30}agent\"   AND   for d in ~/.claude/projects/*/; do rg -NI --no-messages 'Cogitated for' -g '*.jsonl' \"$d\"; done  (same loop for -i 'waiting for [0-9]+ background agents to finish'), each hit line then re-parsed and bucketed by its top-level `type`",
          "observed": "Binary, one string: `@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.` (`Cogitated` also appears as a past-tense spinner verb alongside the `Cogitating` verb list). On disk: `Cogitated for` on 56 lines - 27 assistant, 24 user, 4 attachment, 1 queue-operation, ZERO on any `system` record; `waiting for N background agents to finish` on 10 lines - 4 user, 3 last-prompt, 1 assistant, 2 attachment, ZERO on any `system` record.",
          "rule": "one observation per matching physical line, each line re-parsed as JSON and counted under its top-level `type`; the scan is scoped one project directory per rg invocation.",
          "note": "The load-bearing part - zero occurrences on a `system` record, so searching for the rendered phrase finds only quoted prose - reproduces exactly, and the binary string confirming the REPL/record relationship is present verbatim in 2.1.258. Only the line counts moved (56 and 10, up from 11 and 20), which is itself the point: the counts grow because sessions keep quoting the phrases, and the type bucketing is what separates prose from telemetry. The second code site (src/model/taxonomy.rs:122-127) is verbatim at its cited lines."
        }
      ]
    },
    {
      "id": "QT-018",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes a `type:\"system\"` / `subtype:\"away_summary\"` record whose top-level `content` is ALWAYS a string (1,630 of 1,630): the recap shown when the operator returns to a session after a blur of five minutes or more. The text is model-generated by a side call rather than templated (the 2.1.258 binary carries `Away summary cannot use tools` and an `away_summary_generate` telemetry event) and the feature is config-gated (`awaySummaryEnabled`). Measured corpus: 1,630 records, 1,587 of 1,630 contents distinct, content length min/median/max 76/219/8,333 characters (p99 314; one outlier). The trailing suffix ` (disable recaps in /config)` is a first-few-recaps-per-session nudge, not an early-version artefact: 185 records carry it, spanning every version in the sample up to 2.1.258, and it is on 91 of the first 93 recaps of a session.",
      "depends": "csift promotes it to the gated leaf `harness.meta.away-summary` and matches on the VERBATIM `content` (a raw byte substring, so no synthesized marker is needed); it is the most valuable promoted prose, and because it is model-written it must never be presented as operator text.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "190",
          "snippet": "        Class::UserQueued | Class::MetaAwaySummary => rec.content_str().map(str::to_string),"
        }
      ],
      "instrument": "`rg -cNI '\"subtype\":\"away_summary\"' ~/.claude/projects -g '*.jsonl'` for the population (counting rule = one per matching line), then a content census over those lines for distinctness and length. `csift search '' <target> -t harness.meta.away-summary --no-truncate` must print the verbatim recap.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 5.1; src/model/taxonomy.rs:128-132 comment; CHANGELOG 0.10.0; SKILL.md label table (harness.meta.away-summary)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for d in ~/.claude/projects/*/; do rg -NI --no-messages '\"subtype\":\"away_summary\"' -g '*.jsonl' \"$d\"; done > away.jsonl   then a python census over away.jsonl for content type, length, distinctness, the trailing-suffix rate and the key sets; render check: csift search '' @<session-prefix> -t harness.meta.away-summary --no-truncate --max-count 2",
          "observed": "1,630 records (matching csift's own `search '' -t harness.meta.away-summary --count-by session` total of 1,630 across 39 sessions). `content` is a string on 1,630 of 1,630. Distinct contents 1,587 of 1,630. Length min/median/max 76 / 219 / 8,333 characters; p90 271, p99 314, exactly one record over 1,760. The trailing suffix ` (disable recaps in /config)` is on 185 records spanning 2026-05-25 to 2026-09-02 and every CC version in the sample including 2.1.258; by in-session rank it is on 39/39 first recaps, 30/31 second, 22/23 third, then decays (85 of 1,493 later ones). Three key sets: the base 15, +slug (1,533), +sessionKind (10). The csift render prints the verbatim recap under `⚙ harness.meta.away-summary` with its line number.",
          "rule": "one observation per physical line matching the unescaped `\"subtype\":\"away_summary\"` form, scoped one project directory per rg invocation; distinctness is over exact content strings; the suffix test is an exact endswith; in-session rank orders a session's records by timestamp.",
          "note": "Everything structural holds - always a string, essentially all distinct, short prose, config-gated, model-written. Two corrections matter. The max length is 8,333 rather than 1,760 (a single outlier; p99 is 314), so a consumer sizing a buffer on the old number would clip. And `the earliest ones carry the trailing suffix` is wrong: the suffix is a per-SESSION nudge on the first few recaps and is still emitted at 2.1.258, so its presence dates nothing. Code site: the classify arm (src/model/classify_promoted.rs:18) is verbatim; the render arm sits one line lower than cited."
        }
      ]
    },
    {
      "id": "QT-019",
      "area": "queue-and-telemetry",
      "behavior": "The away recap is capped (`[awaySummary] recap capped from `) and is SKIPPED in SEVEN named conditions in 2.1.258: cache age unknown, cache stale, at or near a rate limit, draft input present, background work pending, a loop wakeup pending, and a StructuredOutput recap already present. Four further paths abort generation (a new turn already running, no saved cache-safe params, a failed params rebuild, a failed generation), and the whole feature is config-gated behind `awaySummaryEnabled`. Its absence after a long gap is therefore expected behaviour, not a missing record: on two specimen transcripts only 59% and 34% of gaps of five minutes or more were followed by a recap.",
      "depends": "csift must not treat a missing away recap as evidence about the gap; the leaf is searchable when present and silent when the harness skipped it.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "18",
          "snippet": "                \"away_summary\" => Some(Class::MetaAwaySummary),"
        }
      ],
      "instrument": "Run `strings -n 6` over the shipped Claude Code binary for the located version and grep for `awaySummary` to read the skip-condition list and generation telemetry keys; counting rule = one observation per extracted string. Cross-check on disk by finding session gaps of five minutes or more that carry no `away_summary` record.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > s.txt; rg -n 'awaySummary' s.txt   plus an on-disk cross-check: for two specimen transcripts, count gaps of >= 300 s between consecutive timestamped records and how many are followed by an away_summary record before the next such gap",
          "observed": "SEVEN skip strings in 2.1.258, contiguous in the binary: `[awaySummary] skipped: cache age unknown\"`, `[awaySummary] skipped: cache stale`, `[awaySummary] skipped: at or near rate limit*`, `[awaySummary] skipped: draft input present`, `[awaySummary] skipped: background work pending`, `[awaySummary] skipped: loop wakeup pending`, `[awaySummary] skipped: StructuredOutput recap present`. Cap confirmed by `[awaySummary] recap capped from `. Four further abort paths: `[awaySummary] ccr recap dropped: new turn already running`, `[awaySummary] no CacheSafeParams saved, skipping `, `[awaySummary] fallback params rebuild failed: `, `[awaySummary] generation failed: `. Adjacent: `awaySummaryEnabled`, `away_summary_generate`, `Away summary cannot use tools`, ` (disable recaps in /config)`, `pendingAgents`, `pendingWorkflows`. On disk: 150 gaps >= 300 s in one specimen with 89 followed by a recap (59%), and 383 gaps with 130 followed by a recap (34%).",
          "rule": "one observation per extracted binary string; on disk, one observation per gap of >= 300 s between consecutive timestamped records, `followed by a recap` meaning an away_summary record appears before the next such gap.",
          "note": "Refined upward: the skip list is seven conditions, not five - the two the claim omits are cache-related (`cache age unknown`, `cache stale`), which matters because they are the ones that fire on a fresh or long-running session rather than on operator state. The on-disk cross-check the claim asks for now has numbers. Code site verified verbatim: src/model/classify_promoted.rs:18."
        }
      ]
    },
    {
      "id": "QT-020",
      "area": "queue-and-telemetry",
      "behavior": "An away recap never re-enters the conversation: 1,629 of 1,630 measured `away_summary` contents never re-appear anywhere downstream in the surviving conversation of the same file. The single exception re-appeared on a later `type:\"user\"` record, i.e. it was quoted back in, not delivered by the harness.",
      "depends": "csift classifies the leaf LLM-invisible on that evidence and words it as `not in the surviving conversation`; treating a model-generated recap as delivered context would inflate every accounting of what the assistant actually received.",
      "code": [
        {
          "path": "src/cli/search_args.rs",
          "lines": "558-560",
          "snippet": "    pub fn reaches_gated(&self, c: Class) -> bool {\n        !self.labels.is_empty() && self.label_filter().selected(c.path())\n    }"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "128-132",
          "snippet": "    /// `harness.meta.away-summary` - the `system`/`away_summary` recap the harness\n    /// generates (a side model call, config-gated) when the operator returns after 5+\n    /// minutes away. Model-generated prose, yet no `message{}`: shown in the UI, not in\n    /// the surviving conversation (measured 1,196/1,198 never re-appear downstream).\n    MetaAwaySummary,"
        }
      ],
      "instrument": "`csift search '' -t harness.meta.away-summary --count-by session` for the population; then for each record take a distinctive phrase of its text and run `csift search '<phrase>' <same target>` looking for a later non-away record. Counting rule = away_summary records whose text recurs on any later record of the same file.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "SPEC.md section 5.1; src/model/taxonomy.rs:128-132 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each of the 41 transcripts containing an away_summary record, load the file, and for every away_summary take its content minus the ` (disable recaps in /config)` suffix, take the first 60 characters, JSON-escape them, and scan every LATER line that is not itself an away_summary for that escaped string; then re-parse the one hit and read its top-level `type`",
          "observed": "1,630 away_summary records checked across 41 files; 1 recurs downstream, 1,629 never do. The single recurrence lands on a later `type:\"user\"` record carrying a `message{}` object (i.e. it re-entered as operator/injected text, not as harness output).",
          "rule": "one observation per away_summary record; `recurs` = a 60-character JSON-escaped prefix of its content appears on any later line of the same file that is not itself an away_summary record.",
          "note": "The law holds and is stronger than stated (99.94% never recur). Two corrections: the numbers, and the fact that the one exception is a user record rather than harness output - which is the right way to word it, since a recap quoted back by a human is not the harness delivering it. The taxonomy.rs doc comment is verbatim at its cited lines but embeds the stale 1,196/1,198 count and should be updated in the same pass. The search_args.rs gate moved six lines down."
        }
      ]
    },
    {
      "id": "QT-021",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes a type:\"system\" / subtype:\"stop_hook_summary\" record per Stop-hook run: an execution ledger carrying hookCount (observed value set {1,3,6,7,8,9}), hookInfos[] of {command, durationMs} elements where durationMs is OPTIONAL (41,382 of 41,412 measured elements carry it, 30 carry command alone), hookErrors[] (non-empty on 30 of 5,644 measured records), preventedContinuation, stopReason, hasOutput and toolUseID; all 5,644 measured records also carry level:\"suggestion\", which is one of the four values (info/notice/suggestion/warning) the 2.1.258 schema allows.",
      "depends": "csift promotes it to the gated leaf `harness.meta.stop-hooks` and renders a fabricated `[stop hooks: count=N errors=M prevented=B]` head (M = the LENGTH of `hookErrors`, not its text) plus one line per `hookInfos` element: `command (Nms)` when `durationMs` is present, the bare `command` when it is not, and no line at all for an element without a `command` string. The leaf is kept distinct from `harness.meta.hook`, which is the text a hook INJECTED, so run telemetry is never attributed to injected context.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "14-19",
          "snippet": "        match self.r#type.as_deref()? {\n            \"queue-operation\" => self.queued_class(),\n            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "218-232",
          "snippet": "            let prevented = rec.prevented_continuation.unwrap_or(false);\n            let mut lines = vec![format!(\n                \"[stop hooks: count={count} errors={errors} prevented={prevented}]\"\n            )];\n            if let Some(infos) = rec\n                .hook_infos\n                .as_ref()\n                .and_then(serde_json::Value::as_array)\n            {\n                for info in infos {\n                    let Some(cmd) = info.get(\"command\").and_then(serde_json::Value::as_str) else {\n                        continue;\n                    };\n                    match Record::u64_field(info.get(\"durationMs\")) {\n                        Some(ms) => lines.push(format!(\"{cmd} ({ms}ms)\")),"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "394-400",
          "snippet": "        // v0.10.0 promoted lines, each behind its own explicit-selector gate.\n        || (gates.queued && QUEUED_FINDER.find(line).is_some())\n        || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n        || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n        || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n        || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())\n        || (gates.system && SUBTYPE_FINDER.find(line).is_some())"
        }
      ],
      "instrument": "`rg -NI '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl'` into a JSON reader for a key-set and value census; counting rule = one observation per matching line, and one per ARRAY ELEMENT for the `hookInfos` total. `csift search '' <target> -t harness.meta.stop-hooks --max-count 2` must print the fabricated head plus the command lines.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 5.1; SPEC.md v0.10.0 ledger; src/model/taxonomy.rs:133-138 comment; CHANGELOG 0.10.0; src/model/record.rs stop_hook_summary fields"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'json.loads each line; Counter over keys, hookCount values, hookInfos element key-sets, level, stopReason/preventedContinuation cross-tab, hookAdditionalContext type+len'  (2) strings -n 200 ~/.local/share/claude/versions/2.1.258 | rg -o 'subtype:x\\(\"stop_hook_summary\"\\).{0,700}'  (3) csift search '' <transcript> --no-subagents -t harness.meta.stop-hooks --max-count 2",
          "observed": "5,644 stop_hook_summary lines, 0 unparseable. hookCount value set is exactly {1:2, 3:306, 6:2042, 7:702, 8:2, 9:2590} - the same six values the claim lists. hookInfos holds 41,412 elements: 41,382 with key-set (command, durationMs) and 30 with (command) only. hookErrors is a non-empty array on 30 records. level=='suggestion' on 5,644 of 5,644. hookCount / hookInfos / hookErrors / preventedContinuation / stopReason / hasOutput / toolUseID are present on 5,644 of 5,644 (hasOutput true on 5,641, false on 3). Earliest version carrying the record is 2.1.150 (24 records, earliest timestamp 2026-05-25T09:04:06Z), not 2.1.170; 2.1.258 carries 24. Binary schema at 2.1.258: hook_count:int, hook_infos:[{command, prompt_text?, duration_ms?}], hook_errors:[string], hook_additional_context:[string]?, prevented_continuation:bool, stop_reason?:string, has_output:bool, level:enum[\"info\",\"notice\",\"suggestion\",\"warning\"], tool_use_id?, hook_label?, total_duration_ms?, uuid, session_id; describe = \"@internal Summary of Stop/SubagentStop hook execution at turn end — which hooks ran, their output, and whether any prevented continuation. From internal SystemMessage 'stop_hook_summary'.\" csift printed '[stop hooks: count=6 errors=0 prevented=false]' followed by one '<command> (<N>ms)' line per hook entry; in the sampled transcript --count-by label gave harness.meta.stop-hooks 990 and harness.meta.hook 3, two separate keys.",
          "rule": "One observation per matching jsonl line for record-level counts; one observation per ARRAY ELEMENT for the hookInfos total. A field counts as present when the key exists (a null value still counts as present). Corpus = every *.jsonl under ~/.claude/projects (7,781 files, 1,291,324 lines by the csift stats census).",
          "note": "Behavior holds; only corpus-growth numbers and the hookInfos element shape needed correction. Two forward notes from the 2.1.258 binary schema, neither yet observed on disk here: the record gained optional hook_label (a sibling render path keys on hookLabel===\"PreToolUse\", so the same subtype can now describe hook events other than Stop/SubagentStop) and optional total_duration_ms and per-hook prompt_text. hookLabel appears on 0 of 5,644 corpus records, and a grep of src/ finds no csift field for hook_label, total_duration_ms or prompt_text - so a PreToolUse-labeled record would today render as if it were a turn-end Stop summary."
        }
      ]
    },
    {
      "id": "QT-022",
      "area": "queue-and-telemetry",
      "behavior": "stopReason is empty on 5,641 of 5,644 measured stop_hook_summary records; the 3 carrying the string 'Stop hook prevented continuation' are exactly the 3 that also carry preventedContinuation:true, so a blocked turn end is a rare, positively marked event.",
      "depends": "the fabricated stop-hooks excerpt prints `prevented=<bool>` from `preventedContinuation`, which is the field a consumer should key on; the free-text `stopReason` is empty on almost every record and carries no independent information.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "211-221",
          "snippet": "        Class::MetaStopHooks => {\n            let count = Record::u64_field(rec.hook_count.as_ref()).unwrap_or(0);\n            let errors = rec\n                .hook_errors\n                .as_ref()\n                .and_then(serde_json::Value::as_array)\n                .map_or(0, Vec::len);\n            let prevented = rec.prevented_continuation.unwrap_or(false);\n            let mut lines = vec![format!(\n                \"[stop hooks: count={count} errors={errors} prevented={prevented}]\"\n            )];"
        },
        {
          "path": "src/model/record.rs",
          "lines": "195-201",
          "snippet": "    /// `stop_hook_summary`: the hooks that errored (an array; empty on most records).\n    #[serde(default, rename = \"hookErrors\")]\n    pub hook_errors: Option<serde_json::Value>,\n\n    /// `stop_hook_summary`: true when a Stop hook blocked the turn from ending.\n    #[serde(default, rename = \"preventedContinuation\")]\n    pub prevented_continuation: Option<bool>,"
        }
      ],
      "instrument": "Parse every `stop_hook_summary` line and cross-tabulate `preventedContinuation` against a non-empty `stopReason`; counting rule = one observation per matching line.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'parse each line; cross-tabulate a non-empty stopReason against preventedContinuation===true and collect the distinct non-empty stopReason values'",
          "observed": "5,644 records. stopReason non-empty on 3; preventedContinuation true on 3; the intersection is 3 (so the two sets are identical). The single distinct non-empty value is the string 'Stop hook prevented continuation', seen 3 times. stopReason is empty on the other 5,641.",
          "rule": "One observation per matching jsonl line. 'Non-empty stopReason' = the value is a string whose strip() is not empty. Same corpus as QT-021.",
          "note": "The interesting number - 3 blocked turn ends, and stopReason carrying no information the boolean does not already carry - is unchanged from the ledger; only the denominator grew with the corpus. The 2.1.258 schema confirms the shape: stop_reason is declared optional while prevented_continuation is a required boolean, so keying on the boolean is the right advice."
        }
      ]
    },
    {
      "id": "QT-023",
      "area": "queue-and-telemetry",
      "behavior": "The hookAdditionalContext field of a stop_hook_summary record, when present, was an EMPTY list in every one of the 4,916 measured instances - so the hook COMMANDS in hookInfos[] are the only text these records carry. Emptiness is a property of the measured corpus, not of the format: Claude Code 2.1.258 documents the field as the feed for hookSpecificOutput.additionalContext, so a Stop hook that emitted additionalContext would fill it.",
      "depends": "csift joins the `hookInfos[].command` lines as the record text; treating `hookAdditionalContext` as the context the hooks contributed would yield nothing at all, and the fabricated head is what makes the leaf matchable.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "222-236",
          "snippet": "            if let Some(infos) = rec\n                .hook_infos\n                .as_ref()\n                .and_then(serde_json::Value::as_array)\n            {\n                for info in infos {\n                    let Some(cmd) = info.get(\"command\").and_then(serde_json::Value::as_str) else {\n                        continue;\n                    };\n                    match Record::u64_field(info.get(\"durationMs\")) {\n                        Some(ms) => lines.push(format!(\"{cmd} ({ms}ms)\")),\n                        None => lines.push(cmd.to_string()),\n                    }\n                }\n            }"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "133-138",
          "snippet": "    /// `harness.meta.stop-hooks` - the `system`/`stop_hook_summary` execution ledger\n    /// of the Stop hooks that ran at turn end (`hookInfos[].command` + durations,\n    /// `hookErrors`, `preventedContinuation`). Distinct from [`Class::MetaHook`], which\n    /// is the text a hook INJECTED into the model; this record is the run itself and\n    /// its `hookAdditionalContext` is empty on every measured instance.\n    MetaStopHooks,"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "506-513",
          "snippet": "    if args.reaches_gated(Class::MetaTurnDuration) {\n        verifiable.push(b\"turn_duration\");\n    }\n    if args.reaches_gated(Class::MetaStopHooks) {\n        verifiable.push(b\"stop_hook_summary\");\n    }\n    if args.reaches_gated(Class::MetaSnapshot) {\n        verifiable.push(b\"file-history-\");"
        }
      ],
      "instrument": "Parse all `stop_hook_summary` lines and count the instances where `hookAdditionalContext` is present and its length is 0; counting rule = one observation per present instance (measured 4,865 of 4,865 empty).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 5.1; SPEC.md v0.10.0 ledger; src/model/taxonomy.rs:133-138 comment"
      },
      "first_seen_claude_code": "2.1.170",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) rg -NI --no-heading -F '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'count records where hookAdditionalContext is present, its Python type, and len(value)==0; also bucket presence by the record version field'  (2) strings -n 200 ~/.local/share/claude/versions/2.1.258 | rg -o 'hook_additional_context.{0,260}'  (3) rg -n 'hookAdditionalContext|hook_additional_context' src/ --type rust",
          "observed": "hookAdditionalContext present on 4,916 of 5,644 records; its value is a list on 4,916 of 4,916 and its length is 0 on 4,916 of 4,916 - no non-empty instance anywhere in the corpus. Presence is version-gated: 0 of the 728 records at 2.1.150 / 2.1.156 / 2.1.159 carry the key, and 100% of records carry it from 2.1.170 onward (2.1.258: 24 of 24), so the claim's first_seen_cc 2.1.170 is right for this FIELD. Binary describe: \"Non-error feedback from hookSpecificOutput.additionalContext — kept separate from hook_errors so the sanctioned feedback channel is not labeled an error. Absent in sessions persisted before this field existed.\" The src/ grep finds hook_additional_context only as the ATTACHMENT payload helper (Record::hook_additional_context_text, src/model/predicates.rs:336) that feeds harness.meta.hook - Record has no field for the stop_hook_summary key of the same name.",
          "rule": "One observation per record where the key exists (measured 4,916 of 4,916 empty). Version buckets read the record's own version field. Same corpus as QT-021.",
          "note": "Holds as a measurement, refined as a law. Actionable gap this verification exposes: csift's stop-hooks render reads hookCount, hookErrors, preventedContinuation and hookInfos only, and Record models no hookAdditionalContext field for this record type - so on the first transcript where a Stop hook returns additionalContext, that text would be silently absent from the excerpt while the taxonomy comment at src/model/taxonomy.rs:137 still asserts it 'is empty on every measured instance'. The comment should say 'on every instance measured through CC 2.1.258' or the field should be read."
        }
      ]
    },
    {
      "id": "QT-024",
      "area": "queue-and-telemetry",
      "behavior": "At Claude Code 2.1.258 a stop_hook_summary record and a turn_duration record are written as a pair at each turn end, stop_hook_summary first, with the turn_duration record naming it as parentUuid: 24 of each over 4 transcripts, strictly alternating, with no unpaired instance. Neither subtype is a safe turn counter on its own, because turn_duration is suppressed by the showTurnDuration user setting and stop_hook_summary is absent when no Stop hook is installed.",
      "depends": "csift classifies the record as the gated searchable leaf `harness.meta.stop-hooks` and never as a turn boundary: `opens_turn` fires only on the four user-record cases, so a `type:\"system\"` stop-hook line is a turn MEMBER, never a delimiter. Turn counts stay keyed on the human's own records, which is what keeps them stable when either subtype is absent by configuration.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "14-19",
          "snippet": "        match self.r#type.as_deref()? {\n            \"queue-operation\" => self.queued_class(),\n            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "161-219",
          "snippet": "    #[must_use]\n    pub fn opens_turn(&self) -> bool {\n        self.is_genuine_user()\n            || self.is_auq_answer_boundary()\n            || self.is_plan_rejection_boundary()\n            || self.is_peer_message_record()\n    }"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "394-400",
          "snippet": "        // v0.10.0 promoted lines, each behind its own explicit-selector gate.\n        || (gates.queued && QUEUED_FINDER.find(line).is_some())\n        || (gates.turn_duration && TURN_DURATION_FINDER.find(line).is_some())\n        || (gates.away_summary && AWAY_SUMMARY_FINDER.find(line).is_some())\n        || (gates.stop_hooks && STOP_HOOKS_FINDER.find(line).is_some())\n        || (gates.snapshot && SNAPSHOT_FINDER.find(line).is_some())\n        || (gates.system && SUBTYPE_FINDER.find(line).is_some())"
        },
        {
          "path": "src/cli/show_stats_args.rs",
          "lines": "84-88",
          "snippet": "    /// `42..` = turn 42 → the end). Fetches every user and assistant record of the named\n    /// turns (reads a turn's whole back-and-forth); the gated non-record lines a turn\n    /// carries (turn-duration, stop-hooks, away-summary, queue, snapshot, system) belong\n    /// to no turn fetch: address them by `--line`/`--uuid`, or `search -t harness.meta`.\n    /// The turn numbering matches the `·t<N>` in `search`'s exchange headers exactly."
        }
      ],
      "instrument": "`rg -c '\"subtype\":\"stop_hook_summary\"' ~/.claude/projects -g '*.jsonl'` and the same for `turn_duration`, compared PER FILE; counting rule = one record per matching line.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "(1) rg -NI --no-heading -H '\"subtype\":\"stop_hook_summary\"|\"subtype\":\"turn_duration\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-file and per-version counts of each subtype, plus the per-file S/T marker string in file order'  (2) python3 -c 'in the largest transcript carrying both, collect every turn_duration uuid and count records naming one as parentUuid'  (3) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{200}showTurnDuration.{200}'",
          "observed": "At CC 2.1.258 the two are 1:1: 24 stop_hook_summary and 24 turn_duration records, spread over 4 transcripts (19/19, 2/2, 2/2, 1/1), and in every one of those 4 the marker string in file order is a strict alternation STSTST... - no transcript has a stop_hook_summary that is not immediately followed by a turn_duration. Across the whole corpus (54 files carrying either) the totals are 5,644 vs 4,632, but per-file the direction is mixed: 14 files have more stop_hook_summary, 8 files have FEWER, 32 are equal, and 0 files carry one type without the other. The largest transcript measured (158,923 lines) has 990 stop_hook_summary against 1,005 turn_duration - the opposite of the claim. In that same transcript 989 of the 990 stop_hook_summary uuids are the parentUuid of a turn_duration record, so the two are a linked pair at one turn end rather than independently clocked events. The corpus-wide surplus is concentrated in two version windows (2.1.231: 556 vs 112; 2.1.217: 139 vs 10; 2.1.208: 104 vs 4; 2.1.196: 41 vs 7) which show up as long unbroken S-runs (128 records at 2.1.220, 446 at 2.1.231); the 2.1.258 binary carries a per-user config toggle showTurnDuration (default true) described as 'Show \"Cooked for Nm Ns\" after each assistant turn', so those windows are turn_duration being suppressed by configuration, not stop_hook_summary firing extra times.",
          "rule": "One record per matching jsonl line. Per-file comparison counts the two subtypes within one file and compares them; the S/T marker string preserves file order. The parent-chain count is one hit per record whose parentUuid equals a turn_duration uuid in the same file.",
          "note": "New behavior: the claim's two observable predictions both fail at 2.1.258 - stop_hook_summary does NOT appear more often than turn_duration in the same file (24 vs 24 corpus-wide at that version; 990 vs 1,005 in the largest transcript measured), and it does NOT land in turn-end gaps carrying no turn_duration (every 2.1.258 transcript alternates S,T with no S-run of length 2 or more). The historical imbalance the claim rests on is explained by the showTurnDuration setting, so it was never a per-invocation-versus-per-turn law. csift's downstream posture is unaffected and still correct: it treats the record as searchable only and never as a turn boundary, which is right for a different reason - either subtype can be absent by configuration."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "refined",
          "instrument": "csift show --help text read against search/scan.rs (the `reach` closure admits a gated line only under a reaching selector or a --line/--uuid address; a --turn range is no address)",
          "observed": "0.10.1 help promised EVERY record of the named turns while a --turn fetch admits no gated promoted line; 0.10.2 help names the exclusion and the --line/--uuid and search -t routes",
          "rule": "help text is surface; the data path is the documented address exception",
          "note": "no data-path change; the omission is now disclosed in the flag's help"
        }
      ]
    },
    {
      "id": "QT-025",
      "area": "queue-and-telemetry",
      "behavior": "file-history-delta is a distinct line type from file-history-snapshot: it is written per tracked file per write, carries a top-level timestamp (which the snapshot form does not - the snapshot's instant is nested at snapshot.timestamp) and holds messageId, snapshotMessageId, trackingPath and a backup object. Only version and backupTime are non-null on every delta; backupFileName is present but NULL on 1,454 of 1,985 and realParentDir is absent on 934 of 1,985.",
      "depends": "csift renders both forms under the gated leaf `harness.meta.snapshot` with the tracked paths and versions as matchable text, and the byte prefilter admits them on the shared `\"file-history-` type prefix; the finer-grained delta form is not yet consumed by `recover`.",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "272-284",
          "snippet": "    if rec.is_type(\"file-history-delta\") {\n        let path = rec.tracking_path.as_deref()?;\n        let backup = rec.backup.as_ref();\n        let version = ver(backup.and_then(|b| b.get(\"version\")));\n        let at = rec.timestamp.as_deref().unwrap_or(\"?\");\n        let name = backup\n            .and_then(|b| b.get(\"backupFileName\"))\n            .and_then(serde_json::Value::as_str)\n            .map(|n| format!(\" backup={n}\"))\n            .unwrap_or_default();\n        return Some(format!(\n            \"[file-history delta at {at}: {path}@v{version}{name}]\"\n        ));"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "197-202",
          "snippet": "    if let Some(snap) = rec.snapshot.as_ref() {\n        if let Some(tfb) = snap.get(\"trackedFileBackups\").and_then(|v| v.as_object()) {\n            for (path, entry) in tfb {\n                if path_matches(target_file, path) {\n                    events.push(FileEvent {\n                        line_no,"
        }
      ],
      "instrument": "`csift search 'file-history-' <target> -t harness.meta.snapshot --max-count 4`; expected: both snapshot and delta excerpts. Counting rule = one record per write for the delta form, one per prompt for the snapshot form.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md v0.10.0 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) rg -NI --no-heading -H '\"type\":\"file-history-snapshot\"|\"type\":\"file-history-delta\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-type key-set counters, backup sub-key non-null vs null vs absent counts, deltas per messageId, distinct trackingPath'  (2) csift search '' <transcript> -t harness.meta.snapshot --max-count 40  (3) csift show <transcript> --line <a delta line with a non-null backupFileName>  (4) rg -n 'file-history-delta|trackingPath' src/recover/",
          "observed": "4,062 snapshot lines and 1,985 delta lines corpus-wide. The two shapes are disjoint: snapshot key-set is {type, messageId, snapshot, isSnapshotUpdate} with a top-level timestamp on 0 of 4,062; delta key-set is {type, messageId, snapshotMessageId, trackingPath, backup, timestamp} with a top-level timestamp on 1,985 of 1,985. The snapshot's own instant is nested at snapshot.timestamp (4,062 of 4,062) alongside snapshot.trackedFileBackups (an object, empty on 158 of 4,062). Inside the delta backup object: version non-null 1,985/1,985, backupTime non-null 1,985/1,985, backupFileName present on 1,985 but NULL on 1,454 (non-null on 531), realParentDir absent on 934 (present and non-null on 1,051). Delta counting: 1,985 deltas over 1,553 distinct messageIds (1,121 messages with 1 delta, 432 with 2) and 1,337 distinct trackingPath values; newest delta timestamp 2026-09-02, so the form is live at 2.1.258. csift renders both under harness.meta.snapshot in one query - '[file-history snapshot at <ts>: ]' and '[file-history delta at <ts>: <path>@v1]' rows in the same result - and a delta whose backupFileName is a string renders the documented '... @v1 backup=<name>' suffix. rg over src/recover/ finds no reference to file-history-delta or trackingPath.",
          "rule": "One observation per matching jsonl line for line and key counts; for backup sub-keys, three disjoint buckets per record (non-null / present-but-null / absent). Deltas per messageId is one observation per delta record grouped by its messageId.",
          "note": "Consequence of the backupFileName nulls, worth a doc line: the delta excerpt documented at src/search/record_text.rs:184 as '[file-history delta at <ts>: <path>@vN backup=<name>]' omits the backup= suffix on 73% of real deltas, because snapshot_record_text (src/search/record_text.rs:276-280) correctly drops a null through and_then(as_str). The rendered form is right; the doc comment reads as if the suffix were unconditional. recover still does not consume the delta form, as the claim says."
        }
      ]
    },
    {
      "id": "QT-026",
      "area": "queue-and-telemetry",
      "behavior": "Exactly five jsonl line types carry the queue and telemetry facts while carrying no `message{}` object: `queue-operation` (with the queued content), the `system` subtypes `turn_duration`, `away_summary` and `stop_hook_summary`, and the `file-history-snapshot` / `file-history-delta` pair.",
      "depends": "csift promotes each to exactly one LLM-invisible leaf (`user.queued`, `harness.meta.turn-duration`, `harness.meta.away-summary`, `harness.meta.stop-hooks`, `harness.meta.snapshot`) and admits them to a scan ONLY behind an explicit `-t` selector reaching that leaf or a `show` address, so a bare scan, a bare role selector, a `-T`-only filter and a flagless `--count-by label` never parse them.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "14-28",
          "snippet": "        match self.r#type.as_deref()? {\n            \"queue-operation\" => self.queued_class(),\n            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n                // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n                // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n                // informational, api_error, model_refusal_*, agents_killed, local_command,\n                // scheduled_task_fire, and whatever a later build adds).\n                \"compact_boundary\" => None,\n                _ => Some(Class::MetaSystem),\n            },\n            \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),\n            _ => None,"
        }
      ],
      "instrument": "`jq -r '[.type, (.subtype // \"\")] | @tsv' <a transcript> | sort | uniq -c` for the census (counting rule = one line per type/subtype pair), then `csift search '' <target> -t harness.meta --count-by label` against the same command without `-t`: the promoted leaves appear only in the gated run.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "SPEC.md section 6 v0.10.0 ledger item 5; AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) rg -NI --no-heading '\"type\":\"(attachment|last-prompt|permission-mode|mode|ai-title|queue-operation|system|agent-name|file-history-snapshot|file-history-delta|atis-latch|bridge-session|fork-context-ref|cost-state|continued-in)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket by type (system split by subtype) and count lines carrying a top-level message key'  (2) csift search '' <transcript> --no-subagents --count-by label, then the same with -t harness.meta, with -t user, with -T user, and with -t user.queued",
          "observed": "0 of 629,829 non-record lines carry a top-level message key, across all 25 non-record type/subtype buckets. Gating, measured on one 158,923-line transcript whose raw line counts are turn_duration 1,005, stop_hook_summary 990, queue-operation 4,177, last-prompt 5,679: a bare --count-by label returns 18 label keys / 42,100 records with NO promoted leaf among them; -t harness.meta returns turn-duration 1,005, stop-hooks 990, snapshot 353, away-summary 342, hook 3; -t user returns only user.message 101 and user.answer 9; -T user alone returns 16 keys with no promoted leaf; -t user.queued returns 20. The turn-duration and stop-hooks census numbers equal the raw line counts exactly (1,005 and 990), so the gated admission is lossless once a selector reaches the leaf.",
          "rule": "One observation per physical jsonl line, bucketed by top-level type (system split by subtype); a line counts as carrying message only when the top-level key 'message' exists. Census comparison rule: run the same --count-by label query with and without the selector against one transcript and diff the key sets.",
          "note": "The behavior and the gating both reproduce exactly; only the code site needed correction. Two scope notes so the sentence is not over-read. First, 'exactly five' is true only within the queue-and-telemetry scope: 19 further non-record type/subtype buckets in the same census also carry no message key, and they are simply not queue or telemetry facts. Second, the working tree (unreleased, version still 0.10.0 in Cargo.toml) adds a SIXTH gated leaf harness.meta.system for the remaining system subtypes, so 'exactly five promoted leaves' will need re-dating at the next release. Separately observed while running the gating checks and outside this claim: -T user alone left 11 user.unsent rows in the census, which looks inconsistent with the include-minus-exclude contract and is worth its own look."
        }
      ]
    },
    {
      "id": "QT-027",
      "area": "queue-and-telemetry",
      "behavior": "The presence of a message{} field is the one measured instrument for whether a line is part of the conversation: ZERO of 629,829 lines across the 25 measured non-record type/subtype buckets carries message{}, while every user record (229,721 of 229,721) and every assistant record (432,052 of 432,052) carries it as an object. Claude Code's own internal wording labels the promoted system subtypes render internals - turn_duration is documented as the line the REPL renders as 'Done in Ns' / 'Waiting for N agents'.",
      "depends": "csift decides `Class::llm_visible` on that single instrument, which is what keeps a bare role selector (`-t user`) expanding to visible leaves only while the glob form and explicit paths reach the invisible ones.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "183-196",
          "snippet": "    #[must_use]\n    pub fn llm_visible(self) -> bool {\n        !matches!(\n            self,\n            Class::UserUnsent\n                | Class::UserQueued\n                | Class::CompactionBoundary\n                | Class::MetaTurnDuration\n                | Class::MetaAwaySummary\n                | Class::MetaStopHooks\n                | Class::MetaSnapshot\n                | Class::MetaSystem\n        )\n    }"
        },
        {
          "path": "src/cli/selectors.rs",
          "lines": "59-72",
          "snippet": "    if let Some(prefix) = selector.strip_suffix(\".*\") {\n        return selector_is_segment_prefix(prefix, path);\n    }\n    if !selector_is_segment_prefix(selector, path) {\n        return false;\n    }\n    if selector.contains('.') {\n        return true; // intermediate prefix or exact leaf: any visibility.\n    }\n    // A bare role: visible leaves only.\n    Class::ALL\n        .iter()\n        .find(|c| c.path() == path)\n        .is_none_or(|c| c.llm_visible())"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "172-181",
          "snippet": "    /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n    /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n    /// over every non-record line type in the corpus; every user/assistant record\n    /// does), and Claude Code's\n    /// own source labels them REPL-render internals. DAG threading is NOT the\n    /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n    /// (chain continuity), and `preservedMessages` lists these uuids at the same rate\n    /// as messages (it is a tail window, not a visibility filter).\n    ///\n    /// A bare ROLE selector (`-t user`) expands to visible leaves only; the"
        }
      ],
      "instrument": "`csift stats <target>` prints a whole-file line-type census; for each non user/assistant type, count the lines of that type carrying `\"message\":{`. Counting rule = one observation per physical line, per type; expect 0 for every non-record type.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 5; src/model/taxonomy.rs:164-170 comment; SPEC.md section 6 v0.10.0 ledger item 5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) csift stats --format json --max-count 0, summed over the line_types census of all 7,602 session rows  (2) rg -NI --no-heading '\"type\":\"(the 15 non-record types)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket by type/subtype, count lines carrying a top-level message key'  (3) rg -NI --no-heading -e '\"type\":\"user\"' -e '\"type\":\"assistant\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'count top-level user/assistant lines and those whose message value is an object'  (4) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'From internal SystemMessage'",
          "observed": "25 non-record type/subtype buckets over 629,829 lines, ZERO of which carries a top-level message key. Every top-level user line carries a message OBJECT: 229,721 of 229,721. Every assistant line likewise: 432,052 of 432,052. The binary carries the wording verbatim: \"@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'.\", \"@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage 'away_summary'.\" and \"@internal Summary of Stop/SubagentStop hook execution at turn end — which hooks ran, their output, and whether any prevented continuation. From internal SystemMessage 'stop_hook_summary'.\" Sibling subtypes carry the same 'REPL renders' phrasing (permission_retry, memory_saved, agents_killed, api_error, scheduled_task_fire).",
          "rule": "One observation per physical jsonl line, per bucket. 'Carries message' = the top-level key exists; for the record types the stronger test was used (the value is a JSON object). Counting scope differs slightly between instruments and this matters: csift stats censuses the 7,602 files it enumerates as sessions while the rg pass reads all 7,781 *.jsonl files, which is why the user total reads 229,671 under stats and 229,721 under rg.",
          "note": "The instrument is unusually clean - a 0/629,829 versus 661,773/661,773 split with no exception in either direction - so keying llm_visible on it is well founded. Two number corrections: 25 non-record buckets rather than 24 (the corpus has since recorded system/model_refusal_no_fallback and other rare subtypes), and the record totals are several times the claim's 40,490 / 81,024, which look like a single-project scope rather than the corpus. The taxonomy comment should carry its counting scope so the numbers stay checkable."
        }
      ]
    },
    {
      "id": "QT-028",
      "area": "queue-and-telemetry",
      "behavior": "DAG threading is NOT a visibility instrument: the promoted system telemetry records carry a real uuid and are not marked meta (isMeta:false on 1,005 of 1,005 turn_duration and 342 of 342 away_summary records measured; stop_hook_summary omits the isMeta key entirely on all 5,644), later user records name a turn_duration uuid as their parentUuid (667 in one transcript), and the conversation chain runs straight through a record the model never received.",
      "depends": "csift keys visibility on `message{}` presence alone; keying it off parentUuid continuity would flip all five promoted leaves to visible and break the role-selector expansion contract, while the same threading is what makes the records addressable and lets the superseded-draft detector see them as shared parents.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "13-25",
          "snippet": "    pub fn promoted_class(&self) -> Option<Class> {\n        match self.r#type.as_deref()? {\n            \"queue-operation\" => self.queued_class(),\n            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n                // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n                // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n                // informational, api_error, model_refusal_*, agents_killed, local_command,\n                // scheduled_task_fire, and whatever a later build adds).\n                \"compact_boundary\" => None,\n                _ => Some(Class::MetaSystem),"
        }
      ],
      "instrument": "Take a `turn_duration` uuid from `csift show <target> -t harness.meta.turn-duration --raw`, then `rg '\"parentUuid\":\"<that uuid>\"' over the same file - a user record comes back. Counting rule = one hit per record naming it as parent.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "SPEC.md section 6 v0.10.0 ledger item 5; src/model/taxonomy.rs:164-170 comment; SPEC.md v0.10.0 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c 'over one 158,923-line transcript: collect every uuid of a system/turn_duration, system/stop_hook_summary and system/away_summary record, then count every record whose parentUuid is one of them, bucketed by the child record type; also tabulate isMeta on the three subtypes' - plus the corpus-wide stop_hook_summary key census from QT-021 for the isMeta question",
          "observed": "In that transcript: 1,005 turn_duration uuids, 990 stop_hook_summary uuids, 342 away_summary uuids. 667 records of type user name a turn_duration uuid as their parentUuid, so the conversation chain runs straight through a record that carries no message. The full chain is visible: stop_hook_summary -> turn_duration (989 of 990), turn_duration -> user (667) or turn_duration -> away_summary (339), away_summary -> user (342). isMeta is false on 1,005 of 1,005 turn_duration and on 342 of 342 away_summary records, but stop_hook_summary carries NO isMeta key at all - 0 of 5,644 corpus-wide.",
          "rule": "One hit per record whose parentUuid equals a collected uuid, bucketed by the child's type/subtype, within a single transcript. isMeta buckets distinguish false, true and key-absent.",
          "note": "Strongly confirmed, and the measured chain is richer than the claim states: it is not merely that a user record can name a turn_duration parent, it is that stop_hook_summary, turn_duration and away_summary form a linked run at every turn end, with the next human turn hanging off the tail of it. One correction: 'carry a real uuid with isMeta:false' is true for turn_duration and away_summary but not for stop_hook_summary, which has no isMeta field - which only strengthens the point that isMeta is not the instrument."
        }
      ]
    },
    {
      "id": "QT-029",
      "area": "queue-and-telemetry",
      "behavior": "Membership in a compaction boundary's compactMetadata.preservedMessages is NOT a visibility instrument either: the field is an OBJECT {anchorUuid, uuids[], allUuids[]}, not a list, and its uuids are an unfiltered TAIL WINDOW of 5 to 11 records anchored on a user record - it lists the promoted telemetry uuids at the same rate as message uuids (telemetry 0.0020-0.0029, message records 0.0014-0.0029, measured over 32 boundaries in one transcript).",
      "depends": "csift refuses preservedMessages as a delivery signal and keeps `message{}` presence as the only instrument; reading the list as a visibility filter would mark telemetry as delivered context.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "172-181",
          "snippet": "    /// v0.10.0 adds the promoted non-record line types, all invisible by the same\n    /// instrument as the boundary: ZERO of them carries a `message{}` field (measured\n    /// over every non-record line type in the corpus; every user/assistant record\n    /// does), and Claude Code's\n    /// own source labels them REPL-render internals. DAG threading is NOT the\n    /// instrument here - later user records name a `turn_duration` uuid as parentUuid\n    /// (chain continuity), and `preservedMessages` lists these uuids at the same rate\n    /// as messages (it is a tail window, not a visibility filter).\n    ///\n    /// A bare ROLE selector (`-t user`) expands to visible leaves only; the"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "184-196",
          "snippet": "    pub fn llm_visible(self) -> bool {\n        !matches!(\n            self,\n            Class::UserUnsent\n                | Class::UserQueued\n                | Class::CompactionBoundary\n                | Class::MetaTurnDuration\n                | Class::MetaAwaySummary\n                | Class::MetaStopHooks\n                | Class::MetaSnapshot\n                | Class::MetaSystem\n        )\n    }"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "315-328",
          "snippet": "pub(crate) fn compact_metadata_excerpt(meta: &serde_json::Value) -> Option<String> {\n    let obj = meta.as_object()?;\n    let mut fields: Vec<String> = Vec::new();\n    for key in [\"trigger\", \"preTokens\", \"postTokens\", \"durationMs\"] {\n        if let Some(v) = obj.get(key) {\n            let rendered = match v {\n                serde_json::Value::String(s) => s.clone(),\n                other => other.to_string(),\n            };\n            fields.push(format!(\"{key}={rendered}\"));\n        }\n    }\n    (!fields.is_empty()).then(|| format!(\"[compaction boundary: {}]\", fields.join(\" \")))\n}"
        },
        {
          "path": "src/cli/search_args.rs",
          "lines": "155-158",
          "snippet": "        it. Eight leaves are invisible and need naming or a glob: `user.unsent`\\n  \\\n        (a superseded draft is not in the surviving conversation - CC's own\\n  \\\n        preservedMessages accounting excludes every draft), `harness.compaction.boundary`\\n  \\\n        (a metrics-only system record), and the six gated leaves below. The glob form\\n  \\"
        }
      ],
      "instrument": "Read `compactMetadata.preservedMessages` from a boundary record via `csift show <target> --uuid <boundary uuid> --raw` and compare the uuid mix against the same file line-type census from `csift stats`. Counting rule = uuids per type over one preserved list, reported as a rate against that type total in the file.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.4",
        "source": "SPEC.md section 5; src/model/taxonomy.rs:164-170 comment; SPEC.md section 6 v0.10.0 ledger item 5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) python3 -c 'over one 158,923-line transcript: index every uuid to its line type, then for each compact_boundary read compactMetadata.preservedMessages and resolve each listed uuid against that index, reporting per-type counts and each count divided by that type total in the file'  (2) rg -NI --no-heading -F '\"subtype\":\"compact_boundary\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'bucket the JSON type of compactMetadata.preservedMessages by record version'",
          "observed": "preservedMessages is never a bare list: it is an object on all 232 compact_boundary records in the corpus, shaped {anchorUuid, uuids[], allUuids[]} from 2.1.156 through 2.1.258 (the single 2.1.150 boundary has {anchorUuid, uuids} without allUuids). In the sampled transcript, 32 boundaries, compactMetadata keys = cumulativeDroppedTokens, durationMs, postTokens, preCompactDiscoveredTools, preTokens, preservedMessages, preservedSegment, trigger; preservedSegment = {anchorUuid, headUuid, tailUuid} on 32 of 32. The uuids lists are short tail windows of 5 to 11 entries, 264 entries in total, 243 of them resolvable in the same file, and anchorUuid resolves to a user record on 32 of 32. Per-type preserved counts and their rate against that type's total in the file: attachment 122 of 86,933 = 0.0014, assistant 84 of 28,812 = 0.0029, user 32 of 13,267 = 0.0024, stop_hook_summary 2 of 990 = 0.0020, turn_duration 2 of 1,005 = 0.0020, away_summary 1 of 342 = 0.0029. Telemetry lands in the 0.0020-0.0029 band, message records in the 0.0014-0.0029 band.",
          "rule": "One observation per uuid listed in a boundary's preservedMessages.uuids, resolved to a line type by an index of every uuid in the same file; the rate is that per-type count divided by that type's total line count in the same file. Unresolved uuids (21 of 264) are reported separately and excluded from the rates.",
          "note": "The conclusion holds and is now quantified with a rerunnable rate rule rather than an assertion. Two wording corrections: preservedMessages is an object, not a list, so the claim's stated instrument (reading it as a list of uuids) would not run as written; and the object has two uuid arrays, uuids and allUuids, which resolved identically here (243 in-file hits each) but differ in length (264 versus 279 entries), so a future reader should say which one it counted. located_csift on this claim is 0.9.4 while the neighbours are 0.10.0."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "refined",
          "instrument": "count of `Class::llm_visible` arms returning false in model/taxonomy.rs against the search --help visibility sentence",
          "observed": "eight leaves return false (user.unsent, harness.compaction.boundary, user.queued, harness.meta.turn-duration, away-summary, stop-hooks, snapshot, system); the 0.10.1 help said exactly two",
          "rule": "one false arm = one invisible leaf",
          "note": "help sentence corrected in 0.10.2 to name the two plus the six gated leaves"
        }
      ]
    },
    {
      "id": "QT-030",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes five per-turn session-state CACHE line types with NO uuid, NO timestamp, NO parentUuid and NO message: last-prompt (keys leafUuid, sessionId, lastPrompt), permission-mode (permissionMode, sessionId), mode (mode, sessionId), ai-title (aiTitle, sessionId) and agent-name (agentName, sessionId) - each also carries sessionId. They are rewritten per turn - measured last-prompt 29,289, permission-mode 29,229, mode 29,140, ai-title 28,811, agent-name 12,361, 128,830 lines together or 9.98% of a 1,291,324-line corpus - and lastPrompt (itself absent on 87 of 29,289 lines) mostly duplicates text already present as a user.message record: 73% of distinct values verbatim, 90% counting truncated or normalised copies.",
      "depends": "csift deliberately leaves them unmodeled and unaddressable by label: they carry no fact `stats` or `list` does not already expose, and a leaf would inject about 28,891 duplicate hits and skew `--count-by label`. They reach only `show --raw` and the `stats` line-type census.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "9-13",
          "snippet": "    /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n    /// message record and for every line type that stays unmodeled (the session-state\n    /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`).\n    #[must_use]\n    pub fn promoted_class(&self) -> Option<Class> {"
        },
        {
          "path": "src/show/run.rs",
          "lines": "251-262",
          "snippet": "    if !misses.is_empty() {\n        bail!(\n            \"no such record(s): {} — an explicit address renders message lines \\\n             (role:user/role:assistant, superseded drafts included), attachment lines, and \\\n             the promoted non-record lines (a queue-operation with text, turn_duration, \\\n             away_summary, stop_hook_summary, file-history-snapshot/-delta, and every \\\n             other system subtype such as informational or api_error); session-state \\\n             cache lines (last-prompt, mode, ai-title, …), a content-less queue dequeue \\\n             and torn lines are inspectable with `--raw`\",\n            misses.join(\", \")\n        );\n    }"
        },
        {
          "path": "src/stats.rs",
          "lines": "128-141",
          "snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {\n    if line.iter().all(u8::is_ascii_whitespace) {\n        return Ok(None);\n    }\n    #[derive(serde::Deserialize)]\n    struct TypeProbe {\n        #[serde(rename = \"type\")]\n        r#type: Option<String>,\n    }\n    match serde_json::from_slice::<TypeProbe>(line) {\n        Ok(p) => Ok(Some(p.r#type.unwrap_or_else(|| \"(untyped)\".to_string()))),\n        Err(_) => Err(()),\n    }\n}"
        }
      ],
      "instrument": "`csift stats <project-dir> --format json --max-count 0` and read the whole-file line-type census (counting rule = one observation per LINE, every line syntax-validated); then `rg -m1 '\"type\":\"last-prompt\"' <transcript> | jq 'keys'` shows the absent uuid, timestamp and message.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.1",
        "source": "dev session 2026-09-02; SPEC.md v0.10.0 ledger; SPEC.md section 6 v0.10.0 ledger item 8; AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) csift stats --format json --max-count 0 summed over all session rows for the whole-file line-type census  (2) rg -NI --no-heading '\"type\":\"(the 15 non-record types)\"' ~/.claude/projects -g '*.jsonl' | python3 -c 'per-type key-presence counters'  (3) csift show <transcript> --line <a last-prompt line>, then the same with --raw  (4) python3 -c 'in one transcript, collect distinct non-empty lastPrompt values and test each for verbatim containment in the joined text of that file user records, then for containment of its first 40 characters'",
          "observed": "Corpus counts: last-prompt 29,289, permission-mode 29,229, mode 29,140, ai-title 28,811, agent-name 12,361 - 128,830 lines together, 9.98% of the 1,291,324-line corpus. Key sets, with no uuid, no timestamp, no parentUuid and no message on any line: last-prompt {type, leafUuid, sessionId, lastPrompt} where lastPrompt itself is absent on 87 of 29,289; permission-mode {type, permissionMode, sessionId}; mode {type, mode, sessionId}; ai-title {type, aiTitle, sessionId}; agent-name {type, agentName, sessionId}. Reachability: csift show <transcript> --line <N> against a last-prompt line exits non-zero with 'no such record(s): L14', while the identical address with --raw returns the verbatim jsonl line - so show --raw and the stats census are the only surfaces, as claimed. Duplication, over 86 distinct non-empty lastPrompt values in one transcript: 63 (73.3%) occur verbatim inside some user record's text in the same file, a further 14 have their first 40 characters present (89.5% combined), and 9 have no trace at all - and none of those 9 appears in that file's queue-operation content either.",
          "rule": "One observation per physical jsonl line for the counts (every line syntax-validated by the stats census). For duplication: one observation per DISTINCT stripped lastPrompt value, tested against the newline-joined text of every user record in the same file; verbatim containment first, then first-40-character containment, then no trace.",
          "note": "The decision not to model these holds up: the reachability test reproduces exactly (address errors, --raw returns the line) and the volume argument is if anything stronger at 128,830 lines. One honesty correction: 'merely duplicates' is too strong at the value level - 9 of 86 distinct lastPrompt values in the sampled transcript had no trace anywhere in that file's user records or its queue-operation content, so the cache line is not strictly redundant with the transcript it sits in. That does not change the conclusion (those 9 carry no addressable record, timestamp or uuid, so a leaf would still be unusable), but the claim should say 'mostly duplicates' and name the rate."
        }
      ]
    },
    {
      "id": "QT-031",
      "area": "queue-and-telemetry",
      "behavior": "Beyond `compact_boundary` and the three promoted subtypes, Claude Code emits exactly seven more `type:\"system\"` subtypes (eleven distinct subtypes in total), at snapshot counts (2026-09-02T10:06:11Z): `scheduled_task_fire` 459 (carrying a `content` render string in 459/459, written beside the delivered wakeup - an `isMeta:true` user record within 2s in 97.4% of cases - and a `queue-operation` enqueue line), `model_refusal_fallback` 51, `api_error` 29 (keys `error`, `retryAttempt`, `maxRetries`, `retryInMs`, `level`), `local_command` 15, `agents_killed` 10 (envelope only: no `content`, no payload key), `informational` 5 and `model_refusal_no_fallback` 2.",
      "depends": "the csift classify `system` arm pushes a label only for the compaction boundary plus the three promoted subtypes, so every other subtype yields an empty label vector and is dropped from search results; the `stats` whole-file line-type census stays the enumerating authority, so a new subtype shows up there rather than vanishing silently.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "177-181",
          "snippet": "            Some(\"system\") => {\n                if self.is_compact_boundary() {\n                    push_unique(&mut out, Class::CompactionBoundary);\n                }\n            }"
        },
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "16-26",
          "snippet": "            \"system\" => match self.subtype.as_deref()? {\n                \"turn_duration\" => Some(Class::MetaTurnDuration),\n                \"away_summary\" => Some(Class::MetaAwaySummary),\n                \"stop_hook_summary\" => Some(Class::MetaStopHooks),\n                // The compaction boundary has its own leaf (`classify`, D7); every OTHER\n                // system subtype is the harness talking to its own UI (v0.10.1 catch-all:\n                // informational, api_error, model_refusal_*, agents_killed, local_command,\n                // scheduled_task_fire, and whatever a later build adds).\n                \"compact_boundary\" => None,\n                _ => Some(Class::MetaSystem),\n            },"
        },
        {
          "path": "src/stats.rs",
          "lines": "128",
          "snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {"
        }
      ],
      "instrument": "`cd ~/.claude/projects && rg -oNI -uu --no-messages '\"subtype\":\"[A-Za-z_]+\"' -g '*.jsonl' . | sort | uniq -c`; counting rule = one observation per MATCH. The total must equal the `system` line count from `csift stats --format json --max-count 0` read in the same minute (measured: both 12,712 - independent cross-instrument agreement). The corpus is live, so both numbers rise between runs; only their agreement is stable.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && rg -oNI -uu --no-messages '\"subtype\":\"[A-Za-z_]+\"' -g '*.jsonl' . | sort | uniq -c | sort -rn   # then, same minute: csift stats --format json --max-count 0 | tail -1  (read line_types.system)",
          "observed": "Snapshot 2026-09-02T10:06:11Z, 11 distinct subtypes: stop_hook_summary 5645, turn_duration 4633, away_summary 1631, scheduled_task_fire 459, compact_boundary 232, model_refusal_fallback 51, api_error 29, local_command 15, agents_killed 10, informational 5, model_refusal_no_fallback 2. Total matches 12712; csift stats line_types.system = 12712 the same minute - exact cross-instrument agreement. The wider needle '\"subtype\":\"[^\"]*\"' also totals 12712, so no subtype value falls outside [A-Za-z_]+. Record shapes (one observation per record, parsed): scheduled_task_fire carries a non-empty string content in 459/459; api_error keys are exactly {error, retryAttempt, maxRetries, retryInMs, level} plus the record envelope; agents_killed carries no content and no payload key beyond the envelope. ZERO of the seven carries a message{} field. Companion test: 447/459 (97.4%) of scheduled_task_fire records have an isMeta:true user record within +/-2s in the same session (455/459 within +/-5s), but only 1/459 has a <task-notification> record within +/-5s. csift 0.10.0 side: `csift search 'Claude resuming /loop wakeup' -t harness -c` = 0 while the text occurs 492 times on disk, so the fire records are indeed dropped from search.",
          "rule": "One observation per MATCH of the subtype needle across every *.jsonl under ~/.claude/projects (7,782 files at the snapshot), cross-checked one-observation-per-LINE by the csift stats whole-file line-type census. Counts are a live-corpus snapshot and grow during a session: away_summary moved 1630 -> 1631 between two runs six minutes apart, moving the total 12711 -> 12712. Companion pairing rule: a fire is 'paired' if any record of the stated shape shares its sessionId and lies within the stated window of its timestamp; first match wins, one observation per fire.",
          "note": "Claude Code's behaviour is unchanged: the seven extra subtypes are all still emitted, and the subtype set is exactly seven beyond compact_boundary plus the three promoted ones. Three corrections. (1) Two counts moved with the live corpus - model_refusal_fallback 50 -> 51 and informational 1 -> 5 - and the cross-check total 12,577 -> 12,712; the claim should carry a census instant, since the corpus grew twice during this verification alone. (2) The scheduled_task_fire companion was mis-named: it is NOT a schedule-wakeup / task-notification pulse (1/459 within 5s) but an isMeta:true user record carrying the delivered resume prompt (447/459 within 2s), written alongside a queue-operation enqueue line and an attachment; all 459 fires in this corpus render the same content prefix. (3) The csift-side 'depends' is verified for the 0.10.0 binary that ran the instruments (a -t harness search for the fire's render text returns 0 while the text sits on disk 492 times) but is going stale in the working tree, where a v0.10.1 catch-all arm gives every other system subtype the harness.meta.system leaf. One wording nuance on the depends: the stats census keys on the top-level `type`, so a new subtype raises the `system` total rather than being named individually - stats enumerates the bucket, not the subtype."
        }
      ]
    },
    {
      "id": "QT-032",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code additionally writes at least five line types csift leaves unmodeled. Four carry neither `uuid` nor `timestamp`: `atis-latch` (852, key `atis`), `bridge-session` (788, keys `bridgeSessionId`, `lastSequenceNum`, `ownerAccountUuid`, `ownerOrganizationUuid`), `fork-context-ref` (33, keys `agentId`, `contextLength`, `parentSessionId`, `parentLastUuid`) and `cost-state` (18, keys `totalCostUSD`, `modelUsage`, `startTime`, `hasUnknownModelCost`, `totalAPIDuration`, `totalAPIDurationWithoutRetries`, `totalDuration`, `totalToolDuration`, `totalLinesAdded`, `totalLinesRemoved`). The fifth, `continued-in` (1, keys `continuedInSessionId`, `sessionId`, `timestamp`), does carry a timestamp. Every one of the five also carries `sessionId` except `fork-context-ref`, which names the parent session instead.",
      "depends": "csift leaves all four unmodeled; they reach only `show --raw` and the `stats` line-type census. `fork-context-ref` is the only line type that names a fork parent session id and is not yet consumed by the clone-detection path.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "48-52",
          "snippet": "    /// Whole-file census: every parseable physical line counted by its top-level `type`\n    /// value (`user`, `assistant`, `attachment`, `file-history-snapshot`, …; `(untyped)`\n    /// when the field is absent). A FILE fact like `lines`: never windowed by\n    /// `--since`/`--turn`.\n    line_types: BTreeMap<String, usize>,"
        },
        {
          "path": "src/stats.rs",
          "lines": "128",
          "snippet": "fn line_type_probe(line: &[u8]) -> std::result::Result<Option<String>, ()> {"
        }
      ],
      "instrument": "`csift stats <project-dir> --format json --max-count 0` line-type census; counting rule = one observation per LINE. Cross-check a single type with `rg -cNI '\"type\":\"fork-context-ref\"' -g '*.jsonl'`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.8.1",
        "source": "dev session 2026-09-02; SPEC.md section 6 v0.10.0 ledger item 8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift stats --format json --max-count 0 | tail -1   (read line_types)   # keys per type: cd ~/.claude/projects && rg -NI -uu --no-messages '\"type\":\"<t>\"' -g '*.jsonl' . | head -1 | python3 -c 'import sys,json; print(sorted(json.loads(sys.stdin.readline()).keys()))'   # reachability: csift show <file> --line N   vs   csift show <file> --line N --raw",
          "observed": "Snapshot 2026-09-02T10:06:11Z line_types over 7,602 scanned transcripts: atis-latch 852, bridge-session 788, fork-context-ref 33, cost-state 18, continued-in 1. Key sets read off a live line of each: atis-latch = [atis, sessionId, type]; bridge-session = [bridgeSessionId, lastSequenceNum, ownerAccountUuid, ownerOrganizationUuid, sessionId, type]; fork-context-ref = [agentId, contextLength, parentLastUuid, parentSessionId, type]; cost-state = [hasUnknownModelCost, modelUsage, sessionId, startTime, totalAPIDuration, totalAPIDurationWithoutRetries, totalCostUSD, totalDuration, totalLinesAdded, totalLinesRemoved, totalToolDuration, type]. None of the four carries `uuid` or `timestamp`. A fifth unmodeled type is present: continued-in (1 line, keys [continuedInSessionId, sessionId, timestamp, type]) - it DOES carry a timestamp. Reachability on an atis-latch line: `show --line` fails with `csift: error: no such record(s): L4 - an explicit address renders message lines ... session-state cache lines ..., a content-less queue dequeue, unpromoted system subtypes and torn lines are inspectable with --raw`, while `show --line --raw` returns the verbatim line (keys atis, sessionId, type).",
          "rule": "One observation per LINE: the csift stats whole-file line-type census counts every parseable physical line by its top-level `type`, summed over every session in scope. Key sets are read from the first matching line of each type (the key set was constant across the sampled lines of a type). Live-corpus counts, so they rise between runs.",
          "note": "Claude Code's behaviour is unchanged and every key set the claim listed is exactly right - all four confirmed key-for-key, and all four confirmed to carry neither uuid nor timestamp. Two corrections. (1) Three of the four counts moved with the live corpus: atis-latch 450 -> 852, bridge-session 382 -> 788, cost-state 14 -> 18; fork-context-ref held at 33. (2) There is a fifth unmodeled line type, `continued-in`, which the claim does not mention; it is the second type after `fork-context-ref` to name another session (its successor rather than its parent), and unlike the four it carries a timestamp. The depends clause is confirmed directly: a rendered `show --line` address refuses these lines and its error text enumerates what an address does render, while `show --line --raw` returns the verbatim bytes - so `show --raw` plus the stats census really are the only two surfaces that reach them. The observation that `fork-context-ref` names a fork parent and is not consumed by clone detection is a design gap, not a behaviour claim, and no instrument here bears on it."
        }
      ]
    },
    {
      "id": "QT-033",
      "area": "queue-and-telemetry",
      "behavior": "The internal documentation strings inside Claude Code, extractable from the shipped binary, describe `turn_duration` as the source of the rendered end-of-turn line ('Done in Ns' / 'Waiting for N agents'), `away_summary` as the summary of what happened while the user was away, `scheduled_task_fire` as a cron fire whose `content` is the render text, `agents_killed` as an agents-killed banner and `api_error` as the retry banner - that is, all of them as render internals rather than conversation. The 'five or more minutes away' threshold is real but comes from a separate string documenting the recap setting, corroborated by a 300000 ms constant on the focus-transition path, not from the `away_summary` string itself.",
      "depends": "these strings corroborate the `message{}` instrument from the producing side, and they are why csift words the five promoted leaves as `not in the surviving conversation` rather than making the stronger, unprovable claim that the model never saw them.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "181-182",
          "snippet": "    /// A bare ROLE selector (`-t user`) expands to visible leaves only; the\n    /// glob form and explicit paths reach the invisible ones."
        }
      ],
      "instrument": "Run `strings -n 6` over the shipped Claude Code binary for the located version and grep for `turn_duration|away_summary|scheduled_task_fire`, then read the surrounding doc comments; counting rule = one observation per extracted string.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o \"@internal[^\\\"]{0,200}(turn_duration|away_summary|scheduled_task_fire|agents_killed|api_error)'\" | sort -u   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '@internal When false, the session recap[^\"]{0,120}'   ;   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '@internal Retryable-API-error[^\"]{0,220}'",
          "observed": "Five doc strings, verbatim: (1) \"@internal Per-turn wall-clock duration plus budget and pending-background-work counts. REPL renders the 'Done in Ns' / 'Waiting for N agents' line. From internal SystemMessage 'turn_duration'\"; (2) \"@internal Summary of what happened while the user was away (background tasks completed, notifications accumulated). From internal SystemMessage 'away_summary'\"; (3) \"@internal Emitted when a scheduled task (cron) fires. content is the render text. From internal SystemMessage 'scheduled_task_fire'\"; (4) \"@internal Emitted when background agents are terminated (e.g. on interrupt). REPL renders an 'agents killed' banner. From internal SystemMessage 'agents_killed'\"; (5) \"@internal Retryable-API-error frame carrying the plain-data error snapshot and retry counters. REPL renders the retry banner from this. Wire twin is SDKAPIRetryMessage ('api_retry'). From internal SystemMessage 'api_error'.\" The '5+ minutes' figure is in a SIXTH, separate string about the recap setting: \"@internal When false, the session recap (shown when you return after being away for 5+ minutes) is disabled. When absent or true, recap is enabled.\" It is corroborated by the constant 300000 (ms) gating the away path on a focus transition. The away_summary schema in the same binary is `{type:\"system\", subtype:\"away_summary\", content, uuid, session_id}` - no message field. Corpus side: 0 of the 106 sampled records across the seven unmodeled subtypes carries a message{} field. csift wording site: 'not in the surviving conversation' appears at src/model/taxonomy.rs:54, 126 and 131.",
          "rule": "One observation per extracted string: a `strings -n 6` dump of the shipped binary, matched by a literal-anchored regex, deduplicated with `sort -u`; a string counts as evidence only if it names the subtype it is being cited for. The message{} check is one observation per record over every record of the seven subtypes in the corpus.",
          "note": "Every one of the five doc strings exists verbatim in the 2.1.258 binary and each says what the claim says it says, so the substance holds. One wording correction: the claim attributes the 'five or more minutes' figure to the away_summary documentation, but that string carries no threshold - the figure lives in a separate string about the recap setting, independently corroborated by the 300000 ms constant on the focus-transition gate. Two facts strengthen the claim beyond what it states: the binary's own away_summary schema has no message field, and the api_error string names its SDK wire twin, so the producing side and the message{} instrument agree. The depends clause checks out - the 'not in the surviving conversation' wording is in the source at three sites. The code anchor moved down eight lines; the text is identical."
        }
      ]
    },
    {
      "id": "QT-034",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code carries an explicit display-only concept for records rendered in the UI but filtered before the API send, and it NEVER lands on disk: the corpus-wide count of the key `\"isVirtual\":` is 0. There is therefore no serialized flag distinguishing a rendered-only record, which is why visibility has to be inferred from the record shape.",
      "depends": "csift cannot read a visibility flag off the line and keys on `message{}` presence instead; a future serialized flag would be a stronger instrument than the shape inference.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "181-182",
          "snippet": "    /// A bare ROLE selector (`-t user`) expands to visible leaves only; the\n    /// glob form and explicit paths reach the invisible ones."
        }
      ],
      "instrument": "`rg -c '\"isVirtual\":' ~/.claude/projects -g '*.jsonl'` must be 0; counting rule = one observation per matching line.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'is_virtual:[^\"]{0,40}\"@internal Display-only:[^\"]{0,80}'   ;   cd ~/.claude/projects && rg -oNI -uu --no-messages '\"isVirtual\"' -g '*.jsonl' . | wc -l   ;   rg -oNI -uu --no-messages '\"is_virtual\"' -g '*.jsonl' . | wc -l   ;   rg -oNI -uu --no-messages '\"isMeta\"' -g '*.jsonl' . | wc -l",
          "observed": "Binary string, verbatim: `is_virtual:x(!0).optional().describe(\"@internal Display-only: rendered in the UI but filtered before API send.` - and its sibling `is_visible_in_transcript_only:x(!0).optional().describe(\"@internal True when the message is stored in the transcript but not rendered in the live UI.` The token appears 24 times in the dump, including predicates that skip virtual records when walking back for the last assistant message. Corpus: `\"isVirtual\"` = 0 occurrences and `\"is_virtual\"` = 0 occurrences across 7,782 *.jsonl files; control key `\"isMeta\"` = 9,074 occurrences in the same scan.",
          "rule": "One observation per MATCH over every *.jsonl under ~/.claude/projects, needle taken WITHOUT the trailing colon (a strictly weaker needle than the claim's `\"isVirtual\":`, so a zero here implies a zero there). The control needle `\"isMeta\"` is counted in the identical scan to prove the scan reaches record interiors rather than silently matching nothing.",
          "note": "Both halves confirmed, and the binary string is almost word-for-word the claim: Claude Code documents the flag as 'Display-only: rendered in the UI but filtered before API send', which is exactly the display-only concept the claim asserts. On disk the flag is absent - zero occurrences in either the camelCase or the wire spelling over 7,782 transcripts, against a control key that matched 9,074 times in the same pass - so the shape inference csift relies on is still the only available instrument. One scope caveat a stranger should keep: the binary does contain a path that copies `is_virtual` from an SDK input message onto an internal record, so a session driven through the SDK with that field set is the case that could put the flag on disk; nothing in this corpus exercises it, and a corpus containing such a session would decide whether 'never lands on disk' is universal or merely true of interactive sessions. The claim's code anchor moved down eight lines; the quoted text is unchanged."
        }
      ]
    },
    {
      "id": "QT-035",
      "area": "queue-and-telemetry",
      "behavior": "The promoted queue and telemetry lines are high-volume duplicates of records already labelled elsewhere: admitting the queue lines alone would add 13,514 lines to a default scan surface, of which 9,276 carry content and 87.4% of those (8,106) are automation pulses whose delivered twin is already searchable. Admitting all five promoted leaves would add 31,470 lines and 19,125 labelled records.",
      "depends": "csift gates each promoted leaf behind an explicit selector or a `show` address through `CandidateGates` - one `&&`-gated SIMD memmem per leaf, so a default scan pays nothing. The gate is STRICTER than the compaction-boundary keep, which a match-all scan admits while these leaves stay out.",
      "code": [
        {
          "path": "src/search/scan.rs",
          "lines": "312-319",
          "snippet": "pub(crate) struct CandidateGates {\n    /// D7: the `compact_boundary` metrics record (selected label).\n    pub(crate) compact_boundary: bool,\n    /// `--additional-context` (or an address): hook-injected context attachments.\n    pub(crate) hook_context: bool,\n    /// `--attachments` / the attachment axis (or an address): every attachment line.\n    pub(crate) attachments: bool,\n    /// v0.10.0 (explicit selector or address): `queue-operation` lines."
        },
        {
          "path": "src/search/scan.rs",
          "lines": "338-343",
          "snippet": "pub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {\n    let needs_compact_boundary = gates.compact_boundary;\n    let needs_hook_context = gates.hook_context;\n    let needs_attachments = gates.attachments;\n    // Every user/assistant record carries a `\"role\":\"user\"`/`\"role\":\"assistant\"`\n    // marker (genuine-user string content, tool carriers, assistant blocks all do)."
        }
      ],
      "instrument": "Compare `csift search '' --count-by label` with `csift search '' -t 'user.*' --count-by label` and `csift search '' -t harness.meta --count-by label`: the gated leaves are absent from the first and present in the others. Counting rule = matched records per leaf per invocation; a zero-match run with no reaching selector emits the stderr gated-leaves note and the JSON `gated_leaves_unreached` key. (The claim's `-c` form counts EXCHANGES, not records, so it cannot show a per-leaf admission.)",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' --count-by label   vs   csift search '' -t 'user.*' --count-by label   vs   csift search '' -t harness.meta --count-by label   ;   csift search '<absent token>' -t user.message --format json   ;   cd ~/.claude/projects && rg -NI -uu --no-messages '\"type\":\"queue-operation\"' -g '*.jsonl' . | python3 (classify each line's content by section-boundary tag)",
          "observed": "Default scan (no -t) emits 27 label keys and NONE of the gated leaves: no user.queued, no harness.meta.turn-duration, no harness.meta.away-summary, no harness.meta.stop-hooks, no harness.meta.snapshot. With -t 'user.*': user.queued appears at 1,170 records. With -t harness.meta: snapshot 6,047, stop-hooks 5,645, turn-duration 4,633, away-summary 1,630 appear. A zero-match run under a non-reaching selector returns JSON `\"gated_leaves_unreached\": true` alongside `\"definitive_absence\": true`, and stderr prints: `csift: note: the gated leaves (user.queued, harness.meta.turn-duration, harness.meta.away-summary, harness.meta.stop-hooks, harness.meta.snapshot) are scanned only under an explicit -t that reaches them - this absence does not cover those lines.` Volume: 13,514 queue-operation lines corpus-wide (NOT ~22,000), of which 9,276 carry non-empty string content and 4,238 do not; of the content-bearing lines 8,106 are automation pulses (8,076 `<task-notification>` + 30 `<agent-message>`) = 87.4%, and 1,170 are the human's text = 12.6%, matching the user.queued census exactly. Operation field: enqueue 6,761, dequeue 3,442, remove 3,203, popAll 108.",
          "rule": "Gating: one observation per RECORD via `--count-by label`, run three times over the same whole-corpus scope, changing only the -t selector; a leaf is 'gated' iff it is absent from the no-selector census and present under a reaching selector. Volume: one observation per LINE of `type:\"queue-operation\"`; a line is an automation pulse iff its `content`, after left-trimming, STARTS with `<task-notification>` or `<agent-message` (a section-boundary test, so a mid-prose quotation does not count). Percentages are over content-bearing lines only.",
          "note": "The gating mechanism is confirmed exactly as described, by the strongest available instrument: three label censuses over one scope differing only in the selector show the five promoted leaves absent by default and present under a reaching -t, and the zero-match diagnosis names all five leaves on stderr and sets gated_leaves_unreached in JSON. The 87% figure is right to the decimal - 8,106 of 9,276 content-bearing queue lines are automation pulses, 87.4% - and the 1,170 human-content remainder matches the user.queued census exactly, an independent cross-check. One number is wrong: 'roughly 22,000' queue lines overstates by 63%; the corpus holds 13,514 queue-operation lines, and no reading recovers 22,000 (all five promoted leaves together come to 31,470 lines). Worth recording alongside it: a queue line is written on enqueue AND on dequeue/remove, so a single pulse is counted about twice, which is part of why the queue duplicates so heavily. The claim's own suggested instrument uses -c, which counts exchanges rather than records and so cannot demonstrate the gating; the corrected instrument uses --count-by label."
        }
      ]
    },
    {
      "id": "QT-036",
      "area": "queue-and-telemetry",
      "behavior": "An assistant record's `message.usage` object carries eleven keys - `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `service_tier`, `cache_creation`, `inference_geo`, `server_tool_use`, `iterations`, `speed`, `output_tokens_details` - and Claude Code 2.1.258 emits all eleven on every assistant record: 1,424 of 1,424 records written by 2.1.258 carry all eleven. Presence rates below 100% are a fossil of OLDER Claude Code versions in the same corpus: over a mixed-version 15-transcript sample (12,514 records) the first seven keys are on 100%, `server_tool_use`/`iterations`/`speed` on 12,478 (99.71%), and `output_tokens_details` on only 1,919 (15.3%). PRESENT is not the same as CARRIES A NUMBER: 2.1.258's own schema declares `cache_creation_input_tokens`, `cache_read_input_tokens`, `server_tool_use`, `service_tier` and `cache_creation` nullable, and `inference_geo`, `speed` and `output_tokens_details` nullable AND optional (`iterations` optional), and the corpus shows null values for five of them. Value shapes: the four token counts are integers, `service_tier` / `speed` / `inference_geo` are strings, `cache_creation` / `server_tool_use` / `output_tokens_details` are objects, and `iterations` is an ARRAY of per-API-call usage objects.",
      "depends": "csift's `TokenUsage` probe deserializes exactly four of the eleven (`input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`), so `csift stats` reports no thinking-token split, no server-tool counts, no service tier and no inference geography; adding any of them means reading a key whose absence rate is already measured.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "270-275",
          "snippet": "pub struct TokenUsage {\n    pub input_tokens: Option<u64>,\n    pub output_tokens: Option<u64>,\n    pub cache_read_input_tokens: Option<u64>,\n    pub cache_creation_input_tokens: Option<u64>,\n}"
        },
        {
          "path": "src/model/record.rs",
          "lines": "281-284",
          "snippet": "    pub fn token_usage(&self) -> Option<TokenUsage> {\n        let raw = self.usage.as_ref()?;\n        serde_json::from_str(raw.get()).ok()\n    }"
        }
      ],
      "instrument": "Two instruments. (1) `strings -n 6` over the 2.1.258 binary for the literal usage schema and the literal zero-value usage template - both name exactly eleven keys. (2) Python over the transcript corpus: count each usage key across assistant records, once per record, reported separately for the version-current subset (records whose `version` is 2.1.258) and for a mixed-version sample.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'usage:c\\(\\{input_tokens:A\\(\\).{0,420}' | sort -u   AND   python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n    for raw in open(p,'rb'):\n        if VER not in raw or b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        n+=1\n        for k in u: keys[k]+=1\n        for o in ('cache_creation','server_tool_use','output_tokens_details'):\n            if isinstance(u.get(o),dict):\n                for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY   AND   python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nkeys=collections.Counter(); types=collections.defaultdict(collections.Counter); n=0\nfor p in sample:\n    for raw in open(p,'rb'):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        n+=1\n        for k,v in u.items(): keys[k]+=1; types[k][type(v).__name__]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in types.items()})\nPY",
          "observed": "Binary schema literal (one unique hit): usage:c({input_tokens:A(),output_tokens:A(),cache_creation_input_tokens:A().nullable(),cache_read_input_tokens:A().nullable(),server_tool_use:c({web_search_requests:A(),web_fetch_requests:A()}).nullable(),service_tier:i().nullable(),cache_creation:c({ephemeral_1h_input_tokens:A(),ephemeral_5m_input_tokens:A()}).nullable(),inference_geo:i().nullable().optional(),speed:i().nullable().optional(),iterations:de().optional(),output_tokens_details:c({thinking_tokens:A().nullable().optional()}).nullable().optional()}) -- exactly 11 keys, the same 11 the claim names. A second literal gives the zero-value runtime template: output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\". Corpus, version-current subset: 1,424 records over 5 transcripts, and ALL ELEVEN keys present on 1,424/1,424 (100%), output_tokens_details included. Corpus, mixed-version 15-file sample: 12,514 records; input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens / service_tier / cache_creation / inference_geo = 12,514 (100%); server_tool_use / iterations / speed = 12,478 (99.71%); output_tokens_details = 1,919 (15.3%). Value types: input/output/cache_* int, service_tier / inference_geo / speed string, cache_creation / server_tool_use / output_tokens_details object, iterations ARRAY (of per-API-call usage objects). Null-VALUED occurrences in the mixed sample: service_tier 38, inference_geo 38, speed 38, iterations 38, output_tokens_details 1.",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. A key counts as PRESENT when it is a key of the usage object, whether or not its value is null.",
          "note": "Code sites re-checked: src/model/record.rs:269-274 and :280-283 contain the claimed snippets verbatim at the claimed lines, so `TokenUsage` still deserializes four of the eleven keys and csift still reports no thinking split, no server-tool counts, no service tier and no inference geography. The claim's presence rates are correct for the corpus it sampled but are NOT a statement about current Claude Code: on 2.1.258 the rate is 100% for all eleven, so `output_tokens_details` is no longer a 6.4% rarity but a field a reader will meet on every record."
        }
      ]
    },
    {
      "id": "QT-037",
      "area": "queue-and-telemetry",
      "behavior": "Three of the `message.usage` values are nested objects with fixed shapes, not flat numbers: `cache_creation` carries `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`, `server_tool_use` carries `web_search_requests` and `web_fetch_requests`, and `output_tokens_details` carries `thinking_tokens`. The first two held on every record measured (12,514/12,514 and 12,478/12,478 in a mixed-version 15-transcript sample; 1,424/1,424 each on records written by 2.1.258). `output_tokens_details` is the one that can be absent as an object: it read NULL rather than an object on 1 of 1,919 records, and 2.1.258's own schema declares `thinking_tokens` nullable AND optional inside an `output_tokens_details` that is itself nullable and optional - so a reader must tolerate a null wrapper and a missing inner key, not just a missing outer key. Where the object was present its key set was exactly {thinking_tokens} on all 1,918 occurrences.",
      "depends": "csift reads none of the nested objects, so `stats` cannot report the 5m/1h cache split, web-tool call counts, or a thinking/non-thinking output split; a future addition must know these are nested, not flat, and that the flat `cache_creation_input_tokens` is a separate sibling key.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "281-284",
          "snippet": "    pub fn token_usage(&self) -> Option<TokenUsage> {\n        let raw = self.usage.as_ref()?;\n        serde_json::from_str(raw.get()).ok()\n    }"
        },
        {
          "path": "src/model/record.rs",
          "lines": "270-275",
          "snippet": "pub struct TokenUsage {\n    pub input_tokens: Option<u64>,\n    pub output_tokens: Option<u64>,\n    pub cache_read_input_tokens: Option<u64>,\n    pub cache_creation_input_tokens: Option<u64>,\n}"
        }
      ],
      "instrument": "Two instruments. (1) `strings -n 6` over the 2.1.258 binary for the nested usage schema and the zero-value usage template. (2) The same Python sweep over assistant records carrying `message.usage`, additionally iterating the keys of `usage['cache_creation']`, `usage['server_tool_use']` and `usage['output_tokens_details']` when each is a dict, and counting the non-dict (null) occurrences separately.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'output_tokens_details:c\\(\\{thinking_tokens:.{0,60}' | sort -u   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'output_tokens_details:\\{thinking_tokens:0\\},input_tokens:0,.*speed:\"standard\"' | sort -u   AND   python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n    for raw in open(p,'rb'):\n        if VER not in raw or b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        n+=1\n        for k in u: keys[k]+=1\n        for o in ('cache_creation','server_tool_use','output_tokens_details'):\n            if isinstance(u.get(o),dict):\n                for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY",
          "observed": "Binary: output_tokens_details:c({thinking_tokens:A().nullable().optional()}).nullable().optional() -- the inner `thinking_tokens` is BOTH nullable and optional inside a wrapper that is itself nullable and optional. Binary zero template: cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0}, server_tool_use:{web_search_requests:0,web_fetch_requests:0}, output_tokens_details:{thinking_tokens:0} -- the three nested shapes exactly as claimed. Corpus, version-current subset (1,424 records): cache_creation carried ephemeral_1h_input_tokens and ephemeral_5m_input_tokens on 1,424/1,424; server_tool_use carried web_search_requests and web_fetch_requests on 1,424/1,424; output_tokens_details was an object on 1,423 and NULL on 1, and every one of the 1,423 objects carried thinking_tokens. Mixed-version 15-file sample: cache_creation sub-keys 12,514/12,514, server_tool_use sub-keys 12,478/12,478, output_tokens_details object on 1,918 (plus 1 null) whose key SET was exactly ('thinking_tokens',) on all 1,918.",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per (record, nested key) pair; a nested value that is null rather than an object contributes no nested-key observations and is counted separately.",
          "note": "Code sites re-checked: src/model/record.rs:269-274 and :280-283 contain the claimed snippets verbatim at the claimed lines, so csift still reads none of the nested objects. The claim's 'on all 456' for `thinking_tokens` over-states universality - the field is optional and nullable by schema, and one null wrapper turned up in 1,919 records. The rest of the claim reproduced exactly, and the binary's literal zero-value template is a stronger witness than any sample because it enumerates the shape rather than a frequency."
        }
      ]
    },
    {
      "id": "QT-038",
      "area": "queue-and-telemetry",
      "behavior": "`cache_creation.ephemeral_5m_input_tokens + cache_creation.ephemeral_1h_input_tokens` equals the flat `cache_creation_input_tokens` on 12,428 of 12,514 records (99.31%) in a mixed-version 15-transcript sample and disagrees on 86 (0.69%); on records written by Claude Code 2.1.258 it agreed on 1,424 of 1,424 (100%). The nested pair therefore very nearly partitions the flat field but is not guaranteed to, and the disagreement has a shape: all 86 involved a non-zero `ephemeral_1h_input_tokens` that the flat field did not account for (55 with a flat 0, 31 with a flat above 0 but a zero 5m). Claude Code's own code makes the independence explicit - it takes the API's flat value when present and only falls back to the nested sum when it is absent, and elsewhere picks the flat value when above 0 and the nested sum otherwise, never adding the two.",
      "depends": "Summing the nested pair alongside the flat field would double-count cache-creation tokens in `csift stats` on 99% of records; a future implementation must pick one or the other, and must not assume the identity holds on every record.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "258-263",
          "snippet": "                let vals = [\n                    u.input_tokens.unwrap_or(0),\n                    u.output_tokens.unwrap_or(0),\n                    u.cache_read_input_tokens.unwrap_or(0),\n                    u.cache_creation_input_tokens.unwrap_or(0),\n                ];"
        }
      ],
      "instrument": "Python over the mixed-version sample and over the version-current subset: for each assistant record with a dict `usage.cache_creation`, compare `(ephemeral_5m_input_tokens or 0) + (ephemeral_1h_input_tokens or 0)` against `(cache_creation_input_tokens or 0)`, and bucket the disagreements by which of the three is zero; plus `strings -n 6` over the 2.1.258 binary for the fallback expression and the pick-one expression.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\neq=ne=tot=0; shapes=collections.Counter()\nfor p in sample:\n    for raw in open(p,'rb'):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        cc=u.get('cache_creation')\n        if not isinstance(cc,dict): continue\n        tot+=1\n        a=cc.get('ephemeral_5m_input_tokens') or 0; b=cc.get('ephemeral_1h_input_tokens') or 0\n        flat=u.get('cache_creation_input_tokens') or 0\n        if a+b==flat: eq+=1\n        else:\n            ne+=1\n            shapes[('flat=0' if flat==0 else 'flat>0','5m=0' if a==0 else '5m>0','1h=0' if b==0 else '1h>0')]+=1\nprint(tot,eq,ne,shapes.most_common())\nPY   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'cache_creation_input_tokens:n\\.cache_creation_input_tokens\\?\\?.{0,120}' | sort -u   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '_=r\\?\\.cache_creation_input_tokens\\?\\?0,v=\\(r\\?\\.cache_creation\\?\\.ephemeral_5m_input_tokens\\?\\?0\\)\\+\\(r\\?\\.cache_creation\\?\\.ephemeral_1h_input_tokens\\?\\?0\\),C=_>0\\?_:v' | sort -u",
          "observed": "Mixed-version 15-transcript sample: 12,514 records carry a dict `cache_creation`; the nested pair sums to the flat field on 12,428 (99.31%) and disagrees on 86 (0.69%). EVERY disagreement had ephemeral_1h_input_tokens > 0 - 55 with flat == 0 and 5m == 0, 31 with flat > 0 and 5m == 0 - i.e. the flat field never tracked a 1-hour cache write in those records. Version-current subset: 1,424 of 1,424 records agree (100%), 0 disagreements. Binary: cache_creation_input_tokens:n.cache_creation_input_tokens??(r?(r.ephemeral_1h_input_tokens??0)+(r.ephemeral_5m_input_tokens??0):e.cache_creation_input_tokens) -- the flat field is the API's own value when present and only FALLS BACK to the nested sum, so the identity is not enforced anywhere. Binary, the pick-one rule stated in code: _=r?.cache_creation_input_tokens??0,v=(r?.cache_creation?.ephemeral_5m_input_tokens??0)+(r?.cache_creation?.ephemeral_1h_input_tokens??0),C=_>0?_:v",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per record with a dict `usage.cache_creation`; missing sub-keys treated as 0; EQUAL means (5m or 0) + (1h or 0) == (cache_creation_input_tokens or 0).",
          "note": "Code site re-checked: src/stats.rs:258-263 contains the claimed four-element `vals` array verbatim at the claimed lines, so csift still sums only the flat field and cannot double-count today. The claim's direction and its warning are both right; only the percentages moved (99.08/0.92 claimed, 99.31/0.69 measured on a different sample) and the disagreement now has a named shape - it is the 1-hour cache TTL, and it did not occur at all in the 2.1.258 records."
        }
      ]
    },
    {
      "id": "QT-039",
      "area": "queue-and-telemetry",
      "behavior": "`output_tokens_details.thinking_tokens` is a SUBSET of `output_tokens`, not an addition to it - but only where `output_tokens` carries a real number. Over a mixed-version 15-transcript sample, 1,916 of 1,918 records carrying the key satisfied thinking_tokens <= output_tokens, and among the 1,916 records whose output_tokens is above 0 there were 0 violations and 0 ties. The 2 violations were records whose four top-level token counts had all been zeroed while the nested fields kept their values, so the comparison was against a 0 that is an artifact rather than a real output count. On records written by Claude Code 2.1.258 the relation held on all 1,423 that carry the key.",
      "depends": "If `csift stats` ever reports a thinking split it must SUBTRACT rather than add, otherwise the per-model output total would exceed what the API billed; the measured direction is what makes that safe to assert.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "307",
          "snippet": "    for ((model, _), vals) in usage_peak {"
        },
        {
          "path": "src/stats.rs",
          "lines": "258-263",
          "snippet": "                let vals = [\n                    u.input_tokens.unwrap_or(0),\n                    u.output_tokens.unwrap_or(0),\n                    u.cache_read_input_tokens.unwrap_or(0),\n                    u.cache_creation_input_tokens.unwrap_or(0),\n                ];"
        }
      ],
      "instrument": "Python over a mixed-version 15-transcript sample and over the version-current subset: for each assistant record with a dict `usage.output_tokens_details` carrying `thinking_tokens`, compare it against `usage.output_tokens`; report the <= and > counts BOTH over all such records and over the sub-population with output_tokens > 0, so a zeroed record cannot masquerade as a counterexample.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'\nimport os,glob,json\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\ntot=le=gt=posout=posviol=0\nfor p in sample:\n    for raw in open(p,'rb'):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        d=u.get('output_tokens_details')\n        if not (isinstance(d,dict) and 'thinking_tokens' in d): continue\n        tot+=1; t=d.get('thinking_tokens') or 0; o=u.get('output_tokens') or 0\n        le+= t<=o; gt+= t>o\n        if o>0:\n            posout+=1; posviol+= t>o\nprint(tot,le,gt,posout,posviol)\nPY   AND   python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects'); VER=b'\"version\":\"2.1.258\"'\nkeys=collections.Counter(); nested=collections.defaultdict(collections.Counter); n=0\nfor p in glob.glob(os.path.join(root,'*','*.jsonl')):\n    for raw in open(p,'rb'):\n        if VER not in raw or b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant' or r.get('version')!='2.1.258': continue\n        u=(r.get('message') or {}).get('usage')\n        if not isinstance(u,dict): continue\n        n+=1\n        for k in u: keys[k]+=1\n        for o in ('cache_creation','server_tool_use','output_tokens_details'):\n            if isinstance(u.get(o),dict):\n                for k in u[o]: nested[o][k]+=1\nprint(n, keys.most_common(), {k:dict(v) for k,v in nested.items()})\nPY",
          "observed": "Mixed-version 15-transcript sample: 1,918 records carry `output_tokens_details.thinking_tokens`; thinking_tokens <= output_tokens on 1,916 and thinking_tokens > output_tokens on 2. Restricting to the 1,916 records whose `output_tokens` is above 0: 0 violations, and 0 records where the two were equal. The 2 violating records are the two per-block copies of ONE API message sitting immediately after a record with isCompactSummary true; their usage reads input_tokens 0, output_tokens 0, cache_read_input_tokens 0, cache_creation_input_tokens 0 while output_tokens_details.thinking_tokens is 2218, cache_creation.ephemeral_1h_input_tokens is 2140 and the `iterations` array still carries the real numbers (input_tokens 2, output_tokens 2973, cache_read_input_tokens 962321, cache_creation_input_tokens 2140). Version-current subset: 1,423 records carry the key, 1,423 satisfied <=, 0 violations.",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). VERSION-CURRENT SUBSET = every line in every top-level transcript under ~/.claude/projects/*/*.jsonl whose raw bytes contain '\"version\":\"2.1.258\"', re-checked after parse as record.version == \"2.1.258\", type == \"assistant\", message.usage a JSON object. One observation per record. One observation per record whose `usage.output_tokens_details` is a dict carrying `thinking_tokens`; missing values treated as 0; reported both over all such records and over the sub-population with output_tokens > 0.",
          "note": "Code sites re-checked: src/stats.rs:307 and :258-263 contain the claimed snippets verbatim at the claimed lines. The claim's 'with 0 violations' does not survive a larger sample - 2 of 1,918 records violate it - but the violations are exactly the zeroed-usage shape QT-041 describes, so the engineering conclusion is unchanged and in fact sharpened: a subtraction is safe, and a future implementation should guard it against a zeroed output_tokens rather than trusting an unconditional inequality."
        }
      ]
    },
    {
      "id": "QT-040",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code repeats the IDENTICAL `message.usage` object on every per-block record of ONE API message, keyed by a shared `message.id`: over a 15-transcript sample, 3,864 of the 3,865 message.ids appearing on more than one record had every copy byte-identical, and the duplication persists at 2.1.258 (1,424 usage records over 624 distinct message.ids, a mean of 2.28 copies and a maximum of 9). A per-record sum therefore over-reports the true totals, by a factor that depends on which field is summed: over the four fields together the 15 transcripts measured 1.91x to 3.17x with a median of 2.64x, while `input_tokens` alone reached 4.73x on the worst file.",
      "depends": "`csift stats` dedupes per FILE by `message.id`, taking the per-field MAX across that id's admitted records, and a record with no `message.id` counts on its own; the scope TOTAL then sums files, so the counting rule differs between a per-file figure and a scope figure.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "250-256",
          "snippet": "                // CC repeats the IDENTICAL message.usage on every per-block record of\n                // one API message; summing per record over-reports 2.2-3.5x (measured).\n                // Dedupe per FILE by message.id, taking the per-field MAX across the\n                // id's admitted records: identical on clean data, and immune to the\n                // compaction-replay shape where a replayed copy carries ZEROED usage\n                // (first-wins would depend on traversal order). An id-less record\n                // counts on its own, as before."
        },
        {
          "path": "src/stats.rs",
          "lines": "258-263",
          "snippet": "                let vals = [\n                    u.input_tokens.unwrap_or(0),\n                    u.output_tokens.unwrap_or(0),\n                    u.cache_read_input_tokens.unwrap_or(0),\n                    u.cache_creation_input_tokens.unwrap_or(0),\n                ];"
        },
        {
          "path": "src/stats.rs",
          "lines": "307",
          "snippet": "    for ((model, _), vals) in usage_peak {"
        }
      ],
      "instrument": "`csift stats @<id> --format json` versus a naive per-record sum of `message.usage.input_tokens` over the same file; the ratio is the over-read factor. Counting rule: naive = one addition per assistant record carrying usage; csift = one addition per distinct `message.id` per file.",
      "located": {
        "claude_code": null,
        "csift": "0.9.2",
        "source": "AGENTS.md section 1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'\nimport os,glob,json,statistics\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]; out=[]\nfor p in sample:\n    naive=[0]*4; peak={}; n=0\n    for raw in open(p,'rb'):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        m=r.get('message') or {}; u=m.get('usage')\n        if not isinstance(u,dict): continue\n        n+=1\n        v=[u.get('input_tokens') or 0,u.get('output_tokens') or 0,\n           u.get('cache_read_input_tokens') or 0,u.get('cache_creation_input_tokens') or 0]\n        naive=[a+b for a,b in zip(naive,v)]\n        k=(m.get('model'),m['id']) if m.get('id') else ('none',n)\n        peak[k]=[max(a,b) for a,b in zip(peak.get(k,[0]*4),v)]\n    ded=[sum(x[i] for x in peak.values()) for i in range(4)]\n    out.append((n,len(peak),round(sum(naive)/max(sum(ded),1),2),round(naive[0]/max(ded[0],1),2)))\nprint(out); r=[o[2] for o in out]\nprint('all4 min/median/max', min(r), statistics.median(r), max(r), 'input max', max(o[3] for o in out))\nPY   AND   python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nmulti=ident=diff=mixed=0\nfor p in sample:\n    g=collections.defaultdict(list)\n    for i,raw in enumerate(open(p,'rb'),1):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        m=r.get('message') or {}; u=m.get('usage')\n        if not isinstance(u,dict) or not m.get('id'): continue\n        g[m['id']].append((i,json.dumps(u,sort_keys=True)))\n    for mid,rows in g.items():\n        if len(rows)<2: continue\n        multi+=1; s={x[1] for x in rows}\n        if len(s)==1: ident+=1\n        else:\n            diff+=1\n            t=[json.loads(x) for x in s]\n            z=[x for x in t if not any((x.get(k) or 0) for k in\n               ('input_tokens','output_tokens','cache_read_input_tokens','cache_creation_input_tokens'))]\n            if z and len(z)<len(t): mixed+=1; print('differing group at lines', [x[0] for x in rows])\nprint('multi',multi,'identical',ident,'differing',diff,'zero+nonzero',mixed)\nPY   AND   csift stats <one transcript path under ~/.claude/projects> --no-subagents --format json",
          "observed": "Duplication is real and byte-exact: over the 15-transcript sample, 3,865 message.ids appear on more than one record, and on 3,864 of them (99.97%) every copy's whole usage object is byte-identical after canonical JSON serialization; exactly 1 differs (the QT-041 compaction-replay case). Multiplicity persists in the current version: the 1,424 records written by 2.1.258 that carry usage span 624 distinct (file, message.id) pairs - a mean of 2.28 records per API message, maximum 9 copies. Over-read factor, per transcript, naive sum divided by the deduped sum: on the four fields summed together the 15 files gave min 1.91, median 2.64, max 3.17; taking input_tokens alone the same files reached 4.73. Cross-check that csift implements the stated rule: on one transcript `csift stats --no-subagents --format json` reported, summed across the models in its `tokens` map, input 664, output 530,169, cache_read 93,433,115, cache_creation 16,454,008 - identical in all four fields to an independent Python per-(model, message.id) MAX reimplementation, against a naive per-record sum on the same file of input 3,141, output 2,216,048, cache_read 293,906,300, cache_creation 53,836,886.",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). NAIVE = one addition per assistant record carrying usage. DEDUPED = one addition per distinct (model, message.id) pair per file, taking the per-field MAX across that pair's records; a record with no message.id counts on its own. RATIO = naive divided by deduped, computed per file, reported over the four fields summed and for input_tokens alone.",
          "note": "Code sites re-checked: src/stats.rs:250-256, :258-263 and :307 contain the claimed snippets verbatim at the claimed lines. The mechanism and csift's counting rule both verified - an independent reimplementation of per-(model, message.id) MAX per file reproduced csift's reported totals exactly in all four fields on the test transcript. Only the quoted over-read range needed correction, and it is worth restating as a per-field range since a single number cannot cover both the aggregate and input_tokens alone. code-site note: src/stats.rs:250-256 still carries the comment quoting '2.2-3.5x (measured)'. Measured now over 15 transcripts the four-field aggregate factor spans 1.91-3.17x (median 2.64x) and the input_tokens-only factor reaches 4.73x, so the comment's range is narrow at the bottom and understates the single-field worst case."
        }
      ]
    },
    {
      "id": "QT-041",
      "area": "queue-and-telemetry",
      "behavior": "A compaction replay can emit a SECOND record for the same `message.id` whose four top-level `message.usage` counts are ZEROED while the original copy carries the real values, so the copies of one id are not interchangeable. Measured over a 15-transcript sample, this is rare but real: 1 of the 3,865 message.ids appearing on more than one record within a file had non-identical usage across its copies, and that group's zeroed copies sit immediately after a `compact_boundary` record and repeat the pre-boundary records' timestamps. The zeroing is PARTIAL: it clears `input_tokens`, `output_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens` while the nested `cache_creation`, `output_tokens_details` and `iterations` values on the same record keep their real numbers - so 'zeroed usage' means the four fields csift reads, not the whole object.",
      "depends": "this is why the `stats` dedupe takes the per-field MAX rather than first-wins: on clean data the two rules agree, but a first-wins rule would let traversal order decide whether an API message contributes its real tokens or zeros.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "250-256",
          "snippet": "                // CC repeats the IDENTICAL message.usage on every per-block record of\n                // one API message; summing per record over-reports 2.2-3.5x (measured).\n                // Dedupe per FILE by message.id, taking the per-field MAX across the\n                // id's admitted records: identical on clean data, and immune to the\n                // compaction-replay shape where a replayed copy carries ZEROED usage\n                // (first-wins would depend on traversal order). An id-less record\n                // counts on its own, as before."
        },
        {
          "path": "src/stats.rs",
          "lines": "307",
          "snippet": "    for ((model, _), vals) in usage_peak {"
        }
      ],
      "instrument": "Census the `message.id` values appearing on more than one record within ONE file (one observation per (message.id, record) pair), compare those copies' whole `message.usage` objects as canonical JSON, then re-read the lines of any differing group printing record type, subtype, isCompactSummary and the four counts, so the zeroed copies can be located relative to the `compact_boundary` record. Cross-check that `csift stats` on that file reports the non-zero values.",
      "located": {
        "claude_code": null,
        "csift": "0.9.2",
        "source": "AGENTS.md section 1; src/stats.rs:250-256 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'\nimport os,glob,json,collections\nroot=os.path.expanduser('~/.claude/projects')\ncand=[(os.stat(p).st_mtime,os.stat(p).st_size,p) for p in glob.glob(os.path.join(root,'*','*.jsonl'))\n      if (1<<20)<=os.stat(p).st_size<=50*(1<<20)]\ncand.sort(reverse=True); sample=[p for _,_,p in cand[:15]]\nmulti=ident=diff=mixed=0\nfor p in sample:\n    g=collections.defaultdict(list)\n    for i,raw in enumerate(open(p,'rb'),1):\n        if b'\"usage\"' not in raw: continue\n        try: r=json.loads(raw)\n        except Exception: continue\n        if r.get('type')!='assistant': continue\n        m=r.get('message') or {}; u=m.get('usage')\n        if not isinstance(u,dict) or not m.get('id'): continue\n        g[m['id']].append((i,json.dumps(u,sort_keys=True)))\n    for mid,rows in g.items():\n        if len(rows)<2: continue\n        multi+=1; s={x[1] for x in rows}\n        if len(s)==1: ident+=1\n        else:\n            diff+=1\n            t=[json.loads(x) for x in s]\n            z=[x for x in t if not any((x.get(k) or 0) for k in\n               ('input_tokens','output_tokens','cache_read_input_tokens','cache_creation_input_tokens'))]\n            if z and len(z)<len(t): mixed+=1; print('differing group at lines', [x[0] for x in rows])\nprint('multi',multi,'identical',ident,'differing',diff,'zero+nonzero',mixed)\nPY   AND   csift stats <that transcript> --no-subagents --format json   AND   a targeted Python re-read of the record lines the census named, printing type, subtype, isCompactSummary, timestamp, block types and the four usage counts",
          "observed": "Over the 15-transcript sample, 3,865 message.ids appear on more than one record within one file; 3,864 have byte-identical usage across every copy and exactly 1 differs. That one group spans 13 records in a single transcript: 10 records before a `type:\"system\" subtype:\"compact_boundary\"` line carry (input 2, output 4,849, cache_read 951,431, cache_creation 16,024), and 3 records AFTER the boundary carry (0, 0, 0, 0) while reusing the SAME message.id and repeating the pre-boundary timestamps to the second. A second, partial form of the same shape appeared in another transcript: the two per-block copies of one API message immediately following an isCompactSummary record have all four top-level counts zeroed while output_tokens_details.thinking_tokens (2218), cache_creation.ephemeral_1h_input_tokens (2140) and the `iterations` array (input_tokens 2, output_tokens 2,973, cache_read_input_tokens 962,321, cache_creation_input_tokens 2,140) all still carry real values - so the zeroing hits the four top-level scalars, not the whole object. Cross-check: `csift stats` on the first transcript reports the NON-ZERO values (summed across models: input 664, output 530,169, cache_read 93,433,115, cache_creation 16,454,008), matching an independent per-(model, message.id) MAX reimplementation exactly.",
          "rule": "SAMPLE = the 15 most recently modified top-level transcripts under ~/.claude/projects/*/*.jsonl with 1 MiB <= size <= 50 MiB (250.9 MB total). RECORD = a line with type==\"assistant\" whose message.usage is a JSON object. One observation per RECORD (per-block duplication included by design). GROUP = all records in ONE file sharing a message.id; one observation per (message.id, record) pair. A group is DIFFERING when the canonical JSON of its copies' usage objects is not all one string; it is MIXED when at least one copy has all four token counts 0 and at least one does not.",
          "note": "Code sites re-checked: src/stats.rs:250-256 and :307 contain the claimed snippets verbatim at the claimed lines. The claim is confirmed by instrument, and the per-field MAX is vindicated for a reason worth recording: in the group measured here the zeroed copies come LAST, so a first-wins rule would have survived by luck, while in the partial-zeroing case the zeroed copies are the ONLY copies of that id - which is why a MAX cannot rescue that one either and the honest statement is that MAX is order-independent, not that it recovers every case."
        }
      ]
    },
    {
      "id": "REC-001",
      "area": "record-model",
      "behavior": "Claude Code writes exactly one JSON object per LINE into `~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl`, so the record unit is the physical line: a compact line and a space-reserialized line are each one whole record, while a pretty-printed multi-line object breaks the jsonl framing.",
      "depends": "csift mmaps the file, splits it on newlines with `memchr` and parses only candidate lines, and every address (`show --line`), every count and the malformed-line census are expressed in lines; a record spread over several lines would be booked as several malformed lines rather than one record.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "101-109",
          "snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n    let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n    let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n    if line.iter().all(u8::is_ascii_whitespace) {\n        None\n    } else {\n        Some(line)\n    }\n}"
        }
      ],
      "instrument": "`wc -l <transcript>` equals the summed line-type census PLUS `skipped_lines` from the same `csift stats --format json` object, not the census alone: a torn or pretty-printed record contributes lines to skipped_lines and to none of the line_types buckets. On a clean file skipped_lines is 0 and the two figures coincide, which is why the shorter form usually appears to work.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.9",
        "source": "dev session 2026-09-02; CHANGELOG 0.6.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) live: `wc -l ~/.claude/projects/<enc>/<session>.jsonl` beside `csift stats @<session> --no-subagents --format json | jq '.line_types, .skipped_lines'`; (b) fixture: a 3-record file written one record compact, one record space-reserialized (Python json.dumps defaults), one record pretty-printed (indent=2), then `csift --claude-home <tmp-home> stats @<session> --format json` and `csift --claude-home <tmp-home> search <NEEDLE> -c` once per record.",
          "observed": "Live file: wc -l = 14283; summed line_types = 14283 (assistant 2316, attachment 8116, user 1278, last-prompt 467, permission-mode 465, mode 465, ai-title 462, agent-name 439, queue-operation 120, system 98, file-history-snapshot 57); skipped_lines = 0. Fixture: wc -l = 18 physical lines (1 compact + 1 space-reserialized + 16 lines of one pretty-printed object); census line_types = {\"user\": 2}; skipped_lines = 16; 2 + 16 = 18. Needle in the compact record: 1 exchange. Needle in the space-reserialized record: 1 exchange. Needle in the pretty-printed record: 0.",
          "rule": "One record per physical jsonl line, whole file. The identity that must hold is sum(line_types) + skipped_lines == wc -l, counted once per non-blank physical line.",
          "note": "Both halves of the framing law were exercised, not just the positive one. The pretty-printed object was booked as 16 malformed lines and zero records, and its text was unreachable by search - exactly the failure mode the claim predicts. The space-reserialized record stayed a full citizen (matched, censused as `user`), confirming the serialization-tolerant candidate matching the claim's depends clause rests on. Code site src/parse/lines.rs 101-109 is verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-002",
      "area": "record-model",
      "behavior": "The top-level `type` discriminator is an OPEN set: the harness maps a known type to a persistence class and falls back to a default for anything unrecognised, so an unknown value is written and read without error. All thirteen values the claim names were observed on disk, and three more the claim omits were observed too - `atis-latch`, `bridge-session` and `cost-state` - for sixteen distinct values over the whole local corpus. `type:\"summary\"` is an ON-DISK absence (0 of 1285920 lines), not a harness fact: `summary` IS a registered type in the 2.1.258 routing table (class `last-wins`). A compaction summary on disk is a `type:\"user\"` record flagged `isCompactSummary`.",
      "depends": "csift keeps `type` as a tolerant `Option<String>` and branches in logic rather than deserializing an enum, and compaction detection keys on `isCompactSummary` instead of a `summary` type; a closed enum would panic every scanning subcommand on the next type Claude Code ships.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "17-20",
          "snippet": "    /// Record discriminator: \"user\", \"assistant\", \"system\", \"summary\",\n    /// \"last-prompt\", \"attachment\", … (open set - keep as String, never enum-panic).\n    #[serde(default)]\n    pub r#type: Option<String>,"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "333-338",
          "snippet": "/// §7d stage-1 category prefilter on raw bytes: keep a line only if it could be a\n/// transcript message (user/assistant role marker) - drops `attachment`,\n/// `file-history-*`, `queue-operation`, and metadata noise pre-JSON unless the\n/// matching [`CandidateGates`] flag admits them. Kept deliberately permissive\n/// (substring, not structural) so no genuine turn is lost.\npub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {"
        }
      ],
      "instrument": "`csift stats --format json | tail -1` over the whole corpus is the sound form: stats fully validates every line for the census, so a type it does not list has zero records. The `rg -o '\"type\":\"[a-z-]+\"'` alternative also matches the string `\"type\":\"text\"` and every other nested block/payload discriminator, so it over-reports unless anchored to line start.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 3.1; src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "`csift stats --format json | tail -1` (whole corpus under ~/.claude/projects, subagent transcripts included - stats fully validates every line for its line-type census) and `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'user:\"transcript\".{0,1400}'`.",
          "observed": "Corpus census over 7577 transcripts / 1285920 lines, skipped_lines 0, 16 distinct top-level `type` values: attachment 466495, assistant 428960, user 228125, last-prompt 29233, permission-mode 29174, mode 29085, ai-title 28756, queue-operation 13494, system 12690, agent-name 12309, file-history-snapshot 4055, file-history-delta 1971, atis-latch 797, bridge-session 728, fork-context-ref 33, cost-state 15. `summary` = 0 records. The 2.1.258 binary carries a record-type routing table with 38 keys and an open default: 'user:\"transcript\",assistant:\"transcript\",system:\"transcript\",attachment:\"transcript\",progress:\"boundary-cleared\",\"file-history-snapshot\":\"boundary-cleared\",\"file-history-delta\":\"boundary-cleared\",\"last-prompt\":\"boundary-cleared\",\"continued-in\":\"boundary-cleared\",... ,\"fork-context-ref\":\"accumulate\",\"frame-link\":\"accumulate\",summary:\"last-wins\",\"custom-title\":\"last-wins\",\"ended-by-model\":\"last-wins\",\"ai-title\":\"last-wins\",tag:\"last-wins\",relocated:\"last-wins\",\"agent-name\":\"last-wins\",\"agent-color\":\"last-wins\",\"agent-setting\":\"last-wins\",\"pr-link\":\"last-wins\",...,\"bridge-session\":\"last-wins\",\"history-suppression\":\"last-wins\",\"attribution-snapshot\":\"last-wins\",mode:\"last-wins\",\"permission-mode\":\"last-wins\",\"isolation-latch\":\"last-wins\",\"atis-latch\":\"last-wins\",\"worktree-state\":\"last-wins\",\"cost-state\":\"last-wins\",\"queue-operation\":\"last-wins\",\"observer-ref\":\"last-wins\"};function fts(e){return dts[e]??\"accumulate\"}'",
          "rule": "One count per raw jsonl line, whole file, across every transcript under ~/.claude/projects (top-level and subagent). Binary side: one registry entry per key in the routing table; the `?? \"accumulate\"` default is what makes the set open by construction.",
          "note": "The binary registry is the stronger instrument for openness than any disk census: it enumerates 38 record types of which only 16 have ever been written here, and 22 of the 38 (progress, continued-in, content-replacement, frame-link, custom-title, ended-by-model, tag, relocated, agent-color, agent-setting, pr-link, history-suppression, attribution-snapshot, isolation-latch, worktree-state, observer-ref, summary and the four-name origami/artifact families) are unobserved locally. Both code sites are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-003",
      "area": "record-model",
      "behavior": "Only the two message-bearing types carry a `message{}` field: re-measured over 413 transcripts in one project directory, assistant 37218/37218 and user 18431/18431 carry one while all 13 other Claude-Code-written line types in scope carry zero.",
      "depends": "csift makes `message{}` presence the decisive instrument for `Class::llm_visible()`, so a bare-role selector (`-t user`) returns only leaves that could become an API message; if a non-message type ever gained a `message{}` the visibility split behind `search -t` would silently invert.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "184-190",
          "snippet": "    pub fn llm_visible(self) -> bool {\n        !matches!(\n            self,\n            Class::UserUnsent\n                | Class::UserQueued\n                | Class::CompactionBoundary\n                | Class::MetaTurnDuration"
        },
        {
          "path": "src/model/record.rs",
          "lines": "111-113",
          "snippet": "    /// The role-bearing message payload (present on user/assistant records).\n    #[serde(default)]\n    pub message: Option<Message>,"
        }
      ],
      "instrument": "Parse every line of a set of transcripts with a strict JSON reader and cross-tabulate top-level `type` against presence of the key `message`; counting rule = one count per LINE, per file. Expect 100% presence on `user`/`assistant` and 0 on every other type. Re-check whenever a new line type appears in the `csift stats --format json` line-type census.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Strict-JSON deep parse of every line of all 413 transcripts under one project directory (`find <project-dir> -name '*.jsonl' ! -name journal.jsonl`), cross-tabulating top-level `type` against presence of the top-level key `message`.",
          "observed": "136059 lines, 0 unparseable. With `message`: assistant 37218/37218 (100%), user 18431/18431 (100%). Without `message`, all zero with: attachment 61147, last-prompt 3507, mode 3484, permission-mode 3484, ai-title 3103, agent-name 2651, queue-operation 1083, system 863, file-history-snapshot 433, atis-latch 233, bridge-session 233, file-history-delta 144, fork-context-ref 18. Two further zero-message types in the sweep were csift's own elicitation sidecar markers (csift-elicitation 4, csift-elicitation-resolved 23), not written by Claude Code.",
          "rule": "One count per non-blank physical line per file; a line counts under its top-level `type` and under `message` present / absent. 100% presence expected on user and assistant, 0 on every other type.",
          "note": "The decisive-instrument property the depends clause rests on is intact: zero non-message line types carry a `message{}`, so `Class::llm_visible` never has to arbitrate an ambiguous line. Both code sites (src/model/taxonomy.rs 175-181, src/model/record.rs 104-106) are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-004",
      "area": "record-model",
      "behavior": "The eleven named fields (type, uuid, parentUuid, timestamp, sessionId, cwd, version, gitBranch, isSidechain, userType, message) are present on 100% of user and assistant records. `subtype` is present on 100% of system records but `content` is NOT: only 13 of 98 system records carried it in the sampled file, so `content` is a per-subtype field, not a system-record field. `isCompactSummary` appeared on 5 user records, matching the file's 5 compactions. The set is wide and evolving: user records carried 26 distinct top-level keys, 15 of them beyond the claim's list (promptId, entrypoint, slug, session_id, toolUseResult, sourceToolAssistantUUID, promptSource, permissionMode, origin, isMeta, isVisibleInTranscriptOnly, isCompactSummary, toolDenialKind, queuePriority, interruptedMessageId).",
      "depends": "csift deserializes only what it uses and ignores the rest, so a new harness field never crashes a scan; tightening the model into a strict schema would break every subcommand on the next Claude Code release.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "92-99",
          "snippet": "    /// `system` record inline content (e.g. away_summary text, or the `compact_boundary`\n    /// `\"Conversation compacted …\"` line). Read by `search` as the message-less fallback text (D7).\n    #[serde(default)]\n    pub content: Option<serde_json::Value>,\n\n    /// `compact_boundary` metrics (§3.5 / D7): `{trigger, preTokens, postTokens, durationMs}` on a\n    /// `type:\"system\"`/`subtype:\"compact_boundary\"` record. Kept RAW; `search` renders it as a\n    /// readable excerpt (`record_raw_text`) so `-t harness.compaction.boundary` can enumerate"
        }
      ],
      "instrument": "`head -200 ~/.claude/projects/*/<any>.jsonl | jq -r 'keys[]' | sort | uniq -c` - expect the listed keys plus extras; then `csift stats @<session>` on the same file and confirm zero malformed lines. Counting rule: one key occurrence per record.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Strict-JSON parse of one 14283-line transcript, tabulating every top-level key by record `type`; then `csift stats @<session> --no-subagents --format json` on the same file for the malformed count.",
          "observed": "user records 1278: parentUuid, isSidechain, promptId, type, message, uuid, timestamp, userType, entrypoint, cwd, sessionId, version, gitBranch all 1278/1278; then slug 1261, session_id 1219, toolUseResult 1197, sourceToolAssistantUUID 1197, promptSource 43, permissionMode 41, origin 40, isMeta 8, isVisibleInTranscriptOnly 5, isCompactSummary 5, toolDenialKind 2, queuePriority 2, interruptedMessageId 1 (26 distinct keys). assistant records 2316: parentUuid, isSidechain, message, type, uuid, timestamp, userType, entrypoint, cwd, sessionId, version, gitBranch all 2316/2316; requestId 2315, session_id 2315, slug 2291, attributionSkill 14, isApiErrorMessage 2, error 1, errorDetails 1, apiErrorStatus 1. system records 98: subtype 98/98 but content only 13/98; compactMetadata 5 and logicalParentUuid 5 (the file's 5 compactions). stats reported skipped_lines 0 on the same file.",
          "rule": "One key occurrence per record; percentages are over records of that `type` in the one file.",
          "note": "The tolerance the depends clause asks for is doing real work: a strict schema built from the claim's eleven fields would have rejected keys on every single record of the sampled file. Code site src/model/record.rs 85-92 is verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-005",
      "area": "record-model",
      "behavior": "`timestamp` is an ISO-8601 UTC string with a trailing `Z` (for example `2026-06-07T05:43:00.000Z`) on message-bearing records, but it is ABSENT entirely on many line types - the session-state cache lines (`last-prompt`, `ai-title`, `agent-name`, `mode`, `permission-mode`), `file-history-snapshot`, `fork-context-ref`, `cost-state`, `atis-latch` and `bridge-session` - and those metadata-only records also lack `uuid` and `parentUuid`.",
      "depends": "Every csift field is `Option<T>` and no surface unwraps a timestamp: the time windows (`--since`/`--until`), turn ordering and the `list`/`stats` spans are computed only over timestamped records, and `show --uuid`/`--turn` can never address a cache line (only `show --line`/`--raw` reaches it).",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "28-30",
          "snippet": "    /// ISO8601 UTC, e.g. `2026-06-07T05:43:00.000Z`. Absent on metadata-only records.\n    #[serde(default)]\n    pub timestamp: Option<String>,"
        },
        {
          "path": "src/timez.rs",
          "lines": "79-81",
          "snippet": "pub fn format_timestamp(raw: Option<&str>) -> String {\n    render_local(raw, false)\n}"
        },
        {
          "path": "src/stats.rs",
          "lines": "240-243",
          "snippet": "        if let Some(ts) = rec.timestamp.as_deref() {\n            if out.first_utc.as_deref().is_none_or(|f| ts < f) {\n                out.first_utc = Some(ts.to_string());\n            }"
        },
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "9-11",
          "snippet": "    /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n    /// message record and for every line type that stays unmodeled (the session-state\n    /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`)."
        }
      ],
      "instrument": "`rg -m5 '\"type\":\"mode\"' ~/.claude/projects/**/*.jsonl | jq -r 'has(\"timestamp\"), has(\"uuid\")'` must print `false false`, and `csift stats` over the same file must still report a span; then `csift show @<id> --line <a cache line number>` must bail with the 'no such record(s)' message while `--raw` prints the bytes. Counting rule: per raw line.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "SPEC.md section 3.2; src/model/record.rs comment; AGENTS.md section 3.2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Strict-JSON parse of all 407 Claude-Code-written transcripts under one project directory, tabulating presence of `timestamp` / `uuid` / `parentUuid` by top-level `type` and regex-checking every timestamp string against ^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$; a separate keys dump of a `cost-state` record from another project directory; then `csift show @<session> --line 2` and `csift show @<session> --line 2 --raw` against a line that is a `mode` cache record.",
          "observed": "All 118992 timestamp strings matched the trailing-Z ISO-8601 shape; zero other shapes. Zero timestamp, zero uuid and zero parentUuid on: agent-name 2653, ai-title 3105, atis-latch 235, bridge-session 235, file-history-snapshot 433, fork-context-ref 18, last-prompt 3509, mode 3486, permission-mode 3486. cost-state keys were exactly [hasUnknownModelCost, modelUsage, sessionId, startTime, totalAPIDuration, totalAPIDurationWithoutRetries, totalCostUSD, totalDuration, totalLinesAdded, totalLinesRemoved, totalToolDuration, type] - no timestamp, no uuid, no parentUuid. By contrast user 18471, assistant 37265, system 863 and attachment 61166 carried all three at 100%. `csift show --line 2` on the mode line exited 1 with `csift: error: no such record(s): L2 - ... session-state cache lines (last-prompt, mode, ai-title, ...) ... are inspectable with --raw`, while `--raw` exited 0 and printed {\"type\":\"mode\",\"mode\":\"normal\",\"sessionId\":\"...\"}.",
          "rule": "One count per non-blank physical line, bucketed by top-level `type`; a field counts as present iff the key exists on the parsed object. Timestamp shape is checked per timestamp string, whole corpus of the sampled directory.",
          "note": "Every one of the ten line types the claim names was confirmed timestampless, uuidless and parentUuidless, and the timestamp shape held on all 118992 strings with no exception. Two types NOT in the claim's list behave differently and are worth recording so a future reader does not over-generalise: `queue-operation` (1083/1083) and `file-history-delta` (144/144) DO carry a top-level `timestamp` while still carrying no `uuid` and no `parentUuid` - so 'timestampless' and 'uuidless' are not the same partition of the line types. All four code sites are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-006",
      "area": "record-model",
      "behavior": "Every record carries a top-level `version` stamp naming the Claude Code build that wrote it and a `gitBranch` field, and BOTH can change mid-session: an upgrade or a branch switch lands inside one transcript (one deep sample spanned builds 2.1.150 through 2.1.258).",
      "depends": "`list` reports `version`/`git_branch` as LAST-seen with the opening values on `version_first`/`git_branch_first` plus a drift arrow, and `search --count-by version` censuses where an upgrade landed; reporting only the opening sample was stale.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "134-140",
          "snippet": "        // The base fields are LAST-seen (what the session is on NOW); the head\n        // capture becomes the *_first pair. Either window can be empty - fall back\n        // to the other so a one-record session reports the same value everywhere.\n        version: version_last.clone().or_else(|| version.clone()),\n        version_first: version.or(version_last),\n        git_branch: git_branch_last.clone().or_else(|| git_branch.clone()),\n        git_branch_first: git_branch.or(git_branch_last),"
        }
      ],
      "instrument": "`csift search '' @<uuid> --count-by version` (records per stamp, stampless records excluded and disclosed) beside `csift list @<uuid> --format json | jq '{version_first, version_last, git_branch_first, git_branch_last}'`. Counting rule: one record per version key.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.3",
        "source": "CHANGELOG 0.8.1 (list last-seen version/branch; --count-by version); dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "`csift search '' @<session> --count-by version --no-subagents` on two sessions, and `csift list --no-subagents --max-count 0 --format json` over every project, comparing version_first/version_last and git_branch_first/git_branch_last per session row.",
          "observed": "One session's version census returned three keys in one transcript: 2.1.233 7051 records, 2.1.227 1818, 2.1.258 1811 (10680 matched across 3 version keys); a second session returned a single key (2.1.207, 3597). Across 64 top-level session rows, 11 had version_first != version_last and 2 had git_branch_first != git_branch_last (main -> HEAD in one, HEAD -> main in the other). Distinct build stamps across the head/tail samples: 32, min 2.1.150, max 2.1.258. The text row rendered the drift arrow as `(branch main, CC 2.1.227->2.1.258)`, and the JSON row carried version 2.1.258 / version_first 2.1.227 / version_last 2.1.258 / git_branch main / git_branch_first main / git_branch_last main.",
          "rule": "Version census: one record per version key, records with no version stamp excluded and disclosed. Drift census: one session row per top-level transcript; a session counts as drifted iff its head-window value differs from its tail-window value.",
          "note": "Both halves are confirmed independently. Version drift is common (11 of 64 sessions, 17%) and one transcript carried three distinct build stamps including a downgrade (2.1.233 before 2.1.227), which a first-sample-only reader would report as a single wrong build. gitBranch drift is rarer (2 of 64) and both instances were a detached-HEAD checkout crossing the session. The corpus range 2.1.150 through 2.1.258 reproduces the claim's figure exactly, though here it spans 32 builds over 64 sessions rather than one deep sample. Code site src/session/summarize.rs 134-140 is verbatim at the claimed lines, and the JSON key names the claim's instrument uses (version_last, git_branch_last) do exist alongside version_first/git_branch_first."
        }
      ]
    },
    {
      "id": "REC-007",
      "area": "record-model",
      "behavior": "On an `assistant` record the `message` object is the FULL model-API message object. Nine keys are universal - model, id, type, role, content, stop_reason, stop_sequence, stop_details, usage - `diagnostics` is near-universal but NOT guaranteed (99.98%, absent on 7 of 37374 records), and rarer keys appear on a handful of records (context_management, container).",
      "depends": "csift models `role` + `content` plus a lazily-parsed `usage` blob; `stats` reads `message.model` and `message.usage` for its per-model token table and the live tail state machine reads `stop_reason`, so tightening `Message` breaks both.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "261-263",
          "snippet": "    /// `Record::tool_use_result`): only `stats` reads it, via [`Message::token_usage`].\n    #[serde(default)]\n    pub usage: Option<Box<serde_json::value::RawValue>>,"
        }
      ],
      "instrument": "`rg -m1 '\"type\":\"assistant\"' <one transcript> | jq '.message | keys'` samples exactly one record, which cannot distinguish a universal key from a 99.98% key. Tabulate keys over every assistant record of a file instead.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 3.3"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Strict-JSON parse of all 408 transcripts under one project directory, tabulating every key of `message{}` on `type:\"assistant\"` records; plus `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\{[a-z_]+:!0,(?:[a-z_]+:!0,){4,}[a-z_]+:!0\\}'` for the harness's stop_reason registry.",
          "observed": "37374 assistant records. Present on 100%: model, id, type, role, content, stop_reason, stop_sequence, stop_details, usage. Present on 37367/37374 (99.98%): diagnostics. Rare: context_management 13 (0.03%), container 12 (0.03%). The 2.1.258 binary carries the stop_reason value registry '{end_turn:!0,max_tokens:!0,stop_sequence:!0,tool_use:!0,pause_turn:!0,compaction:!0,refusal:!0,model_context_window_exceeded:!0}'.",
          "rule": "One count per key per assistant record, over every assistant record in the sampled directory; percentage is over the 37374 assistant records.",
          "note": "The two consumers the depends clause names are both safe: `message.model` and `message.usage` are universal, so the stats token table never loses a record to an absent key, and `stop_reason` is universal on assistant records too. The stop_reason value space the harness recognises is wider than the five values usually quoted - it includes pause_turn, compaction and model_context_window_exceeded - which matters for any consumer that branches on the value rather than on its inequality to end_turn. Code site src/model/record.rs 254-256 is verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-008",
      "area": "record-model",
      "behavior": "A record's `message.content` is polymorphic: either a bare STRING (older format, genuine user text, a compaction summary) or an ARRAY of typed blocks; both shapes occur in current data.",
      "depends": "csift models `Content` as an untagged string-or-array enum; a struct that assumed the array form would drop every string-content genuine user turn, which is the majority shape for human prose.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "53-58",
          "snippet": "    pub fn content_str(&self) -> Option<&str> {\n        match self.content.as_ref()? {\n            serde_json::Value::String(s) => Some(s.as_str()),\n            _ => None,\n        }\n    }"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "68-73",
          "snippet": "        match &msg.content {\n            Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n            Some(Content::Blocks(blocks)) => {\n                let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n                let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n                if !has_text || has_tool_result {"
        }
      ],
      "instrument": "`rg -m20 '\"type\":\"user\"' <transcript> | jq -r '.message.content | type' | sort | uniq -c` must show both `string` and `array`. Counting rule: one per sampled user record.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 3.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(a) per-project-directory byte census with `rg -c --no-filename -I -F '\\\"role\\\":\\\"user\\\",\\\"content\\\":[' <dir> -g '*.jsonl'` against the same needle ending in a quote, summed over all 14 project directories; (b) `csift search '' -t user.message --raw --max-count 0` piped into a strict-JSON reader that types `message.content` per record.",
          "observed": "Corpus byte census: 212800 user lines with array-form content, 15568 with string-form content - both shapes present, neither rare. Among the records csift labels as genuine human turns (`-t user.message`, whole corpus): 2421 records, of which 1946 string (80.4%) and 475 array (19.6%). A separate strict parse of one project directory gave user array 17452 / user string 1077 with the same conclusion.",
          "rule": "Byte census: one count per raw line matching the anchored needle, summed per project directory. Label census: one count per record returned by `-t user.message`, typed on the parsed `message.content` value.",
          "note": "The depends clause's stronger sub-claim was checked and holds: the bare-string shape is the MAJORITY shape for human prose, 1946 of 2421 genuine user turns (80.4%). An array-only reader would therefore drop four fifths of the human turns, not an edge case. Both code sites (src/model/classify_promoted.rs 48-53, src/model/predicates.rs 68-73) are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-009",
      "area": "record-model",
      "behavior": "Assistant `message.content` is ALWAYS a block array in Claude Code 2.1.x; a bare string body would be a genuine surprise.",
      "depends": "`agent_text` still handles the bare-string case rather than dropping it, so a format change surfaces as content instead of a silent blank in `verbatim` and `search -t agent.message`.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "357-360",
          "snippet": "        let Content::Blocks(blocks) = content else {\n            // Assistant content is always a block array in CC 2.1.x; a bare string\n            // would be a genuine surprise - surface it rather than silently drop.\n            if let Content::Text(s) = content {"
        }
      ],
      "instrument": "`rg -c '\\\"type\\\":\\\"assistant\\\".*\\\"content\\\":\\\"'` is not sound as written - the `.*` lets any later `\\\"content\\\":\\\"` on the same line satisfy it, including one nested inside a tool_use input - so a nonzero result would not prove a string body. It returned 0 on each of the three largest transcripts tested, agreeing with the deep parse, but the anchored form `\\\"role\\\":\\\"assistant\\\",\\\"content\\\":\\\"` plus a deep parse of the residual lines is the form that actually decides it.",
      "located": {
        "claude_code": "2.1.x",
        "csift": null,
        "source": "src/model/exchange.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Two passes. (a) per-project-directory byte census over all 14 project directories: `rg -c -F '\\\"type\\\":\\\"assistant\\\"'`, `rg -c -F '\\\"role\\\":\\\"assistant\\\",\\\"content\\\":['` and the same needle ending in a quote. (b) a strict-JSON deep parse of every line of all 7579 transcripts that carries the assistant tag but NOT the compact array needle, typing `message.content` on each, so the byte census's key-order blind spot is closed rather than assumed away.",
          "observed": "Corpus: 429501 assistant lines; 428539 matched the compact array needle; 0 matched the string needle. The 828 residual lines (those carrying the assistant tag and lacking the compact needle) all deep-parsed to `content = array` - a different key order, not a different shape - and none was a bare string. A separate full strict parse of one project directory independently gave 37374 assistant records with 0 bare-string content.",
          "rule": "One count per raw line for the byte pass; one count per parsed assistant record for the deep pass. An assistant record counts as string-shaped iff `message.content` deserialises to a JSON string.",
          "note": "Zero bare-string assistant bodies in 429501 assistant records across 7579 transcripts and 32 distinct build stamps (2.1.150 through 2.1.258). The claim is as strong as an on-disk negative can be here. The tolerant arm in src/model/exchange.rs 357-360 is verbatim at the claimed lines and is currently dead on this corpus, which is exactly its purpose - it exists so the surprise surfaces as content rather than as a blank."
        }
      ]
    },
    {
      "id": "REC-010",
      "area": "record-model",
      "behavior": "Content blocks are internally tagged on `type` and the set is open and considerably wider than the six named. Observed on disk in one project directory: tool_use (id, name, input, and a `caller` field valued exactly {\"type\":\"direct\"} on 17363 of 17363), tool_result (tool_use_id, content, and is_error on only 68.9%), thinking (thinking plus a signature that was present on 11674 of 11674 blocks here, so optional in the schema but universal in practice), text, image (source), and `fallback` - a model-fallback marker {\"type\":\"fallback\",\"from\":{\"model\":...},\"to\":{\"model\":...}} the claim omits entirely. `redacted_thinking` is registered by the harness and carries an opaque `data` payload but was never written on this machine (0 blocks corpus-wide). The 2.1.258 binary registers 22 block types, so 16 of them are unobserved here.",
      "depends": "csift's `Block` enum carries a `#[serde(other)] Unknown` arm so an unmodeled future block type parses instead of failing the whole line; losing it turns one new block type into a malformed-line count across every command.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "340",
          "snippet": "    Thinking {"
        },
        {
          "path": "src/model/record.rs",
          "lines": "362-367",
          "snippet": "    ToolResult {\n        #[serde(default, rename = \"tool_use_id\")]\n        tool_use_id: Option<String>,\n        /// String OR array of {type:text,text}/{type:image} - keep raw.\n        #[serde(default)]\n        content: Option<serde_json::Value>,"
        },
        {
          "path": "src/model/narration.rs",
          "lines": "51-54",
          "snippet": "pub(crate) fn thinking_block_class(signature: Option<&str>) -> Class {\n    let Some(sig) = signature else {\n        return Class::AgentThinking;\n    };"
        }
      ],
      "instrument": "`rg -o '\"type\":\"(text|thinking|redacted_thinking|tool_use|tool_result|image)\"' <transcript> | sort | uniq -c` enumerates the live set, and `csift search '' -t agent --count-by label` must account for each. Counting rule: one per block occurrence in raw bytes for the rg, one per record for the census.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "SPEC.md section 3.5; AGENTS.md section 3.2; src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) strict-JSON parse of all 408 transcripts under one project directory, censusing every content block by its `type` and every key within each block type; (b) `csift search 'redacted thinking' -t agent.thinking --raw` over the whole corpus, piped to `grep -c '\\\"type\\\":\\\"redacted_thinking\\\"'` to separate real blocks from prose mentions; (c) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\\{[a-z_]+:!0,(?:[a-z_]+:!0,){4,}[a-z_]+:!0\\}'`.",
          "observed": "Block census: tool_use 17363 (type/id/name/input/caller all 100%), tool_result 17352 (tool_use_id 100%, content 100%, is_error 68.9%), thinking 11674 (type/thinking/signature all 100%), text 8509 (type/text 100%), image 46 (type/source 100%), and one block of a type the claim does not list: fallback 1, shaped {\"type\":\"fallback\",\"from\":{\"model\":\"<model-id>\"},\"to\":{\"model\":\"<model-id>\"}}. Every one of the 17363 tool_use blocks carried caller exactly {\"type\": \"direct\"}. Corpus-wide, 0 real `redacted_thinking` blocks exist (the 37 records matching the phrase under -t agent.thinking were prose in development sessions; grep over their raw lines found 0 occurrences of the block type). The 2.1.258 binary registers 22 block types: '{advisor_tool_result:!0,bash_code_execution_tool_result:!0,code_execution_tool_result:!0,compaction:!0,container_upload:!0,document:!0,fallback:!0,image:!0,mcp_tool_result:!0,mcp_tool_use:!0,mid_conv_system:!0,redacted_thinking:!0,search_result:!0,server_tool_use:!0,text:!0,text_editor_code_execution_tool_result:!0,thinking:!0,tool_result:!0,tool_search_tool_result:!0,tool_use:!0,web_fetch_tool_result:!0,web_search_tool_result:!0}'. The binary also confirms the redacted payload field: 'if(e.type===\"redacted_thinking\")return Wc(e.data,n)'.",
          "rule": "One count per block occurrence inside a parsed `message.content` array, per file; key percentages are over blocks of that type. Binary side: one entry per key of the block-type registry.",
          "note": "The `#[serde(other)] Unknown` arm the depends clause names is at src/model/record.rs 369 and is load-bearing right now, not hypothetically: the single `fallback` block on disk is already an unmodeled type, and 16 further registered types (document, search_result, server_tool_use, mcp_tool_use, mcp_tool_result, container_upload, compaction, mid_conv_system and the five *_tool_result execution families) would each become a malformed-line count without it. All three code sites (src/model/record.rs 333, src/model/record.rs 355-360, src/model/narration.rs 51-54) are verbatim at the claimed lines."
        }
      ]
    },
    {
      "id": "REC-011",
      "area": "record-model",
      "behavior": "A `tool_result` block's `content` is either a plain STRING or an ARRAY containing `{type:\"text\",text}`, `{type:\"image\",...}` and `{type:\"tool_reference\", tool_name}` entries - the last emitted by ToolSearch results.",
      "depends": "csift keeps `tool_result.content` as a raw `Value` and inspects it on demand; a `String`-typed field would drop every array-form tool result, which is where deferred-tool schemas and image results live.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "362-367",
          "snippet": "    ToolResult {\n        #[serde(default, rename = \"tool_use_id\")]\n        tool_use_id: Option<String>,\n        /// String OR array of {type:text,text}/{type:image} - keep raw.\n        #[serde(default)]\n        content: Option<serde_json::Value>,"
        }
      ],
      "instrument": "`rg -c '\"type\":\"tool_reference\"' ~/.claude/projects/**/*.jsonl` in a session that used ToolSearch, then `csift show @<id> --line <that line>` must render without error; `jq -r '.message.content[]? | select(.type==\"tool_result\") | (.content|type)' <file> | sort | uniq -c` gives the string-vs-array split. Counting rule: one per raw occurrence.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 3.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, collections, glob\nshape = collections.Counter(); inner = collections.Counter()\nfiles = [f for f in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in f]\nfor f in files:\n    for line in open(f, errors='replace'):\n        line = line.strip()\n        if not line: continue\n        try: r = json.loads(line)\n        except Exception: continue\n        m = r.get('message')\n        if not isinstance(m, dict): continue\n        ct = m.get('content')\n        if not isinstance(ct, list): continue\n        for b in ct:\n            if isinstance(b, dict) and b.get('type') == 'tool_result':\n                cc = b.get('content')\n                shape[type(cc).__name__] += 1\n                if isinstance(cc, list):\n                    for e in cc:\n                        if isinstance(e, dict): inner[str(e.get('type'))] += 1\nprint(dict(shape), dict(inner))\nPY\n# run over two separate project dirs; plus:\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'tool_reference' | head -3",
          "observed": "project dir A (414 transcripts): content shapes str=16938, list=348; array element types text=257, tool_reference=137. project dir B (7 transcripts): str=11877, list=392; element types text=251, tool_reference=120, image=71. A verbatim array element: {\"type\": \"tool_reference\", \"tool_name\": \"EnterPlanMode\"}. Binary 2.1.258 contains the literal string `tool_reference`. Code site src/model/record.rs:355-360 present verbatim, including the doc line `/// String OR array of {type:text,text}/{type:image} - keep raw.`",
          "rule": "One count per tool_result block, reached as record -> message.content[] -> block.type=='tool_result', keyed by the JSON type of that block's own `content` (str vs list). Array element counts are one per element, keyed by element `type`. Whole project dir, every *.jsonl including subagent transcripts, journal.jsonl excluded. Two project dirs scanned separately.",
          "note": "All three element shapes the claim names are present: text and tool_reference in both dirs, image in the second. tool_reference carries exactly the `tool_name` key the claim states. The string form dominates (16938:348 and 11877:392), so a String-typed field would have dropped ~2% of tool results including every deferred-tool schema and every image result."
        }
      ]
    },
    {
      "id": "REC-012",
      "area": "record-model",
      "behavior": "A thinking block can arrive as `redacted_thinking`: Claude Code writes `\"type\":\"redacted_thinking\"` with no readable `thinking` text, only an opaque `data` payload.",
      "depends": "csift classifies it `agent.thinking` and emits the constant placeholder `[redacted thinking]` as the hit text; the placeholder is synthesized but stays prefilter-sound only because both of its words occur verbatim in the raw bytes of `\"type\":\"redacted_thinking\"` and any bracketed or whitespaced pattern is prefilter-ineligible anyway.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "346-348",
          "snippet": "    /// A `redacted_thinking` block - encrypted/opaque reasoning CC emits in place of a visible\n    /// `thinking` block (no readable text, only an opaque `data` payload). Classified\n    /// `agent.thinking` exactly like a normal thinking block"
        },
        {
          "path": "src/search/types.rs",
          "lines": "17",
          "snippet": "pub(crate) const REDACTED_THINKING_PLACEHOLDER"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "64-65",
          "snippet": "    /// `agent.thinking` - a thinking block (see the GOLD-gap note re `redacted_thinking`).\n    AgentThinking,"
        }
      ],
      "instrument": "`rg -cNI '\"redacted_thinking\"' ~/.claude/projects -g '*.jsonl'` then `csift search 'redacted' @<id> -t agent.thinking`; counting rule = per record. A zero corpus count means the block type is absent locally, not that the arm is wrong - it is exercised by a synthetic fixture and the e2e taxonomy tests pin the placeholder render.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "src/model/record.rs comment; AGENTS.md section 3.3a (redacted_thinking mapping); dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'redacted_thinking'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '\"redacted_thinking\"[^\"]{0,40}' | sort -u | head -20\ncd ~/.claude/projects && for d in */; do n=$(rg -cNI --no-filename '\"redacted_thinking\"' -g '*.jsonl' \"$d\" | awk '{s+=$1} END{print s+0}'); echo \"$n  $d\"; done",
          "observed": "Binary 2.1.258 carries the block schema `\"redacted_thinking\"),data:i()})` (a `data` field and no `thinking` field), the reader `\"redacted_thinking\":return d.inputTextCharLength+=f.data?.l`, the bare 17-byte string `redacted_thinking`, and the guard regex '`?(thinking|redacted_thinking)`?\\s+(or\\s+`?redacted_thinking`?\\s+)?blocks?\\s+.{0,60}cannot be modified'. Corpus: 0 matching lines in each of 14 project dirs (14 separate scans, all zero). Code sites present verbatim: src/model/record.rs:339 doc line, src/model/record.rs:343 `RedactedThinking {`, src/search/types.rs:17 `pub(crate) const REDACTED_THINKING_PLACEHOLDER: &str = \"[redacted thinking]\";`, src/model/taxonomy.rs:64-65 (`AgentThinking,` under the redacted_thinking doc note).",
          "rule": "Binary: one count per distinct `strings -n 6` line containing the token. Corpus: one count per raw jsonl line containing the quoted token `\"redacted_thinking\"`, summed within a project dir, each of the 14 project dirs scanned separately.",
          "note": "The block type is live in CC 2.1.258 and the binary settles the payload shape the claim asserts: the serialized block is `{type:\"redacted_thinking\", data: <opaque>}` with no `thinking` text, and a separate reader measures its length off `.data`. Zero occurrences in this machine's corpus is the claim's own stated expectation, not a refutation; the csift arm is exercised by fixture instead. The prefilter-soundness sub-argument checks out by inspection: the raw bytes `\"type\":\"redacted_thinking\"` contain both the substrings `redacted` and `thinking`, and the bracketed, space-bearing placeholder is prefilter-ineligible under csift's own whitespace rule."
        }
      ]
    },
    {
      "id": "REC-013",
      "area": "record-model",
      "behavior": "`tool_result` blocks live inside a `type:\"user\"` CARRIER record's `message.content` array and never on an assistant record (46,916/46,916 measured). Every block carries its own `tool_use_id`; the carrier also repeats the join structurally as a top-level `toolUseResult` and `sourceToolAssistantUUID`, but no measured carrier depends on those to be joinable.",
      "depends": "csift joins `tool_result.tool_use_id == tool_use.id` for the `agent.tool.use` / `agent.tool.result` pairing marker and the `pairing` JSON enum. csift implements NO fallback to `toolUseResult` or `sourceToolAssistantUUID`; a block-level `tool_use_id` is the only join key, and a result whose use is out of scope renders as `orphan`.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "122-124",
          "snippet": "    /// tree on demand via [`Record::tool_use_result_value`].\n    #[serde(default, rename = \"toolUseResult\")]\n    pub tool_use_result: Option<Box<serde_json::value::RawValue>>,"
        }
      ],
      "instrument": "`csift search '' -t agent.tool.result --count-by pairing --format json` - the `orphan` bucket counts results whose use is out of scope or unjoinable. Counting rule: one per matched record, each counted once on the pairing axis.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 4.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, collections, glob\nby = collections.Counter(); noid = 0; tot = 0\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n    for line in open(f, errors='replace'):\n        line = line.strip()\n        if not line: continue\n        try: r = json.loads(line)\n        except Exception: continue\n        m = r.get('message')\n        if not isinstance(m, dict): continue\n        ct = m.get('content')\n        if not isinstance(ct, list): continue\n        for b in ct:\n            if isinstance(b, dict) and b.get('type') == 'tool_result':\n                tot += 1; by[(r.get('type'), m.get('role'))] += 1\n                if not b.get('tool_use_id'): noid += 1\nprint(tot, dict(by), 'missing tool_use_id:', noid)\nPY\ncsift search '' @<session> -t agent.tool.result --count-by pairing\ncd <csift repo> && rg -n 'sourceToolAssistantUUID|source_tool_assistant' src/",
          "observed": "46,916 tool_result blocks across 6 project dirs (5 dirs scanned whole, plus the 4 oldest sessions of a sixth). 46,916/46,916 sit on records with top-level type \"user\" AND message.role \"user\"; 0 on any assistant record. 0/46,916 blocks lacked a block-level `tool_use_id` - including the 4 oldest sessions, whose `version` stamps span 2.1.170 to 2.1.187. `sourceToolAssistantUUID` IS present as a carrier field (17,353 lines in one project dir). csift pairing census on one session: 6944 paired, 0 orphan, 0 pending. `rg -n 'sourceToolAssistantUUID|source_tool_assistant' src/` returns NO matches - csift implements no such fallback. Code site src/model/record.rs:115-117 present verbatim.",
          "rule": "One count per tool_result block (record -> message.content[] -> block.type=='tool_result'), keyed by the pair (record top-level `type`, message `role`); a block is 'missing tool_use_id' when the block-level key is absent or falsey. Whole project dirs, journal.jsonl excluded. The pairing census counts each matched record once on the pairing axis.",
          "note": "The carrier-placement half of the claim holds with a strong count. The 'some legacy carriers omit the block-level tool_use_id' half is refuted for every build this corpus reaches (0 of 46,916, back to CC 2.1.170) and, more decisively, the fallback the claim's `depends` credits csift with does not exist in the source. What would decide the legacy half: a transcript written by a Claude Code build older than 2.1.170, which this machine has none of."
        }
      ]
    },
    {
      "id": "REC-014",
      "area": "record-model",
      "behavior": "`toolUseResult` is a structured echo on tool-result carriers that routinely embeds the whole file body or stdout a tool returned (plus structured patches and persisted-output metadata). Measured over four transcripts it is 7-18% of ALL candidate-line bytes (0.074, 0.111, 0.171, 0.178) and 54-60% of the bytes of the carrier lines that actually have one (0.538, 0.545, 0.575, 0.598).",
      "depends": "csift keeps `toolUseResult` and `attachment` UNPARSED as `Box<RawValue>` and reads the small fields the hot paths need through a cheap typed probe, building a `Value` tree only for the deep consumers (`files`, `recover`, `image`); reverting either to an eager `Value` regresses the performance contract on 200MB+ transcripts.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "115-119",
          "snippet": "    /// Structured echo on tool-result carriers (§4.6). Kept as UNPARSED raw JSON text\n    /// (`Box<RawValue>`) rather than a built `Value` tree: this blob routinely carries\n    /// the full file/output content a tool returned (≈20-25% of a candidate line's\n    /// bytes), and eagerly tree-building it for EVERY carrier dominated the parse cost\n    /// of every scanning subcommand."
        }
      ],
      "instrument": "Sum the `toolUseResult` byte length over a transcript's candidate lines and divide by total candidate bytes (jq over `csift search '' @<id> --raw`); counting rule = bytes of the `toolUseResult` value versus bytes of the whole line, over candidate (role-bearing) lines only. Then benchmark `csift search <literal> <project-dir>` with hyperfine before and after any change to the raw-field handling.",
      "located": {
        "claude_code": null,
        "csift": "0.6.0",
        "source": "SPEC.md section 7c; src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json\ndec = json.JSONDecoder()\nfor f in [<four transcripts, one per project dir>]:\n    cand_bytes = tur_bytes = carr_bytes = cand = carr = 0\n    for line in open(f, errors='replace'):\n        s = line.rstrip('\\n')\n        if not s: continue\n        role = ('\"role\":\"user\"' in s) or ('\"role\":\"assistant\"' in s)\n        if role: cand += 1; cand_bytes += len(s.encode())\n        i = s.find('\"toolUseResult\":')\n        if i < 0: continue\n        carr += 1; carr_bytes += len(s.encode())\n        start = i + len('\"toolUseResult\":')\n        _, end = dec.raw_decode(s, start)\n        tur_bytes += len(s[start:end].encode())\n    print(f, cand, cand_bytes, carr, carr_bytes, tur_bytes)\nPY",
          "observed": "Four transcripts, one per project dir. Share of CANDIDATE (role-bearing) line bytes: 0.1776 (cand=10,640 lines / 53,999,530 B; toolUseResult=9,588,461 B), 0.1107 (30,774 / 290,092,392 B; 32,119,165 B), 0.1712 (18,505 / 58,279,708 B; 9,977,525 B), 0.0741 (1,849 / 35,379,227 B; 2,621,115 B). Share of CARRIER-line bytes only (lines that actually carry a toolUseResult): 0.5745, 0.5378, 0.5445, 0.5980. Carriers per file: 3,361 / 7,563 / 5,903 / 551. Code sites present verbatim: src/model/record.rs:108-112 (the Box<RawValue> rationale doc, which itself states the disputed \"~20-25% of a candidate line's bytes\") and src/model/predicates.rs:358-360 (the tur_probe doc).",
          "rule": "The toolUseResult VALUE span is measured exactly, not estimated: locate the byte offset of the literal key `\"toolUseResult\":` in the raw line, then `json.JSONDecoder().raw_decode` from just past it to get the value's end offset; the span is UTF-8 byte length. Denominator A = summed UTF-8 byte length of every candidate line, where candidate = csift's stage-1 keep (the raw line contains a user or assistant role marker). Denominator B = summed byte length of only those lines that carry a toolUseResult. Whole file, one file per project dir.",
          "note": "The design rationale is unaffected and confirmed - the blob is genuinely huge (54-60% of the bytes of any line that carries one) and keeping it as Box<RawValue> is what avoids paying for it. Only the headline percentage is wrong: 20-25% matches neither natural counting rule. The claimed figure sits between the two measured denominators, which is consistent with it having been produced by a third, unrecorded rule; the correction states both rules explicitly so the number is reproducible."
        }
      ]
    },
    {
      "id": "REC-015",
      "area": "record-model",
      "behavior": "When a tool output is too large Claude Code replaces the inline `tool_result.content` with a `<persisted-output>` block reading `Output too large (<human size>). Full output saved to: <ABSOLUTE_PATH>` plus `Preview (first <human size>):` - both sizes are rendered by a byte-formatting function, observed as `(81.1KB)` and `(2KB)`, not a literal `NNN KB`. It writes the real bytes to the session sidecar `<ENCODED>/<session-uuid>/tool-results/<id>.txt`, where `<id>` is either a short tool id or, for hook output, `hook-<tool_use_id>-<n>-additionalContext` (the API `toolu_...` tool_use id, not a session uuid) - 166 of 1,863 sidecar files corpus-wide take the hook form. It ALSO records the location structurally as `toolUseResult.persistedOutputPath` with `persistedOutputSize` in bytes alongside (100/100 paired in the measured project dir).",
      "depends": "`search --resolve-persisted` prefers the structured path (exact, no regex) and falls back to scraping the inline `Full output saved to:` marker, substituting the file content BEFORE matching; a read failure is non-fatal and appends an explicit note. Without the resolution the searchable text is only the 2 KB preview.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "313-320",
          "snippet": "    /// The persisted-output file path for this carrier (§4.6), preferring the\n    /// structured `toolUseResult.persistedOutputPath` (exact - no regex) and falling\n    /// back to scraping the inline `Full output saved to: <path>` marker from a\n    /// `tool_result` block. Returns `None` when there is no persisted pointer.\n    #[must_use]\n    pub fn persisted_output_path(&self) -> Option<String> {\n        // Structured field first (SPEC §4.6 resolution rule).\n        if let Some(probe) = self.tur_probe() {"
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "249-256",
          "snippet": "/// Scrape the inline persisted-output pointer (§4.6 fallback): the line\n/// `Full output saved to: <ABSOLUTE_PATH>` inside a `<persisted-output>` block.\n/// Returns the trimmed path, or `None` if the marker is absent.\npub(crate) fn scrape_persisted_path(text: &str) -> Option<String> {\n    const MARKER: &str = \"Full output saved to:\";\n    let idx = text.find(MARKER)?;\n    let rest = &text[idx + MARKER.len()..];\n    // The path runs to end-of-line."
        },
        {
          "path": "src/model/record.rs",
          "lines": "306-307",
          "snippet": "    #[serde(rename = \"persistedOutputPath\")]\n    pub(crate) persisted_output_path: Option<serde_json::Value>,"
        }
      ],
      "instrument": "`rg -c 'persistedOutputPath' ~/.claude/projects/**/*.jsonl` and `ls ~/.claude/projects/<enc>/<uuid>/tool-results/ | head`, then `csift search '<a needle only in the persisted file>' @<id> --resolve-persisted` must match while the same search without the flag must not. Counting rule: matched records with and without the flag; one pointer per externalised tool result.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "SPEC.md section 4.6; AGENTS.md section 3.6; src/model/exchange.rs resolve doc; SPEC.md section 4.6 as consumed by section 6.2; src/model/exchange.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\nkeys = collections.Counter(); forms = collections.Counter()\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n    for line in open(f, errors='replace'):\n        if 'persistedOutput' not in line: continue\n        t = (json.loads(line).get('toolUseResult') or {})\n        if not isinstance(t, dict) or not t.get('persistedOutputPath'): continue\n        for k in t:\n            if 'persisted' in k.lower(): keys[k] += 1\n        base = t['persistedOutputPath'].rsplit('/', 1)[-1]\n        forms['hook-form' if base.startswith('hook-') else 'short-id-form'] += 1\nprint(dict(keys), dict(forms))\nPY\ncd ~/.claude/projects && find . -path '*/tool-results/*' -name '*.txt' | sed 's|.*/||' > /tmp/n.txt; wc -l < /tmp/n.txt; rg -c '^hook-' /tmp/n.txt\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Output too large \\(\\$\\{[^}]*\\}[^\"`]{0,60}|Preview \\(first[^\"`]{0,40}'\ncsift search '<a token present only in the persisted file>' @<session>            # -> matched 1 exchange\ncsift search '<same token>' @<session> --resolve-persisted                        # -> matched 3 exchanges",
          "observed": "1,863 tool-results/*.txt files corpus-wide; 166 of them named `hook-toolu_<id>-<n>-additionalContext.txt`, the rest short tool ids like `b0425lu0i.txt`. In one project dir, 100/100 records carrying `persistedOutputPath` also carry `persistedOutputSize` (a byte count, e.g. 83070); the path shape is ~/.claude/projects/<ENCODED>/<session-uuid>/tool-results/<id>.txt. A verbatim inline marker: `<persisted-output>\\nOutput too large (81.1KB). Full output saved to: <ABS_PATH>\\n\\nPreview (first 2KB):`. Binary 2.1.258 templates: `Output too large (${Ut(n.originalSize)}). Full output saved to: ${n.filepath}` and `Preview (first ${Ut(pNe)}):`. Round trip: the same search returned 'matched 1 exchange' bare and 'matched 3 exchanges' with --resolve-persisted. Code sites present verbatim: src/model/exchange.rs:313-320, src/model/grouping.rs:249-256, src/model/record.rs:299-300.",
          "rule": "One pointer per externalised tool result: a record whose `toolUseResult` object has a non-empty `persistedOutputPath`; the field pairing is counted per such record. Sidecar file counts are one per *.txt under any `tool-results/` directory, corpus-wide, bucketed by whether the basename starts with `hook-`. The round trip counts matched exchanges for one literal token chosen because it occurs in the persisted file beyond byte 200,000 and nowhere in the transcript itself.",
          "note": "Every structural assertion holds; two surface details needed correcting. The `<id>` hook form embeds an API tool_use id, not a uuid, and the marker's size is a formatted human size rather than the `NNN KB` shape the claim spelled out. The --resolve-persisted round trip is the strongest part: the needle exists only past byte 200,000 of the sidecar file, well beyond the 2 KB preview, and only the flagged run finds it."
        }
      ]
    },
    {
      "id": "REC-016",
      "area": "record-model",
      "behavior": "`attachment` is a TOP-LEVEL sibling of `message`, not a content block, and each payload carries its own `type`. 28 distinct payload types were observed in a single project dir, including every value the claim names except the auto_mode pair: hook_success, hook_additional_context, edited_text_file, `file` snapshots, compact_file_reference, plan_mode (and plan_mode_exit), queued_command, task_reminder, date_change, deferred_tools_delta and skill_listing, plus total_tokens_reminder, batching_reminder_sent, bash_output_audience_note, hook_cancelled, ultrathink_effort, silent_turn_reminder, remote_session_change, agent_listing_delta, command_permissions, read_truncation_notice, plan_file_reference, hook_system_message, workflow_keyword_request, invoked_skills, hook_blocking_error and task_status. The steering configs `auto_mode` / `auto_mode_exit` / `auto_mode_scan` are defined in Claude Code 2.1.258 but absent from this corpus; `auto_mode` carries `{autoModeConsentFlow, bashFirst, steerOnly, bypass}` and `auto_mode_exit` carries only `{bashFirst, steerOnly}`.",
      "depends": "`search --attachments` (and `--count-by attachment`, which implies the gate) makes them searchable with the verbatim payload JSON as matchable text under `harness.meta.attachment`, and `plan`'s binding read and `recover`'s external-edit reader parse the top-level field directly; steering PROSE is often not persisted while its config attachment is, so a zero-match for a steering keyword proves nothing without the gate.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "126-129",
          "snippet": "    /// Top-level `attachment` payload (a sibling of `message`, not a content block).\n    /// Real records carry attachments for hook output, `edited_text_file` external\n    /// edits, `file` snapshots, etc. Kept as UNPARSED raw JSON text (same rationale as\n    /// `tool_use_result` - attachments embed whole file snapshots)"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "306-310",
          "snippet": "    pub fn attachment_type(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }\n        let v = self.attachment_value()?;"
        }
      ],
      "instrument": "`csift search '' @<session> --count-by attachment` lists every payload `type` with its record count; counting rule = one count per `type:\"attachment\"` record, keyed by `attachment.type`.",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "src/model/record.rs comment; CHANGELOG 0.8.1; SKILL.md wrong-assumption row on rollout/steering keywords"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\natt = collections.Counter(); sibling = collections.Counter()\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n    for line in open(f, errors='replace'):\n        line = line.strip()\n        if not line: continue\n        try: r = json.loads(line)\n        except Exception: continue\n        if 'attachment' not in r: continue\n        a = r['attachment']\n        att[a.get('type') if isinstance(a, dict) else type(a).__name__] += 1\n        sibling['has_message=' + str('message' in r)] += 1\n        sibling['top_level_type_is_attachment=' + str(r.get('type') == 'attachment')] += 1\nprint(len(att), att.most_common(), dict(sibling))\nPY\ncsift search '' @<session> --count-by attachment\ncd ~/.claude/projects && for d in <4 project dirs>; do rg -cNI --no-filename '\"type\":\"auto_mode' -g '*.jsonl' \"$d\" | awk '{s+=$1} END{print s+0}'; done\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'type:\"auto_mode[^,}]{0,10}[^\"]{0,80}' | sort -u\ncsift search 'total_tokens_reminder' @<session> -c                                        # -> 20\ncsift search 'total_tokens_reminder' @<session> --attachments -t harness.meta.attachment -c # -> 148",
          "observed": "61,181 records carrying a top-level `attachment` in one project dir; 61,181/61,181 have top-level `type:\"attachment\"` and 61,181/61,181 have NO `message` key, confirming attachment is a top-level sibling rather than a content block. 28 distinct payload `type` values in that dir: hook_success 45448, hook_additional_context 9909, total_tokens_reminder 2627, task_reminder 638, skill_listing 545, batching_reminder_sent 488, deferred_tools_delta 460, queued_command 237, bash_output_audience_note 224, edited_text_file 112, hook_cancelled 70, ultrathink_effort 62, silent_turn_reminder 58, remote_session_change 57, file 45, agent_listing_delta 41, compact_file_reference 39, date_change 34, command_permissions 25, read_truncation_notice 22, plan_file_reference 15, plan_mode 6, plan_mode_exit 6, hook_system_message 4, workflow_keyword_request 4, invoked_skills 3, hook_blocking_error 1, task_status 1. `auto_mode` / `auto_mode_exit`: 0 lines in each of 4 project dirs, but present in binary 2.1.258 as `type:\"auto_mode\",autoModeConsentFlow:!o&&GKe(n),bashFirst:_,steerOnly:d,bypass:o}` and `type:\"auto_mode_exit\",bashFirst:r.bashFirst,steerOnly:r.steerOnly}`, alongside a third value `type:\"auto_mode_scan\"`. Gate round trip on one literal: 20 matches bare, 148 with `--attachments -t harness.meta.attachment`. Code sites present verbatim: src/model/record.rs:119-122, src/model/predicates.rs:306-310.",
          "rule": "One count per raw jsonl line carrying a top-level `attachment` key, keyed by `attachment.type`; whole project dir, subagent transcripts included, journal.jsonl excluded. The sibling check counts the same lines by whether they also carry a top-level `message` key. The gate round trip counts matched records (`-c`) for one literal with and without the attachment gate, same session, same pattern.",
          "note": "The structural core is confirmed exactly. Two corrections: the auto_mode payload shape the claim gives is incomplete (it omits `autoModeConsentFlow`, and `auto_mode_exit` has no `bypass`), and the observed payload set is materially wider than the claim's list - 28 types in one project dir, so the enumeration should read as open. The `--attachments` searchability the `depends` asserts is measured working: the same literal goes from 20 to 148 matched records once the gate admits attachment lines."
        }
      ]
    },
    {
      "id": "REC-017",
      "area": "record-model",
      "behavior": "`attachment` is the DOMINANT record type - 54.3% of all lines in the largest sampled file (49,902 of 91,975) and 57.7% in the next one sampled - across 29 payload subtypes in that largest file. The bulk is `hook_success` (76.6% of its attachment records), which is hook output for EVERY hook event, dominated by PostToolUse (31,973) and PreToolUse (10,608); SessionStart is a small minority (231 of 45,448 in the project dir censused).",
      "depends": "csift's stage-1 prefilter drops attachment lines pre-JSON unless a gate admits them (`--additional-context`, `--attachments`, `--count-by attachment`, or an explicit `show` address); admitting them by default doubles scan cost and floods every search.",
      "code": [
        {
          "path": "src/search/scan.rs",
          "lines": "333-338",
          "snippet": "/// §7d stage-1 category prefilter on raw bytes: keep a line only if it could be a\n/// transcript message (user/assistant role marker) - drops `attachment`,\n/// `file-history-*`, `queue-operation`, and metadata noise pre-JSON unless the\n/// matching [`CandidateGates`] flag admits them. Kept deliberately permissive\n/// (substring, not structural) so no genuine turn is lost.\npub(crate) fn line_is_transcript_candidate(line: &[u8], gates: &CandidateGates) -> bool {"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "356-357",
          "snippet": "    static ATTACHMENT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"attachment\\\"\"));"
        }
      ],
      "instrument": "`csift stats @<id> --format json | jq '.line_types'` gives the whole-file census, and `rg -o '\"attachment\":\\{\"type\":\"[a-z_]+\"' <transcript> | sort | uniq -c | sort -rn` enumerates subtypes. Counting rule: attachment lines over all lines in one file for the 54% figure, one count per raw line.",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "SPEC.md section 4.8"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json, collections\nfor f in [<the two largest transcripts>]:\n    types = collections.Counter(); att = collections.Counter(); tot = 0\n    for line in open(f, errors='replace'):\n        line = line.strip()\n        if not line: continue\n        tot += 1\n        try: r = json.loads(line)\n        except Exception: types['__malformed__'] += 1; continue\n        types[str(r.get('type'))] += 1\n        a = r.get('attachment')\n        if isinstance(a, dict): att[a.get('type')] += 1\n    print(tot, types['attachment'], 100*types['attachment']/tot, len(att), att.most_common(8))\nPY\ncsift stats @<session> --format json --no-subagents | jq '.line_types'",
          "observed": "Largest transcript: 91,975 lines, 49,902 with `type:\"attachment\"` = 54.3%, 29 distinct payload subtypes; within its attachments hook_success 38,225 (76.6%), hook_additional_context 7,559 (15.1%), total_tokens_reminder 2,591 (5.2%), task_reminder 657 (1.3%). Second-largest sampled transcript: 47,602 lines, 27,454 attachment = 57.7%, 24 subtypes. `csift stats --format json` line_types on that second file: attachment 27454 of lines 47602, skipped_lines 0. hookEvent census over one project dir's 45,448 hook_success records: PostToolUse 31,973, PreToolUse 10,608, Stop 2,087, SubagentStart 335, SessionStart 231, PostToolUseFailure 113, SubagentStop 100, UserPromptSubmit 1. Code sites present verbatim: src/search/scan.rs:322-327 (line_is_transcript_candidate doc) and src/search/scan.rs:345-346 (ATTACHMENT_FINDER).",
          "rule": "One count per raw jsonl line, whole file, keyed by top-level `type`; the percentage is attachment lines over all non-blank lines of that one file. Subtype count = distinct `attachment.type` values in the same file. The hookEvent split counts one per hook_success attachment record, keyed by `attachment.hookEvent`, over a whole project dir.",
          "note": "The headline 54% is exact for the largest file. Two corrections: the subtype count is 29 in that file, not roughly 21, and the claim's attribution of the hook_success bulk to SessionStart output is wrong by two orders of magnitude - PostToolUse supplies 70% of hook_success records while SessionStart supplies 0.5%. The prefilter rationale is unaffected and independently confirmed by the REC-016 and REC-018 gate round trips, where the same literal's match count jumps once attachment lines are admitted."
        }
      ]
    },
    {
      "id": "REC-018",
      "area": "record-model",
      "behavior": "Exactly ONE `attachment` payload shape carries hook-injected conversation context: `{\"type\":\"hook_additional_context\",\"content\":[...]}` - the text a SessionStart / UserPromptSubmit / other hook injected - where `content` is a string ARRAY in real data (one element per injected block; a bare string is tolerated), carried alongside `hookName` / `hookEvent`, and the record itself has `uuid`, `timestamp` and `parentUuid`.",
      "depends": "csift joins the array with a newline in `Record::hook_additional_context_text`, classifies the record `harness.meta.hook`, and scans it only under `search --additional-context` (or the `--attachments` superset) with the candidate needle `&&`-gated so a default scan never parses attachment lines; `show --line`/`--uuid` renders it flag-free under the refetch law. A bare-string-only reader returns nothing on real data.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "329-335",
          "snippet": "\n    /// Hook-injected `additionalContext` text: a `type:\"attachment\"` record whose payload is\n    /// `{\"type\":\"hook_additional_context\",\"content\":[…],…}` - the context a SessionStart /\n    /// UserPromptSubmit / … hook injected into the turn. `content` is a string ARRAY in real\n    /// data (one element per injected block; joined with `\\n`), tolerated as a bare string.\n    /// `None` for every other record shape. Cheap for non-attachment records (one type\n    /// compare before any parse).\n    #[must_use]"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "336-343",
          "snippet": "    pub fn hook_additional_context_text(&self) -> Option<String> {\n        if !self.is_type(\"attachment\") {\n            return None;\n        }\n        let v = self.attachment_value()?;\n        let att = v.as_object()?;\n        if att.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"hook_additional_context\") {\n            return None;"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "348-352",
          "snippet": "                (!s.is_empty()).then(|| s.to_string())\n            }\n            serde_json::Value::Array(parts) => {\n                let texts: Vec<&str> = parts.iter().filter_map(serde_json::Value::as_str).collect();\n                (!texts.is_empty()).then(|| texts.join(\"\\n\"))"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "350-351",
          "snippet": "    static HOOK_CONTEXT_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memmem::Finder::new(b\"hook_additional_context\"));"
        },
        {
          "path": "src/cli/search_args.rs",
          "lines": "493",
          "snippet": "    /// Also scan hook-injected `additionalContext`: the `attachment` records a SessionStart /"
        }
      ],
      "instrument": "`jq -r 'select(.type==\"attachment\") | .attachment.type' <transcript> | sort | uniq -c` must show `hook_additional_context` and `jq '.attachment.content | type'` on one such line must print `array`; then `csift search '<a string your hook injects>' @<session> --additional-context` must match while the same search without the flag must not. Counting rule: one attachment record per hook injection.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.7.6",
        "source": "AGENTS.md section 3.2; CHANGELOG 0.7.6; src/model/predicates.rs hook_additional_context_text doc; SPEC.md section 4.8; SPEC.md section 6 v0.7.6 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\nctype = collections.Counter(); keys = collections.Counter()\ntop = collections.Counter(); ev = collections.Counter(); alen = collections.Counter(); n = 0\nfor f in [x for x in glob.glob('**/*.jsonl', recursive=True) if 'journal.jsonl' not in x]:\n    for line in open(f, errors='replace'):\n        if 'hook_additional_context' not in line: continue\n        try: r = json.loads(line)\n        except Exception: continue\n        a = r.get('attachment')\n        if not isinstance(a, dict) or a.get('type') != 'hook_additional_context': continue\n        n += 1; c = a.get('content'); ctype[type(c).__name__] += 1\n        if isinstance(c, list):\n            alen[len(c)] += 1\n            for e in c: ctype['elem:' + type(e).__name__] += 1\n        for k in a: keys[k] += 1\n        for k in ('uuid', 'timestamp', 'parentUuid'): top[k + '=' + str(k in r)] += 1\n        ev[str(a.get('hookEvent'))] += 1\nprint(n, dict(ctype), dict(alen.most_common(6)), dict(keys), dict(top), dict(ev))\nPY\ncsift search 'Background operation detected' @<session> -c                        # -> 3\ncsift search 'Background operation detected' @<session> --additional-context -c   # -> 94",
          "observed": "9,909 hook_additional_context records in one project dir. `content` JSON type: list 9,909, str 0 - a string ARRAY in 100% of real records. 9,983 array elements, all of them strings; array length 1 in 9,890 records, then 5 (8), 6 (7), 2 (2), 4 (1), 3 (1). Payload keys, each present on all 9,909: `type`, `content`, `hookName`, `toolUseID`, `hookEvent`. Record top level: uuid present 9,909/9,909, timestamp 9,909/9,909, parentUuid 9,909/9,909. hookEvent values: PreToolUse 5,865, PostToolUse 3,180, SubagentStart 405, UserPromptSubmit 254, PostToolUseFailure 113, SubagentStop 48, SessionStart 44. Gate round trip on one injected literal: 3 matched records bare (its own echoes in shell-command records), 94 with `--additional-context`. Code sites present verbatim: src/model/predicates.rs:328-335 and 336-343 and 348-352, src/search/scan.rs:339-340 (HOOK_CONTEXT_FINDER, needle b\"hook_additional_context\"), src/cli/search_args.rs:488.",
          "rule": "One count per `type:\"attachment\"` record whose `attachment.type` is `hook_additional_context`, whole project dir including subagent transcripts, journal.jsonl excluded. `content` is keyed by its Python JSON type; element counts are one per array element. Presence counts are per record. The gate round trip counts matched records (`-c`) for one literal that a PostToolUse hook injects, same session and pattern, with and without --additional-context.",
          "note": "Every assertion measured true and the array-vs-string point is decisive: 9,909 of 9,909 use the array form and zero use a bare string, so a bare-string-only reader really would return nothing on real data. Two additions worth folding in later rather than corrections: the payload also carries `toolUseID` beside `hookName`/`hookEvent`, and the hook events that inject context are dominated by PreToolUse (5,865) and PostToolUse (3,180) rather than the SessionStart/UserPromptSubmit pair the claim leads with - the claim's 'other hook' wording already covers them."
        }
      ]
    },
    {
      "id": "REC-019",
      "area": "record-model",
      "behavior": "Assistant messages carry `stop_reason` with the values `tool_use` / `end_turn` / `stop_sequence` / `max_tokens` / `refusal` / null, persisted per record; it is trustworthy on the MAIN lane (measured 0.0-0.3% null) but is NORMALLY null mid-message on subagent lanes, which flush per content block.",
      "depends": "csift's `status`/`wait` verdicts classify a child lane `generating` only when the last assistant record's `stop_reason` is not `end_turn`; treating a null on a subagent lane as a finished turn would mark a mid-generation child settled.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "247-252",
          "snippet": "    /// `stop_reason` on assistant messages (`tool_use` / `end_turn` / `stop_sequence` /\n    /// `max_tokens` / `refusal` / null). Persisted per record; trustworthy on the MAIN\n    /// lane (measured 0.0-0.3% null), NORMALLY null mid-message on subagent lanes (which\n    /// flush per content block). Read by the live-truth surfaces. Additive + tolerant.\n    #[serde(default, rename = \"stop_reason\")]\n    pub stop_reason: Option<String>,"
        },
        {
          "path": "src/live/tail.rs",
          "lines": "24-26",
          "snippet": "    /// The newest assistant record's `stop_reason` (trustworthy on the MAIN lane;\n    /// null is NORMAL mid-message on subagents).\n    pub(crate) last_stop_reason: Option<String>,"
        }
      ],
      "instrument": "`rg -o '\"stop_reason\":(\"[a-z_]+\"|null)' <main transcript> | sort | uniq -c` versus the same over a subagent transcript of the same session. Counting rule: assistant records only, one per line; report null assistant records over all assistant records, per lane.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.0",
        "source": "SPEC.md section 6 v0.9.0 ledger; src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "cd ~/.claude/projects/<project-dir> && python3 - <<'PY'\nimport json, glob, collections\ndef census(paths, label):\n    c = collections.Counter(); n = 0\n    for f in paths:\n        for line in open(f, errors='replace'):\n            if '\"assistant\"' not in line: continue\n            try: r = json.loads(line)\n            except Exception: continue\n            if r.get('type') != 'assistant': continue\n            m = r.get('message')\n            if not isinstance(m, dict): continue\n            n += 1\n            c['null' if m.get('stop_reason') is None else str(m['stop_reason'])] += 1\n    print(label, n, c['null'], 100*c['null']/n, dict(c.most_common()))\ncensus([<main transcript>], 'MAIN')\ncensus([s for s in glob.glob('<session>/subagents/**/*.jsonl', recursive=True)\n        if not s.endswith('journal.jsonl')], 'SUBAGENTS')\nPY\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'stop_sequence|end_turn|max_tokens|\"refusal\"' | sort | uniq -c\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'stop_reason:[a-zA-Z_.?]{0,30}' | sort -u",
          "observed": "Session 1 MAIN lane: 7,036 assistant records, 0 null = 0.00%; values tool_use 6,773, end_turn 257, stop_sequence 6. Its 85 subagent transcripts: 6,890 assistant records, 5,023 null = 72.90%; tool_use 1,845, end_turn 22. Session 2 MAIN lane: 2,316 assistant records, 0 null = 0.00%; tool_use 2,240, end_turn 74, stop_sequence 2. Its 5 subagent transcripts: 493 assistant records, 298 null = 60.45%; tool_use 126, end_turn 69. Binary 2.1.258 string counts: `max_tokens` 110, `\"refusal\"` 80, `stop_sequence` 24, `end_turn` 16, and the literal `stop_reason:null`. Code sites present verbatim: src/model/record.rs:240-245 and src/live/tail.rs:24-26.",
          "rule": "Assistant records only - top-level `type == \"assistant\"` with a `message` object - one count per line, keyed by `message.stop_reason` with a missing/None value bucketed as `null`. Null rate = null assistant records over all assistant records, computed separately per lane: the main transcript alone, versus every subagent transcript under that session's `subagents/` tree with journal.jsonl excluded. Two sessions measured independently.",
          "note": "The lane split is stark and reproduces on both sessions: the main lane is 0.00% null on 9,352 combined assistant records, while subagent lanes are 60-73% null. That is stronger than the claim's stated 0.0-0.3% main-lane band, so no correction is needed - the band already contains the measurement. Values `max_tokens` and `refusal` were not observed on disk here but are present in the 2.1.258 binary, so the enumeration is the model's stop-reason union rather than a per-corpus observation; a session that hits a token ceiling or a refusal would be what confirms those two on disk."
        }
      ]
    },
    {
      "id": "REC-020",
      "area": "record-model",
      "behavior": "One API assistant message spans MULTIPLE jsonl records - Claude Code writes one line per content block and repeats the IDENTICAL `message` envelope, `message.usage` included, on every one of them (2,183 of 2,183 multi-record ids measured byte-identical) - so `message.id` (`msg_...`) is the only field joining them; summing usage per record over-reports token totals by a factor measured between 1.66 and 3.48 across 24 (session, model, field) cells, with a typical id spanning 2-3 records and a maximum of 11.",
      "depends": "`csift stats` dedupes usage PER FILE by `message.id`, taking the per-field MAX across that id's admitted records; a naive per-record sum makes every token figure csift reports wrong, and that is exactly what pre-0.9.2 csift printed.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "254-258",
          "snippet": "    /// The API message id (`msg_...`). One API message spans MULTIPLE records (CC\n    /// writes one line per content block and repeats the message envelope on each),\n    /// so this is the usage-dedupe key for `stats`. Additive + tolerant.\n    #[serde(default)]\n    pub id: Option<String>,"
        },
        {
          "path": "src/stats.rs",
          "lines": "249-256",
          "snippet": "            if let Some(u) = msg.token_usage() {\n                // CC repeats the IDENTICAL message.usage on every per-block record of\n                // one API message; summing per record over-reports 2.2-3.5x (measured).\n                // Dedupe per FILE by message.id, taking the per-field MAX across the\n                // id's admitted records: identical on clean data, and immune to the\n                // compaction-replay shape where a replayed copy carries ZEROED usage\n                // (first-wins would depend on traversal order). An id-less record\n                // counts on its own, as before."
        }
      ],
      "instrument": "For one transcript, sum `message.usage.{input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens}` per RECORD and again per distinct `message.id`, then divide; counting rule = per session, per model, per field. Measured ratios ranged 2.15-3.48. `csift stats @<id> --format json | jq .tokens` must match the per-msgid figure.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.2",
        "source": "SPEC.md section 3.3; SPEC.md section 6 v0.9.2 ledger item 2; AGENTS.md section 1; src/stats.rs comment; CHANGELOG 0.9.2 (stats token sums corrected); CHANGELOG 0.9.2; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "cd ~/.claude/projects && python3 - <<'PY'\nimport json, collections\nFIELDS = ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']\nfor f in [<three transcripts>]:\n    per = collections.Counter(); byid = {}\n    for line in open(f, errors='replace'):\n        if '\"usage\"' not in line: continue\n        try: r = json.loads(line)\n        except Exception: continue\n        m = r.get('message')\n        if not isinstance(m, dict): continue\n        u = m.get('usage')\n        if not isinstance(u, dict): continue\n        mdl = m.get('model') or '(unknown)'; mid = m.get('id')\n        for fld in FIELDS:\n            v = u.get(fld) or 0\n            per[(mdl, fld)] += v\n            k = (mdl, mid, fld)\n            byid[k] = v if mid is None else max(byid.get(k, 0), v)\n    ded = collections.Counter()\n    for (mdl, mid, fld), v in byid.items(): ded[(mdl, fld)] += v\n    for k in per: print(f, k, per[k], ded[k], per[k]/ded[k])\nPY\n# identity + join check, same shape, grouping message.usage by message.id\ncsift stats @<session> --format json --no-subagents | jq .tokens",
          "observed": "24 (session x model x field) cells over three transcripts. Ratio of the per-RECORD sum to the per-message.id sum ranged 1.663 to 3.481; the extremes were a cache_creation_input_tokens cell at 1.663 and another cache_creation_input_tokens cell at 3.481. The output_tokens cell the claim cites as 3.114 measures 3.119 here. Envelope identity on one session: 3,145 distinct message.id, 2,183 of them spanning >1 record, and 2,183/2,183 of those have byte-identical `message.usage` objects across all their records; 0 ids span more than one `message.model` and 0 span more than one record `type`. Records-per-id histogram: 3 -> 1,242 ids, 1 -> 962, 2 -> 765, 4 -> 110, 5 -> 42, 6 -> 12, 7 -> 5, 8 -> 4; max 11 records for one id. Cross-check: `csift stats --format json | jq .tokens` on that session returned exactly the per-message.id figures for all four fields of both models (e.g. cache_read 1,482,850,339 and output 3,138,067 for one model; cache_read 298,624,093 and output 690,829 for the other), never the per-record sums. Code sites present verbatim: src/model/record.rs:247-251 and src/stats.rs:249-256.",
          "rule": "Restrict to records with a `message.usage` object. Numerator = sum of the field over every such RECORD. Denominator = sum over distinct `message.id` of the per-field MAX across that id's records (an id-less record counts on its own). The ratio is computed per cell, one cell per (session, model, field); the 24 cells are 3 sessions x 2 models each x 4 usage fields. Identity check: group usage objects by message.id and compare their canonical JSON.",
          "note": "The mechanism, the join key and the dedupe rule all confirm exactly, and the cross-check is the strong part: csift's own stats output equals the per-message.id figure to the token for all eight model-field pairs, so the shipped dedupe is doing what the claim says. Only the ratio band needed widening - the measured floor is 1.66, below the claimed 2.15, and the claim's cited 3.114 output-token reading measures 3.119 on the current file (that session is still being appended to, which accounts for the drift). The claimed ceiling 3.48 reproduces to three decimals."
        }
      ]
    },
    {
      "id": "REC-021",
      "area": "record-model",
      "behavior": "`role:\"user\"` records NEVER carry `message.usage` (measured 0 across three large sessions); every usage-bearing record is `type:\"assistant\"`.",
      "depends": "csift `stats` reads usage only off `rec.message`, so a tool_result carrier contributes nothing to any token total.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "248-249",
          "snippet": "        if let Some(msg) = rec.message.as_ref() {\n            if let Some(u) = msg.token_usage() {"
        }
      ],
      "instrument": "Filter transcript lines to those with a `message.usage` object and tabulate top-level `type`; counting rule = per line. Expect `assistant` = 100%.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.2",
        "source": "dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 probe walking every *.jsonl under ~/.claude/projects/<project-dir> (top-level transcripts plus their subagents/, journal.jsonl excluded), json.loads per line, tabulating top-level `type` and `message.role` for every line whose `message.usage` deserializes to an object ; grep -n on src/stats.rs",
          "observed": "415 transcripts scanned. 37,379 usage-bearing lines: top-level type = {'assistant': 37379}, message.role = {'assistant': 37379} - zero under any other type or role. The same scope holds 18,531 lines with message.role == 'user', none of which carries a usage object. Restricted to the 23 top-level transcripts alone: 16,713 usage-bearing lines, all assistant. src/stats.rs:248 is `        if let Some(msg) = rec.message.as_ref() {` and 249 `            if let Some(u) = msg.token_usage() {`.",
          "rule": "One count per raw jsonl line. A line is usage-bearing iff its top-level `message` deserializes to an object carrying a `usage` object. Percentages are lines, not turns.",
          "note": "Refutation attempt failed in the strongest available direction: the scope deliberately included the 392 subagent transcripts, where tool_result carriers are densest, and still produced 0 user-role usage records out of 18,531 user-role lines. Code site verified verbatim at the stated lines."
        }
      ]
    },
    {
      "id": "REC-022",
      "area": "record-model",
      "behavior": "Because Claude Code streams per-block writes, timestamps DIFFER on essentially every multi-record `message.id` (5,190 of 5,191 measured, median spread 2.6 s, p90 16 s, max 224 s), while `message.model` and the record `type` never differ within one id (0 mismatches measured).",
      "depends": "csift's `--since`/`--until`/`--turn` windows can admit some records of an API message and not others; the usage dedupe runs over the ADMITTED set, so max-of-admitted still yields the message's usage once.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "257",
          "snippet": "                let model = msg.model_id().unwrap_or(\"(unknown)\").to_string();"
        }
      ],
      "instrument": "Group records by `message.id` and compute max(timestamp) - min(timestamp) per group; counting rule = per id. Expect a nonzero spread on most multi-record ids and zero model/type disagreement within an id.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.2",
        "source": "dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 probe over the 23 top-level transcripts of ~/.claude/projects/<project-dir>: group every record carrying a non-empty `message.id` by that id, then per group compute max(timestamp)-min(timestamp) and the cardinality of the `message.model` and top-level `type` sets ; grep -n on src/stats.rs",
          "observed": "6,816 distinct message.id groups, of which 5,191 span more than one record. 5,190 of those 5,191 (99.98%) show a nonzero timestamp spread. Spread over multi-record groups: median 2.572 s, p90 15.981 s, max 224.243 s. Groups whose `message.model` set has more than one member: 0. Groups whose top-level `type` set has more than one member: 0. src/stats.rs:257 is `                let model = msg.model_id().unwrap_or(\"(unknown)\").to_string();`.",
          "rule": "One group per distinct `message.id` within one file. Spread = max minus min of the group's ISO timestamps in seconds. A group counts as a mismatch iff the set of its non-null `message.model` (resp. top-level `type`) values has cardinality > 1.",
          "note": "The mechanism and the zero-mismatch half reproduce exactly. The number needed correcting: the claim's '~50 s' upper bound is far too low - the measured maximum in this 23-transcript scope is 224.243 s, and the p90 is 16 s. The consequence for csift is unchanged and, if anything, sharper: a four-minute-wide API message is easy for a narrow --since/--until window to bisect."
        }
      ]
    },
    {
      "id": "REC-023",
      "area": "record-model",
      "behavior": "Claude Code writes a fabricated stand-in assistant record - an API-error notice, for example - whose `message.model` is the literal placeholder `<synthetic>`, and that model key always reports zero tokens.",
      "depends": "`search --count-by model` reports the raw `message.model` value verbatim rather than dropping or normalising it, so a scope's model census discloses the fabricated records instead of hiding them.",
      "code": [
        {
          "path": "src/cli/search_args.rs",
          "lines": "422-424",
          "snippet": "    /// pending under any selector) · `model` (per assistant model, the raw `message.model`\n    /// value; Claude Code's own `<synthetic>` placeholder, a CC-fabricated stand-in\n    /// assistant record such as an API-error notice, is reported verbatim) · `attachment`"
        }
      ],
      "instrument": "`csift search '' <project> --count-by model | rg synthetic` - a `<synthetic>` key must appear verbatim on a corpus that has hit an API error. Counting rule: one record per model key, records with no model excluded and disclosed.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.1",
        "source": "SPEC.md section 6 v0.6.1 ledger item 3; CHANGELOG 0.6.1; src/cli/search_args.rs --count-by help; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' <project-dir> --count-by model ; rg --no-filename '\"model\":\"<synthetic>\"' *.jsonl piped to a python3 probe tabulating type, the usage object and the text blocks ; csift stats <project-dir> --format json --max-count 0 summed per model ; strings -n 2 ~/.local/share/claude/versions/2.1.258 | rg -o '.{140}<synthetic>.{60}'",
          "observed": "The model census over one project dir prints 7 keys and `<synthetic>` appears verbatim among them with 12 records (largest key 18,434). All 12 are top-level type 'assistant'. Every one carries a usage object whose input_tokens, output_tokens, cache_read_input_tokens and cache_creation_input_tokens are all 0 (two field layouts, 7 + 5). csift stats over the same scope reports `<synthetic> {'cache_creation': 0, 'cache_read': 0, 'input': 0, 'output': 0}` while every real model key is nonzero. Their texts are harness stand-ins, e.g. `API Error: Can't reach the API server - check your internet or DNS (ENOTFOUND)`, `API Error: Server error mid-response. The response above may be incomplete.` and `No response requested.`. The 2.1.258 binary carries the constant table `up=\"(no content)\",TR=\"No response requested.\",Jc=\"<synthetic>\"`.",
          "rule": "One record per model key; a record with no `message.model` is excluded from the axis and the excluded count is disclosed (18,551 here). A model key reports zero tokens iff all four summed usage fields are 0.",
          "note": "Three independent instruments agree: the census key, the raw per-record usage tabulation, and the binary's own constant. src/cli/search_args.rs:417-419 verified verbatim; the help text's own example ('an API-error notice') is exactly what the 12 records turned out to be."
        }
      ]
    },
    {
      "id": "REC-024",
      "area": "record-model",
      "behavior": "The session-state cache line types - `last-prompt`, `mode`, `ai-title`, `agent-name`, `permission-mode` - carry no record `uuid`, no `timestamp` and no `message` (they carry only `sessionId`, their one payload field, and on last-prompt a `leafUuid`). The `lastPrompt` value is not a byte copy of a user record: the harness collapses newlines to spaces and truncates at 200 characters plus an ellipsis, and it captures whatever prompt was last DELIVERED - including peer-session inbox messages, which are not genuine user records.",
      "depends": "csift deliberately leaves them unpromoted, so they contribute no searchable leaf and no duplicate human turn; promoting them would double-count the last prompt in every `user.message` census.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "9-11",
          "snippet": "    /// The single promoted leaf a NON-message line carries (v0.10.0), or `None` for a\n    /// message record and for every line type that stays unmodeled (the session-state\n    /// cache lines, the unpromoted system subtypes, a content-less queue `dequeue`)."
        }
      ],
      "instrument": "`jq -r 'select(.type==\"last-prompt\") | keys | join(\",\")' <file> | sort | uniq -c` shows the absent uuid/timestamp/message; then `csift search '<the last prompt text>' @<session> --count-by label` must count it once. Counting rule: one line per cache write, excluded from every leaf.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.10.0",
        "source": "AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 probe over the 23 top-level transcripts of ~/.claude/projects/<project-dir> tabulating the sorted top-level key set of every line whose type is last-prompt / mode / ai-title / agent-name / permission-mode ; a second probe comparing each distinct `lastPrompt` value against the text of every user record in one transcript, raw and whitespace-normalized ; rg -c of a distinctive prompt phrase in that transcript vs `csift search '<phrase>' @<session> --count-by label` ; strings -n 2 ~/.local/share/claude/versions/2.1.258 | grep -A3 -F 'normalizeLastPrompt(e){let n=e.replaceAll('",
          "observed": "Key sets, one per line type: last-prompt = {lastPrompt, leafUuid, sessionId, type} (3,486 lines) or {leafUuid, sessionId, type} (24 lines); mode = {mode, sessionId, type} (3,487); ai-title = {aiTitle, sessionId, type} (3,106); agent-name = {agentName, sessionId, type} (2,654); permission-mode = {permissionMode, sessionId, type} (3,487). No `uuid`, no `timestamp`, no `message` on any of the 16,244 lines. In one transcript, 312 last-prompt lines carry only 58 distinct values; 45 of the 58 are exactly 201 characters long. Only 9 of 58 appear verbatim inside a user record's text, and 11 of 58 after whitespace normalization. The binary's writer is `normalizeLastPrompt(e){let n=e.replaceAll(`\\n`,\" \").trim();return n.length>200?le(n,200).trim()+\"\\u2026\":n}`. Unpromoted behavior: a distinctive prompt phrase occurs on 6 raw lines of that transcript (2 user, 3 last-prompt, 1 attachment) and `csift search ... --count-by label` reports 2 matched records (user.message 1, harness.compaction.summary 1).",
          "rule": "One count per raw jsonl line for the key-set census. lastPrompt duplication is judged per DISTINCT value: a value counts as a duplicate iff some user record's text equals it or starts with it (raw, then again after collapsing whitespace runs to single spaces). The search comparison counts raw LINES against census RECORDS.",
          "note": "The structural half - no uuid, no timestamp, no message - reproduces exactly across 16,244 lines and is the half csift's non-promotion rests on; the search comparison shows the 3 cache copies of a phrase contributing nothing to the census. Two wording corrections: the lines do carry `sessionId` (and last-prompt a `leafUuid`), so 'no uuid' means no RECORD uuid; and 'duplicates a record already present as a genuine user message' is too strong - the value is a normalized 201-character-capped rendering, and for peer deliveries its twin classifies agent.communication.inbox rather than user.message. The anti-double-count rationale survives both corrections. Code site verified verbatim at src/model/classify_promoted.rs:9-11."
        }
      ]
    },
    {
      "id": "REC-025",
      "area": "record-model",
      "behavior": "Claude Code writes a `type:\"file-history-delta\"` line - a sibling of `file-history-snapshot` carrying one path's version bump. Its fields are `{messageId, snapshotMessageId, trackingPath, backup:{backupFileName, version, backupTime}, timestamp}`; unlike the snapshot it DOES carry a top-level timestamp. 1,971 such lines corpus-wide.",
      "depends": "`Record::promoted_class` maps both `file-history-snapshot` and `file-history-delta` to `harness.meta.snapshot`; omitting the delta arm loses roughly 1,971 corpus lines from the SEARCHABLE surface. It does not affect recover's backup instrument or files' ExternalWrite inference, which key on `file-history-snapshot` records and the on-disk store.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "27",
          "snippet": "\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),"
        }
      ],
      "instrument": "`csift search '' -t harness.meta.snapshot --count-by label --format json` over a corpus session, or `rg -c '\"type\":\"file-history-delta\"' ~/.claude/projects/**/*.jsonl`. Counting rule: one per raw jsonl line.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "SPEC.md section 5.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift stats --format json --max-count 0 over ~/.claude/projects, summing line_types across rows with kind == 'session' only ; rg --no-filename '\"type\":\"file-history-delta\"' *.jsonl in one project dir piped to a python3 key-set and backup-field tabulation ; csift search '' -t harness.meta.snapshot <project-dir> --count-by label ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'insertFileHistoryDelta.{0,300}' ; sed -n on src/model/classify_promoted.rs",
          "observed": "Corpus-wide over 7,579 sessions: file-history-delta = 1,971 lines, file-history-snapshot = 4,055. In one project dir, 144 delta lines, all with the identical key set {backup, messageId, snapshotMessageId, timestamp, trackingPath, type}; backup.version was 1 on all 144 and backup.backupFileName was non-null on 79 of 144. `-t harness.meta.snapshot` reaches 577 records in that dir and renders e.g. `[file-history delta at 2026-08-12T01:14:54.653Z: src/path.rs@v1 backup=<hash>@v1]`. The binary writes `insertFileHistoryDelta({type:\"file-history-delta\",messageId:e,snapshotMessageId:n,trackingPath:r,backup:o,timestamp:new Date().toISOString()},d)`. src/model/classify_promoted.rs:13-25 holds the quoted match, with the `\"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),` arm at line 22.",
          "rule": "One count per raw jsonl line. Corpus totals sum `line_types` over rows with kind == 'session' ONLY - the stats stream also emits one kind == 'summary' row that re-totals the whole scope, and summing every row double-counts every line type exactly 2x.",
          "note": "The line type exists and the mapping is real, so the claim holds in substance. Three corrections. (1) The count is 1,971, not 1,934 - and the counting rule has to travel with it, because summing every stats row instead of only the session rows yields exactly 3,942. (2) `@vN` is csift's own render of `backup.version`, not a Claude Code field name; the CC fields are `trackingPath` and `backup.{backupFileName, version, backupTime}`. (3) The delta feeds only the searchable surface: a grep for tracking_path / backup across src/ reaches src/model/record.rs (the field) and src/search/record_text.rs (the excerpt) and nothing else, while src/files/external.rs and src/recover/snapshots.rs key on `file-history-snapshot`."
        }
      ]
    },
    {
      "id": "REC-026",
      "area": "record-model",
      "behavior": "A `file-history-snapshot` record nests its timestamp INSIDE the `snapshot` object rather than at top level, so the line has no top-level `timestamp`.",
      "depends": "csift's `harness.meta.snapshot` render carries a null hit timestamp and puts the nested instant into the excerpt (`[file-history snapshot at <snapshot.timestamp>: <path>@vN, ...]`); reading a top-level timestamp yields nothing and would drop the record from any time window.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "27-29",
          "snippet": "            \"file-history-snapshot\" | \"file-history-delta\" => Some(Class::MetaSnapshot),\n            _ => None,\n        }"
        }
      ],
      "instrument": "`rg -m1 '\"type\":\"file-history-snapshot\"' <transcript> | jq 'has(\"timestamp\"), .snapshot.timestamp'` must print `false` then an ISO instant. Counting rule: one record inspected.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "SPEC.md sections 4.8 and 5.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg --no-filename '\"type\":\"file-history-snapshot\"' *.jsonl in one project dir piped to a python3 probe counting top-level `timestamp` presence and `snapshot.timestamp` presence and tabulating key sets ; csift search 'file-history snapshot at' -t harness.meta.snapshot <project-dir> --max-count 2 --format json reading ts_utc / ts_local / excerpt ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'insertFileHistorySnapshot.{0,200}'",
          "observed": "433 file-history-snapshot lines in the dir. With a top-level `timestamp`: 0. With a non-empty `snapshot.timestamp`: 433. All 433 share one key set: {isSnapshotUpdate, messageId, snapshot, type}; the nested `snapshot` object carries {messageId, timestamp, trackedFileBackups}. The binary's writer is `insertFileHistorySnapshot(e,n,r,o){return this.trackWrite(async()=>{let d={type:\"file-history-snapshot\",messageId:e,snapshot:n,isSnapshotUpdate:r};await this.appendEntry(d,void 0,void 0,o)})}` - no timestamp field is added. csift JSON hits carry ts_utc null and ts_local null with excerpt `[file-history snapshot at 2026-06-25T15:25:52.125Z: AGENTS.md@v2, SKILL.md@v2, ...]`.",
          "rule": "One record inspected per raw jsonl line whose top-level type is file-history-snapshot; presence means the key exists at top level (resp. inside the `snapshot` object) with a non-empty value.",
          "note": "433 of 433 rather than the claim's single sampled record, and the binary's own writer confirms the omission is by construction rather than an artifact of this corpus. The consequence is directly observable: hit ts_utc and ts_local are null and the nested instant appears only inside the excerpt. Code site verified verbatim at src/model/classify_promoted.rs:22-24."
        }
      ]
    },
    {
      "id": "REC-027",
      "area": "record-model",
      "behavior": "A `/fork` child's LINE 1 is a timestampless `type:\"fork-context-ref\"` record carrying `parentLastUuid` (the parent's last record uuid at fork time), `contextLength` (messages carried into the fork), `agentId` and `parentSessionId`; the `parentLastUuid` + `contextLength` field pair appears on no other line type. The fork child is written as a SUBAGENT transcript under <session>/subagents/agent-<id>.jsonl with a meta.json reading agentType 'fork' and isFork true - not as a top-level session file (0 of 33 corpus-wide fork-context-ref lines sit in a top-level transcript).",
      "depends": "`agents`' head-walk captures fork provenance for free and surfaces the fork point as a re-feedable `show --uuid` address; losing it silently drops the only on-disk link from a fork child to its parent.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "52-56",
          "snippet": "    /// Fork provenance (line 1 of a `/fork` child transcript, `type:\"fork-context-ref\"`):\n    /// the PARENT session's last record uuid at fork time - the fork point, feedable to\n    /// `csift show @<parent> --uuid <it>`. Absent everywhere else.\n    #[serde(default, rename = \"parentLastUuid\")]\n    pub parent_last_uuid: Option<String>,"
        },
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "18-20",
          "snippet": "    // A `/fork` child's LINE 1 is a timestampless `fork-context-ref` record carrying\n    // the parent's last uuid at fork time + the carried context length; it precedes\n    // the first timestamped record, so the same head walk captures it for free."
        }
      ],
      "instrument": "`rg -l '\"fork-context-ref\"' ~/.claude/projects/*/*.jsonl` then `head -1` that file and check it carries `parentLastUuid` and `contextLength` and no `timestamp`. Counting rule = files whose FIRST line has that type.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -l --glob '*.jsonl' '\"fork-context-ref\"' over one project dir ; python3 probe over that dir's subagent transcripts recording the 0-based line index of the fork-context-ref line and its key set ; python3 structural probe splitting every line whose bytes contain 'parentLastUuid' into top-level-field vs payload-text mentions ; csift stats per project dir summing line_types['fork-context-ref'] over kind == 'session' rows ; csift agents @<session> --format json counting agent rows with non-null fork fields ; csift show @<parent-session> --uuid <the parentLastUuid> ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -F 'fork-context-ref' ; sed -n on src/model/record.rs and src/subagent/lifecycle.rs",
          "observed": "18 transcripts in the project dir carry a fork-context-ref line; the 0-based line-index histogram is {0: 18}, i.e. all 18 are file line 1. All 18 share the key set {agentId, contextLength, parentLastUuid, parentSessionId, type} - no `timestamp`, no `uuid`, no `message`. Corpus-wide the type totals 33 lines across three project dirs (18 / 14 / 1) and 0 of them sit in a top-level transcript: every one is under <session>/subagents/agent-<id>.jsonl, and the companion meta.json reads agentType 'fork', isFork true, spawnDepth 1. Field exclusivity, structurally: 119 lines in the dir contain the bytes 'parentLastUuid'; exactly 18 carry it as a TOP-LEVEL field and all 18 are fork-context-ref; the other 101 (user 48, assistant 41, attachment 14, queue-operation 2) merely quote the string in payload text. csift agents emits 85 agent rows for that session, 18 of them with non-null fork_parent_last_uuid + fork_context_length (e.g. 7725, 8198) and agent_type 'fork'; feeding one such uuid to `csift show @<parent> --uuid` fetches 1 record unit. The binary carries `fork-context-ref`, `Failed to record fork-context-ref: ` and `[fork-context-ref] parent uuid `.",
          "rule": "Counted files = transcripts whose FIRST line has type fork-context-ref. 'Appears nowhere else' is judged structurally: a line counts only if `parentLastUuid` is a top-level JSON key, not if the byte sequence merely occurs in a payload string.",
          "note": "Every operative part of the claim reproduces: line 1, timestampless, the two named fields, exclusivity, the head walk capturing it, and the re-feedable show --uuid address. One correction of consequence for anyone re-running the claim's own instrument: it proposes `rg -l '\"fork-context-ref\"' ~/.claude/projects/*/*.jsonl`, which matches NOTHING here, because the fork child is a subagent transcript one directory deeper. The record also carries two fields the claim omits (agentId, parentSessionId). Code sites verified verbatim at src/model/record.rs:51-55 and src/subagent/lifecycle.rs:18-20."
        }
      ]
    },
    {
      "id": "REC-028",
      "area": "record-model",
      "behavior": "A `compact_boundary` system record carries a `compactMetadata` object and a TOP-LEVEL `logicalParentUuid` naming the true predecessor record the compaction re-links to, while its own `parentUuid` is null. `compactMetadata` carries more than four fields: the dominant shape is {trigger, preTokens, postTokens, durationMs, cumulativeDroppedTokens, preservedMessages, preservedSegment, preCompactDiscoveredTools}.",
      "depends": "csift models `logical_parent_uuid` tolerantly and renders it in the boundary excerpt; it is the harness's own ground truth for the post-compaction chain and the only way to rejoin the graph across a boundary.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "100-107",
          "snippet": "    /// compaction points and inspect what each clipped. Absent on every other record. Tolerant.\n    #[serde(default, rename = \"compactMetadata\")]\n    pub compact_metadata: Option<serde_json::Value>,\n\n    /// `logicalParentUuid` (top-level on `compact_boundary` system records): the TRUE\n    /// predecessor record the compaction re-links to (`parentUuid` is null on a\n    /// boundary) - harness ground truth for the post-compaction chain. Additive +\n    /// tolerant."
        }
      ],
      "instrument": "`csift search '' @<session> -t harness.compaction.boundary --max-count 1` then read the raw line with `csift show @<session> --line <n> --raw`; expected: `logicalParentUuid` at top level, `parentUuid` null. Counting rule: one boundary record per compaction.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.8.1",
        "source": "AGENTS.md section 3.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg --no-filename '\"subtype\":\"compact_boundary\"' *.jsonl over one project dir piped to a python3 probe counting compactMetadata presence, non-null top-level logicalParentUuid and null parentUuid, and tabulating both key sets ; csift search '' -t harness.compaction.boundary <project-dir> --max-count 2 ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,180}compactMetadata.{0,260}' and rg -o 'logicalParentUuid.{0,160}' ; sed -n on src/model/record.rs",
          "observed": "17 compact_boundary records in the dir. With a compactMetadata object: 17/17. With a non-null top-level logicalParentUuid: 17/17. With parentUuid null: 17/17. compactMetadata key sets: 14 records carry {cumulativeDroppedTokens, durationMs, postTokens, preCompactDiscoveredTools, preTokens, preservedMessages, preservedSegment, trigger}, 1 the same minus preCompactDiscoveredTools, 1 the full set plus `precomputed`. The binary corroborates the wider set - `let{cumulativeDroppedTokens:o,preTokens:d,postTokens:f}=r.compactMetadata`, `r.compactMetadata.preservedMessages?.anchorUuid??r.compactMetadata.preservedSegment?.anchorUuid` - and writes the link as `logicalParentUuid:e.logical_parent_uuid`. csift renders `Conversation compacted [compaction boundary: trigger=auto preTokens=970403 postTokens=22464 durationMs=123871] [logicalParent=<uuid>]`.",
          "rule": "One boundary record per compaction; presence is judged on the parsed JSON, and parentUuid counts as null only when the key is present with a JSON null or absent.",
          "note": "The three load-bearing facts hold 17/17 with no exception, and the binary shows the logicalParentUuid link is written by the boundary constructor rather than incidental. The correction is the field list: the claim's four are a subset - four more ride along in 16 of 17 records, and csift keeps compactMetadata RAW so the extra fields cost nothing, but a reader treating the four as exhaustive would wrongly conclude the boundary cannot tell you how much was dropped (cumulativeDroppedTokens) or what was preserved (preservedSegment / preservedMessages anchorUuid). Code site verified verbatim at src/model/record.rs:93-100."
        }
      ]
    },
    {
      "id": "REC-029",
      "area": "record-model",
      "behavior": "Claude Code writes `type:\"user\"` records for far more than human turns: peer-session inbox messages and harness notifications carry the same `type` and `message.role` of `user`, so a naive `type:\"user\"` human-turn filter overcounts by 6.00x corpus-wide (15,014 records versus 2,501 genuine turns) and by 13.14x in the most multi-agent lane.",
      "depends": "csift gates the human turn on `Record::is_genuine_user`, which every `-t user.message` selection, the `list` first/last user excerpt and turn segmentation key on; if the shape drifts, `search -t user.message`, `stats` turn counts and `list` rows all silently over- or under-count.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "68-73",
          "snippet": "        match &msg.content {\n            Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n            Some(Content::Blocks(blocks)) => {\n                let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n                let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n                if !has_text || has_tool_result {"
        }
      ],
      "instrument": "Run `csift search \"\" <target> --count-by label` over a multi-agent session and compare the `user.message` count against `rg -c '\"type\":\"user\"' <the session jsonl>`; counting rule: census rows count RECORDS once per surviving leaf, rg counts LINES. The ratio should exceed 1 and reach ~3x on a corpus-wide run.",
      "located": {
        "claude_code": null,
        "csift": "0.8.2",
        "source": "SKILL.md wrong-assumption table (Why not hand-roll this format, row 1)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 probe over all 7,580 transcripts under ~/.claude/projects applying a NAIVE human-turn filter and counting raw type:\"user\" records ; csift search '' --count-by label over the whole corpus for the genuine user.message total ; csift search '' -t user.message --count-by session --format json for the per-transcript genuine counts, joined to the same probe's per-transcript naive counts ; sed -n on src/model/predicates.rs",
          "observed": "Corpus-wide: 228,455 records with top-level type \"user\". The naive filter keeps 15,014 of them. csift's label census over the same corpus reports user.message = 2,501 (against agent.tool.result 211,055, agent.communication.inbox 8,651, user.unsent 848, and 2,811 records across the five harness.notification.* leaves). Ratio 15,014 / 2,501 = 6.00x. Per transcript, restricted to the 15 transcripts with at least 20 genuine turns, the worst lane is 644 naive against 49 genuine = 13.14x, then 1,152 against 101 = 11.41x; the median of that set is far lower. src/model/predicates.rs:68-73 holds the quoted match arms verbatim, directly under a comment recording that 106 peer messages were once mislabeled as the user in one real session.",
          "rule": "NAIVE filter, stated so a stranger can rerun it: top-level type == 'user' AND message.role == 'user' AND NOT isMeta AND NOT isCompactSummary AND (content is a string OR content is a block array containing a text block and no tool_result block). Genuine = csift's user.message census, which counts records once per surviving leaf. Per-lane ratios are computed only where the genuine denominator is at least 20, so a 1-turn transcript cannot manufacture a headline ratio.",
          "note": "The mechanism holds and the failure is now worse, not better, so the claim errs on the safe side. Both numbers moved: 3.03x -> 6.00x corpus-wide and 8.6x -> 13.14x in the worst lane. The driver is visible in the census - agent.communication.inbox alone is 8,651 records, more than three times the entire genuine user.message population, which is what peer-to-peer sessions do to this ratio. Note the naive filter is deliberately generous to the hand-roller: a bare `type:\"user\"` grep would report 228,455, a 91x overcount. Code site verified verbatim at src/model/predicates.rs:68-73."
        }
      ]
    },
    {
      "id": "REC-030",
      "area": "record-model",
      "behavior": "A `type:\"user\"` record whose `message.content` is a block array containing a `tool_result` block is a tool-result CARRIER, not a human turn (measured 3,361 carriers to 243 prose-shaped records in one transcript, a 13.83x dominance), and 100% of the human turns that ride such a carrier - the `user.answer` and `user.rejection` openers, 106 distinct records corpus-wide - extract as ZERO characters if only `text` blocks are read. Those openers are 4.87% of all human turn openers (128 of 2,629 census records).",
      "depends": "csift excludes any block array containing a `tool_result` from `is_genuine_user` and recovers those turns through the `user.answer` / `user.rejection` leaves; drift breaks `search -t user.answer -t user.rejection` and the whole turn-boundary count.",
      "code": [
        {
          "path": "src/model.rs",
          "lines": "20",
          "snippet": "//! carriers. A genuine user turn is: string content (and NOT `isCompactSummary`),"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "68-73",
          "snippet": "        match &msg.content {\n            Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n            Some(Content::Blocks(blocks)) => {\n                let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n                let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n                if !has_text || has_tool_result {"
        }
      ],
      "instrument": "`csift search \"\" <target> --count-by label` and compare `user.message` + `user.answer` + `user.rejection` against a raw count of `\"role\":\"user\"` lines; counting rule: records, not blocks. Expect the tool_result carriers to dominate the raw count.",
      "located": {
        "claude_code": null,
        "csift": "0.8.2",
        "source": "SKILL.md wrong-assumption table (row 2); AGENTS.md section 3.3"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 probe over the largest top-level transcript in one project dir classifying every type:\"user\" record as tool_result carrier vs prose-shaped ; csift search '' @<session> --no-subagents --count-by label on that same transcript ; csift search '' -t user.answer -t user.rejection --count-by label and the same query with --raw --max-count 0 corpus-wide, piped to a python3 probe extracting only `type:\"text\"` blocks from message.content ; the same query with --format json to count hit records and distinct (session, line) addresses ; sed -n on src/model.rs and src/model/predicates.rs",
          "observed": "One transcript: 3,604 records with type \"user\" = 3,361 tool_result carriers vs 243 prose-shaped, a 13.83x carrier dominance; csift labels 77 of them user.message and 14 user.unsent, with agent.tool.result at 3,361. Corpus-wide the answer/rejection census is user.answer 118 + user.rejection 10 = 128 records, which resolve to 106 distinct (session, line) addresses; `--raw` emits exactly those 106 physical lines and 106 of 106 (100%) yield ZERO characters when only `type:\"text\"` blocks of message.content are read - none yielded even one character. As a share of human turn openers that is 128 / (2,501 + 128) = 4.87%. src/model.rs:20 is `//! carriers. A genuine user turn is: string content (and NOT \\`isCompactSummary\\`),`, sitting under a comment recording 332 + 61 genuine against 1619 carriers; src/model/predicates.rs:68-73 holds the quoted arms.",
          "rule": "Records, not blocks. A record is a tool_result CARRIER iff its message.content is a block array containing at least one tool_result block; prose-shaped iff string content or a block array with a text block and no tool_result. Zero-extract is judged on the verbatim line: concatenate the `text` fields of every `type:\"text\"` block in message.content and measure the character length. 128 census records collapse to 106 physical lines because a record selected by more than one view is counted once per surviving leaf by the census but emitted once by --raw.",
          "note": "The qualitative claim is not merely intact, it is absolute where the claim was statistical: not 'some' but every single one of the 106 answer/rejection records extracts as zero characters from text blocks, because the answer rides in the tool_result payload and there is no text block at all. Two numbers moved. The carrier ratio in the transcript I measured is 13.83x rather than the 1,619-to-393 (4.1x) recorded in the src/model.rs comment - that comment's session is a real, older measurement, so this is corpus drift toward MORE carriers, not a contradiction. The 16.5% share is now 4.87%, because the denominator (user.message) grew far faster than AskUserQuestion and plan-rejection turns did. Code sites verified verbatim at src/model.rs:20 and src/model/predicates.rs:68-73."
        }
      ]
    },
    {
      "id": "REC-031",
      "area": "record-model",
      "behavior": "A subagent transcript's first record is an isSidechain:true type:\"user\" seed carrying the spawn prompt in 7483 of 7516 measured transcripts (99.56%). The other 33 open with a type:\"fork-context-ref\" line - a context-fork spawn shape carrying {agentId, contextLength, parentLastUuid, parentSessionId} and no message - after which the first real record is an isSidechain:true assistant; 31 of those 33 carry no user seed anywhere. In TOP-LEVEL transcripts a sidechain record does not occur at all (0 lines across 64 transcripts).",
      "depends": "`is_genuine_user` deliberately does NOT gate on `isSidechain`, so the `list` per-subagent preview can treat the seed as the subagent's first user message; the seed classifies `agent.communication.inbox` (parent to self), not `user.message`. Since v0.10.2 a subagent transcript whose line 1 is a `fork-context-ref` record has NO spawn seed: its openers are the parent's own human messages and classify `user.message`; only a genuine child's first opener is the parent-to-self seed.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "48-53",
          "snippet": "        // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n        // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n        // per-subagent preview legitimately treats that seed as the subagent's \"first\n        // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n        // any real corpus. Gating it would silently blank the subagent preview for zero\n        // real benefit; the per-surface scan owns subagent-vs-parent context instead."
        },
        {
          "path": "src/model/classify.rs",
          "lines": "330-335",
          "snippet": "        // The spawn-prompt seed of a subagent transcript is an inbound comm (parent ⇨ self),\n        // not the operator (GOLD §3) - unchanged, regardless of isMeta.\n        if ctx.is_subagent && ctx.is_transcript_opener {\n            push_unique(out, Class::CommInbox);\n            return;\n        }"
        },
        {
          "path": "src/search/turns_match.rs",
          "lines": "63-70",
          "snippet": "    let first_opener_line = if head_is_fork {\n        None\n    } else {\n        records\n            .iter()\n            .find(|k| k.rec.opens_turn())\n            .map(|k| k.line_no)\n    };"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "254-255",
          "snippet": "    let head_end = memchr::memchr(b'\\n', bytes)\n        .unwrap_or(bytes.len())"
        }
      ],
      "instrument": "`head -1 <a subagent transcript> | rg -c isSidechain` (expect 1) and `rg -c '\"isSidechain\":true' <a top-level transcript>` (expect 0). Counting rule = physical lines carrying the flag, per lane.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "src/model/predicates.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 scan of ~/.claude/projects: for every */*/subagents/**/*.jsonl (journal.jsonl excluded) read line 1 and bucket (type, isSidechain, content-is-string); for every */*.jsonl count physical lines containing b'\"isSidechain\":true'. Plus: strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'fork-context-ref'. Plus: csift show @<agent-id> --line 1 --format json on a subagent whose line 1 is the seed.",
          "observed": "7516 subagent transcripts scanned: 7483 open with ('user', True, True) and 33 open with ('fork-context-ref', None, False). 64 top-level transcripts scanned: 0 files with any isSidechain:true line, 0 such lines total. Of the 33 fork-context-ref openers, only 2 carry a user isSidechain string seed anywhere in the file. Binary 2.1.258 matches 9 strings for fork-context-ref, including the literal 'Failed to record fork-context-ref: ' and '\"fork-context-ref\":\"route-by-agent\"'. A fork-context-ref opener's own record fields are {agentId, contextLength, parentLastUuid, parentSessionId, type} and the record after it is type:assistant, isSidechain:true. On a normal subagent, csift show --line 1 gives labels ['agent.communication.inbox'] with a parent-to-self direction.",
          "rule": "Per-lane count of physical first lines (subagent lane) and of physical lines carrying the flag (top-level lane); one row per transcript file.",
          "note": "csift is not broken by the fork shape: on a fork-context-ref transcript, csift show --line 1..3 books the opener as non_record_lines:1 with skipped_lines:0 and does not hand the following assistant record a spurious opener label (it classified agent.tool.use + agent.communication.sent). The claim's corollary that a subagent's first record can be treated as its first user message therefore has a 33-transcript exception class where there is no seed record at all."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "drifted",
          "instrument": "corpus census of subagent transcripts whose line 1 contains `fork-context-ref`, counting those that carry a genuine user record (role user, string content, no tool_result, not isMeta); e2e on 0.10.2 with a fork clone beside a genuine child",
          "observed": "42 fork transcripts, 10 of them with a genuine user record - on 0.10.1 that record's first opener classified agent.communication.inbox (the positional seed rule); on 0.10.2 the clone's opener classifies user.message and the genuine child keeps its seed",
          "rule": "one file = one clone; a clone with any genuine user record is a reachable mislabel",
          "note": "fix: search/scan.rs reads the file's first line for the fork marker and turns_match assigns no seed to a clone; e2e a_fork_clone_has_no_spawn_seed_but_a_genuine_child_does"
        }
      ]
    },
    {
      "id": "REC-032",
      "area": "record-model",
      "behavior": "Same mechanism, re-measured numbers: 0 of 856 superseded drafts appear in preservedMessages (the ledger and the src/model/taxonomy.rs doc comment both say 0 of 772, a smaller earlier corpus). The exclusion is discriminating rather than vacuous: 11 of 560 resend siblings DO appear in the same preserved sets.",
      "depends": "csift assigns the draft the single leaf `user.unsent` at the scan layer (a per-record classify cannot see the later sibling), keeps it outside turn numbering, and uses the `preservedMessages` exclusion as the LLM-visibility instrument for a bare `-t user`; a recalled-then-abandoned message has no sibling and is structurally undetectable. Since v0.10.2 a superseded record never fans out into per-section classes: the single `user.unsent` view is emitted whatever the draft's text shape, so a hit's `label` is always a member of its own `labels[]` and a notification selector never sees a draft.",
      "code": [
        {
          "path": "src/search/hits.rs",
          "lines": "168-176",
          "snippet": "    // A superseded turn-opener draft (the scan layer computed the set - a pure\n    // per-record classify cannot see the LATER same-parent sibling) carries the single\n    // label `user.unsent`, whatever kind of opener it was: the draft is not part of the\n    // conversation, so it never rides `user.message` (whose counts stay pure).\n    let labels = if superseded {\n        vec![Class::UserUnsent]\n    } else {\n        rec.classify(ctx)\n    };"
        },
        {
          "path": "src/search/turns_match.rs",
          "lines": "214-224",
          "snippet": "    // C-18 + user.unsent: a superseded draft is a real record OUTSIDE turn numbering.\n    // A scan emits it as its own annotated unit when it hits (searchable, labeled\n    // `user.unsent`) - except under a `--turn` window, which asks about NUMBERED turns\n    // and a draft belongs to none. An explicit address always reaches it (refetch law).\n    if address.is_some() || turn_bounds.is_none() {\n        let mut draft_idxs: Vec<usize> = skip.iter().copied().collect();\n        draft_idxs.sort_unstable();\n        for i in draft_idxs {\n            out.extend(build_exchange(0, &[i], true));\n        }\n    }"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "182-196",
          "snippet": "    /// glob form and explicit paths reach the invisible ones.\n    #[must_use]\n    pub fn llm_visible(self) -> bool {\n        !matches!(\n            self,\n            Class::UserUnsent\n                | Class::UserQueued\n                | Class::CompactionBoundary\n                | Class::MetaTurnDuration\n                | Class::MetaAwaySummary\n                | Class::MetaStopHooks\n                | Class::MetaSnapshot\n                | Class::MetaSystem\n        )\n    }"
        },
        {
          "path": "src/search/hits.rs",
          "lines": "257-261",
          "snippet": "    let sections = if superseded {\n        Vec::new()\n    } else {\n        rec.record_text_sections(ctx)\n    };"
        }
      ],
      "instrument": "compactMetadata.preservedMessages is a JSON OBJECT {anchorUuid, uuids:[...], allUuids?:[...]}, not an array. Treating it as a list of uuids yields 0 preserved uuids and makes the instrument vacuously pass; the object shape is confirmed in the 2.1.258 binary, which builds 'preservedMessages:{anchorUuid:r.anchor_uuid,uuids:[...r.uuids],...r.all_uuids!==void 0&&{allUuids'.",
      "located": {
        "claude_code": null,
        "csift": "0.9.2",
        "source": "CHANGELOG 0.9.2 (user.unsent); CHANGELOG 0.9.4 (LLM-visibility instrument)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over all 64 top-level transcripts: group non-meta non-summary non-tool_result user records by parentUuid, call all but the last in each multi-member group a superseded draft, and intersect their uuids with the union of compactMetadata.preservedMessages.uuids / .allUuids / .anchorUuid from every compact_boundary record in the same file. Cross-check: csift search \"\" -t user.unsent --no-subagents --format json | tail -1, and csift show @<session> --line <draft> --raw.",
          "observed": "64 transcripts, 229 compact_boundary records, 3785 preserved uuids collected. 560 parentUuid groups with more than one user opener; 856 superseded drafts; 0 of the 856 appear in any preservedMessages uuid set, while 11 of the 560 resend siblings do. csift's own count over the same scope: matched 848 user.unsent records across 26 sessions. A sampled draft renders label user.unsent, superseded_draft true, turn_index null, and it plus its resend two lines later share one parentUuid.",
          "rule": "One draft per resend sibling (all but the last member of a parentUuid group); intersection counted per uuid, per file.",
          "note": "csift's 848 and the python 856 differ because csift's draft set is derived from opens_turn (which also admits answer/rejection/peer openers and excludes slash-command wrappers) while the python approximation keys on plain user records; both agree on 0 overlap with preservedMessages."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "drifted",
          "instrument": "code reading of search/hits.rs at 0.10.1 (the sections branch ran for a superseded record and emitted the section class with the forced labels vector) plus an e2e on 0.10.2: a pulse-shaped user record sharing its parentUuid with a later human message, `search -t user.unsent --format json` and `search -t harness.notification -c`",
          "observed": "0.10.1 path: class harness.notification.background-command carried labels [user.unsent]; 0.10.2: one hit with label and labels both user.unsent, the notification selector counts 0",
          "rule": "a hit's label must be a member of its own labels[]",
          "note": "fix: search/hits.rs computes no sections for a superseded record; e2e a_sectioned_draft_keeps_the_single_unsent_label"
        }
      ]
    },
    {
      "id": "REC-033",
      "area": "record-model",
      "behavior": "A conversation fork, rewind, retry or parallel lane leaves exactly one plain DAG fact: some record has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is live is NOT computable from the jsonl, because a parallel tool fan-out leaves the same shape; tool-result carriers, isMeta records and compaction summaries are never conversation children.",
      "depends": "`show --branch-points` reports the fork FACTS ranked by the widest inter-child time gap and never classifies; a live/abandoned classifier was prototyped and REFUTED against real corpora because parallel tool fan-out false-positives as abandoned branches on most sessions.",
      "code": [
        {
          "path": "src/show/branch.rs",
          "lines": "3-7",
          "snippet": "//! A Claude Code rewind, retry, or parallel lane leaves one plain DAG fact: some record\n//! has MORE THAN ONE conversation child (a later `parentUuid` re-attach). Which side is\n//! \"live\" is NOT computable from the jsonl: a live/abandoned classifier was prototyped\n//! and refuted against real corpora (parallel tool fan-out makes sibling leaves that\n//! false-positive as abandoned branches on most sessions)."
        }
      ],
      "instrument": "`csift show @<uuid> --branch-points --format json | jq 'select(.kind==\"branch-point\") | {uuid, line, widest_gap_seconds, children: [.children[].line]}'`; counting rule = one row per record with two or more conversation children, where a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
      "located": {
        "claude_code": null,
        "csift": "0.8.1",
        "source": "CHANGELOG 0.8.1 (show --branch-points); src/show/branch.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift show @<session> --branch-points --format json on the largest top-level transcript (415 MB) and on an 8.9 MB one, versus an independent python recount over the same 8.9 MB file that groups records by parentUuid after dropping non-user/assistant records, isMeta, isCompactSummary and any user record carrying a tool_result block.",
          "observed": "415 MB transcript: 319 branch-point rows, widest_gap_seconds min 0 / median 31 / max 14197, and 120 of the 319 have a widest gap below 2 seconds. 8.9 MB transcript: python recount = 4 branch points with child-line sets [[10,40],[57,60],[1569,1575,1582],[1845,1847,1849]]; csift emitted exactly 4 rows with exactly those child-line sets, ordered by widest gap 201 / 45 / 40 / 29 seconds. Emitted keys are exactly ['children','kind','line','uuid','widest_gap_seconds'] - no live/abandoned field on any row.",
          "rule": "One row per record with two or more conversation children; a conversation child is a user/assistant record that is not a tool_result carrier, not isMeta and not a compaction summary.",
          "note": "The independent recount reproduced csift's rows exactly, and the near-zero-gap population (120 of 319, 38%) is direct evidence for the refutation the claim records: a classifier keyed on sibling-ness would call that whole third of the rows abandoned branches when they are parallel tool fan-out."
        }
      ]
    },
    {
      "id": "REC-034",
      "area": "record-model",
      "behavior": "Mechanism confirmed exactly, including that the real numbers survive in usage.iterations on the zeroed copy. Numbers re-measured: across 5 large transcripts, 3 had 0 mismatching ids and 2 had exactly 1 each (not 3 in one session). A SECOND disagreement shape exists that the claim does not describe: a non-zeroed, non-uuid-duplicated pair where the first record carries a smaller output_tokens (2) than its siblings (1404) - a partial first record rather than a compaction replay. A per-field MAX is correct for both shapes; first-wins would be wrong for both, and in the second shape wrong in the direction of under-reporting.",
      "depends": "csift takes a per-field MAX rather than first-wins, which is identical on clean data, immune to the zeroed replay, and commutative so the rayon fold stays correct.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "252-256",
          "snippet": "                // Dedupe per FILE by message.id, taking the per-field MAX across the\n                // id's admitted records: identical on clean data, and immune to the\n                // compaction-replay shape where a replayed copy carries ZEROED usage\n                // (first-wins would depend on traversal order). An id-less record\n                // counts on its own, as before."
        }
      ],
      "instrument": "Group a large transcript's records by `message.id` and flag ids whose members disagree on any usage field; counting rule = per id, per file. Measured: 2 of 3 sampled sessions had 0 mismatches, one large session had 3 mismatching ids, each a duplicate-uuid replay whose second copy was all-zero.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.2",
        "source": "src/stats.rs comment; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over the 5 largest top-level transcripts under 300 MB: group records by message.id, build the tuple (input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens) per record, and flag ids with more than one distinct tuple; for each flagged id report whether any copy is all-zero and whether the copies share a uuid. Then read the two disagreeing lines directly.",
          "observed": "415 MB file: 7538 distinct message.ids, 1 id with more than one distinct usage tuple - 6 records, three at (2,708,474834,3997) and three at (0,0,0,0), sharing uuids across the two triples. 298 MB file: 12693 ids, 0 mismatches. 119 MB file: 6208 ids, 1 mismatch, NOT zeroed and NOT uuid-duplicated - (2,2,20853,80392) versus (2,1404,20853,80392). 99 MB file: 2906 ids, 0 mismatches. Direct read of the two zeroed-versus-real lines: identical uuid, identical timestamp 2026-06-06T08:47:12.527Z, DIFFERENT parentUuid, top-level usage all zero on the replay, and usage.iterations[0] on BOTH copies carrying the real (2,708,474834,3997).",
          "rule": "Per message.id, per file: count ids whose member records disagree on any of the four usage fields.",
          "note": "The two shapes together make the ordering-independence argument stronger than the claim states: neither first-wins nor last-wins is safe, because the bad copy is first in one shape and second in the other."
        }
      ]
    },
    {
      "id": "REC-035",
      "area": "record-model",
      "behavior": "Mechanism confirmed with a sharper signature than the claim gives: the child copy keeps the parent's cache_read_input_tokens byte-identical while carrying a near-zero output_tokens, which is what identifies it as the spawning message replayed into the child's opening context rather than an independent generation. Numbers re-measured on this corpus: 2 of 25 sessions affected; 32 shared (id,file) pairs over 20 ids; the largest single session's residual double-count is 14 records / 29419 output / 1586046 cache-read (the ledger's 4 ids / 12 files / 12 records / 19884 output / 7401557 cache-read describes a different session).",
      "depends": "csift scopes the usage dedupe PER FILE (one row per transcript, the TOTAL merging rows), because each copy is a genuine per-transcript fact; a pooled dedupe would have to pick arbitrarily between the parent and child values and would under-report a spanning `stats` total.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "337-340",
          "snippet": "/// Scope law: usage dedupe is PER FILE (each row already deduped by message.id). The\n/// same id CAN recur across a session's transcripts - the spawn message is copied into\n/// each child's opening context with its own usage - and each copy is a genuine\n/// per-transcript fact, so the TOTAL row sums them; it never dedupes across files."
        }
      ],
      "instrument": "Collect `message.id` -> (file, usage) pairs across one top-level transcript and its `subagents/` tree; counting rule = per (id, file). Expect a small number of ids present in both domains with differing usage. Measured: 4 ids shared across 12 subagent files in one session; the residual double-count in the scope TOTAL was 12 records / 19,884 output / 7,401,557 cache-read.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.2",
        "source": "SPEC.md section 6 v0.9.2 ledger item 2; src/stats.rs comment; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every top-level transcript under 250 MB that has a sibling subagents/ tree: build message.id -> (record count, max output_tokens, max cache_read_input_tokens) separately for the parent file and for each subagent transcript, then intersect the parent's id set with each child's.",
          "observed": "25 sessions with a subagents tree scanned. 2 of them carry at least one message.id present in BOTH the parent and a subagent transcript: 32 (id,file) shared pairs over 20 distinct ids, of which 15 pairs disagree with the parent on max output_tokens. In the 15-subagent session, the largest divergences were parent 4 records / 5598 output versus child 1 record / 4 output, and 4 / 5507 versus 1 / 4 - with cache_read_input_tokens IDENTICAL across the two sides (51101 and 77496 respectively). Residual that per-file dedupe leaves in that session's scope TOTAL: 14 records, 29419 output tokens, 1586046 cache-read tokens.",
          "rule": "Per (message.id, file) pair; a pair counts as shared when the id appears with a usage object in both the parent transcript and one subagent transcript.",
          "note": "The claim's design conclusion is unaffected: because the two sides genuinely differ, a pooled cross-file dedupe would have to choose between 5598 and 4 output tokens for one id, and either choice misstates one of the two transcripts."
        }
      ]
    },
    {
      "id": "REC-036",
      "area": "record-model",
      "behavior": "Transcript string content is JSON-encoded on the wire: a literal `\"` becomes `\\\"` and every control character below 0x20 plus DEL becomes an escape such as `\\n` or `\\uXXXX`, so a rendered `hello world` may be raw `hello\\nworld` and a rendered quote is never a raw quote - rendered text differs byte-for-byte from the raw line wherever whitespace or an escapable character sits.",
      "depends": "csift derives literal prefilter needles necessity-only and applies a per-needle safety predicate (no whitespace, no JSON-escaped character, at least 3 bytes); a needle failing it would silently drop real matches, which is the one place the no-silent-truncation contract actually bites.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "338-342",
          "snippet": "    // A char that JSON escapes inside a string does not survive verbatim in the raw\n    // line bytes the prefilter scans - emitting a literal for it causes false\n    // negatives. `\\` is already excluded via META above; guard `\"`, control chars,\n    // and DEL here.\n    if pattern.chars().any(json_escapes_in_string) {"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "535-537",
          "snippet": "/// True when `c` is escaped inside a JSON string literal (so it never appears\n/// verbatim in the raw line bytes): `\"`, any C0 control char (`< 0x20`), or DEL.\n/// (`\\` is handled separately as a regex metacharacter.)"
        }
      ],
      "instrument": "Compare `csift search 'hello world' @<session> -c` against `csift search 'hello\\s+world' @<session> -c` on a transcript whose text carries a newline between the words; the counts must agree with a `jq -r '.message.content[]?.text' <file> | grep -c` baseline. Counting rule: matched records with and without the whitespace in the pattern.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "AGENTS.md section 4; SPEC.md section 7d"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Live corpus: python3 read of one assistant/user text block containing an embedded newline plus a byte-level test of the carrying raw line. Fixture: a --claude-home tree holding a compact-JSON transcript whose user content is 'alpha\\nbravo and a quote \" here' and whose assistant text is 'charlie\\ndelta', then csift --claude-home <fixture> search '<pattern>' -c for four patterns.",
          "observed": "Live raw line: 37657 bytes, 343 occurrences of the two-byte sequence backslash-n, 388 of backslash-quote, and ZERO real 0x0A inside the line. The rendered pair 'instruction' + newline + 'agent' is present in the raw bytes only as 'instruction\\\\nagent'; the rendered form with a space is absent. Fixture counts: 'alpha\\s+bravo' -> 1, 'alpha bravo' -> 1, 'quote \" here' -> 1, 'charlie\\s+delta' -> 1.",
          "rule": "Byte-occurrence counts within one raw line; and matched-record counts (csift -c) per pattern against a fixture with exactly one matching record each.",
          "note": "Every pattern that a naive whitespace or quote needle would have gated away still returns its record, which is the property the per-needle safety predicate exists to guarantee. Note that a literal-space pattern also matched across a rendered newline, so the render-normalization path the claim mentions is live as well as the prefilter guard. Both matcher.rs snippets (338-342 and 526-528) are verbatim in the current file."
        }
      ]
    },
    {
      "id": "REC-037",
      "area": "record-model",
      "behavior": "EVERY `tool_use` block's matchable text is its `name` plus the RE-SERIALIZED JSON `input`, so a real newline inside for example a Bash `input.command` is already the two-character sequence backslash-n by match time.",
      "depends": "A csift regex must match the literal backslash-n there and `--multiline` is correctly irrelevant; `--multiline` helps only where rendered text keeps real newlines (message text, thinking, tool_result bodies).",
      "code": [
        {
          "path": "src/search/record_text.rs",
          "lines": "374-385",
          "snippet": "/// Render a `tool_use` block to searchable text: `name {json-input}`. The name is\n/// matched first so `csift search AskUserQuestion -t agent.tool.use` works; the input JSON is\n/// included so a regex can match arguments too.\npub(crate) fn render_tool_use(name: Option<&str>, input: Option<&serde_json::Value>) -> String {\n    let mut s = String::new();\n    if let Some(n) = name {\n        s.push_str(n);\n    }\n    if let Some(v) = input {\n        s.push(' ');\n        s.push_str(&v.to_string());\n    }"
        }
      ],
      "instrument": "`csift search 'cd /tmp\\\\n' <target> -t agent.tool.use` (hits) versus the same pattern with a real newline under `--multiline` (no hits). Counting rule: matched records.",
      "located": {
        "claude_code": null,
        "csift": "0.6.10",
        "source": "CHANGELOG 0.6.10; SKILL.md search --multiline caveat"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Fixture transcript carrying one tool_use block whose input.command is the REAL two-line string 'cd /tmpfx' newline 'echozulu', then: csift --claude-home <fixture> search 'cd /tmpfx\\\\nechozulu' -t agent.tool.use -c versus the same words with an actual newline in the pattern under --multiline.",
          "observed": "Raw line carries the bytes 'cd /tmpfx\\\\nechozulu' (verified true before searching). Literal two-character backslash-n pattern: 1 matched record. Real newline in the pattern with --multiline: 0 matched records.",
          "rule": "Matched-record count (csift -c) per pattern against a fixture holding exactly one tool_use record.",
          "note": "The asymmetry is exactly as claimed and is a consequence of render_tool_use serializing input with Value::to_string, so the JSON escape is what reaches the matcher. src/search/record_text.rs 351-362 is verbatim in the current file."
        }
      ]
    },
    {
      "id": "REC-038",
      "area": "record-model",
      "behavior": "Claude Code's own writers emit compact JSON (`\"role\":\"user\"`), but a line reserialized with whitespace around the colon (`\"role\": \"user\"` - a default JSON dump, a jq or editor round-trip) is valid JSON and the SAME record.",
      "depends": "csift's stage-1 candidate keeps use the serialization-tolerant matchers `line_has_role_marker` / `line_has_user_role_marker`; the old exact compact byte pair silently DROPPED a reserialized line one layer BEFORE any malformed counter could see it - no preview, no count, no match, zero disclosure. Every other prefilter needle must be a bare value substring or a key-only form, never a compact `\"key\":\"value\"` pair.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "42-56",
          "snippet": "/// Serialization-tolerant role-marker test - THE stage-1 candidate needle for\n/// message records (R13). CC's own wire format is compact JSON, but a hand-authored\n/// or reserialized line may carry whitespace around the colon (`\"role\": \"user\"`) -\n/// valid JSON, the same record. The old exact-byte needles (`\"role\":\"user\"`)\n/// silently DROPPED such lines one layer BEFORE any malformed counter could see\n/// them: not skipped, not counted, simply invisible on every surface. One `memmem`\n/// pass for the quoted key + an O(1) verify per hit; a keyless line costs ONE scan\n/// where the old disjunct cost two, so §7 holds (benchmarked on the real corpus).\n/// The quoted-key needle cannot match content text: inside a JSON string the quotes\n/// are escaped (`\\\"role\\\"`), which breaks the needle bytes.\npub(crate) fn line_role_value_matches(\n    line: &[u8],\n    accept_user: bool,\n    accept_assistant: bool,\n) -> bool {"
        },
        {
          "path": "src/parse/lines.rs",
          "lines": "87-98",
          "snippet": "/// `\"role\"` is `\"user\"` OR `\"assistant\"` (any JSON whitespace around the colon) -\n/// the candidate test for `search`/`show`/`verbatim`/`list`/`stats` stage-1 filters.\npub fn line_has_role_marker(line: &[u8]) -> bool {\n    line_role_value_matches(line, true, true)\n}\n\n/// `\"role\"` is `\"user\"` only - the genuine-user/carrier hook `files`/`recover` use\n/// (their assistant-side coverage rides tool-name needles, so admitting every\n/// assistant text record here would repeal their §7 prefilter).\npub fn line_has_user_role_marker(line: &[u8]) -> bool {\n    line_role_value_matches(line, true, false)\n}"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "373-378",
          "snippet": "    // v0.10.1 catch-all system subtypes: the key-only needle `\"subtype\"` (every system\n    // record carries it; a quoted KEY survives a reserialize, and the classify arm\n    // decides which subtype it is - the already-modeled ones simply reclassify).\n    static SUBTYPE_FINDER: std::sync::LazyLock<memmem::Finder<'static>> =\n        std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"subtype\\\"\"));\n    crate::parse::line_has_role_marker(line)"
        }
      ],
      "instrument": "Write a fixture transcript whose lines carry `\"role\": \"user\"` (a default `json.dumps`) into a `--claude-home` tree and confirm `csift search` finds its content and `--count-by label` counts the record. Counting rule: matched records for one known needle, expected 1. The regression is pinned in the `search` e2e modules under `tests/cli/`.",
      "located": {
        "claude_code": null,
        "csift": "0.6.9",
        "source": "AGENTS.md section 3.3a; SPEC.md section 7d; src/parse/lines.rs comment; AGENTS.md section 3.3a R13 needle law; CHANGELOG 0.6.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Fixture transcript written with python3 json.dumps at DEFAULT separators (so the wire form is '\"role\": \"user\"'), then: grep -c '\"role\":\"user\"' <file>; grep -c '\"role\": \"user\"' <file>; csift --claude-home <fixture> search 'needlefoxtrot' -c; csift --claude-home <fixture> search 'needlefoxtrot' --count-by label.",
          "observed": "File is one line, 299 bytes. Compact byte-pair needle: 0 hits. Spaced form: 1 hit. csift search -c: 1. csift --count-by label: '1  user.message', summary '1 matched record(s) across 1 label key(s)'.",
          "rule": "grep -c counts matching LINES; csift -c counts matched records. The fixture holds exactly one record, so the expected value is 1 in both csift modes and 0 for the compact needle.",
          "note": "The counterfactual is the load-bearing half and it reproduced: the compact byte pair scores zero on a record that is unambiguously a user message, so a prefilter built on it would drop the record with no malformed count and no disclosure. Only the code coordinates needed correction."
        }
      ]
    },
    {
      "id": "REC-039",
      "area": "record-model",
      "behavior": "The `Full output saved to:` and `persistedOutputPath` markers name content that lives OUTSIDE the transcript bytes, so text matched after resolution is by definition not a substring of the raw jsonl line.",
      "depends": "csift registers both as CONSERVATIVE synth markers that force a full scan when `--resolve-persisted` is on, because the byte prefilter would otherwise silently drop every match that lives only in the external file - the exact silent-truncation class the prefilter laws exist to prevent.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "522-526",
          "snippet": "    let mut conservative: Vec<&[u8]> = vec![b\"To tell you how to proceed\"];\n    if args.resolve_persisted {\n        conservative.push(b\"persistedOutputPath\");\n        conservative.push(b\"Full output saved to:\");\n    }"
        }
      ],
      "instrument": "Read the csift unit tests beside `src/search/` that pin `synth_marker_finders` under `--resolve-persisted`; expect the two conservative needles present only when the flag is set. Counting rule: one needle per pushed literal.",
      "located": {
        "claude_code": null,
        "csift": "0.6.0",
        "source": "AGENTS.md section 4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Two fixture transcripts, each with a tool_result pointing at an external tool-results file whose content holds a needle VERIFIED absent from the transcript bytes. Fixture A carries toolUseResult.persistedOutputPath; fixture B carries only the inline 'Full output saved to: <path>' line and no persistedOutputPath field. Then csift --claude-home <fixture> search '<needle>' -c with and without --resolve-persisted.",
          "observed": "Fixture A: needle in transcript bytes = False; without the flag 0 matched records, with --resolve-persisted 1. Fixture B: 'persistedOutputPath' absent from the file = True, needle in transcript bytes = False; without the flag 0, with --resolve-persisted 1. Control needle that IS in the transcript ('persisted-output'): 1 either way.",
          "rule": "Matched-record count (csift -c) per (fixture, flag) cell; each fixture holds exactly one resolvable record, so the expected cells are 0 without the flag and 1 with it.",
          "note": "This is a behavioural check rather than the test-reading the ledger proposed, and it is stronger: the needle appears nowhere in the file's bytes, so the stage-1 byte prefilter must have gated the whole file out unless the conservative marker forced the full scan. Both markers were exercised independently, so neither is dead. src/search/matcher.rs 513-517 is verbatim, including the args.resolve_persisted gate."
        }
      ]
    },
    {
      "id": "REC-040",
      "area": "record-model",
      "behavior": "The growth half is measured and holds. The torn-fragment half was NOT witnessed in the live corpus: 0 of 7607 transcripts end without a trailing newline at rest, so Claude Code's appends are line-atomic at these sizes and the torn tail is a defensive property rather than an observed shape. Shrink was not exercised.",
      "depends": "csift does not silently skip a torn final fragment - it COUNTS it. A 120-byte truncated final line is reported as skipped_lines: 1 on both search and the stats whole-file census, with exit 0 and the complete records still matched. The accurate statement is that csift never crashes or mis-parses on a torn tail and always discloses it, which is the no-silent-failure contract rather than a skip.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "101-109",
          "snippet": "pub(crate) fn line_payload(line: &[u8]) -> Option<&[u8]> {\n    let line = line.strip_suffix(b\"\\n\").unwrap_or(line);\n    let line = line.strip_suffix(b\"\\r\").unwrap_or(line);\n    if line.iter().all(u8::is_ascii_whitespace) {\n        None\n    } else {\n        Some(line)\n    }\n}"
        }
      ],
      "instrument": "Run `csift search '' @main` repeatedly against the currently-writing session while it works; every run must exit 0 with a stable `skipped_lines` count. Counting rule: `skipped_lines` from the JSON summary across repeated runs.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 7a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) Growth: a background python3 appender writing 300 records at 20 ms intervals with flush+fsync into a fixture transcript, while 10 successive csift --claude-home <fixture> search 'growneedle' @<prefix> -c runs (plus a --format json run per iteration for skipped_lines) scan the same growing file. (b) Torn line: a fixture whose final line is a JSON record truncated to 120 bytes with NO trailing newline, read by csift search --format json and by csift stats --format json. (c) Corpus census: seek to the last byte of every *.jsonl under ~/.claude/projects (journal.jsonl excluded) and count files whose final byte is not 0x0A.",
          "observed": "(a) file size climbed 0 -> 1456 -> 3245 -> 5039 -> 7132 -> 8926 -> 10720 -> 13112 -> 14906 -> 16700 bytes across the 10 scans; every scan exited 0; matched rose monotonically 0,5,12,18,24,30,37,44,50,56; skipped_lines was 0 on all 10; the post-run scan matched all 300 records at 89856 bytes. (b) exit 0, the complete record still matched, and the torn fragment was reported as skipped_lines: 1 by BOTH search and stats (stats line_types {'user': 1}, turns 1). (c) 7607 transcripts sampled, 0 zero-byte, 0 whose final byte is not a newline.",
          "rule": "(a) one scan per iteration, comparing exit code, matched count and skipped_lines against the file size read immediately before. (b) skipped_lines from the JSON summary. (c) one row per transcript file, final byte only.",
          "note": "What would decide the unwitnessed halves: sampling a transcript's final byte in a tight loop while a session streams a very large tool result would catch a partial write if one is ever observable; and a truncate-under-open-mmap test would decide the shrink half, which no instrument here exercised."
        }
      ]
    },
    {
      "id": "REC-041",
      "area": "record-model",
      "behavior": "A cloned transcript's copied records are REWRITTEN, not byte-copied: each carries the CLONING build's `version`, not the version the origin stamped on that same record. `uuid` and `timestamp` survive byte-identically, while `version`, `sessionId` and `sessionKind` are rewritten and `slug` is stripped. Measured on the one clone/origin pair in the corpus: all 558 shared uuids read 2.1.251 in the origin and 2.1.252 in the clone (0/558 equal), with identical timestamps, `slug` present->absent and `sessionKind` null->\"bg\".",
      "depends": "csift reports `version_first` from the head window and `version` from the tail window, so a clone reports the clone-time build as its first - one of the visible symptoms of the fork.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "134-138",
          "snippet": "        // The base fields are LAST-seen (what the session is on NOW); the head\n        // capture becomes the *_first pair. Either window can be empty - fall back\n        // to the other so a one-record session reports the same value everywhere.\n        version: version_last.clone().or_else(|| version.clone()),\n        version_first: version.or(version_last),"
        }
      ],
      "instrument": "`csift search '' @<id> --count-by version` on the clone beside the same census on its origin is only SUGGESTIVE - the clone's single head version could in principle be the origin's own tail version. The decisive instrument is a uuid JOIN across the two transcripts: intersect the top-level `uuid` sets and compare `version` (and every other top-level field) on the shared records. Counting rule = one per shared uuid; the claim holds iff origin.version != clone.version on all of them.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.3",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) csift list --max-count 0 --no-subagents --format json > list_all.json  (enumerate every top-level transcript under ~/.claude/projects and read the clone_of / version_first / version_last fields); (2) csift search '' @<clone-id> --no-subagents --count-by version  and  csift search '' @<origin-id> --no-subagents --count-by version; (3) the decisive one - a python3 uuid JOIN over the two .jsonl files in the single project dir that holds the pair: load every non-blank line of each file, key by top-level `uuid`, intersect the key sets, and for each shared uuid compare every top-level field of the origin record against the clone record. Session ids and the project dir are redacted here; the commands are otherwise literal.",
          "observed": "Corpus scope: 64 top-level transcripts across 14 project dirs; EXACTLY 1 row has a non-null `clone_of`. That row: version_first=2.1.252, version(last-seen)=2.1.257; its named origin row: version_first=2.1.220, version_last=2.1.252. csift census, clone: `1037 2.1.252` + `813 2.1.257` (1850 matched records, 2 version keys). csift census, origin: `8654 2.1.231`, `7219 2.1.220`, `2283 2.1.223`, `756 2.1.221`, `371 2.1.251`, `9 2.1.252` (19292 matched records, 6 version keys). uuid JOIN: clone = 3943 distinct uuids over 5941 non-blank lines, origin = 70729 over 91975, intersection = 558 uuids. On those 558: `version` is 2.1.251 in the origin and 2.1.252 in the clone for 558/558 - shared-with-SAME-version = 0. `timestamp` is byte-identical on 558/558. The other fields that differ on all 558 are `sessionId` (rewritten to the clone's own id), `slug` (present in the origin, ABSENT in the clone) and `sessionKind` (null in the origin, \"bg\" in the clone); `promptId` differs on 89, `cwd` on 5, `parentUuid` on 2, `message` on 2. The clone's first TIMESTAMPED record is `type:\"system\" subtype:\"compact_boundary\"` at file line 28, version 2.1.252, ts 2026-08-31T11:41:41.866Z, compactMetadata.trigger=auto, preTokens=976521, postTokens=165848; the SAME uuid in the origin carries version 2.1.251, a slug, and sessionKind null. The clone file's birthtime is 2026-09-01 13:00:42 local, i.e. AFTER the earliest record timestamp it carries (2026-08-31T11:35:10.097Z) - timestamps predate the file. Control that the writer stamps its own build: `csift search '' --count-by version --no-subagents --since 6h` returns `1229 2.1.258` as the top key, and 2.1.258 is the build in Claude Code's versions directory (~/.local/share/claude/versions/).",
          "rule": "Clone enumeration: one row per top-level transcript from `csift list --format json` (kind==\"session\"), counted as a clone iff `clone_of` is non-null. Version census: one count per RECORD keyed by that record's top-level `version` (csift's --count-by version counts matched records; the empty pattern makes it a whole-scope census, so it sees only role-bearing candidate records - 1850 of the clone's 5941 lines). uuid join: one count per top-level `uuid` present in BOTH files; a uuid is 'version-rewritten' iff origin.version != clone.version. All-line version census on the clone (every non-blank line, not just candidates): 2161 x 2.1.252, 1782 x 2.1.257, 1998 with no version field.",
          "note": "Code site confirmed: the 5-line snippet is present VERBATIM in src/session/summarize.rs at lines 134-138 (byte-exact substring test), so no code correction was needed. Coverage caveat a stranger should know: the corpus contains exactly ONE clone (1 of 64 top-level transcripts), and it was minted while Claude Code was on 2.1.252 - so the rewrite mechanism is witnessed at 2.1.252, not re-witnessed at 2.1.258. What was independently confirmed at 2.1.258 is only the underlying premise that the writing build stamps its own `version` on every record (1229 records written in the last 6h all read 2.1.258). A 2.1.258-minted background fork would be needed to re-witness the rewrite at the current build; the prediction is that its copied records would read 2.1.258 while the same uuids in the origin read whatever build wrote them. Two supporting details surfaced by the join that the claim did not mention and that make the fork mechanically legible: the clone stamps `sessionKind:\"bg\"` on the copied records (the origin has sessionKind null on all 91975 of its lines), and `strings -n 6 <claude-versions-dir>/2.1.258 | rg sessionKind` shows 16 hits in the current build including the literal ` filtered from /resume: sessionKind=#` and a guard reading `G1(t,\"sessionKind\");return a===\"daemon\"||a===\"daemon-worker\"`, so sessionKind is still a live field at 2.1.258."
        }
      ]
    },
    {
      "id": "REC-042",
      "area": "record-model",
      "behavior": "A `Read` tool result echoes `toolUseResult` as `{type, file:{...}}`, and for the `text` variant the `file` object carries exactly five always-present keys - `filePath`, `content`, `startLine` (1-based), `numLines`, `totalLines` - plus one optional sixth, `truncatedByTokenCap`, which the schema alone declares `.optional()`.",
      "depends": "csift `recover` reads those five keys to mint a read anchor or a windowed splice - a rename of any one silently degrades every Read into a zero-line partial and `recover --file` reports `no recoverable history` - and it never reads `truncatedByTokenCap`, so its full-versus-partial decision is blind to the one key that says the window was capped.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "55-58",
          "snippet": "    // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "64-71",
          "snippet": "            let start_line = file\n                .get(\"startLine\")\n                .and_then(serde_json::Value::as_u64)\n                .unwrap_or(1) as usize;\n            let total_lines = file\n                .get(\"totalLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "179-181",
          "snippet": "    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        }
      ],
      "instrument": "Parse every jsonl line under ~/.claude/projects whose `toolUseResult.file` is an object and tally `tuple(sorted(file))`. Counting rule: one tally per echo. Measured 2026-09-02 over the 120 most-recently-modified top-level transcripts: the modal tuple is `('content','filePath','numLines','startLine','totalLines')` with 5600 records, the next is the same tuple plus `truncatedByTokenCap` (99). A second sweep over 20 sampled transcripts of 500 KB-25 MB: 433 five-key, 30 six-key, 29 `('filePath',)` (the `file_unchanged` variant), 1 `('count','filePath','originalSize','outputDir')` (the `parts` variant). Schema authority: the Read-result zod union in <the Claude Code 2.1.258 binary> declares only `truncatedByTokenCap` optional.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.7 (the recover event-source table, `full Read` / `windowed Read` rows); measured now"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > cc258.strings; rg -F truncatedByTokenCap cc258.strings   # then print the 400 chars before each hit\nAND the corpus key-tuple tally: python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n  for raw in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in raw: continue\n   try: r=json.loads(raw)\n   except Exception: continue\n   t=r.get('toolUseResult')\n   if not isinstance(t,dict): continue\n   fo=t.get('file')\n   if not isinstance(fo,dict): continue\n   c[(sc,t.get('type'),tuple(sorted(fo)))]+=1\nprint(c)\nEOF",
          "observed": "Binary (2.1.258), the Read-result zod union, verbatim: 'c({type:x(\"text\"),file:c({filePath:i().describe(\"The path to the file that was read\"),content:i().describe(\"The content of the file\"),numLines:A().describe(\"Number of lines in the returned content\"),startLine:A().describe(\"The starting line number\"),totalLines:A().describe(\"Total number of lines in the file\"),truncatedByTokenCap:M().optional().describe(\"True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page). A programmatic signal for internal consumers; survives output reconstruction (unlike the render-time banner).\")})' - the five keys carry no .optional(), only truncatedByTokenCap does. Corpus tally 2026-09-02: file key tuple ('content','filePath','numLines','startLine','totalLines') 8434 records and the same tuple plus 'truncatedByTokenCap' 174 records = every one of the 8608 `text` echoes; no `text` echo carries any other tuple and none is missing a key. Top-level transcripts alone read 5601 five-key + 99 six-key (the claim's 5600/99, +1 as the corpus grew). Other variants in the same tally: ('filePath',) 120 (file_unchanged), ('count','filePath','originalSize','outputDir') 26 (parts), ('base64','dimensions','originalSize','type') 435 and ('base64','originalSize','type') 9 (image), ('base64','filePath','originalSize') 4 (pdf).",
          "rule": "One observation per jsonl record under ~/.claude/projects whose parsed toolUseResult is a dict with a dict `file`; key = (top-level vs subagent transcript, toolUseResult.type, sorted tuple of file's keys). Scope = every *.jsonl under the projects root (86 top-level + 7736 subagent transcripts, 1.31M lines).",
          "note": "Both instruments agree with the claim as written. The schema is unchanged at 2.1.258 and every text echo on disk carries the five keys. Counts are live-corpus totals and drift upward as sessions run; the claim's top-level-only numbers reproduced to +1. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58, :64-71 and :179-181."
        }
      ]
    },
    {
      "id": "REC-043",
      "area": "record-model",
      "behavior": "A `Read` result's `toolUseResult` is a `type`-discriminated union with exactly six variants - `text`, `image`, `notebook`, `pdf`, `parts` and `file_unchanged` - each carrying its own differently-shaped `file` sub-object (`image` gives `{base64, originalSize, type}` plus an OPTIONAL `dimensions`; `parts` gives `{count, filePath, originalSize, outputDir}` and may carry the sibling keys `firstPage` and `pages`, `pages` being transient and absent on the persisted record).",
      "depends": "csift's `recover` Read arm keys only on the presence of a `file` object with a matching `filePath`, never on `type`, so every non-`text` variant reaches the same code path and only the `total > 0` gate in `push_read_event` stops a contentless variant from minting a bogus empty anchor; csift's own doc comment enumerates just five values (`create`, `update`, `file_unchanged`, `text`, `image`) and is the incomplete map a future contributor reads.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "55-58",
          "snippet": "    // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "178-181",
          "snippet": "    let lines: Vec<String> = split_lines(content);\n    let observed = num_lines.unwrap_or(lines.len());\n    let total = total_lines.unwrap_or(observed.max(start_line + lines.len().saturating_sub(1)));\n    let is_full = start_line == 1 && observed >= total && total > 0;"
        },
        {
          "path": "src/model/mutation.rs",
          "lines": "181-184",
          "snippet": "    /// `toolUseResult.type` ∈ {`create`, `update`, `file_unchanged`, `text`, `image`};\n    /// only `create` ⇒ `is_create = true`, everything else ⇒ `false`. When there is no\n    /// `toolUseResult`, it is not an object, or it has no `filePath`, the result is\n    /// empty (defensive arms, each tested)."
        }
      ],
      "instrument": "Tally `Counter(tur['type'])` over every jsonl record whose `toolUseResult.file` is an object. Counting rule: one tally per echo. Measured 2026-09-02 over every *.jsonl under ~/.claude/projects: text 8608, image 444, file_unchanged 120, parts 26, pdf 4 (9202 file-bearing echoes); `notebook` is declared by the schema but occurs 0 times in this corpus. Schema authority: the Read-result zod union in the Claude Code 2.1.258 binary carries all six discriminants.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now; src/recover/carriers.rs:55-89 (the Read arm keys on `file`, not on `type`); src/model/mutation.rs:181-184 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import sys;l=open('cc258.strings',errors='replace').read().split('\\n');s=[x for x in l if 'truncatedByTokenCap' in x][2];i=s.find('vi(\\\"type\\\",[');print(s[i:i+3400])\"   # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND the corpus variant tally: python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n  for raw in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in raw: continue\n   try: r=json.loads(raw)\n   except Exception: continue\n   t=r.get('toolUseResult')\n   if not isinstance(t,dict): continue\n   fo=t.get('file')\n   if not isinstance(fo,dict): continue\n   c[(sc,t.get('type'),tuple(sorted(fo)))]+=1\nprint(c)\nEOF",
          "observed": "Binary (2.1.258): one discriminated union, in order - c({type:x(\"text\"),file:c({filePath,content,numLines,startLine,totalLines,truncatedByTokenCap optional}),artifactRead optional}), c({type:x(\"image\"),file:c({base64,type,originalSize,dimensions:c({originalWidth,originalHeight,displayWidth,displayHeight}).optional()})}), c({type:x(\"notebook\"),file:c({filePath,cells})}), c({type:x(\"pdf\"),file:c({filePath,base64,originalSize})}), c({type:x(\"parts\"),file:c({filePath,originalSize,count,outputDir}),firstPage optional,pages optional}), c({type:x(\"file_unchanged\"),file:c({filePath}),source:x(\"seeded\").optional()}) - exactly six variants. Corpus 2026-09-02: text 8608, image 444, file_unchanged 120, parts 26, pdf 4 (9202 file-bearing echoes); notebook 0. Image file shapes split ('base64','dimensions','originalSize','type') 435 vs ('base64','originalSize','type') 9. Top-level toolUseResult key tuples: ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2.",
          "rule": "One observation per record whose toolUseResult is a dict with a dict `file`, keyed by toolUseResult.type and by the sorted key tuples of both the toolUseResult and its `file`. Same whole-projects-root scope as REC-042.",
          "note": "The six-variant union is confirmed verbatim in the 2.1.258 binary and the five observable discriminants reproduce on disk (counts +3 on `text` as the corpus grew). Two wording corrections: `dimensions` is declared .optional() and is absent on 9 of 444 image echoes, so the image `file` shape is not fixed at four keys; and `parts` carries two further siblings of `file` (`firstPage` seen on 2 records, `pages` documented as never persisted). `notebook` cannot be exercised here - no .ipynb has been read on this machine - so its on-disk shape stays schema-only. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58, :178-181 and src/model/mutation.rs:181-184."
        }
      ]
    },
    {
      "id": "REC-044",
      "area": "record-model",
      "behavior": "The `Read` echo's `toolUseResult.file.content` is RAW file text: no line-number gutter, no `<system-reminder>` wrapper and no ellipsis marker, even when the read was windowed or token-cap truncated. The `NNN\\t<text>` gutter exists only in the model-facing `tool_result` block text.",
      "depends": "csift `recover` splices `file.content` into the reconstruction buffer verbatim with no gutter strip, so `recover --file X > X` would emit gutter-prefixed garbage if the echo ever carried the decorated form.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "59-63",
          "snippet": "            let content = file\n                .get(\"content\")\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default()\n                .to_string();"
        },
        {
          "path": "src/recover/diff.rs",
          "lines": "165-170",
          "snippet": "/// Split content into lines WITHOUT a trailing empty element for a final newline (so a\n/// file `\"a\\nb\\n\"` is `[\"a\",\"b\"]`, matching how CC numbers lines).\npub(crate) fn split_lines(content: &str) -> Vec<String> {\n    if content.is_empty() {\n        return Vec::new();\n    }"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "59-64",
          "snippet": "            let content = file\n                .get(\"content\")\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default()\n                .to_string();\n            let start_line = file"
        }
      ],
      "instrument": "For every `text` Read echo carrying a string `file.content`, classify its FIRST line by whether it matches a digits-then-tab or digits-then-arrow gutter. Counting rule: one tally per echo. Measured 2026-09-02 over the 120 most-recently-modified top-level transcripts: none 5699, tab 0, arrow 0; a corpus-wide sweep read 0 gutter-shaped of 8608.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.7 (`content is RAW, no gutter`); measured now"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  for raw in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in raw: continue\n   try: t=json.loads(raw).get('toolUseResult')\n   except Exception: continue\n   if not isinstance(t,dict) or t.get('type')!='text': continue\n   ct=(t.get('file') or {}).get('content')\n   if not isinstance(ct,str): continue\n   c['gutter' if re.match(r'^\\s*\\d+[\\t\\u2192]',ct[:60]) else 'raw']+=1\n   c['sysrem'] += ct.startswith('<system-reminder>')\n   c['trunc_banner'] += '[Truncated: PARTIAL view' in ct[:200]\nprint(c)\nEOF",
          "observed": "{'raw': 8609} - zero gutter-shaped first lines, zero contents opening `<system-reminder>`, zero contents carrying the `[Truncated: PARTIAL view` banner; the 122 echoes stamped version 2.1.258 are all raw. Firsthand cross-check at 2.1.258: a Read of a purpose-built 12-line file whose model-facing render came back as `1\\tzeta01` ... `13\\t` - the gutter lives only on the render side.",
          "rule": "One observation per `text` Read echo carrying a string `file.content`; classify the first 60 characters by the regex `^\\s*\\d+[\\t\\u2192]` (tab or arrow gutter) versus raw. Whole projects root.",
          "note": "8609 of 8609 text echoes carry undecorated file text; the count is one above the claim's 8608 only because the corpus grew during the sweep. Neither the render-time truncation banner nor a system-reminder ever leaks into the structured `file.content`. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:59-63 and :59-64, src/recover/diff.rs:165-170."
        }
      ]
    },
    {
      "id": "REC-045",
      "area": "record-model",
      "behavior": "The MODEL-facing side of a Read - the `tool_result` block's `content` string, the only copy a subagent lane keeps - IS gutter-rendered as `<line-number><TAB><text>` per line, and 265 of 8608 instead carry a bare `<system-reminder>` warning such as `Warning: the file exists but the contents are empty.` or `the file exists but is shorter than the provided offset`.",
      "depends": "csift never parses the rendered side for content, so it loses nothing on a top-level session - and it loses nothing on a built-in Task or teammate subagent transcript either, since those DO carry the structured echo (3129 file-bearing echoes corpus-wide, 18 at CC 2.1.258). It is the WORKFLOW subagent transcript (subagents/workflows/**) that carries no `toolUseResult` at all - 0 file-bearing echoes corpus-wide - so there the gutter-rendered `tool_result` is the ONLY surviving copy of the file text and `recover` cannot use it.",
      "code": [
        {
          "path": "src/recover/diff.rs",
          "lines": "185-188",
          "snippet": "/// Strip a leading line-number gutter from each line of a cat -n style snippet. Handles\n/// BOTH the TAB gutter (`\\d+\\t<text>`, what current CC Read content uses) and the arrow\n/// gutter (`\\d+→<text>`, an older form). Returns `(file_line_no, text)` pairs; a line\n/// with no recognizable gutter is skipped (we never fabricate a number)."
        }
      ],
      "instrument": "Corpus: for records carrying both a `toolUseResult.file` and a string `tool_result` block, test the render's first line against `^\\s*\\d+\\t`; one observation per record. Measured now: 8343 gutter-rendered, 265 other (all `<system-reminder>` warnings).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections,re\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  shape='top' if os.sep+'subagents'+os.sep not in dp else ('workflow' if os.sep+'workflows'+os.sep in dp else 'builtin')\n  for raw in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in raw: continue\n   try: r=json.loads(raw)\n   except Exception: continue\n   t=r.get('toolUseResult')\n   if not isinstance(t,dict) or not isinstance(t.get('file'),dict): continue\n   c[('echo',shape)]+=1\n   if t.get('type')!='text': continue\n   txt=None\n   for b in (r.get('message') or {}).get('content') or []:\n    if isinstance(b,dict) and b.get('type')=='tool_result' and isinstance(b.get('content'),str): txt=b['content']\n   if txt is None: continue\n   if re.match(r'^\\s*\\d+\\t',txt.split('\\n',1)[0]): c['gutter']+=1\n   else: c['sysrem' if txt.startswith('<system-reminder>') else 'other']+=1\nprint(c)\nEOF",
          "observed": "gutter 8343, sysrem 265, other 0 - an exact reproduction of the claim's 8343/265 of 8608. The 265 break down as `<system-reminder>Warning: the file exists but the contents are empty.` 100, `...is shorter than the provided offset` 27, `<system-reminder>[Truncated: PARTIAL view - showing lines 1-N of M ...` and `This memory is N days old` for the rest. Structured-echo census by transcript shape: top 6074, built-in-Task/teammate subagent 3129 (18 of them stamped 2.1.258), WORKFLOW subagent 0. Firsthand at 2.1.258: a Read issued inside a workflow-subagent lane produced a tool_result record whose render was `1\\tzeta01` ... `13\\t` and which carried no `toolUseResult` field at all (that transcript: 87 records, 0 with toolUseResult).",
          "rule": "One observation per record that carries BOTH a dict `toolUseResult.file` of type `text` AND a string `tool_result` block; test the render's first line against `^\\s*\\d+\\t`, else bucket by whether it opens `<system-reminder>`. The shape census counts one observation per file-bearing echo, keyed by transcript location (top-level / subagents/agent-*.jsonl / subagents/workflows/**).",
          "note": "The behavior claim reproduces exactly (8343 gutter / 265 system-reminder of 8608). The refinement is in the depends clause: 'a subagent transcript has no structured echo' is true only of workflow-subagent transcripts. Built-in Task/teammate subagent transcripts carry structured echoes at every CC version in this corpus including 2.1.258, so `recover` retains full fidelity there; the blind spot is narrower than stated but real - verified firsthand by reading two probe files from inside a workflow subagent lane and finding zero toolUseResult fields in that transcript. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/diff.rs:185-188."
        }
      ]
    },
    {
      "id": "REC-046",
      "area": "record-model",
      "behavior": "A re-Read of a file unchanged since the last Read echoes `{type:\"file_unchanged\", file:{filePath}}` - the `file` object carries ONLY `filePath`, no `content`, no `startLine`, no `numLines`, no `totalLines` - plus an optional `source:\"seeded\"` when the dedup matched a startup-seeded entry (CLAUDE.md / nested memory) rather than a prior Read result. The model-facing tool_result text depends on which of the two it was: without `source` it opens `Wasted call - file unchanged since your last Read. Refer to that earlier tool_result instead.` (the dash is an em dash), while a `source:\"seeded\"` record instead opens `<system-reminder>This file is already in your context (see \"Contents of ...\") and has not changed on disk.`",
      "depends": "csift's `recover` Read arm matches this record on `file.filePath` and then defaults `content` to the empty string and `startLine` to 1, so the echo would become a zero-line snapshot; the `total > 0` clause of `is_full` demotes it to an empty `PartialRead`, so a `file_unchanged` echo neither anchors nor corrupts the buffer.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "181-188",
          "snippet": "    let is_full = start_line == 1 && observed >= total && total > 0;\n    if is_full {\n        events.push(FileEvent {\n            line_no,\n            turn_index,\n            timestamp_utc: ts.clone(),\n            kind: EventKind::FullSnapshot {\n                content: content.to_string(),"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "56-63",
          "snippet": "    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {\n            let content = file\n                .get(\"content\")\n                .and_then(serde_json::Value::as_str)\n                .unwrap_or_default()\n                .to_string();"
        }
      ],
      "instrument": "Tally `tuple(sorted(tur))`, `tuple(sorted(tur['file']))` and (tur.get('source'), tool_result render prefix) for every `toolUseResult` whose `type` is `file_unchanged`. Counting rule: one tally per record. Measured 2026-09-02 over every *.jsonl under ~/.claude/projects: 120 records, `('filePath',)` on all 120 file objects and zero carrying `content`; 116 keyed ('file','type') opening the Wasted-call text, 4 keyed ('file','source','type') with source=='seeded' opening the already-in-your-context reminder; observed on Claude Code 2.1.156 through 2.1.231. Binary cross-check: `rg -c -F 'file unchanged since your last Read'` over the 2.1.258 strings returns 1.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -c -F 'file unchanged since your last Read' cc258.strings   # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND: the corpus sweep of REC-045's shape, restricted to toolUseResult.type=='file_unchanged', tallying (tur['source'], first 28 chars of the tool_result render) and tuple(sorted(tur)) / tuple(sorted(tur['file'])).",
          "observed": "Binary count 1, and the surrounding definitions verbatim: q=\"File unchanged since last read. The content from the earlier Read tool_result in this conversation is still current \\u2014 refer to that instead of re-reading.\", A=\"Wasted call \\u2014 file unchanged since your last Read. Refer to that earlier tool_result instead.\", R=\"<system-reminder>This file is already in your context\". Schema: c({type:x(\"file_unchanged\"),file:c({filePath:i().describe(\"The path to the file\")}),source:x(\"seeded\").optional().describe(\"Set when the dedup matched a startup-seeded entry (CLAUDE.md / nested memory) rather than a prior Read tool_result\")}). Corpus: 120 file_unchanged records - file keys ('filePath',) on all 120; toolUseResult keys ('file','type') 116 and ('file','source','type') 4 with source=='seeded' on all 4; render pairing is exact - the 116 sourceless records open 'Wasted call \\u2014 file unchanged since your last Read. Refer to that earlier tool_result instead.' and the 4 seeded records open '<system-reminder>This file is already in your context (see \"Contents of ...\")'. Versions carrying the variant: 2.1.156 through 2.1.231, none newer in this corpus.",
          "rule": "One observation per record whose toolUseResult.type == 'file_unchanged', keyed by the sorted key tuples and by (source, render prefix). Whole projects root.",
          "note": "Shape, key tuples and the seeded flag reproduce exactly (116 + 4 = 120). Two corrections: the separator in the render is an em dash and the sentence continues 'Refer to that earlier tool_result instead.', and the seeded variant does NOT use that opener - it emits the '<system-reminder>This file is already in your context' form instead, a 1:1 split in the data. The variant is still declared and its string still present in 2.1.258, but no record newer than 2.1.231 in this corpus exercised it, so the on-disk-at-2.1.258 shape is unobserved here. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:56-63 and :181-188."
        }
      ]
    },
    {
      "id": "REC-047",
      "area": "record-model",
      "behavior": "A `Read` clipped by the token cap sets `toolUseResult.file.truncatedByTokenCap:true` and, in every observed case, keeps `startLine:1` while making `numLines` strictly less than `totalLines`.",
      "depends": "csift `recover` does not read `truncatedByTokenCap`; it relies entirely on `numLines >= totalLines` to decide a full-file anchor, so a token-capped Read staying `numLines < totalLines` is what keeps a partial file from being replayed as ground truth.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "68-75",
          "snippet": "            let total_lines = file\n                .get(\"totalLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);\n            let num_lines = file\n                .get(\"numLines\")\n                .and_then(serde_json::Value::as_u64)\n                .map(|n| n as usize);"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "17-23",
          "snippet": "    /// Full ground-truth content (an anchor): a Write result, a full Read\n    /// (`startLine==1 && numLines==totalLines`), or a `file` attachment.\n    FullSnapshot {\n        content: String,\n        total_lines: usize,\n        source: SnapSource,\n    },"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'truncatedByTokenCap' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    fo=t.get('file') if isinstance(t,dict) else None\n    if isinstance(fo,dict) and fo.get('truncatedByTokenCap'): k[(fo.get('startLine'),fo.get('numLines')==fo.get('totalLines'))]+=1\nprint(k)\nEOF\nCounting rule: one tally per Read echo with `file.truncatedByTokenCap` truthy, keyed by `(startLine, numLines==totalLines)`. Measured 2026-09-02: `(1, False)` 99 records, no other key.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); k=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  sc='sub' if os.sep+'subagents'+os.sep in dp else 'top'\n  for line in open(os.path.join(dp,f),errors='replace'):\n   if 'truncatedByTokenCap' not in line: continue\n   try: t=json.loads(line).get('toolUseResult')\n   except Exception: continue\n   fo=t.get('file') if isinstance(t,dict) else None\n   if isinstance(fo,dict) and fo.get('truncatedByTokenCap'):\n    k[(sc,t.get('type'),fo.get('startLine'),fo.get('numLines')==fo.get('totalLines'))]+=1\nprint(k)\nEOF",
          "observed": "Counter({('top','text',1,False): 99, ('sub','text',1,False): 75}) - 174 token-capped echoes, every one startLine 1, every one numLines != totalLines (and numLines < totalLines in all 174, since they are also the intersection of the partial bucket). The claim's 99 is exactly the top-level-transcript half. Binary corroboration (2.1.258): truncatedByTokenCap is described as 'True when a whole-file read was auto-paginated because it exceeded the token cap (the content is a partial first page)', and the construction clamps only on that path - 'startLine:Ue!==void 0?Math.max(1,f):f' with '...Ue!==void 0&&{truncatedByTokenCap:!0}' in the same object literal.",
          "rule": "One observation per Read echo whose `file.truncatedByTokenCap` is truthy, keyed by (transcript scope, type, startLine, numLines==totalLines). Whole projects root.",
          "note": "Reproduces with no exception in 174 records. The binary explains why: the cap fires only on a WHOLE-file read (hence startLine 1) and the emitted content is a partial first page (hence numLines < totalLines), so csift's numLines>=totalLines full-anchor test cannot be fooled by a token-capped read even though it never reads the flag. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:68-75 and src/recover/types.rs:17-23."
        }
      ]
    },
    {
      "id": "REC-048",
      "area": "record-model",
      "behavior": "`startLine` is echoed back verbatim from the caller's `offset` when the read was not truncated, so it can be `0` - observed on two Read echoes at Claude Code 2.1.211 with `startLine:0, numLines:55, totalLines:601`.",
      "depends": "csift's `is_full` demands `start_line == 1` exactly, so a `startLine:0` whole-file read is demoted to a partial (the safe direction) and `PartialRead` clamps the splice base with `start_line.max(1)`; a future rule keying on `startLine <= 1` would flip that safety.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "64-67",
          "snippet": "            let start_line = file\n                .get(\"startLine\")\n                .and_then(serde_json::Value::as_u64)\n                .unwrap_or(1) as usize;"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "198-202",
          "snippet": "            kind: EventKind::PartialRead {\n                start_line: start_line.max(1),\n                lines,\n                total_lines: total,\n            },"
        }
      ],
      "instrument": "Corpus: count echoes whose `toolUseResult.file.startLine` is exactly `0`, one observation per echo. Measured now over all of ~/.claude/projects: 2 of 9199, both at record `version` 2.1.211. Binary: the construction reads `startLine:Ue!==void 0?Math.max(1,f):f`, so the clamp applies only on the truncated path.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  for line in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in line: continue\n   try: r=json.loads(line)\n   except Exception: continue\n   t=r.get('toolUseResult'); fo=t.get('file') if isinstance(t,dict) else None\n   if isinstance(fo,dict) and fo.get('startLine')==0: c[r.get('version')]+=1\nprint(c)\nEOF\nAND: rg -F 'startLine:Ue' cc258.strings",
          "observed": "Counter({'2.1.211': 2}) - exactly 2 echoes with startLine 0 among the 9202 file-bearing echoes, both stamped version 2.1.211, both top-level. Binary 2.1.258, the emit site verbatim: 'let ht={type:\"text\",file:{filePath:n,content:De,numLines:Ne,startLine:Ue!==void 0?Math.max(1,f):f,totalLines:ke,...Ue!==void 0&&{truncatedByTokenCap:!0}},...pt&&{artifactRead:pt}}' - the Math.max(1,...) clamp is applied only when Ue (the token-cap marker) is set, so the untruncated path echoes the caller's offset unclamped. Distribution of startLine over the same echoes: 0 -> 2, 1 -> 4450, >1 -> 4156.",
          "rule": "One observation per Read echo whose `toolUseResult.file.startLine` is exactly 0, keyed by the record's `version`. Whole projects root.",
          "note": "Both halves confirmed: the 2 zero-offset records are still on disk at exactly the stated version, and the 2.1.258 binary still contains the conditional clamp that lets it happen, so this is live behavior rather than a fossil. csift's `start_line == 1` full-anchor test and its `start_line.max(1)` splice base both remain verbatim at src/recover/carriers.rs:181 and :199. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:64-67 and :198-202."
        }
      ]
    },
    {
      "id": "REC-049",
      "area": "record-model",
      "behavior": "A `text` Read result may carry a second sibling key beside `file` - `artifactRead:{slug, ver}` - set when the read completed a saved artifact source file, so such a record's `toolUseResult` has three top-level keys (`type`, `file`, `artifactRead`) rather than the usual two.",
      "depends": "csift ignores it, which is correct today, but any code that assumes `toolUseResult` for a Read has exactly the keys `type` and `file` will mis-handle those records.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "55-58",
          "snippet": "    // ── (1a) Read result: toolUseResult.file = {filePath, content, startLine, …} ──\n    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        }
      ],
      "instrument": "Structural census over every *.jsonl under ~/.claude/projects: count records whose parsed `toolUseResult` has `artifactRead` as a KEY. Measured 2026-09-02: 0 of 9202 file-bearing echoes; the toolUseResult key tuples observed are ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2. A raw `grep artifactRead` over the corpus is NOT this measurement - it returns prose mentions from sessions that discussed the key. Schema authority: the `artifactRead:c({slug:i(),ver:i()}).optional()` member of the `text` variant in the Read-result zod union of the Claude Code 2.1.258 binary, plus the emit site `...pt&&{artifactRead:pt}`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "rg -c -F artifactRead cc258.strings   # cc258.strings = strings -n 6 ~/.local/share/claude/versions/2.1.258\nAND the corpus sweep: for every record whose toolUseResult is a dict, count separately (a) records where 'artifactRead' is a KEY of toolUseResult and (b) records where the raw line merely CONTAINS the string 'artifactRead'.",
          "observed": "Binary: 33 strings match; the schema member verbatim is 'artifactRead:c({slug:i(),ver:i()}).optional().describe(\"Set when this Read completed a saved Artifact source file: the Artifact and the version of it that now counts as viewed.\")', declared as a sibling of `file` INSIDE the `text` variant, and the emit site spreads it conditionally: '...pt&&{artifactRead:pt}'. Corpus: 0 records carry `artifactRead` as a key of `toolUseResult` (the toolUseResult key-tuple census over 9202 file-bearing echoes reads only ('file','type') 9196, ('file','source','type') 4, ('file','firstPage','type') 2); the only 3 lines that contain both 'toolUseResult' and 'artifactRead' are prose mentions inside transcripts that were discussing this key.",
          "rule": "Key-presence census: one observation per record whose toolUseResult is a dict, testing `'artifactRead' in toolUseResult` (structural) versus `'artifactRead' in raw_line` (textual). Whole projects root.",
          "note": "The mechanism is confirmed at the binary level in 2.1.258 (schema member plus conditional spread at the emit site), so the behavior claim stands. The instrument does not: a textual grep for the key counts sessions that merely TALK about it, which is how the claim arrived at 10 lines. The structural census finds zero real occurrences in this corpus - no artifact source file has been read here - so the on-disk shape of such a record is unobserved and only the schema fixes it. 'SEVENTH sibling key' also misdescribes the nesting: it is the second sibling of `type`/`file`, not the seventh key of `file`. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:55-58."
        }
      ]
    },
    {
      "id": "REC-050",
      "area": "record-model",
      "behavior": "`totalLines` and `numLines` are SEPARATOR counts, so a newline-terminated file reports one more than its content lines (a 12-content-line file ending in `\\n` reports 13, and its model-facing render carries an empty numbered line 13). This is not a minority effect: every newline-terminated echo counts the phantom line, which is a majority of all Read echoes.",
      "depends": "csift normalises the phantom away in `SparseBuffer::normalize_total` once a full anchor has confirmed the trailing newline; without it a fully-recovered file is mis-reported as missing its last line and `recover` restore hard-fails as partial.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "59-65",
          "snippet": "    pub(crate) fn normalize_total(&self, total_lines: usize) -> usize {\n        if self.content_ends_with_newline {\n            total_lines.saturating_sub(1)\n        } else {\n            total_lines\n        }\n    }"
        }
      ],
      "instrument": "Two rules. (1) Controlled, at CC 2.1.258: Read a 12-line file ending in a newline and its twin without the final newline; the renders come back with 13 numbered lines (last one empty) and 12 respectively. (2) Corpus, over every *.jsonl under ~/.claude/projects: for full reads (startLine==1, numLines==totalLines) whose content ends in a newline - 3481 echoes - 3480 report totalLines == content.count('\\n')+1, one more than the content's own line count. Of all 4512 echoes with newline-terminated content, 4512 have numLines == count('\\n')+1 and 4508 have numLines == len(splitlines())+1.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/recover/buffer.rs:19-25 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Controlled read at CC 2.1.258: write a 12-line file ending in a newline (probe_nlterm_12.txt, 84 bytes, 12 newlines) and its byte-identical twin without the final newline (probe_nonl_12.txt, 83 bytes, 11 newlines), Read both, then read back the tool_result renders from the calling transcript.\nAND the corpus rule: for every `text` echo with startLine==1 and numLines==totalLines (a full read), tally (content.endswith('\\n'), totalLines==content.count('\\n')+1, totalLines==len(content.splitlines())).\nAND the claim's own rule re-run: for every `text` echo compare numLines against content.count('\\n')+1.",
          "observed": "Controlled: the newline-terminated 12-line file rendered 13 numbered lines, last line the literal `13\\t` (empty); the non-terminated twin rendered 12, last line `12\\tzeta12`. Corpus full reads: newline-terminated 3481, of which 3480 report totalLines == content.count('\\n')+1 == splitlines+1 (one more than the file's own line count) and 1 does not; non-newline-terminated 265, where totalLines == count+1 == splitlines for 68 and the remainder differ only via splitlines' extra separators (\\r, \\x0b, \\x0c, U+2028). Re-running the claim's own numLines rule over all 8608 text echoes: 8415 equal, 1 where numLines is one MORE than count('\\n')+1, 4 one LESS, 188 other - the claim's 8290/126/189 does not reproduce. The sharper statement: of the 4512 echoes whose content ends in a newline, ALL 4512 have numLines == count('\\n')+1 and 4508 have numLines == splitlines+1, i.e. the phantom line is counted on essentially every newline-terminated echo, a MAJORITY of echoes (52%), not a minority.",
          "rule": "Full-read rule: one observation per `text` echo with startLine==1 and numLines==totalLines, keyed by whether content ends in a newline and whether totalLines equals the separator count (count('\\n')+1) or the terminator count (len(splitlines())). Phantom rule: one observation per `text` echo with a string content and integer numLines, keyed by (endswith newline, numLines==count('\\n')+1, numLines==len(splitlines())). Whole projects root.",
          "note": "The separator semantics that csift's SparseBuffer normalises away are confirmed twice over, including a live 2.1.258 read whose render numbered a 13th empty line for a 12-line file. What needed correction is the framing and the numbers: the claim's own numLines-versus-content rule re-runs to 8415/1/4/188 rather than 8290/126/189, and the phantom is the rule for newline-terminated files rather than 'a minority of echoes'. csift's code sites are verbatim: the SEPARATOR-count comment at src/recover/buffer.rs:19-25 and normalize_total at src/recover/buffer.rs:59-65. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/buffer.rs:59-65 (and the cited :19-25 comment)."
        }
      ]
    },
    {
      "id": "REC-051",
      "area": "record-model",
      "behavior": "Most reads in a real corpus are PARTIAL windows: 4812 of 8608 `file` echoes with both line counts (55.9%) report `numLines < totalLines`, and only 3.6% of those partials (174) are token-cap truncations - the rest are ordinary offset/limit windows.",
      "depends": "csift's replay therefore spends most of its time on `PartialRead` splices rather than full anchors, which is why the gap accounting (`??? lines A..B unknown`) and not the anchor path is `recover`'s dominant correctness surface.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "193-198",
          "snippet": "    } else {\n        events.push(FileEvent {\n            line_no,\n            turn_index,\n            timestamp_utc: ts.clone(),\n            kind: EventKind::PartialRead {"
        }
      ],
      "instrument": "Over every *.jsonl under ~/.claude/projects (no sampling), count echoes with integer `numLines` and `totalLines` where `numLines < totalLines`, one observation per echo. Measured 2026-09-02: 4812 partial of 8608 (55.9%); 174 of those 4812 carry `truncatedByTokenCap`, so token-capping explains 3.6% of the partials.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'EOF'\nimport json,os,collections\nR=os.path.expanduser('~/.claude/projects'); c=collections.Counter()\nfor dp,_,fs in os.walk(R):\n for f in fs:\n  if not f.endswith('.jsonl'): continue\n  for line in open(os.path.join(dp,f),errors='replace'):\n   if 'toolUseResult' not in line: continue\n   try: t=json.loads(line).get('toolUseResult')\n   except Exception: continue\n   fo=t.get('file') if isinstance(t,dict) else None\n   if not isinstance(fo,dict): continue\n   nl,tl=fo.get('numLines'),fo.get('totalLines')\n   if isinstance(nl,int) and isinstance(tl,int):\n    c['lt' if nl<tl else ('eq' if nl==tl else 'gt')]+=1\n    if nl<tl: c['capped' if fo.get('truncatedByTokenCap') else 'uncapped']+=1\nprint(c)\nEOF",
          "observed": "lt 4812, eq 3796, gt 0 - 4812 of 8608 echoes (55.9%) are partial windows. Of those 4812 partials, 174 carry truncatedByTokenCap and 4638 do not, so token-capping explains 3.6% of the partials, not about 11%. Split by transcript: top-level 3548 partial / 2152 full, subagent 1264 partial / 1644 full.",
          "rule": "One observation per Read echo with integer `numLines` and `totalLines`, bucketed numLines<totalLines / == / >; partials further split by whether `file.truncatedByTokenCap` is truthy. Whole projects root - every *.jsonl, no sampling.",
          "note": "The headline - a majority of reads are windows, so replay lives on the PartialRead path - reproduces almost exactly at corpus scale (55.9% versus the claim's 56% on a 20-transcript sample). The token-cap share does not: it is 3.6% corpus-wide rather than about 11%, so the claim's sample over-represented capped reads by roughly 3x. Replacing the sample with the whole corpus also makes the number rerunnable without knowing which 20 transcripts were picked. Code sites re-checked at csift 0.10.0 and present verbatim at the cited lines: src/recover/carriers.rs:193-198."
        }
      ]
    },
    {
      "id": "REC-052",
      "area": "record-model",
      "behavior": "The truncation banner is ALSO persisted as its own record: `type:\"attachment\"` with `attachment:{type:\"read_truncation_notice\", banner:\"<text>\", toolUseID:\"toolu_...\"}`, carrying a normal `uuid`/`timestamp`/`parentUuid`/`sessionId`/`version` and joining back to the Read by `toolUseID`.",
      "depends": "csift classifies it under `harness.meta.attachment`, which is parsed only under `search --attachments` / `--count-by attachment` or an explicit `show` address - so a default scan cannot see that a read was capped, and `recover` ignores the record entirely.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "159-166",
          "snippet": "        // Any OTHER `type:\"attachment\"` record (edited_text_file, compact_file_reference,\n        // file snapshots, …): harness sidecar payload - labeled `harness.meta.attachment`.\n        // Only `search --attachments` / `--count-by attachment` (or an explicit `show`\n        // address) ever parses these lines; the record never opens a turn.\n        if self.attachment_payload_text().is_some() {\n            push_unique(&mut out, Class::MetaAttachment);\n            return out;\n        }"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "216-222",
          "snippet": "    let atype = att.get(\"type\").and_then(serde_json::Value::as_str);\n\n    // (7a) edited_text_file → an external edit (hard boundary).\n    if atype == Some(\"edited_text_file\") {\n        let path = att\n            .get(\"filename\")\n            .or_else(|| att.get(\"filePath\"))"
        }
      ],
      "instrument": "Clean parsed count is now 184 (was 170) - the corpus is live and this class of record keeps accruing. The claim's second half (\"the same search without --attachments returns 0 ... 6 vs 0 on one project\") no longer discriminates on the csift project dir, because that corpus now contains sessions that quote the literal 'read_truncation_notice' in prose: measured there 30 with the gate vs 25 without. Run the contrast on a project whose sessions never discuss the mechanism: measured 40 vs 0 and 22 vs 0 on two such dirs.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": "2.1.206",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{200\\}read_truncation_notice.\\{260\\}' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping records whose attachment.type == 'read_truncation_notice' and tallying sorted(attachment.keys()), the record type, and the presence of uuid/timestamp/parentUuid/sessionId/version; (3) same sweep joining each notice's attachment.toolUseID against the set of tool_use block ids with name=='Read' in the SAME file; (4) csift search 'read_truncation_notice' <one project-dir token> --attachments -t harness.meta.attachment -c  versus the same command without --attachments, and versus -t harness.meta.attachment with no --attachments; (5) csift show <one transcript path> --line <N>",
          "observed": "Binary emitter site, verbatim: we.push({message:Sn({type:\"read_truncation_notice\",banner:_o,toolUseID:n})}) . Corpus: 184 records with attachment.type=='read_truncation_notice'; the attachment key set is 'banner,toolUseID,type' on 184/184 (no other key set observed); the carrying record type is \"attachment\" on 184/184; uuid, timestamp, parentUuid, sessionId and version are all non-null on 184/184; banner is a string on 184/184. toolUseID join: 184/184 hit a Read tool_use id in the same transcript file, 0 misses. csift gate on two project dirs: 40 vs 0 and 22 vs 0 (with --attachments vs default scan); -t harness.meta.attachment WITHOUT --attachments returns 0 on all three project dirs tried. csift show <transcript> --line 23 renders it flag-free as '⚙ harness.meta.attachment  L23  {\"type\":\"read_truncation_notice\",\"banner\":\"[Truncated: PARTIAL view — <path>: showing lines 1-261 of 478 total (38905 tokens, cap 25000). Call Read with offset=262 limit=261 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]\",\"toolUseID\":\"toolu_...\"}'. recover's attachment handler has exactly two arms (grep of src/recover/carriers.rs lines 210-300): '(7a) edited_text_file' and '(7b) a `file` attachment' - no read_truncation_notice arm.",
          "rule": "One count per RECORD whose parsed attachment.type equals the literal 'read_truncation_notice' (prose mentions excluded by requiring the parsed field, not a raw grep). Join rule: one count per notice, hit iff its toolUseID is in the set of Read tool_use ids in the same file. Gate rule: csift -c prints matched exchanges, one project dir per run.",
          "note": "Behavior holds in full and is now pinned tighter than the claim states: the attachment payload key set is exactly {banner, toolUseID, type} on all 184 records (no variant seen), and the toolUseID join to a Read tool_use in the same file is 184/184 with zero misses - the join key is not merely plausible, it is total on this corpus. Both csift code sites verified verbatim at the claimed lines: src/model/classify.rs:159-166 (the MetaAttachment arm, `if self.attachment_payload_text().is_some()` at line 163) and src/recover/carriers.rs:216-222. The 'recover ignores the record entirely' half of the depends is confirmed structurally, not just by absence: the attachment handler branches only on 'edited_text_file' and a 'file' attachment."
        }
      ]
    },
    {
      "id": "REC-053",
      "area": "record-model",
      "behavior": "The `read_truncation_notice` attachment is on Claude Code's short allow-list of attachment types that are explicitly persisted into a forked or async agent's transcript, alongside `session_context`, `date`, `instructions` and `remote_session_change`.",
      "depends": "The notice does survive into subagent transcripts (155 of 184 live there) and csift reaches it only under search --attachments - both confirmed. But the stated reason is wrong: subagent transcripts are not a lane \"that has no toolUseResult echo\". 27152 records under a subagents/ path carry a non-null toolUseResult, among them 1951 Edit echoes and 841 Write echoes. So the notice is one truncation signal among the normal carriers there, not the only structured signal available in that lane.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "159-165",
          "snippet": "        // Any OTHER `type:\"attachment\"` record (edited_text_file, compact_file_reference,\n        // file snapshots, …): harness sidecar payload - labeled `harness.meta.attachment`.\n        // Only `search --attachments` / `--count-by attachment` (or an explicit `show`\n        // address) ever parses these lines; the record never opens a turn.\n        if self.attachment_payload_text().is_some() {\n            push_unique(&mut out, Class::MetaAttachment);\n            return out;"
        }
      ],
      "instrument": "Seek `In.attachment.type===\"read_truncation_notice\"||In.attachment.type===\"session_context\"` in a `strings -a` dump of the 2.1.258 binary; a second site in the forked-agent recorder gates persistence on `xt.type===\"attachment\"&&xt.attachment.type===\"read_truncation_notice\"`. Corpus confirmation: at least one `read_truncation_notice` record exists inside a `subagents/agent-*.jsonl` file under ~/.claude/projects.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": "2.1.206",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{200\\}read_truncation_notice.\\{260\\}' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, splitting the read_truncation_notice records by whether the file path contains a '/subagents/' component; (3) the same sweep tallying every record with a non-null toolUseResult, split by the same '/subagents/' test, and separately counting Edit echoes (toolUseResult with an 'oldString' key) and Write echoes (toolUseResult.type in {create,update} with a 'content' key) per lane",
          "observed": "Async-agent allow-list, verbatim from the binary: if(In.attachment.type===\"read_truncation_notice\"||In.attachment.type===\"session_context\"||In.attachment.type===\"date\"||In.attachment.type===\"instructions\"||In.attachment.type===\"remote_session_change\")await Kne([In],Cn,xp,r.storageV5).catch(fat),xp=In.uuid,yn?.add(In.uuid); Forked-agent recorder, verbatim: je&&(xt.type===\"assistant\"||xt.type===\"user\"||xt.type===\"progress\"||xt.type===\"attachment\"&&xt.attachment.type===\"read_truncation_notice\") ... await Kne([xt],je,Ge,we.storageV5) . Corpus: of the 184 read_truncation_notice records, 155 sit inside subagents/agent-*.jsonl and 29 in top-level transcripts. BUT subagent transcripts are NOT toolUseResult-free: 27152 records under a '/subagents/' path carry a non-null toolUseResult, including 1951 Edit echoes and 841 Write echoes (top-level: 59146 / 11421 / 2048).",
          "rule": "One count per record. Lane test = the literal '/subagents/' appearing as a path component of the transcript file. Edit echo = toolUseResult dict containing the key 'oldString'; Write echo = toolUseResult dict whose 'type' is 'create' or 'update' and which has a 'content' key.",
          "note": "The behavioral claim - that read_truncation_notice is on a five-member allow-list of attachment types explicitly persisted into forked/async agent transcripts, alongside session_context, date, instructions and remote_session_change - is confirmed verbatim at two independent binary sites, and the corpus agrees (155 notices inside subagent transcripts). Only the depends clause needed correcting. csift code site verified verbatim: src/model/classify.rs:159-165."
        }
      ]
    },
    {
      "id": "REC-054",
      "area": "record-model",
      "behavior": "The `edited_text_file` attachment's payload keys are exactly `type`, `filename` and `snippet` - never `filePath` or `content` - and an EMPTY `snippet` string is a real, emitted form.",
      "depends": "csift's `filename`-then-`filePath` and `snippet`-then-`content` fallbacks are dead alternatives on current Claude Code, and the empty-snippet case is the degraded external-edit boundary csift already names in the boundary detail.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "219-224",
          "snippet": "    if atype == Some(\"edited_text_file\") {\n        let path = att\n            .get(\"filename\")\n            .or_else(|| att.get(\"filePath\"))\n            .and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        }
      ],
      "instrument": "Corpus: count `tuple(sorted(attachment.keys()))` for every record whose `attachment.type` is `edited_text_file`, one observation per record. Measured now over all of ~/.claude/projects: 1339 records, all with the key set `('filename','snippet','type')`; 85 of the 1339 carry an empty `snippet` (6.3%).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping records whose attachment.type == 'edited_text_file' and tallying ','.join(sorted(attachment.keys())) plus whether attachment['snippet'] == ''",
          "observed": "1339 edited_text_file records corpus-wide. Key set: 'filename,snippet,type' on 1339/1339 - a single key set, no record carries 'filePath' and none carries 'content'. snippet is a string on 1339/1339 and is the empty string on 85 of them (6.3%).",
          "rule": "One observation per record whose parsed attachment.type equals 'edited_text_file'; buckets are the sorted key tuple.",
          "note": "Reproduces the claim exactly, to the record: 1339 records, one key set, 85 empty snippets. So csift's `.get(\"filename\").or_else(|| att.get(\"filePath\"))` fallback is a dead alternative on this corpus - 0 of 1339 records would take it - and the same holds for a `content` fallback. Code site verified verbatim at src/recover/carriers.rs:219-224. The empty-snippet form is real and not rare enough to ignore."
        }
      ]
    },
    {
      "id": "REC-055",
      "area": "record-model",
      "behavior": "A `Write` tool result echoes toolUseResult as {\"type\":\"create\"|\"update\",\"filePath\",\"content\",\"structuredPatch\",\"originalFile\",\"userModified\"} (plus an optional gitDiff), and the binary's Zod schema declares `type` as ee([\"create\",\"update\"]) with the description \"Whether a new file was created or an existing file was updated\". `type` is present only on the Write shape AMONG FILE-MUTATION CARRIERS - i.e. among toolUseResult objects that carry a top-level `filePath` (16274 corpus-wide: 2889 Write with type, 13372 Edit without, 13 ExitPlanMode without). It is NOT unique to Write across all tool results: the Read family also sets a `type` (text/image/file_unchanged/parts/pdf, 9203 records), but those nest their payload under a `file` key and carry neither a top-level `filePath` nor a top-level `content`, so they never reach csift's carrier gate.",
      "depends": "csift `files` reads that `type` to decide create-vs-edit and csift `recover` uses the presence-of-`type`/absence-of-`oldString` to treat the echo as a full-content anchor; a `type` added to Edit results would turn every edit into a snapshot.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "149-156",
          "snippet": "    // A Write result: full-content anchor.\n    if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n        let total = line_count(content);\n        events.push(FileEvent {\n            line_no,\n            turn_index,\n            timestamp_utc: ts.clone(),\n            kind: EventKind::FullSnapshot {"
        },
        {
          "path": "src/model/mutation.rs",
          "lines": "196",
          "snippet": "        let is_create = probe.r#type.as_ref().and_then(serde_json::Value::as_str) == Some(\"create\");"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'ee([\"create\",\"update\"])' | head -1 | cut -c1-1400\nExpected: the Write-result schema literal `c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an existing file was updated\"),filePath:i()...,content:i()...,structuredPatch:R(...),originalFile:i().nullable()...,gitDiff:...optional(),userModified:M().optional()...})`. Counting rule: one grep hit is the single minified schema constructor; the Edit-result constructor in the same binary (`grep -F 'oldString:i().describe'`) has no `type` member.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.6 (the `files` mutation-source table) and section 6.7"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'ee([\"create\",\"update\"])' and the same grep -F printing the hit; (2) strings -a <the same binary> | grep -cF 'oldString:i().describe' plus the printed hit; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter tallying every toolUseResult dict that carries a 'type' key, by value; (4) the same sweep restricted to toolUseResult dicts carrying a top-level 'filePath', bucketed by (has 'type', has 'content', has 'oldString')",
          "observed": "grep count 1 for ee([\"create\",\"update\"]). Write output schema, verbatim: c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an existing file was updated\"),filePath:i().describe(\"The path to the file that was written\"),content:i().describe(\"The content that was written to the file\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes (empty when nothing changed, the diff timed out, or — with originalFile null on an update — the previous content was too large to diff)\"),originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\"),gitDiff:kQe().optional(),userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")}) . Edit output schema (grep count 1), verbatim, and it has no `type` member: c({filePath:i().describe(\"The file path that was edited\"),oldString:i().describe(\"The original string that was replaced\"),newString:i().describe(\"The new string that replaced it\"),originalFile:i().nullable().describe(\"The original file contents before editing\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes\"),userModified:M().describe(\"Whether the user modified the proposed changes\"),replaceAll:M().describe(\"Whether all occurrences were replaced\"),gitDiff:kQe().optional()}) . BUT corpus-wide, toolUseResult.type is NOT unique to Write: 8609 objects carry type=='text', 444 'image', 120 'file_unchanged', 26 'parts', 4 'pdf' - versus 2598 'create' and 291 'update'. Those 9203 non-Write objects are the Read family, keyed {file,type} (8609/8609 text, 444/444 image, 116/120 file_unchanged) with NO top-level 'filePath' and NO top-level 'content' on any of them. Restricting to the 16274 toolUseResult objects that DO carry a top-level 'filePath': 13372 have oldString and no type and no content (Edit), 2889 have type in {create,update} plus content and no oldString (Write), and 13 have none of the three (ExitPlanMode results, keyed filePath/plan/hasTaskTool/isAgent[/planWasEdited]).",
          "rule": "One grep hit = one minified schema constructor. Corpus: one tally per toolUseResult dict; the filePath partition counts each such dict once under (type present, content present, oldString present).",
          "note": "The schema half of the claim is exact - both constructors quoted above are single grep hits in 2.1.258, and the Edit constructor demonstrably has no `type` member. Only the uniqueness wording overreached. The depends survives that correction intact, and for a structural reason worth recording: csift reads `type` only AFTER a filePath match (src/recover/carriers.rs:93-95 gates on `tur.get(\"filePath\")` + path_matches before anything else; src/model/mutation.rs:190-196 returns early unless probe.file_path is a non-empty string), and no Read-family result carries a top-level filePath. Both code sites verified verbatim at the claimed lines (carriers.rs:149-156, mutation.rs:196)."
        }
      ]
    },
    {
      "id": "REC-056",
      "area": "record-model",
      "behavior": "On a `Write` that CREATES a file the persisted echo is `{type:\"create\", filePath, content, structuredPatch:[], originalFile:null, userModified}` - `originalFile` is present but JSON `null` by definition on a create, and `structuredPatch` is the EMPTY array; the schema documents `originalFile` as null for new files or when the previous content was too large to include.",
      "depends": "csift `recover` never consults `originalFile`/`structuredPatch` on a Write (it anchors on `content`), and `parse_structured_patch` returning `Some(vec![])` is defused by the `!patches.is_empty()` guard in `apply_edit` - without that guard an empty patch array would silently no-op an edit.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "137-143",
          "snippet": "    if let Some(patches) = structured_patch {\n        if !patches.is_empty() {\n            return apply_structured_patch(buf, patches, line_no);\n        }\n    }\n    // Fallback: string replacement over the dense known text.\n    apply_string_edit(buf, hunks, line_no)"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "91-93",
          "snippet": "    // ── (2) Write result: {type:create|update, filePath, content, …} ──\n    // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n    let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);"
        }
      ],
      "instrument": "Replace the live 120-newest-file window (which reported 1801 creates / 247 updates and drifts every run) with the whole-corpus sweep, which a stranger can rerun deterministically against a fixed corpus: 2598 creates, all in the ('create', originalFile null, structuredPatch []) bucket; 291 updates, none in it.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now; SPEC.md section 6.7 extraction table; describe string measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts whose 'type' is 'create' or 'update' and which have a 'content' key, tallied by (type, originalFile is None, structuredPatch == []) and separately by ','.join(sorted(toolUseResult.keys())); plus the Write output-schema grep from REC-055",
          "observed": "Corpus-wide: 2598 create echoes, ALL of them (2598/2598) with originalFile JSON null AND structuredPatch the empty array - the bucket ('create', originalFile-null=True, structuredPatch-empty=True) is 2598 and no other create bucket exists. 291 update echoes, none of which is (null, empty): 226 are (not-null, non-empty) and 65 are (null, non-empty). The `originalFile` KEY is present on 2598/2598 creates (key sets 'content,filePath,originalFile,structuredPatch,type,userModified' 2540 and the same plus 'memdirStamped' 58) - it is present-and-null, never absent. Schema text, verbatim: originalFile:i().nullable().describe(\"The original file content before the write (null for new files, or when the previous content was too large to include)\") and structuredPatch:R(SQe()).describe(\"Diff patch showing the changes (empty when nothing changed, the diff timed out, or — with originalFile null on an update — the previous content was too large to diff)\").",
          "rule": "One tally per Write echo, defined as a toolUseResult dict with type in {create,update} AND a 'content' key, keyed by (type, originalFile is null, structuredPatch == []).",
          "note": "The invariant is stronger than the claim's window suggested: it is 2598/2598 creates corpus-wide, with no exception at any Claude Code version present (2.1.156 through 2.1.258). One extra key, `memdirStamped`, rides 58 of the creates - worth knowing before anyone pins the create key set. Both csift code sites verified verbatim at the claimed lines: src/recover/buffer.rs:137-143 (the `if !patches.is_empty()` guard sits at line 138) and src/recover/carriers.rs:91-93. The guard's necessity is real on this data - every create ships structuredPatch [], so csift's parse_structured_patch yields Some(empty) on 2598 records and only the guard keeps them from short-circuiting the string-replacement fallback."
        }
      ]
    },
    {
      "id": "REC-057",
      "area": "record-model",
      "behavior": "On a `Write` that UPDATES an existing file, `originalFile` is a string only sometimes - corpus-wide 226 of 291 observed updates carry the pre-write text and 65 carry null. The null is not merely the schema's discretionary \"previous content was too large to include\" branch: the transcript writer applies a hard cap on the append path, rewriting originalFile to null whenever it is a string longer than 10000 characters, and 19 of the 65 nulls are instead the stripped form (content == '' as well, see REC-058). Excluding those, 46 of 272 content-bearing updates (16.9%) lack the pre-image.",
      "depends": "csift `recover` builds its Write anchor from `content` alone, so an absent `originalFile` costs nothing there; but any future consumer that assumed an update always carries its pre-image would fabricate a wrong pre-state on 23% of updates.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "124-127",
          "snippet": "                original_file: tur\n                    .get(\"originalFile\")\n                    .and_then(serde_json::Value::as_str)\n                    .map(str::to_string),"
        }
      ],
      "instrument": "Use the whole-corpus cross-tab (226 / 46 / 19 of 291) rather than the live 120-newest-file window that reported 190 str vs 57 null of 247 and drifts every run.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with type=='update' and a 'content' key, cross-tabulated by (content == '', originalFile is None); plus, over every toolUseResult dict corpus-wide, the length distribution of originalFile when it is a string; plus strings -a <the Claude Code 2.1.258 binary> searched for the transcript-append path",
          "observed": "291 update echoes corpus-wide: 226 carry the pre-write text (originalFile a non-empty string), 65 carry JSON null. Cross-tab: (content non-empty, originalFile not null) 226, (content non-empty, originalFile null) 46, (content == '', originalFile null) 19. So 65/291 = 22.3% of updates lack the pre-image, and among the 272 updates that still carry their content, 46 (16.9%) lack it. Mechanism, verbatim from the binary: var rts=1e4;function Tor(e){if(typeof e!==\"object\"||e===null)return e;let n=e;if(typeof n.originalFile===\"string\"&&n.originalFile.length>rts)return{...n,originalFile:null};return e} - and the transcript writer applies it on the append path: if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult);if(await this.appendEntry(ke,C,I,v),...) . Corpus check of that cap: across 3908 persisted toolUseResult.originalFile strings the longest is 9997 characters and 0 exceed 10000 (Write echoes alone: longest 9693, 0 over 10000).",
          "rule": "One tally per type=='update' Write echo, keyed by (content == '', originalFile is null). Cap check: one measurement per toolUseResult dict whose originalFile is a string, bucketed by len(); the counting rule for the cap is max(len(originalFile)) and count(len > 10000).",
          "note": "The claim's shape is right - a Write update's pre-image is present most of the time and missing often enough to matter - and its ~23% null rate reproduces (22.3% corpus-wide). What it missed is the mechanism and a confound. The mechanism is a measurable constant: a 10000-character cap applied at append time to EVERY user record's toolUseResult, which the corpus confirms cleanly (max persisted originalFile 9997 chars, zero above 10000, over 3908 strings). The confound is that 19 of the 65 nulls are the stripped update form, so a consumer that treats null as 'file was too big' is wrong on 29% of them. csift code site verified verbatim at the claimed lines: src/recover/carriers.rs:124-127."
        }
      ]
    },
    {
      "id": "REC-058",
      "area": "record-model",
      "behavior": "Claude Code's Write tool defines a stripForStorage rewrite - stripForStorage(e){... if(e.type!==\"update\")return e; if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e; if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e; return{...e,content:\"\",originalFile:null}} - so an update-type Write carrier CAN carry an empty `content` string and no original content. Two guards exempt it, not one: an already-empty pair, and an empty structuredPatch with a null originalFile. The rewrite is driven over a message LIST by a function that only touches entries older than the newest 200 (a headless drain passes 0, stripping all), and in 2.1.258 the transcript append path itself applies only the 10000-character originalFile cap, not this strip. On disk the empty-content update carrier is confined to two versions: 19 records, 16 at 2.1.208 and 3 at 2.1.211, and none among the 102 update carriers recorded at 2.1.217 through 2.1.258.",
      "depends": "csift's carrier extractor treats any `toolUseResult` with a `filePath` and a `content` string but no `oldString`/`newString` as a full-snapshot anchor and calls `buf.reset_to_full(content, ...)`, so a stripped update carrier resets the sparse reconstruction buffer to a zero-line file and every line known before it becomes an explicit gap.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "149-151",
          "snippet": "    // A Write result: full-content anchor.\n    if let Some(content) = tur.get(\"content\").and_then(serde_json::Value::as_str) {\n        let total = line_count(content);"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "89",
          "snippet": "                buf.reset_to_full(content, *total_lines, e.line_no);"
        }
      ],
      "instrument": "Add the version key to the tally - the phenomenon is version-confined and a version-blind count hides that. Corpus-wide (not the 120-newest window): 291 update carriers, 19 empty; 2598 create carriers, 1 empty.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": "2.1.208",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -o '.\\{260\\}stripForStorage(e){[^}]\\{0,420\\}' ; (2) a python find over the same strings dump for the call sites of the driver function and for the transcript-append path; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter tallying Write echoes (toolUseResult.type in {create,update} with a 'content' key) bucketed by whether content == '' and by the record's `version` field",
          "observed": "Write tool, verbatim and complete: stripForStorage(e){if(typeof e!==\"object\"||e===null)return e;if(e.type!==\"update\")return e;if(e.content===\"\"&&(e.originalFile??\"\")===\"\")return e;if(Array.isArray(e.structuredPatch)&&e.structuredPatch.length===0&&e.originalFile===null)return e;return{...e,content:\"\",originalFile:null}} - two early-return guards, not one. Driver, verbatim: function $_t(e,n,r=200,o=!1){let d=e.length-r;if(d<=0)return e; ... if(v>=d||C.type!==\"user\"||C.isVirtual||C.toolUseResult==null||!Array.isArray(C.message.content))continue; ... if(!F?.stripForStorage)continue; ... _[v]={...C,toolUseResult:B}} - it rewrites a message LIST and only touches entries older than the newest `r` (default 200). Its two call sites are a headless drain (called with r=0, o=true, which strips every entry) and an interactive query_end updater; neither is the file-append path. The append path applies a different transform: if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult) before this.appendEntry(ke,...), i.e. only the 10000-character originalFile cap. Corpus: 291 update carriers, 19 with content == '' - 16 at version 2.1.208 and 3 at 2.1.211, and ZERO at any later version, against 102 update carriers recorded at versions 2.1.217 through 2.1.258. 2598 create carriers with exactly 1 empty content (at 2.1.217; a genuinely empty file). All 19 stripped updates also carry originalFile null.",
          "rule": "One count per carrier RECORD (a toolUseResult dict with type in {create,update} and a 'content' key), bucketed by content == '' and keyed by the record's top-level `version`.",
          "note": "Not marked drifted, because the rule is still verbatim in the 2.1.258 binary and the on-disk artifact is real; but the claim's 'STRIPS ... before persisting it' is the part the instruments do not support for the current version. What would decide it: a session run on 2.1.258 that performs a Write of type update more than 200 messages before the session ends, then re-reading that record from the transcript to see whether content survived. A plausible path by which the stripped form reached disk in the 2.1.208 era is the writer's own session-file compaction (requestCompact(this.sessionFile,...)), which rewrites the file from the in-memory list. The depends is confirmed in code and is exact: src/recover/carriers.rs:149-151 accepts any content string including \"\", src/recover/diff.rs:167-178 makes line_count(\"\") == 0 (split_lines returns an empty Vec on empty input), and src/recover/replay.rs:89 then calls buf.reset_to_full(content, 0, line_no) - a zero-line reset. Both claimed code sites verified verbatim."
        }
      ]
    },
    {
      "id": "REC-059",
      "area": "record-model",
      "behavior": "An `Edit` tool result echoes toolUseResult as {filePath, oldString, newString, originalFile (nullable), structuredPatch, userModified, replaceAll} and carries NO `type` key at all - the absence of `type` is what distinguishes it from a Write carrier. Two optional extras are actually emitted on top of that seven-key core: `staleRecovered` (92 of 13372) and `memdirStamped` (43 of 13372). The `gitDiff` the schema declares optional was observed on 0 of 13372 echoes, so it is a declared-but-unseen field on this corpus, not an expected one.",
      "depends": "csift `recover` discriminates an Edit from a Write purely by `tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some()` after the `filePath` match; a `type` appearing on Edit results, or a rename of the two string keys, silently reclassifies every edit as a full-content snapshot and produces a plausible WRONG reconstruction rather than an error.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "91-98",
          "snippet": "    // ── (2) Write result: {type:create|update, filePath, content, …} ──\n    // ── (3) Edit result: {filePath, oldString, newString, structuredPatch, …} (no type) ──\n    let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);\n    if !path_matches(target_file, path.unwrap_or_default()) {\n        return;\n    }\n    let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n    let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "97-98",
          "snippet": "    let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n    let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
        }
      ],
      "instrument": "Prefer the whole-corpus sweep (13372 Edit echoes, `type` on 0) over the 120-newest-file window (11299 modal-tuple hits) - the window is live and drifts; the corpus-wide top-level-only count is 11421 and subagent transcripts add 1951 more.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.7 (`Edit / MultiEdit | toolUseResult.{oldString,newString,structuredPatch,originalFile} (no type)`); measured now"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'oldString:i().describe' and the printed hit; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts that contain an 'oldString' key and tallying ','.join(sorted(keys)), plus a separate count of how many contain a 'type' key",
          "observed": "Edit output schema (grep count 1), verbatim: c({filePath:i().describe(\"The file path that was edited\"),oldString:i().describe(\"The original string that was replaced\"),newString:i().describe(\"The new string that replaced it\"),originalFile:i().nullable().describe(\"The original file contents before editing\"),structuredPatch:R(SQe()).describe(\"Diff patch showing the changes\"),userModified:M().describe(\"Whether the user modified the proposed changes\"),replaceAll:M().describe(\"Whether all occurrences were replaced\"),gitDiff:kQe().optional()}) - no `type` member. Corpus: 13372 Edit echoes; `type` present on 0 of 13372. Key sets: 'filePath,newString,oldString,originalFile,replaceAll,structuredPatch,userModified' 13237 (99.0%), the same plus 'staleRecovered' 92, the same plus 'memdirStamped' 43. `gitDiff` observed on 0 of 13372. structuredPatch present on 13372/13372; replaceAll present on 13372/13372.",
          "rule": "One tally per Edit echo, defined as a toolUseResult dict containing an 'oldString' key; buckets are the sorted key tuple, and 'type present' is a separate count over the same population.",
          "note": "The discriminator holds absolutely on this corpus: 0 of 13372 Edit echoes carry a `type`, and the 2.1.258 Edit schema constructor has no `type` member, so csift's oldString/newString test cannot be fooled by present data. The claim's corpus-wide figures reproduce exactly where it gave them (structuredPatch on 13372; replaceAll:true on 70). The only real correction is the key list: `memdirStamped` is an emitted key the claim does not name, and `gitDiff` - which it does name - never appears. Both code sites verified verbatim at the claimed lines (src/recover/carriers.rs:91-98 and 97-98)."
        }
      ]
    },
    {
      "id": "REC-060",
      "area": "record-model",
      "behavior": "An `Edit` result's `originalFile` is schema-declared .nullable() (\"The original file contents before editing\") and is null in the MAJORITY of real echoes: 9690 null against 3682 non-null across 13372 Edit echoes corpus-wide. The dominant cause is not editorial - the transcript writer applies a hard 10000-character cap on the append path, rewriting any string originalFile longer than that to null before the record is written. The corpus bears this out: across 3908 persisted originalFile strings the longest is 9997 characters and none exceeds 10000. A third state also exists that a null-vs-string test misses: 209 of the 3682 non-null values are the EMPTY string (the Edit tool's own stripForStorage rewrites originalFile to \"\"), so only 3473 echoes - 26.0% - carry usable pre-image text.",
      "depends": "csift `recover`'s AUTHORITATIVE `original_file_disagreement` boundary can only fire on the ~26% of edits that carry the string, so a clean `recover --coverage` is evidence over that fraction, not over every edit; the check is silently skipped, never reported as skipped.",
      "code": [
        {
          "path": "src/recover/replay.rs",
          "lines": "131-136",
          "snippet": "                // Boundary cross-check: originalFile vs replayed buffer.\n                if let Some(orig) = original_file {\n                    if had_full_anchor && buffer_disagrees_with_original(&buf, orig) {\n                        out.boundaries.push(Boundary {\n                            line_no: e.line_no,\n                            turn_index: e.turn_index,"
        },
        {
          "path": "src/recover/replay.rs",
          "lines": "531-535",
          "snippet": "pub(crate) fn buffer_disagrees_with_original(buf: &SparseBuffer, original_file: &str) -> bool {\n    let orig_lines = split_lines(original_file);\n    if orig_lines.is_empty() {\n        return false;\n    }"
        }
      ],
      "instrument": "Tally three states, not two (null / empty string / non-empty string), and add the length-bucket sweep that exposes the 10000-character cap. Prefer corpus-wide counts over the live 120-newest window (which reported 8443/2975 of 11418).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'originalFile:i().nullable().describe(\"The original file contents before editing\")' ; (2) a python find over the same strings dump for the transcript-append transform; (3) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with an 'oldString' key and tallying originalFile as one of three states - JSON null, the empty string, a non-empty string; (4) the same sweep bucketing len(originalFile) for every toolUseResult dict whose originalFile is a string",
          "observed": "Schema grep returns 1. Corpus: 13372 Edit echoes - 9690 originalFile null, 209 originalFile the EMPTY string, 3473 a non-empty string. (9690 null vs 3682 non-null reproduces the claim's corpus-wide pair exactly; the claim's '2975 string' window figure lumps the empty ones in.) So only 3473/13372 = 26.0% carry usable pre-image text. Cap mechanism, verbatim from the binary: var rts=1e4;function Tor(e){...if(typeof n.originalFile===\"string\"&&n.originalFile.length>rts)return{...n,originalFile:null};return e} applied as if(ke.type===\"user\"&&ke.toolUseResult!=null)ke.toolUseResult=Tor(ke.toolUseResult) immediately before this.appendEntry(ke,...). Corpus confirmation of the cap: over all 3908 persisted toolUseResult.originalFile strings the maximum length is 9997 characters and the count above 10000 is 0; the length buckets stop hard (<=9500: 146, <=9900: 144, <=10000: 39, >10000: 0). Empty-string originalFile appears at versions 2.1.159 through 2.1.221 (84 at 2.1.208, 42 at 2.1.210, 26 at 2.1.220, ...).",
          "rule": "One tally per Edit echo (toolUseResult dict with an 'oldString' key), keyed by which of three states originalFile is in. Cap rule: over every toolUseResult dict whose originalFile is a string, report max(len) and count(len > 10000).",
          "note": "The claim's headline - originalFile is null in the majority, so csift's authoritative original_file_disagreement boundary can only fire on about a quarter of edits - stands, and its corpus-wide numbers reproduce exactly. Two refinements matter for anyone reasoning about WHY. First, the nullness is a mechanical size cap with a measurable constant, not a discretionary schema branch: an edit to any file whose pre-image exceeds 10000 characters is silently unverifiable, which makes the un-checked fraction correlate with file size rather than being random. Second, the 209 empty-string values would pass a naive isinstance(str) test; csift is defused here, not by luck but by an explicit guard - src/recover/replay.rs:529-533 returns false when split_lines(original_file) is empty, so an empty pre-image can never raise a false boundary. Both claimed code sites verified verbatim at the claimed lines (replay.rs:131-136 and 529-533)."
        }
      ]
    },
    {
      "id": "REC-061",
      "area": "record-model",
      "behavior": "`replaceAll` on the Edit echo is the boolean `Whether all occurrences were replaced` and is the ONLY signal distinguishing a one-shot replacement from a global one.",
      "depends": "csift `recover`'s string-replacement fallback branches on it (`text.replace` versus `text.replacen(.., 1)`); if the key were dropped the `unwrap_or(false)` default would silently turn every global rename into a single replacement and produce a plausible wrong file.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "113-117",
          "snippet": "            replace_all: tur\n                .get(\"replaceAll\")\n                .and_then(serde_json::Value::as_bool)\n                .unwrap_or(false),\n        }];"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "149-155",
          "snippet": "/// One hunk of an Edit (old→new strings), from the tool_use `input`.\n#[derive(Debug, Clone)]\npub(crate) struct EditHunk {\n    pub(crate) old_string: String,\n    pub(crate) new_string: String,\n    pub(crate) replace_all: bool,\n}"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'replaceAll:M().describe(\"Whether all occurrences were replaced\")' | wc -l  # expect 1\nCounting rule: one matching `strings` line = the Edit-result schema constructor. Corpus check: one tally per Edit echo keyed by `replaceAll`; the key was present on all 11421 measured Edit echoes.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "(1) strings -a <the Claude Code 2.1.258 binary> | grep -cF 'replaceAll:M().describe(\"Whether all occurrences were replaced\")' ; (2) python3 sweep over every *.jsonl under ~/.claude/projects (7644 files, journal.jsonl and elicitations.jsonl excluded), json.loads per line after a byte prefilter, keeping toolUseResult dicts with an 'oldString' key and tallying presence and value of 'replaceAll'; (3) the same sweep, for the replaceAll==True subset only, tallying len(structuredPatch)",
          "observed": "Schema grep returns 1, and the printed literal is exactly replaceAll:M().describe(\"Whether all occurrences were replaced\"). Corpus: 13372 Edit echoes, `replaceAll` present on 13372/13372 (0 missing at any version), value false on 13302 and true on 70. Of those 70 global replacements, the structuredPatch hunk count is 1 on 16 of them (and 2 on 25, 4 on 13, 9-13 on 7, other counts on the rest).",
          "rule": "One tally per Edit echo (toolUseResult dict with an 'oldString' key), keyed by whether 'replaceAll' is present and by its value; the hunk tally is len(toolUseResult['structuredPatch']) over the replaceAll==True subset only.",
          "note": "Holds, and the claim's own corpus figure reproduces exactly: 70 echoes with replaceAll true. The key is universal on this corpus - present on all 13372 Edit echoes, so csift's unwrap_or(false) default has never had to fire. The 'ONLY signal' wording survives an adversarial test I ran against it: the obvious alternative signal, a structuredPatch with more than one hunk, would misclassify 16 of the 70 global replacements as one-shot, because a replace_all that happens to touch one contiguous region produces exactly one hunk. So there really is no substitute for the flag. Both code sites verified verbatim at the claimed lines (src/recover/carriers.rs:113-117 and src/recover/types.rs:149-155)."
        }
      ]
    },
    {
      "id": "REC-062",
      "area": "record-model",
      "behavior": "Every `structuredPatch` hunk is exactly `{oldStart, oldLines, newStart, newLines, lines}` - five keys, no more, no fewer, all schema-required and shared verbatim between the Write and Edit result shapes - where `lines` is an array of gutter-prefixed diff strings.",
      "depends": "csift's `PatchHunk` mirrors those names verbatim and `parse_structured_patch` hard-requires `oldStart`/`oldLines`/`newLines` (a missing one aborts the whole patch via `?`), so a hunk-key rename drops `recover` to the string-replacement fallback with no diagnostic.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "333-339",
          "snippet": "pub(crate) fn parse_structured_patch(v: Option<&serde_json::Value>) -> Option<Vec<PatchHunk>> {\n    let arr = v?.as_array()?;\n    let mut out = Vec::with_capacity(arr.len());\n    for h in arr {\n        let old_start = h.get(\"oldStart\").and_then(serde_json::Value::as_u64)? as usize;\n        let old_lines = h.get(\"oldLines\").and_then(serde_json::Value::as_u64)? as usize;\n        let new_lines = h.get(\"newLines\").and_then(serde_json::Value::as_u64)? as usize;"
        },
        {
          "path": "src/recover/types.rs",
          "lines": "157-164",
          "snippet": "/// One structured-patch hunk (`toolUseResult.structuredPatch[]`): a mirror of CC's\n/// `{oldStart, oldLines, newStart, newLines, lines:[\" \",\"-\",\"+\", …]}`. `newStart` is not\n/// retained - replay derives the new position from `oldStart` + the running line offset.\n#[derive(Debug, Clone)]\npub(crate) struct PatchHunk {\n    pub(crate) old_start: usize,\n    pub(crate) old_lines: usize,\n    pub(crate) new_lines: usize,"
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "337-339",
          "snippet": "        let old_start = h.get(\"oldStart\").and_then(serde_json::Value::as_u64)? as usize;\n        let old_lines = h.get(\"oldLines\").and_then(serde_json::Value::as_u64)? as usize;\n        let new_lines = h.get(\"newLines\").and_then(serde_json::Value::as_u64)? as usize;"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -cF 'c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())})'  # expect 1\npython3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'structuredPatch' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and isinstance(t.get('structuredPatch'),list):\n      for h in t['structuredPatch']:\n        if isinstance(h,dict): k[tuple(sorted(h))]+=1\nprint(k.most_common(4))\nEOF\nCounting rule: one tally per hunk object across every `structuredPatch` array, keyed by its sorted key tuple. Measured 2026-09-02: `('lines','newLines','newStart','oldLines','oldStart')` 12638 hunks and NO other tuple.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "src/recover/types.rs:157-167 comment; measured now"
      },
      "first_seen_claude_code": "2.1.150",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())})'   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes' | wc -l   AND   python3: for each of ~/.claude/projects/*/*.jsonl sorted by mtime desc, json.loads every line containing 'structuredPatch', tally each element of toolUseResult.structuredPatch by tuple(sorted(hunk_keys))",
          "observed": "binary: 1 hit, the schema constructor is `SQe=m(()=>c({oldStart:A(),oldLines:A(),newStart:A(),newLines:A(),lines:R(i())}))` - five members, none carrying `.optional()`; both file-tool schemas reference it as `structuredPatch:R(SQe())` (2 hits). corpus: 12642 hunks, ALL under the single tuple ('lines','newLines','newStart','oldLines','oldStart'); no second tuple. csift code: src/recover/carriers.rs:333 `pub(crate) fn parse_structured_patch`, :337-339 the three `?`-propagating `h.get(\"oldStart\"|\"oldLines\"|\"newLines\")` reads, and src/recover/types.rs:157-164 `PatchHunk` - all verbatim at the claimed lines.",
          "rule": "one tally per hunk object across every toolUseResult.structuredPatch array in the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by its sorted key tuple; binary counts are exact-literal grep -cF hits.",
          "note": "Schema-required is now positively shown rather than assumed: none of the five members carries `.optional()` in the constructor, and both the Write schema (`LRo`) and the Edit schema (`gJt`) spell the member as `structuredPatch:R(SQe())`, i.e. the same constructor, so the shape is shared verbatim rather than duplicated."
        }
      ]
    },
    {
      "id": "REC-063",
      "area": "record-model",
      "behavior": "Add: the sentinel is not emitted by every patch producer. One producer in the binary strips it and then drops any hunk it emptied - `return r.hunks.map((e)=>({oldStart:e.oldStart,oldLines:e.oldLines,newStart:e.newStart,newLines:e.newLines,lines:e.lines.filter((s)=>!s.startsWith(\"\\\\\\\\\"))})).filter((e)=>e.lines.length>0)` - so a sentinel-free patch is not evidence the file ended in a newline.",
      "depends": "csift's `apply_structured_patch` selects the old region with `starts_with('-') || starts_with(' ')` and the new region with `starts_with('+') || starts_with(' ')`, so the backslash sentinel falls out of both and never becomes a fabricated buffer line; a new prefix character would be dropped just as silently.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "184-193",
          "snippet": "        let old_region: Vec<String> = h\n            .lines\n            .iter()\n            .filter(|l| l.starts_with('-') || l.starts_with(' '))\n            .map(|l| l[1.min(l.len())..].to_string())\n            .collect();\n        let added: Vec<String> = h\n            .lines\n            .iter()\n            .filter(|l| l.starts_with('+') || l.starts_with(' '))"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -cF 'No newline at end of file'  # expect 3\npython3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'structuredPatch' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and isinstance(t.get('structuredPatch'),list):\n      for h in t['structuredPatch']:\n        for l in (h.get('lines') or []): k[l[:1] if l else '<empty>']+=1\nprint(k.most_common(6))\nEOF\nCounting rule: one tally per element of every hunk's `lines` array, keyed by its first character. Measured 2026-09-02: `+` 139469, ` ` 83202, `-` 30191, `\\` 21 - and no other prefix.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'No newline at end of file'   AND   python3: tally the first character of every element of every toolUseResult.structuredPatch hunk's `lines` array across ~/.claude/projects/*/*.jsonl, then re-tally the distinct full strings among those starting with a backslash",
          "observed": "binary: 3 hits, each the literal `\\ No newline at end of file`. corpus prefixes: '+' 144642, ' ' 83814, '-' 33611, '\\\\' 23, and no other first character (no empty-string element either). All 23 backslash entries are the single distinct string `\\ No newline at end of file`. csift code: src/recover/buffer.rs:184-193 verbatim - the old region filters `starts_with('-') || starts_with(' ')`, the new region `starts_with('+') || starts_with(' ')`, so the sentinel falls out of both.",
          "rule": "one tally per element of every hunk's `lines` array over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by its first character; then a second pass keyed by the full string for the backslash bucket.",
          "note": "The 23 sentinel lines split 20 on Edit echoes and 3 on Write `type:\"update\"` echoes (rule: one tally per backslash-prefixed `lines` element, keyed by whether the carrying toolUseResult has `oldString`), which is consistent with those two shapes being built by producers other than the stripping one. csift's filter is unaffected either way: the sentinel is excluded from both regions, so it never becomes a fabricated buffer line."
        }
      ]
    },
    {
      "id": "REC-064",
      "area": "record-model",
      "behavior": "Split the two halves by strength. The Write-create half is a CODE guarantee: the create branch writes the literal `structuredPatch:[]`. The Edit half is EMPIRICAL only - the Edit schema requires the key but not a non-empty array, and the Edit's patch comes from a computed diff, so 11421/11421 non-empty is a measurement, not a contract.",
      "depends": "Also correct the converse: an empty Write patch is NOT exclusively a create. A Write `type:\"update\"` emits `[]` on the too-large-to-diff branch (`let we=fe?[]:CTe(...)`, with `originalFile` set to null on the same branch), so `structuredPatch:[]` alone does not mean a new file.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "292-297",
          "snippet": "    // Only safe when the known lines are one contiguous run starting at line 1.\n    let ranges = buf.covered_ranges();\n    let contiguous_from_one = matches!(ranges.first(), Some(&(1, _))) && ranges.len() == 1;\n    if !contiguous_from_one {\n        return EditOutcome::UnAnchorable;\n    }"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nmissing=empty=total=0\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'oldString' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and 'oldString' in t:\n      total+=1; sp=t.get('structuredPatch')\n      if sp is None: missing+=1\n      elif sp==[]: empty+=1\nprint('edits',total,'missing patch',missing,'empty patch',empty)\nEOF\nCounting rule: one tally per Edit echo. Measured 2026-09-02: 11421 edits, 0 with the key missing, 0 with an empty array.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object, bucket `'oldString' in t` as an Edit and `t.get('type') in ('create','update') and 'content' in t` as a Write, then tally whether structuredPatch is missing / [] / non-empty   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:'   AND   ... | grep -cF 'let we=fe?[]:CTe({filePath:e,oldContent:W,newContent:n,convertTabs:!0})'",
          "observed": "corpus: 11421 Edit echoes - 0 with the key missing, 0 with an empty array. 2048 Write echoes split ('create','EMPTY') 1801 and ('update','NONEMPTY') 247; no create carried a non-empty patch and no update carried an empty one. binary: 1 hit for the hardcoded create branch `{type:\"create\",filePath:e,content:n,structuredPatch:[],originalFile:null,userModified:v??!1,...}`; 1 hit for the update branch `let we=fe?[]:CTe({filePath:e,oldContent:W,newContent:n,convertTabs:!0})`. csift code: src/recover/buffer.rs:292-297 verbatim (`contiguous_from_one` guard returning `EditOutcome::UnAnchorable`).",
          "rule": "one tally per file-tool echo over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by shape and by the structuredPatch state (absent / [] / non-empty).",
          "note": "Corroborating binary detail: a renderer detects that degenerate update explicitly - `i===\"update\"&&Array.isArray(s)&&s.length===0&&l===null&&typeof a===\"string\"&&a!==\"\"` (1 hit) - and re-renders it as a create, which is exactly the ambiguity REC-065 documents."
        }
      ]
    },
    {
      "id": "REC-065",
      "area": "record-model",
      "behavior": "A `Write` result's `structuredPatch` is documented by its own schema as \"empty when nothing changed, the diff timed out, or - with originalFile null on an update - the previous content was too large to diff\", so an empty patch array is ambiguous between three unrelated causes.",
      "depends": "csift parses `structuredPatch` into positional hunks and falls back to a string replacement when it is absent or unusable, and an edit whose anchor neighbourhood is unknown becomes a counted un-anchorable coverage hole rather than a fabricated line - which is why an empty patch degrades coverage rather than corrupting content.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "333-336",
          "snippet": "pub(crate) fn parse_structured_patch(v: Option<&serde_json::Value>) -> Option<Vec<PatchHunk>> {\n    let arr = v?.as_array()?;\n    let mut out = Vec::with_capacity(arr.len());\n    for h in arr {"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes[^\"]*\"' - expect two describe variants, the Write one carrying the three-cause caveat and the Edit one the bare form. Counting rule: distinct describe literals, deduplicated.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -o 'Diff patch showing[^\"]*' | sort -u   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF 'structuredPatch:R(SQe()).describe(\"Diff patch showing the changes' | wc -l",
          "observed": "exactly two distinct describe literals, deduplicated: `Diff patch showing the changes` and `Diff patch showing the changes (empty when nothing changed, the diff timed out, or \\u2014 with originalFile null on an update \\u2014 the previous content was too large to diff)`; the `structuredPatch:R(SQe()).describe(` prefix occurs 2 times, once per schema. Reading the surrounding schema bodies puts the bare form on the Edit schema (`gJt=m(()=>c({filePath:i().describe(\"The file path that was edited\"),...`) and the three-cause form on the Write schema (`LRo=m(()=>c({type:ee([\"create\",\"update\"])...`). csift code: src/recover/carriers.rs:333-336 verbatim.",
          "rule": "distinct describe string literals in the binary, deduplicated with sort -u; then one attribution per literal by which schema constructor body encloses it.",
          "note": "The three causes are separately corroborated in code: the too-large cause is the ternary `let we=fe?[]:CTe(...)` on the update branch (same `fe` flag also nulls `originalFile`, which is why the caveat pairs them), and a downstream renderer detects that exact degenerate shape. Nothing in the echo distinguishes the three, so csift is right to treat an empty patch as a coverage hole rather than a signal."
        }
      ]
    },
    {
      "id": "REC-066",
      "area": "record-model",
      "behavior": "Drop the word 'only'. Two further keys discriminate just as cleanly in the measured corpus and in both schemas: `replaceAll` is on 11421/11421 Edit echoes and 0/2048 Write echoes, and `content` is on 2048/2048 Write echoes and 0/11421 Edit echoes. The accurate statement is that `structuredPatch`, `originalFile` and `userModified` are the three shared keys, while `type`+`content` (Write) and `oldString`/`newString`+`replaceAll` (Edit) are each sufficient discriminators.",
      "depends": "csift `recover` checks `oldString`/`newString` FIRST and only falls through to the `content` full-snapshot arm, so the shared keys are harmless today; a discriminator rewritten to key on `structuredPatch` presence would classify every Write as an edit.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "97-101",
          "snippet": "    let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n    let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));\n\n    if has_edit_strings {\n        // An Edit (carrier side): keep the strings + structuredPatch + originalFile."
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if '\"toolUseResult\"' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if not isinstance(t,dict): continue\n    if 'oldString' in t: k[('edit', 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t)]+=1\n    elif t.get('type') in ('create','update') and 'content' in t: k[('write', 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t)]+=1\nprint(k.most_common(4))\nEOF\nCounting rule: one tally per file-tool echo, keyed by shape and by presence of the three shared keys. Measured 2026-09-02: `('edit',True,True,True)` 11421 and `('write',True,True,True)` 2048 - no other tuple.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: over ~/.claude/projects/*/*.jsonl, key every Edit echo (`'oldString' in t`) and every Write echo (`t.get('type') in ('create','update') and 'content' in t`) by (shape, 'structuredPatch' in t, 'originalFile' in t, 'userModified' in t), and separately tally every top-level key of each echo   AND   strings -n 6 -a <the Claude Code 2.1.258 binary>, reading the `gJt` (Edit) and `LRo` (Write) schema bodies",
          "observed": "corpus: exactly two tuples - ('edit',True,True,True) 11421 and ('write',True,True,True) 2048; no third tuple, so none of the three keys discriminates. Per-key census, Edit echoes: filePath/oldString/newString/originalFile/structuredPatch/userModified/replaceAll all 11421, staleRecovered 80, memdirStamped 41. Write echoes: type/filePath/content/structuredPatch/originalFile/userModified all 2048, memdirStamped 49. binary: Edit schema `c({filePath,oldString,newString,originalFile:i().nullable(),structuredPatch:R(SQe()),userModified:M(),replaceAll:M(),gitDiff:kQe().optional()})`; Write schema `c({type:ee([\"create\",\"update\"]),filePath,content,structuredPatch:R(SQe()),originalFile:i().nullable(),gitDiff:kQe().optional(),userModified:M().optional()})`. csift code: src/recover/carriers.rs:97-101 verbatim.",
          "rule": "one tally per file-tool echo over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by shape and by presence of the three shared keys; plus one tally per (shape, top-level key) pair.",
          "note": "One asymmetry the claim does not mention and which matters if anyone keys on presence: `userModified` is REQUIRED on the Edit schema (`userModified:M()`) but OPTIONAL on the Write schema (`userModified:M().optional()`). It was present on all 2048 measured Write echoes anyway, because the Write builders always spread `userModified:v??!1`, but a consumer must not rely on that."
        }
      ]
    },
    {
      "id": "REC-067",
      "area": "record-model",
      "behavior": "The absence is explained, not merely unwitnessed. The Edit result builder computes the member only inside `if(a.CLAUDE_CODE_REMOTE){...}` and then spreads it conditionally as `...De&&{gitDiff:De}`, so `gitDiff` is emitted only by remote Claude Code sessions. A local session can never produce it, which is why the local corpus reads 0 and will keep reading 0.",
      "depends": "csift's carrier extractor reads only the keys it names, so an unmodelled `gitDiff` is inert today; it matters as a warning that the echo is an OPEN object and a future `type`-like discriminator could arrive inside it.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "93-98",
          "snippet": "    let path = tur.get(\"filePath\").and_then(serde_json::Value::as_str);\n    if !path_matches(target_file, path.unwrap_or_default()) {\n        return;\n    }\n    let has_edit_strings = tur.get(\"oldString\").is_some() || tur.get(\"newString\").is_some();\n    let structured_patch = parse_structured_patch(tur.get(\"structuredPatch\"));"
        }
      ],
      "instrument": "Add the deciding condition to the instrument so a stranger knows what would flip it: `strings -n 6 -a <binary> | grep -cF 'if(a.CLAUDE_CODE_REMOTE)'` (14 hits) plus the `...De&&{gitDiff:De}` spread in the Edit result object.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'kQe=m(()=>c({filename:i(),status:ee([\"modified\",\"added\"]),additions:A(),deletions:A(),changes:A(),patch:i(),repository:i().nullable().optional()'   AND   ... | grep -oF 'gitDiff:kQe().optional()' | wc -l   AND   ... | grep -cF 'if(a.CLAUDE_CODE_REMOTE)'   AND   python3: count raw jsonl lines containing the substring 'gitDiff', and count parsed toolUseResult objects carrying the key",
          "observed": "binary: 1 hit for the constructor `kQe=m(()=>c({filename:i(),status:ee([\"modified\",\"added\"]),additions:A(),deletions:A(),changes:A(),patch:i(),repository:i().nullable().optional()}))`; 2 hits for `gitDiff:kQe().optional()`, one in each file-tool schema. corpus: 0 raw lines containing 'gitDiff' and 0 echoes carrying the key. csift code: src/recover/carriers.rs:93-98 verbatim.",
          "rule": "binary: exact-literal grep -cF hits. corpus: one tally per raw jsonl line containing the substring `gitDiff`, and one per parsed toolUseResult object with the key, over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist).",
          "note": "The 'open object' warning in the depends field is sound and is independently demonstrated by REC-069: `memdirStamped` and `staleRecovered` both ride on the echo without appearing in either zod schema."
        }
      ]
    },
    {
      "id": "REC-068",
      "area": "record-model",
      "behavior": "The behavior text says 11418 measured Edit echoes while its own instrument says 11421; the measured figure now is 11421 with 0 true. Use one number.",
      "depends": "Add the provenance split: in the tool path the field is computed as `userModified:_??!1` (Edit) / `userModified:v??!1` (Write), but a separate record-reconstruction path sets `userModified:r.editedByApproval`. Both feed the same key, so a consumer reading it cannot tell which path produced it.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "146-152",
          "snippet": "            let (op, key) = match name {\n                \"Write\" => (FileOp::Write, \"file_path\"),\n                \"Edit\" => (FileOp::Edit, \"file_path\"),\n                \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n                \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n                _ => continue,\n            };"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "86-92",
          "snippet": "                if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {\n                    events.push(FileEvent {\n                        line_no,\n                        turn_index,\n                        timestamp_utc: ts.clone(),\n                        kind: EventKind::FullSnapshot {\n                            content: content.to_string(),"
        }
      ],
      "instrument": "Tally every Edit echo by its `userModified` value. Counting rule: one tally per carrier record. Measured 2026-09-02: `False` 11421, `True` 0 over the 120 most-recently-modified top-level transcripts, and 0 true corpus-wide - the branch is schema-attested but not corpus-witnessed here. Schema authority: `strings -a <the Claude Code 2.1.258 binary> | grep -F 'userModified:M().describe(\"Whether the user modified the proposed changes\")' | wc -l` returns 1.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: tally `t.get('userModified')` over every Edit echo (`'oldString' in t`) in ~/.claude/projects/*/*.jsonl   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -cF 'userModified:M().describe(\"Whether the user modified the proposed changes\")'   AND   ... | grep -cF 'userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")'   AND   ... | grep -oF 'The user modified your proposed content before accepting it.' | wc -l   AND   ... | grep -oF 'The user modified your proposed changes before accepting them.' | wc -l",
          "observed": "corpus: {False: 11421}, i.e. 11421 Edit echoes all false and 0 true. binary: the Edit describe literal 1 hit; the Write describe literal `userModified:M().optional().describe(\"True when the user edited the proposed content in the permission dialog before accepting\")` 1 hit; each prose note 2 hits. The Edit note is emitted as `let ... v=o?\".  The user modified your proposed changes before accepting them. \":\"\"` inside the Edit `mapToolResultToToolResultBlockParam`, the Write note as `let f=r?\" The user modified your proposed content before accepting it.\":\"\"` inside the Write one - both with exactly the spacing the claim records. csift code: src/model/mutation.rs:146-152 and src/recover/events.rs:86-92 verbatim.",
          "rule": "one tally per Edit carrier record over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by the boolean value; binary counts are exact-literal grep hits.",
          "note": "The true branch remains schema-attested and code-attested (both prose notes exist and are reachable) but is still not corpus-witnessed on this machine. What would decide it: accept a Write or Edit through the permission dialog after editing the proposed content in that dialog, then read back the resulting toolUseResult - no such acceptance appears in the local corpus."
        }
      ]
    },
    {
      "id": "REC-069",
      "area": "record-model",
      "behavior": "`memdirStamped` is not the only unschema'd rider. The per-key census over the same window shows `staleRecovered` on 80 of 11421 Edit echoes, likewise spread conditionally (`...we&&{staleRecovered:!0}`) and likewise absent from both schemas. The general statement - the echo is extensible in place with no schema-version marker - is confirmed by two keys, not one.",
      "depends": "csift's `TurProbe` deserializes only the small named fields and serde's ignore path skips everything else, so an added key costs nothing and never fails the probe - the tolerance invariant is what absorbs this drift.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "300-305",
          "snippet": "#[derive(Debug, Default, Deserialize)]\n#[serde(default)]\npub(crate) struct TurProbe {\n    pub(crate) r#type: Option<serde_json::Value>,\n    #[serde(rename = \"filePath\")]\n    pub(crate) file_path: Option<serde_json::Value>,"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if 'memdirStamped' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and 'memdirStamped' in t: k['edit' if 'oldString' in t else 'write']+=1\nprint(k)\nEOF\nCounting rule: one tally per echo carrying a `memdirStamped` key, split by shape. Measured 2026-09-02: write 49, edit 39.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object carrying a `memdirStamped` key, tally 'edit' when `'oldString' in t` else 'write'   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -oF '&&{memdirStamped:!0}' | wc -l",
          "observed": "corpus: write 49, edit 41. binary: 3 hits for the conditional spread `&&{memdirStamped:!0}`; it appears in the Edit result object `{data:{filePath,oldString,newString,originalFile,structuredPatch,userModified,replaceAll,...we&&{staleRecovered:!0},...Ie&&{memdirStamped:!0},...De&&{gitDiff:De}}}` and in the Write update/create objects, and the key is destructured by both `mapToolResultToToolResultBlockParam` bodies (`let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e`). Neither zod schema (`gJt`, `LRo`) lists it. csift code: src/model/record.rs:299-304 verbatim (`TurProbe` with `#[serde(default)]`).",
          "rule": "one tally per echo carrying a `memdirStamped` key over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), split by whether the echo has `oldString`.",
          "note": "csift already consumes `staleRecovered` on the recover side, so the tolerance point stands for both: `TurProbe` names six small fields and serde's ignore path drops everything else, so neither rider can fail the probe."
        }
      ]
    },
    {
      "id": "REC-070",
      "area": "record-model",
      "behavior": "The `filePath` that joins a file mutation to its result lives at the TOP level of the Write/Edit echo but one level DOWN (`file.filePath`) on the Read echo, and the tool_use record itself carries the path under the snake_case `input.file_path`.",
      "depends": "csift's `carrier_create_paths` probes only the top-level `filePath`, so a Read echo never mints a carrier row and never sets `is_create`; `recover` has a separate `file`-object arm for the Read shape, and the two must stay separate or a Read would be joined as a mutation.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "175-179",
          "snippet": "    /// The carrier side of a file mutation: when this record's `toolUseResult` is an\n    /// object carrying a `filePath`, return `(tool_use_id, filePath, is_create)` for\n    /// each `tool_result` block, so the `files` joiner can set `is_create` on the\n    /// matching structured mutation (and fall back to this `filePath` if the\n    /// tool_use's own path was somehow absent)."
        },
        {
          "path": "src/recover/carriers.rs",
          "lines": "56-58",
          "snippet": "    if let Some(file) = tur.get(\"file\").and_then(|v| v.as_object()) {\n        let path = file.get(\"filePath\").and_then(serde_json::Value::as_str);\n        if path_matches(target_file, path.unwrap_or_default()) {"
        }
      ],
      "instrument": "The claim asserts three placements but only instruments two. Add the tool_use pass above so the snake_case `input.file_path` third of the claim is measured rather than asserted: Edit 11670 / Read 6122 / Write 2087, zero `filePath` in any input.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "src/model/mutation.rs:175-184 doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult object, key by (top-level `filePath` present, `file` object with a `filePath` present)   AND   a second pass over every `tool_use` block named Write/Edit/MultiEdit/Read/NotebookEdit, keyed by which of `file_path` / `filePath` / `notebook_path` its `input` carries   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> | grep -F 'type:\"pdf\"'",
          "observed": "corpus echoes: (True,False) 13482 write/edit, (False,True) 5805 read, (True,True) 0 - the two placements never co-occur. corpus tool_use inputs: Edit ('file_path',) 11670, Read ('file_path',) 6122, Write ('file_path',) 2087 - snake_case in every case, and no block carried `filePath` in its input. binary: the Read PDF branch returns `{type:\"pdf\",file:{filePath:e,base64:a,originalSize:o}}`, showing the nested placement in the producer. csift code: src/model/mutation.rs:175-179 and src/recover/carriers.rs:56-58 verbatim.",
          "rule": "one tally per echo carrying a `filePath` anywhere, keyed by (top-level, nested-under-file), over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist); and one tally per file-tool `tool_use` block, keyed by the path key present in its `input`.",
          "note": "The (True,True) cell being empty is the load-bearing part for csift: it is what lets `carrier_create_paths` probe only the top-level `filePath` without ever minting a carrier row from a Read echo."
        }
      ]
    },
    {
      "id": "REC-071",
      "area": "record-model",
      "behavior": "`toolUseResult.type` is a shared key across unrelated tools with disjoint value sets - `create`/`update` on a Write, `text`/`image`/`file_unchanged`/`parts`/`pdf` on a Read - so `type` alone identifies neither the tool nor a mutation.",
      "depends": "csift `files` treats ONLY the literal `create` as a new file and everything else as an edit, which is what keeps a Read's `type:\"text\"` from being read as a mutation kind; widening that comparison to any non-empty `type` would classify reads as writes.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "186-195",
          "snippet": "    pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n        let Some(probe) = self.tur_probe() else {\n            return Vec::new();\n        };\n        let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n            return Vec::new();\n        };\n        if file_path.is_empty() {\n            return Vec::new();\n        }"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if '\"toolUseResult\"' not in line: continue\n    try: t=json.loads(line).get('toolUseResult')\n    except Exception: continue\n    if isinstance(t,dict) and isinstance(t.get('type'),str):\n      k[(t['type'], 'filePath' in t, isinstance(t.get('file'),dict))]+=1\nprint(k.most_common(8))\nEOF\nCounting rule: one tally per echo with a string `type`, keyed by `(type, has top-level filePath, has file object)`. Measured 2026-09-02: `('text',False,True)` 5700, `('create',True,False)` 1801, `('image',False,True)` 269, `('update',True,False)` 247, `('file_unchanged',False,True)` 85, `('parts',False,True)` 18, `('pdf',False,True)` 1 - create/update always top-level, the Read values always nested, never both.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "src/model/mutation.rs:181-184 doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: over ~/.claude/projects/*/*.jsonl, for every parsed toolUseResult with a string `type`, key by (type, top-level `filePath` present, `file` object present)   AND   strings -n 6 -a <the Claude Code 2.1.258 binary> for the producer literals `type:\"create\"`/`type:\"update\"`/`type:\"file_unchanged\"`/`type:\"parts\"`/`type:\"pdf\"`",
          "observed": "corpus: ('text',False,True) 5701, ('create',True,False) 1801, ('image',False,True) 269, ('update',True,False) 247, ('file_unchanged',False,True) 85, ('parts',False,True) 18, ('pdf',False,True) 1 - create/update always top-level-filePath, the five Read values always nested under `file`, never both, and the two value sets never overlap. binary: `type:ee([\"create\",\"update\"])` is the Write schema's enum (1 hit); the Read side has no single enum but the producers are literal, e.g. `{type:\"pdf\",file:{filePath:e,base64:a,originalSize:o}}` (1 hit), `type:\"file_unchanged\"` (2 hits), `type:\"parts\"` (1 hit). csift code: src/model/mutation.rs:186-195 verbatim, and :196 `let is_create = probe.r#type... == Some(\"create\")` is the only comparison made.",
          "rule": "one tally per echo with a string `type` over the 67 top-level transcripts under ~/.claude/projects/*/*.jsonl (16 project dirs; the [:120] slice is not binding because only 67 exist), keyed by (type, has top-level filePath, has file object).",
          "note": "The disjointness is the checkable part and it held on every one of the 8122 typed echoes measured: no Read-side value ever appeared with a top-level `filePath`, and neither `create` nor `update` ever appeared with a `file` object. That is what keeps csift's single-literal `create` comparison safe. code-site note: src/model/mutation.rs:181 is stale. The doc comment reads ``/// `toolUseResult.type` in {`create`, `update`, `file_unchanged`, `text`, `image`};`` - five values - but the measured corpus carries seven: `parts` (18) and `pdf` (1) are also Read-side values. The code is unaffected (line 196 compares only against the literal `create`); only the comment under-enumerates."
        }
      ]
    },
    {
      "id": "REC-072",
      "area": "record-model",
      "behavior": "The per-tool path key diverges the other way on the RESULT side from the `input.file_path` / `input.notebook_path` split on the call side: `Write`, `Edit` and `MultiEdit` echo the path as `toolUseResult.filePath` (binary `responseMembers:[\"filePath\"]`), but `NotebookEdit` echoes it as `toolUseResult.notebook_path` (binary `responseMembers:[\"notebook_path\"]`) inside a ten-member result object {new_source, old_source, cell_type, language, edit_mode, cell_id, error, notebook_path, original_file, updated_file} - there is no `filePath` member on a NotebookEdit carrier at all. The same table shows the split is a per-tool lookup rather than a naming convention: `Read` declares `responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]` instead of `responseMembers`, and `LSP` uses the camelCase `filePath` on its INPUT side.",
      "depends": "csift's carrier join `Record::carrier_create_paths` reads ONLY `toolUseResult.filePath`, so a NotebookEdit carrier never joins its tool_use: the notebook mutation's `is_create` stays at its `false` default (\"unknown / treat as edit\") forever, and `files --by file` can never report a notebook as created.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "190-192",
          "snippet": "        let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n            return Vec::new();\n        };"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'notebook_path:i().describe(\"The path to the notebook file\")[^`]\\{0,200\\}' - expect the NotebookEdit output schema `{notebook_path, original_file, updated_file}` with no `filePath` member. Counting rule: one hit per schema literal.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var loe=\\{.{0,430}'   # the per-tool path-key table\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,160}original_file.{0,120}'   # the NotebookEdit result schema\ncsift search '' --count-by tool -t agent.tool.use | rg -i 'notebook|multiedit'   # on-disk instances",
          "observed": "table literal: var loe={Read:{input:\"file_path\",responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]},Glob:{input:\"path\",responseArrays:[\"filenames\"]},Grep:{input:\"path\",responseArrays:[\"filenames\"]},LSP:{input:\"filePath\",responseMembers:[\"filePath\"]} ; responseMembers:[\"filePath\"] appears 4x and responseMembers:[\"notebook_path\"] 1x in the whole binary ; NotebookEdit result schema declares notebook_path:i().describe(\"The path to the notebook file\"),original_file:i().describe(\"The original notebook content before modification\"),updated_file:i().describe(\"The updated notebook conte... and the runtime object literal is {new_source, old_source, cell_type, language, edit_mode, cell_id, error, notebook_path, original_file, updated_file} ; csift tool census over the whole corpus lists Read 33113, Edit 16970, Write 4889 and NO NotebookEdit and NO MultiEdit key",
          "rule": "Binary: one count per distinct `strings` output line containing the literal; the table is a single minified line. Corpus: one count per tool_use BLOCK whose `name` equals the tool, over every *.jsonl under ~/.claude/projects excluding journal.jsonl and elicitations.jsonl.",
          "note": "csift code site verified verbatim at the stated line: src/model/mutation.rs:190-192 still reads only `probe.file_path`, and TurProbe declares `#[serde(rename = \"filePath\")]` on that one field (src/model/record.rs:303-304), so the described consequence - a NotebookEdit carrier never joins its tool_use and its `is_create` stays at the `false` default - follows from the current code. It has never been exercised on this corpus because no NotebookEdit call was ever recorded here."
        }
      ]
    },
    {
      "id": "REC-073",
      "area": "record-model",
      "behavior": "A `tool_result` block marked `is_error:true` never carries a structured `toolUseResult` OBJECT - but the field is not absent either: on 4211 of 4211 errored blocks corpus-wide, in all three lanes, `toolUseResult` is present and typed as a JSON STRING holding the error text ('Error: ...', 'InputValidationError: ...'). The structured object echo is emitted only for ops that landed; a failed op degrades the same field to a string.",
      "depends": "csift `recover` derives `has_structured_result` from `rec.tool_use_result.is_some()`, and `Record::tool_use_result` is `Option<Box<serde_json::value::RawValue>>` (src/model/record.rs:123), which is `Some` for a STRING as well as for an object. So on real data a failed op's id IS inserted into `ids_with_result` and the input-side fallback is already suppressed for it there; the separate `failed_ids` set built from `is_error:true` is a redundant second gate on this path rather than the only thing stopping a rejected edit from being replayed. `failed_ids` remains load-bearing as a design guarantee and for the shape the e2e fixture builds (a bare-string tool_result with NO `toolUseResult` key at all), which the live corpus never exhibits.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "417",
          "snippet": "            let has_structured_result = rec.tool_use_result.is_some();"
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "426-431",
          "snippet": "                        if has_structured_result {\n                            ids_with_result.insert(id.clone());\n                        }\n                        if *is_error == Some(true) {\n                            failed_ids.insert(id.clone());\n                        }"
        }
      ],
      "instrument": "python3 - <<'EOF'\nimport json,glob,os,collections\nfiles=sorted(glob.glob(os.path.expanduser('~/.claude/projects/*/*.jsonl')),key=lambda p:-os.path.getmtime(p))[:120]\nk=collections.Counter()\nfor f in files:\n  for line in open(f,errors='replace'):\n    if '\"is_error\":true' not in line: continue\n    try: r=json.loads(line)\n    except Exception: continue\n    c=(r.get('message') or {}).get('content')\n    if not isinstance(c,list): continue\n    for b in c:\n      if isinstance(b,dict) and b.get('type')=='tool_result' and b.get('is_error'):\n        k[isinstance(r.get('toolUseResult'),dict)]+=1\nprint(k)\nEOF\nCounting rule: one tally per errored `tool_result` block, keyed by whether its carrying record has an object `toolUseResult`. Measured 2026-09-02: `False` 986, `True` 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.6 (`A STRUCTURED op whose tool_result is is_error:true is EXCLUDED`)"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every *.jsonl under ~/.claude/projects (journal.jsonl and elicitations.jsonl excluded): for every `tool_result` block with is_error true, tally type(record['toolUseResult']).__name__, or 'ABSENT' when the key is missing, bucketed by lane; cross-checked with `csift search '' --count-by result -t agent.tool.result`.",
          "observed": "errored tool_result blocks by (lane, toolUseResult type): ('main','str') 986, ('subagent','str') 626, ('workflow','str') 2599 - i.e. 4211 of 4211 carry the key typed STRING, 0 ABSENT, 0 dict. An earlier pass minutes before totalled 4208 with the same 0 ABSENT / 0 dict split, and csift independently reported 4208 at that moment ('210389 ok / 4208 error, 214597 matched record(s) across 2 result key(s)'); the 3-block difference is corpus growth between runs, not a disagreement. The string is the error text - the 12 most common 22-character prefixes are 'Error: Output does not' 754, 'Error: Exit code 1' 469+149+119, 'Error: File does not e' 450, 'Error: File has not be' 218, 'Error: String to repla' 180, 'InputValidationError: ' 102 (387 distinct prefixes overall), never empty. Restricted to failed FILE-TOOL ops the type is still 'str' everywhere: main Edit 251, main Write 45, subagent Edit 23, subagent Write 22, workflow Edit 34, workflow Write 84.",
          "rule": "One count per errored `tool_result` BLOCK, keyed by the JSON type of `toolUseResult` on the CARRYING record. Lane = 'main' when the file path has no `/subagents/` component, 'workflow' when it also has `/workflows/`, else 'subagent'. csift's cross-check counts RECORDS, and the two agree, so there is one errored block per record.",
          "note": "The in-repo comment at src/recover/scan.rs:410-411 states 'A failed Edit also has NO `toolUseResult` echo, so its id is absent from `ids_with_result`'. The measurement refutes that sentence: the key is present as a string, so the id IS inserted. The behavioural outcome is unchanged (the ghost edit is not replayed either way) - the defect is in the comment's stated mechanism, and it is worth correcting because the comment is what a reader would rely on when deciding whether `failed_ids` can be removed. Both quoted code snippets verify verbatim at src/recover/scan.rs:417 and :426-431."
        }
      ]
    },
    {
      "id": "REC-074",
      "area": "record-model",
      "behavior": "The result block's `is_error` boolean is written on the `tool_result` BLOCK and never on the record - 125786 blocks carry the key and 0 records do. In workflow lanes the key is present on 72449 of 119879 blocks and true on 2599 (snapshot 2026-09-02 21:52 AEST; the claim's 71339 / 118639 / 2591 was the same shape measured about three hours earlier on the same still-growing corpus).",
      "depends": "csift computes `failed_ids` per TURN from that block flag and applies the same gate in `recover`'s replay, in `files`' structured extraction and in the subagent-topology mutation helper, so all three agree that a failed op never landed; dropping the flag would make `files` report writes `recover` can find no history for.",
      "code": [
        {
          "path": "src/files/mutations.rs",
          "lines": "23-33",
          "snippet": "        if let Some(blocks) = rec.blocks() {\n            for b in blocks {\n                if let crate::model::Block::ToolResult {\n                    tool_use_id: Some(id),\n                    is_error: Some(true),\n                    ..\n                } = b\n                {\n                    failed_ids.insert(id.clone());\n                }\n            }"
        }
      ],
      "instrument": "python3 over ~/.claude/projects/**/subagents/workflows/**/*.jsonl tallying, per `tool_result` block, whether the `is_error` key is present and whether it is true. Counting rule: one count per tool_result BLOCK. Observed 118639 blocks, 71339 carrying the key, 2591 true.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.6 (the failed_ids gate); counts measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every *.jsonl under ~/.claude/projects: for every `tool_result` BLOCK tally (lane, block-count), (lane, 'is_error' key present) and (lane, is_error is True); separately tally records carrying a TOP-LEVEL `is_error` key.",
          "observed": "snapshot 2026-09-02 21:52:39 AEST - workflow lane: 119879 tool_result blocks, 72449 carrying the `is_error` key, 2599 true. main lane: 59146 / 33104 / 986. other-subagent lane: 35684 / 20233 / 626. Records with a top-level `is_error` key: 0. Blocks with an `is_error` key: 125786.",
          "rule": "One count per `tool_result` BLOCK (a record may carry several). 'key present' means the literal `is_error` key exists on the block regardless of value; 'true' means it equals JSON true. Lane assignment as in REC-073. The corpus is live and grows while sessions run, so the absolute counts are a timestamped snapshot; the reproducible facts are the ratio shape and the block-versus-record placement.",
          "note": "Code site verified verbatim: src/files/mutations.rs:23-33 still matches `Block::ToolResult { tool_use_id: Some(id), is_error: Some(true), .. }` on blocks. The claim's structural point - the flag lives on the block - is confirmed decisively by the zero top-level count."
        }
      ]
    },
    {
      "id": "REC-075",
      "area": "record-model",
      "behavior": "A tool_use and its result are joined only by the `tool_use` block's `id` echoed as the result block's `tool_use_id`, and Claude Code always emits the result inside the same genuine-user turn as the call: 0 of about 214000 joins corpus-wide had a turn-opener candidate between the call record and the result record. Line ORDER within the turn is not guaranteed, however - 4 main-lane joins have the result line 1-2 lines BEFORE the call line, all under CC 2.1.156 / 2.1.177.",
      "depends": "csift builds `id_to_path`, `ids_with_result` and `failed_ids` PER TURN and joins the carrier to the structured mutation within the turn, so an id whose result landed in a later turn would be treated as carrier-less: `files` would lose its create-vs-edit accuracy and `recover` would fire the input-side fallback on an op that has an echo.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "334-399",
          "snippet": "    for (turn_index, idxs) in turns.iter().enumerate() {\n        // tool_use_id → file_path for THIS turn's Read/Edit/Write/MultiEdit tool_uses.\n        let mut id_to_path: BTreeMap<String, String> = BTreeMap::new();"
        }
      ],
      "instrument": "The claim's instrument - reading the e2e fixture test `tests/cli/recover/modes.rs::recover_subagent_input_fallback_skips_failed_edit` (the function exists at tests/cli/recover/modes.rs:6) - reads a synthetic fixture and so cannot decide a claim about Claude Code's behaviour. Replace it with the corpus join-locality census above, which measures the real transcripts.",
      "located": {
        "claude_code": null,
        "csift": "0.2.0",
        "source": "src/recover/scan.rs:410-413 comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every *.jsonl under ~/.claude/projects: per file, record the line index of every `tool_use` block id and of the first `tool_result` block echoing it as `tool_use_id`, plus the indices of every TURN-OPENER CANDIDATE record; then bucket each join by whether an opener index lies strictly between call and result. Cross-checked corpus-wide with `csift search '' --count-by pairing -t agent.tool`.",
          "observed": "join locality over ~214k joins: main same_turn 58489, subagent same_turn 34778, workflow same_turn 119693; opener_between 0 in every lane; orphan_result 0 in every lane; unreturned main 1, subagent 29, workflow 2; main result_before_call 4. 14380 turn-opener candidates were found across 7596 of 7649 files (main 6498, workflow 6746, subagent 1136), so the test was not vacuous; the remaining 53 opener-less files contributed 1322 joins that no boundary could separate and are reported apart. csift agrees: '429040 paired / 35 pending' across 2 pairing keys, with no orphan key at all. The 4 reversed pairs (result line before call line by 1-2 lines) are 2 Edit and 2 Read, all on records stamped version 2.1.156 or 2.1.177, none on a sidechain.",
          "rule": "One count per (file, tool_use id) pair whose result appears in the same file. TURN-OPENER CANDIDATE := type=='user' AND message.role=='user' AND not isMeta AND not isCompactSummary AND (content is a string OR content is a block list containing a text block and NO tool_result block) AND the leading text does not start with '<local-command-stdout>', '<command-name>', '<command-message>', '[Request interrupted by user', 'Caveat: The messages below' or '<system-reminder>'. 'opener_between' counts joins where at least one such index is strictly greater than the call index and strictly less than the result index.",
          "note": "The reversed-order pairs do not threaten csift: the per-turn maps at src/recover/scan.rs:397-399 are sets keyed by id, built over the whole turn before any event is emitted, so they are order-independent within a turn. The code snippet verifies verbatim at the stated lines. What would additionally strengthen this claim is a lane where a tool call is interrupted and answered after a new human message; none occurs in this corpus."
        }
      ]
    },
    {
      "id": "REC-076",
      "area": "record-model",
      "behavior": "Claude Code's file-write tool_result TEXT templates are `File created successfully at: <path>` for a create and `The file <path> has been updated successfully.` for an update, both taking the suffix ` (file state is current in your context \\u2014 no need to Read it back)` - the separator is an EM DASH (U+2014), not a hyphen - unless the user modified the content or the write was memdir-stamped; all of those literals live in the 2.1.258 binary.",
      "depends": "csift never parses these strings - `recover` routes on the presence or absence of the structured `toolUseResult` object and supplies bytes from the tool_use input instead - so a reworded success message changes nothing; the templates matter only as the observable marker of the carrier-less workflow-subagent shape, where they are the sole trace of a write and carry the path but never the content.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "84-91",
          "snippet": "        match name.as_str() {\n            \"Write\" => {\n                if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {\n                    events.push(FileEvent {\n                        line_no,\n                        turn_index,\n                        timestamp_utc: ts.clone(),\n                        kind: EventKind::FullSnapshot {"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "36",
          "snippet": "/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with"
        }
      ],
      "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'File created successfully at'  # 2\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'has been updated'  # 5, not 6",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'File created successfully at'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | grep -cF 'has been updated'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{420}File created successfully at: \\$\\{e\\}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'O3t=.{0,220}'",
          "observed": "'File created successfully at' -> 2 matching strings lines (as expected). 'has been updated' -> 5 matching strings lines, NOT 6. The Write tool's result mapper reads: mapToolResultToToolResultBlockParam({filePath:e,type:n,userModified:r,memdirStamped:o},d){let f=r?\" The user modified your proposed content before accepting it.\":\"\",_=r||o?\"\":O3t;switch(n){case\"create\":return{tool_use_id:d,type:\"tool_result\",content:`File created successfully at: ${e}${f}${_}`};case\"update\":return{tool_use_id:d,type:\"tool_result\",content:`The file ${e} has been updated successfully.${f}${_}`}} and the suffix constant is O3t=\" (file state is current in your context \\u2014 no need to Read it back)\".",
          "rule": "`grep -c` counts MATCHING strings OUTPUT LINES, not occurrences; both literals are template fragments the runtime concatenates with the path. The guard is read off the minified ternary: the suffix is appended iff userModified is false AND memdirStamped is false.",
          "note": "The guard clause the claim states is exactly right and now has its minified source: `_=r||o?\"\":O3t` with r=userModified and o=memdirStamped. The claim's independence argument also holds - csift never parses these strings; both quoted code sites verify verbatim (src/recover/events.rs:84-91 and the doc line at :36)."
        }
      ]
    },
    {
      "id": "REC-077",
      "area": "record-model",
      "behavior": "The update-side tool_result STRING is identical for a `Write` of type update and for a successful `Edit` whenever userModified is false - both then render `The file <path> has been updated successfully.` plus the shared suffix - so the result text alone cannot tell a Write from an Edit. The two templates are not byte-identical in general: Write emits `The file ${e} has been updated successfully.${f}${_}` (period before the userModified clause) while Edit emits `The file ${r} has been updated successfully${v}.${C}` (period after it), so under userModified the two texts differ, and Edit alone can substitute a staleRecovered note for the suffix. A `replace_all` Edit renders `The file <path> has been updated. All occurrences were successfully replaced.`",
      "depends": "csift's input-side fallback therefore dispatches on the tool_use `name`, never on the result text: `Write` becomes a full-snapshot anchor that RESETS the reconstruction buffer while `Edit` becomes a hunk applied to it, and confusing the two would either wipe or corrupt the replay.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "84-85",
          "snippet": "        match name.as_str() {\n            \"Write\" => {"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'has been updated[^\"]\\{0,120\\}' | sort -u - expect the three distinct forms (`has been updated successfully.`, `has been updated successfully${v}.${C}` from the Write path, and `has been updated${v}. All occurrences were successfully replaced.${C}` from the Edit replace_all path). Counting rule: distinct string literals in the binary, deduplicated.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'has been updated[^\"]{0,140}' | sort -u\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{760}All occurrences were successfully replaced'",
          "observed": "three template forms, deduplicated: `has been updated successfully.${f}${_}` (Write, case \"update\"), `has been updated successfully${v}.${C}` (Edit, non-replace_all) and `has been updated${v}. All occurrences were successfully replaced.${C}` (Edit, replace_all). The Edit mapper is mapToolResultToToolResultBlockParam(e,n){let{filePath:r,userModified:o,replaceAll:d,staleRecovered:f,memdirStamped:_}=e,v=o?\".  The user modified your proposed changes before accepting them. \":\"\",C=f?\" (note: the file had been modified on disk since you last read it \\u2014 the edit applied cleanly, but the file contains other changes not in your context. Read it before edits that depend on surrounding content.)\":o||_?\"\":O3t;if(d)return{...`The file ${r} has been updated${v}. All occurrences were successfully replaced.${C}`};return{...`The file ${r} has been updated successfully${v}.${C}`}",
          "rule": "Distinct string literals in the binary, deduplicated with sort -u over `strings` output.",
          "note": "The dispatch consequence is unaffected and confirmed: because the ordinary-path texts coincide, csift's input-side fallback must and does dispatch on the tool_use `name` (src/recover/events.rs:84-85, verified verbatim), never on the result text. The Edit mapper also names `staleRecovered` in the same destructure, which is the freshness signal csift's recover consumes."
        }
      ]
    },
    {
      "id": "REC-078",
      "area": "record-model",
      "behavior": "A TOP-LEVEL session transcript always persists the structured echo on a file write: every one of the 1824 `File created successfully at:` tool_results outside a `subagents/` path carries a dict-valued `toolUseResult`, with zero exceptions - and the stronger form holds too, all 13469 successful main-lane Write and Edit results carry a dict `toolUseResult`, none absent.",
      "depends": "This is what makes csift's `ids_with_result` gate safe: in the main lane the fallback can never fire, so main-session reconstruction stays byte-identical to the pre-fallback behaviour and no write is counted twice.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "46-47",
          "snippet": "/// Gated on `ids_with_result` so it never double-emits in a top-level session.\npub(crate) fn extract_input_fallback("
        }
      ],
      "instrument": "python3 over ~/.claude/projects: for every `File created successfully at:` tool_result, bucket by lane (`/subagents/` in the path or not) and by whether the record has a dict `toolUseResult`. Counting rule: one count per matching tool_result BLOCK. Observed ('main','with_tur') 1824 and ('main','no_tur') 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over every *.jsonl under ~/.claude/projects: for every `tool_result` block whose rendered text contains 'File created successfully at:', bucket by lane and by whether the carrying record's `toolUseResult` is a dict; counted twice, once with str.startswith and once with the `in` substring test. A second, sharper pass buckets every SUCCESSFUL Write/Edit result by (lane, tool, type(toolUseResult)).",
          "observed": "substring rule: main 1824 with a dict toolUseResult and 0 without - reproducing the claim's 1824 exactly; workflow 1815, all WITHOUT; other-subagent 800 with and 53 without. startswith rule: main 1801 with and 0 without; workflow 1709 without; subagent 797 with, 47 without. Sharper pass over successful Write/Edit results: ('main','Edit','dict') 11421, ('main','Write','dict') 2048, with zero ABSENT in the main lane; ('workflow','Edit','ABSENT') 2666 and ('workflow','Write','ABSENT') 1788, with zero dict; ('subagent','Edit','dict') 1951 vs ABSENT 627 and ('subagent','Write','dict') 841 vs ABSENT 70.",
          "rule": "One count per matching `tool_result` BLOCK. 'Rendered text' = the block's content when it is a string, else the '\\n'-join of its text parts. Lane assignment as in REC-073. The two counting rules differ because some results carry the sentence after a prefix, so the rule must be stated: the claim's 1824 is the SUBSTRING rule.",
          "note": "The stated safety property is what the measurement supports: in the main lane `ids_with_result` is populated for every write, so the fallback cannot double-emit there. The code site verifies verbatim - the doc line 'Gated on `ids_with_result` so it never double-emits in a top-level session.' is at src/recover/events.rs:46 immediately above `pub(crate) fn extract_input_fallback(` at :47."
        }
      ]
    },
    {
      "id": "REC-079",
      "area": "record-model",
      "behavior": "Claude Code names the target of a file tool in the tool_use `input` under a per-tool key - `Read`, `Edit`, `Write` and `MultiEdit` all use `input.file_path`, `NotebookEdit` alone uses `input.notebook_path` - and the 2.1.258 binary carries the table literally. The snake-versus-camel split is a per-tool lookup, not a global convention: the same table gives `Read` a `responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]` shape rather than a `responseMembers` list, and gives `LSP` the camelCase `filePath` as its INPUT key.",
      "depends": "csift reads the input side in three places - `files`' structured-mutation extractor, `recover`'s `collect_tool_use_paths` (which attributes a later integrity error by `tool_use_id`) and `recover`'s carrier-less input fallback - so a snake/camel drift on either side breaks a different surface, and `NotebookEdit`'s different path key is the one special case each site spells out.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "18-22",
          "snippet": "            let key = match name.as_str() {\n                \"Read\" | \"Edit\" | \"Write\" | \"MultiEdit\" => \"file_path\",\n                \"NotebookEdit\" => \"notebook_path\",\n                _ => continue,\n            };"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "99-106",
          "snippet": "            \"Edit\" => {\n                let hunks = vec![EditHunk {\n                    old_string: input\n                        .get(\"old_string\")\n                        .and_then(serde_json::Value::as_str)\n                        .unwrap_or_default()\n                        .to_string(),\n                    new_string: input"
        },
        {
          "path": "src/model/mutation.rs",
          "lines": "146-152",
          "snippet": "            let (op, key) = match name {\n                \"Write\" => (FileOp::Write, \"file_path\"),\n                \"Edit\" => (FileOp::Edit, \"file_path\"),\n                \"MultiEdit\" => (FileOp::MultiEdit, \"file_path\"),\n                \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\"),\n                _ => continue,\n            };"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'old_string:i().describe(\"The text to replace\")' | head -1 | cut -c1-600\nExpected: the Edit input schema `tt({file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i()...,new_string:i()...,replace_all:...})`. Counting rule: one `strings` line holds the whole minified constructor; the Write input constructor is found with `grep -F 'The content to write to the file'`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "src/recover/events.rs:32-44 doc comment; AGENTS.md section 5 files/ bullet + SPEC.md section 6.6 extraction table; table literal measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var loe=\\{.{0,430}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}old_string:i\\(\\)\\.describe\\(\"The text to replace\"\\).{0,300}'\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) per file-tool tool_use block",
          "observed": "table literal present verbatim: Read:{input:\"file_path\",responseNested:[[\"file\",\"filePath\"],[\"file\",\"outputDir\"]]},Write:{input:\"file_path\",responseMembers:[\"filePath\"]},Edit:{input:\"file_path\",responseMembers:[\"filePath\"]},MultiEdit:{input:\"file_path\",responseMembers:[\"filePath\"]},NotebookEdit:{input:\"notebook_path\",responseMembers:[\"notebook_path\"]} ; Edit input schema: file_path:i().describe(\"The absolute path to the file to modify\"),old_string:i().describe(\"The text to replace\"),new_string:i().describe(\"The text to replace it with (must be different from old_string)\"),replace_all:...,...SCe() ; corpus: 4892 Write blocks and 16971 Edit blocks all key the path as `file_path`, 33116 Read blocks likewise, 0 NotebookEdit and 0 MultiEdit blocks exist.",
          "rule": "Binary: the whole table is one minified `strings` line, so one hit per table. Corpus: one count per tool_use BLOCK, keyed by the sorted tuple of its `input` keys.",
          "note": "All three code sites verify verbatim at the stated lines: src/recover/events.rs:18-22, src/recover/events.rs:99-106 and src/model/mutation.rs:146-152. The Read arm is worth flagging separately for csift: because Read's result is nested under `file.filePath` rather than a top-level `filePath`, a Read carrier never populates TurProbe.file_path - consistent with the corpus, where 9150 Read results have a dict toolUseResult and none of them carries a top-level `filePath`."
        }
      ]
    },
    {
      "id": "REC-080",
      "area": "record-model",
      "behavior": "The `Write` tool's input schema is exactly two members, `file_path` (\"The absolute path to the file to write (must be absolute, not relative)\") and `content` (\"The content to write to the file\"), plus a `...SCe()` spread that is empty in 2.1.258. Persisted blocks match: 4889 of 4892 carry exactly those two keys. The 3 exceptions carry a third `description` key and are all stamped userType 'external' on records from CC 2.1.208 and 2.1.220, i.e. calls authored outside the interactive Write tool, not a drift in the tool's own schema.",
      "depends": "csift's `recover` input-side fallback reads `input.content` as the FullSnapshot body for a carrier-less Write; if the content key were renamed or the payload moved out of the input, a subagent-authored file would silently reconstruct as empty rather than fail loudly.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "85-86",
          "snippet": "            \"Write\" => {\n                if let Some(content) = input.get(\"content\").and_then(serde_json::Value::as_str) {"
        }
      ],
      "instrument": "python3 over ~/.claude/projects: for every `tool_use` block with `name == \"Write\"`, tally `tuple(sorted(input.keys()))`. Counting rule: one count per tool_use BLOCK (not per record, not per file). Observed 4854 Write blocks corpus-wide: 4851 with exactly `('content','file_path')` and 3 with `('content','description','file_path')`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,220}The content to write to the file.{0,60}'\nstrings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function SCe.{0,60}'\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) for every tool_use block named Write, bucketing the outliers by (lane, record version, record userType)",
          "observed": "schema literal: var NRo=m(()=>tt({file_path:i().describe(\"The absolute path to the file to write (must be absolute, not relative)\"),content:i().describe(\"The content to write to the file\"),...SCe()})) and the spread resolves to `function SCe(){return{}}` - empty in this build. corpus snapshot 2026-09-02 21:52:39 AEST: 4889 Write blocks with exactly ('content','file_path') and 3 with ('content','description','file_path'); the 3 outliers are (main, version 2.1.208, userType 'external') x2 and (workflow, version 2.1.220, userType 'external') x1.",
          "rule": "One count per tool_use BLOCK with name == 'Write' (not per record, not per file), keyed by the sorted tuple of its `input` keys. Outliers additionally keyed by the carrying record's `version` and `userType` fields.",
          "note": "csift's fallback reads `input.content` (src/recover/events.rs:85-86, verified verbatim), and an extra `description` key is inert for it, so the outliers do not affect reconstruction. The `...SCe()` spread is worth recording because it is the seam through which a future build could add input members without touching the two named ones."
        }
      ]
    },
    {
      "id": "REC-081",
      "area": "record-model",
      "behavior": "The `Edit` tool's persisted input is exactly four keys - `file_path`, `old_string`, `new_string`, `replace_all` (plus an empty `...SCe()` spread) - and `replace_all` is ALWAYS materialised even when the model omits it, because the schema coerces `undefined` to `false` (`replace_all:fs((e)=>e===void 0?!1:Ure(e),M().default(!1).optional())`) before the call is persisted: 16971 of 16971 Edit blocks carry it. The binary also carries a second, non-transcript edit surface whose JSON schema lists replace_all as optional and not required, so the 100% materialisation is a property of the interactive tool's schema rather than of every edit entry point.",
      "depends": "csift's input-side fallback builds one `EditHunk` from those three payload keys with `.unwrap_or(false)` on `replace_all`; the fallback is the ONLY content source for an Edit in a carrier-less lane, and the defensive `unwrap_or` is never exercised on real data.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "100-105",
          "snippet": "                let hunks = vec![EditHunk {\n                    old_string: input\n                        .get(\"old_string\")\n                        .and_then(serde_json::Value::as_str)\n                        .unwrap_or_default()\n                        .to_string(),"
        }
      ],
      "instrument": "python3 over ~/.claude/projects: tally `tuple(sorted(input.keys()))` for every `tool_use` block with `name == \"Edit\"`. Counting rule: one count per tool_use BLOCK. Observed 16966 Edit blocks, 16966 with exactly `('file_path','new_string','old_string','replace_all')` - i.e. `replace_all` present at 100%.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'replace_all:.{0,160}' | sort -u\npython3 over every *.jsonl under ~/.claude/projects tallying tuple(sorted(input.keys())) for every tool_use block named Edit",
          "observed": "the coercion literal is present verbatim: replace_all:fs((e)=>e===void 0?!1:Ure(e),M().default(!1).optional()).describe(\"Replace all occurrences of old_string (default false)\") ; corpus snapshot 2026-09-02 21:52:39 AEST: 16971 Edit blocks, 16971 with exactly ('file_path','new_string','old_string','replace_all') and no other key tuple - replace_all present at 100%. A SEPARATE edit surface in the same binary declares replace_all:{type:\"boolean\"}},required:[\"file_path\",\"old_string\",\"new_string\"] (an 'edit:'-prefixed error namespace), where replace_all is not required.",
          "rule": "One count per tool_use BLOCK with name == 'Edit', keyed by the sorted tuple of its `input` keys. Binary: distinct `strings` lines after sort -u.",
          "note": "The described consequence holds: the `.unwrap_or_default()` / `unwrap_or(false)` arms in the fallback (src/recover/events.rs:100-105, verified verbatim) are defensive and are never exercised by real data, since every recorded Edit carries all three payload keys and the flag."
        }
      ]
    },
    {
      "id": "REC-082",
      "area": "record-model",
      "behavior": "`NotebookEdit`'s input is `{notebook_path, cell_id?, new_source, cell_type?, edit_mode?}` with `edit_mode` an enum of `[\"replace\",\"insert\",\"delete\"]` defaulting to replace - the cell body rides `new_source`, not `content` and not `new_string` - and the local corpus contains ZERO NotebookEdit tool_use blocks.",
      "depends": "csift's `recover` input-side fallback has arms for `Write`/`Edit`/`MultiEdit` only and reads `input.file_path` unconditionally, so a notebook edited in a carrier-less (workflow-agent) lane is invisible to `recover` even though `files` still attributes it via the `notebook_path` table.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "77-83",
          "snippet": "        let path = input\n            .get(\"file_path\")\n            .and_then(serde_json::Value::as_str)\n            .unwrap_or_default();\n        if !path_matches(target_file, path) {\n            continue;\n        }"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o 'notebook_path:i().describe(\"The absolute path to the Jupyter notebook[^`]\\{0,400\\}' for the schema; then python3 over ~/.claude/projects counting tool_use blocks with `name == \"NotebookEdit\"` (counting rule: one per block; observed 0).",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <claude-code-2.1.258-binary> | grep -o 'notebook_path:i().describe(\"The absolute path to the Jupyter notebook[^`]\\{0,700\\}' | head -1   AND   csift search '' --count-by tool --max-count 0   AND   sed -n '77,83p' src/recover/events.rs ; sed -n '10,22p' src/recover/events.rs ; grep -n notebook_path src/model/mutation.rs",
          "observed": "Binary, one schema literal: notebook_path:i().describe(\"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\"),cell_id:i().optional().describe(\"The ID of the cell to edit. ...\"),new_source:i().describe(\"The new source for the cell\"),cell_type:ee([\"code\",\"markdown\"]).optional().describe(\"...If using edit_mode=insert, this is required.\"),edit_mode:ee([\"replace\",\"insert\",\"delete\"]).optional().describe(\"The type of edit to make (replace, insert, delete). Defaults to replace.\")  --  Corpus: the tool census emitted 35 tool keys (Bash 245899, Read 66265, Edit 33941, WebFetch 20008, Write 9762, ...) over 666154 matched records; NotebookEdit is not among them. Code: src/recover/events.rs:77-83 matches verbatim (the input-side fallback reads only input.file_path); the recover path table at events.rs:19-20 does carry \"NotebookEdit\" => \"notebook_path\", and src/model/mutation.rs:150 reads \"NotebookEdit\" => (FileOp::NotebookEdit, \"notebook_path\") for the files attribution.",
          "rule": "One hit per schema literal in the binary. The csift census counts each matched record once per tool key across every transcript under ~/.claude/projects (16 project dirs, 7827 jsonl files, 5.2 GB); a tool name absent from the key list means zero tool_use/tool_result blocks carrying it.",
          "note": "Both halves reproduce. The schema field set, the edit_mode enum and its documented replace default are byte-for-byte as claimed, and the corpus still contains no NotebookEdit call, so the recover blind spot the claim describes stays theoretical here rather than observed."
        }
      ]
    },
    {
      "id": "REC-083",
      "area": "record-model",
      "behavior": "The written payload of a file tool lives under a different input key per tool: `Write.content`, `Edit.new_string` (and `MultiEdit.edits[].new_string`), `NotebookEdit.new_source` - there is no common content key.",
      "depends": "`csift wait --until 'write:<path-regex>[:<line-regex>]'` scans exactly `content`, `new_string` and `new_source` as string-valued input keys, so a MultiEdit's per-hunk `edits[].new_string` is unreachable to the line regex and such a write can only ever satisfy the path half of the condition.",
      "code": [
        {
          "path": "src/live/conditions.rs",
          "lines": "172-182",
          "snippet": "                } if matches!(n.as_str(), \"Write\" | \"Edit\" | \"MultiEdit\" | \"NotebookEdit\") => {\n                    let path = input\n                        .get(\"file_path\")\n                        .or_else(|| input.get(\"notebook_path\"))\n                        .and_then(serde_json::Value::as_str)\n                        .unwrap_or_default();\n                    if !path_re.is_match(path) {\n                        return false;\n                    }\n                    line_re.as_ref().is_none_or(|re| {\n                        [\"content\", \"new_string\", \"new_source\"].iter().any(|k| {"
        }
      ],
      "instrument": "Read the unit test at src/live/tests/conditions.rs:143-145, which fixtures a `NotebookEdit` tool_use with `notebook_path` plus `new_source` and asserts the path source is consulted and a path miss short-circuits before the line regex.",
      "located": {
        "claude_code": null,
        "csift": "0.9.0",
        "source": "src/live/conditions.rs:24-26 doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <claude-code-2.1.258-binary> > STRINGS ; grep -o 'content:i().describe(\"The content to write[^`]\\{0,120\\}' STRINGS ; grep -o 'new_string:i().describe(\"The text to replace it with[^`]\\{0,90\\}' STRINGS ; grep -o 'edits:\\[{old_string:[^)]\\{0,90\\}' STRINGS ; grep -o 'new_source:i().describe(\"The new source for the cell\"' STRINGS   AND   sed -n '155,181p' src/live/conditions.rs",
          "observed": "Write: content:i().describe(\"The content to write to the file\"),...  Edit: new_string:i().describe(\"The text to replace it with (must be different from old_string)\")  MultiEdit hunks: edits:[{old_string:r,new_string:o,replace_all:d}]  NotebookEdit: new_source:i().describe(\"The new source for the cell\").  Four different keys, no shared one. csift src/live/conditions.rs:161-171 matches the claimed snippet verbatim and the key list at line 171 is exactly [\"content\", \"new_string\", \"new_source\"], read off input at the TOP level with no descent into edits[].",
          "rule": "One hit per schema literal in the binary; four literals, four distinct payload keys. The csift key array is read verbatim from the current file at the claimed lines.",
          "note": "The binary confirms all four payload keys including the nested MultiEdit hunk shape, and the csift line-regex key list is unchanged. The consequence the claim draws is currently unobservable on this machine: the corpus holds zero MultiEdit calls (REC-084), so no real record exercises the edits[] gap."
        }
      ]
    },
    {
      "id": "REC-084",
      "area": "record-model",
      "behavior": "`MultiEdit` and `NotebookEdit` are still registered tool names in the 2.1.258 binary (both appear in the tool registry array and in the file-editing set `[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`) but produce ZERO tool CALLS across the entire local corpus, so their result-echo shapes are unverified by observation - real file-tool traffic is Read/Edit/Write only.",
      "depends": "csift models both (`FileOp::MultiEdit`/`FileOp::NotebookEdit`, the `edits[]` input fallback, the `notebook_path` key) on schema grounds alone; a drift in either echo would go undetected until such a call appears, and `recover` has no carrier-side MultiEdit arm at all - it relies on the Edit arm's `oldString` shape.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "127-133",
          "snippet": "            \"MultiEdit\" => {\n                let hunks: Vec<EditHunk> = input\n                    .get(\"edits\")\n                    .and_then(serde_json::Value::as_array)\n                    .map(|arr| {\n                        arr.iter()\n                            .map(|e| EditHunk {"
        },
        {
          "path": "src/model/mutation.rs",
          "lines": "16-24",
          "snippet": "pub enum FileOp {\n    /// `Write` tool - writes a file whole (a create when the path was new).\n    Write,\n    /// `Edit` tool - a single in-place string replacement in an existing file.\n    Edit,\n    /// `NotebookEdit` tool - edits a Jupyter notebook cell (`notebook_path`).\n    NotebookEdit,\n    /// `MultiEdit` tool - multiple edits to one file in a single call.\n    MultiEdit,"
        },
        {
          "path": "src/recover/events.rs",
          "lines": "127-131",
          "snippet": "            \"MultiEdit\" => {\n                let hunks: Vec<EditHunk> = input\n                    .get(\"edits\")\n                    .and_then(serde_json::Value::as_array)\n                    .map(|arr| {"
        }
      ],
      "instrument": "csift search '' --count-by tool --max-count 0 over every project, plus a direct name search. Counting rule: the census counts each matched RECORD once per tool key across every transcript. Measured 2026-09-02 at 21:5x local: Read 66265, Edit 33941, Write 9762, and NEITHER MultiEdit NOR NotebookEdit appears as a key at all (0 blocks). A bare csift search '\"name\":\"MultiEdit\"' --count-only returns 19 and '\"name\":\"NotebookEdit\"' returns 20, but those are prose mentions in transcripts, not tool calls - which is exactly why the tool census is the right instrument. Cross-check the names are still shipped: strings -a <binary> | grep -c MultiEdit returns 10 and grep -c NotebookEdit returns 15.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' --count-by tool --max-count 0   AND   csift search '\"name\":\"MultiEdit\"' --count-only   AND   csift search '\"name\":\"NotebookEdit\"' --count-only   AND   strings -a <claude-code-2.1.258-binary> | grep -c MultiEdit ; strings -a <claude-code-2.1.258-binary> | grep -c NotebookEdit   AND   sed -n '127,133p' src/recover/events.rs ; sed -n '16,24p' src/model/mutation.rs ; grep -n MultiEdit src/recover/*.rs",
          "observed": "Census 2026-09-02, 35 tool keys: Bash 245899, Read 66265, Edit 33941, WebFetch 20008, WebSearch 19683, Write 9762, ...; MultiEdit and NotebookEdit appear as no key at all. Literal-name searches: '\"name\":\"MultiEdit\"' 19 records, '\"name\":\"NotebookEdit\"' 20 records - prose mentions, not calls. Binary: grep -c MultiEdit = 10 lines, grep -c NotebookEdit = 15 lines. Code: src/recover/events.rs:127-133 and src/model/mutation.rs:16-24 match verbatim; src/recover/carriers.rs:92 reads '// (3) Edit result: {filePath, oldString, newString, structuredPatch, ...} (no type)' and grep finds no MultiEdit arm anywhere in src/recover/carriers.rs.",
          "rule": "The census counts each matched record once per tool key over every transcript under ~/.claude/projects; a name that is not a key has zero blocks. --count-only counts records whose raw bytes match the pattern, so it includes prose. Binary counts are grep -c (LINES containing the token), matching the claim's stated form.",
          "note": "The behavior stands: both names are registered and neither is ever called. Only the recorded counts needed correcting - the corpus grew between the original measurement and this re-run, so Read moved 66151 to 66265, Edit 33935 to 33941 and Write 9718 to 9762, while the two zero counts and both binary counts reproduced exactly. Two housekeeping notes: the claim's third code entry (src/recover/events.rs:127-131) is a narrower duplicate of its first, and the carrier-side gap it asserts is confirmed positively - the recover carrier module names no MultiEdit arm and keys the Edit result purely on the presence of oldString/newString with no type field."
        }
      ]
    },
    {
      "id": "REC-085",
      "area": "record-model",
      "behavior": "The file-tool names Claude Code writes into `message.content[].name` are the literal strings `Read`, `Write`, `Edit`, `MultiEdit` and `NotebookEdit`; the binary keeps them as one closed file-editing set (`[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"]`) alongside the full 2.1.258 tool registry.",
      "depends": "csift's `files` byte prefilter never mentions MultiEdit or NotebookEdit: it relies on `Edit` and `Write` being SUBSTRINGS of every file-editing tool name, so a future tool that mutates files without carrying `Edit`/`Write`/`Bash`/`filePath` in its line would be dropped before the parse and never appear in any `files` rollup.",
      "code": [
        {
          "path": "src/files/run.rs",
          "lines": "183-186",
          "snippet": "                memmem::Finder::new(b\"Edit\"),\n                memmem::Finder::new(b\"Write\"),\n                memmem::Finder::new(b\"Bash\"),\n                memmem::Finder::new(b\"filePath\"),"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' - expect the closed file-editing set literal. Counting rule: one hit per array literal in the binary.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <claude-code-2.1.258-binary> | grep -o '\\[\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"\\]' | wc -l   AND   strings -a <claude-code-2.1.258-binary> | grep -o '\"Bash\"[^`]\\{0,300\\}' | grep -i notebook   AND   sed -n '175,195p' src/files/run.rs",
          "observed": "The closed file-editing set literal [\"Write\",\"Edit\",\"MultiEdit\",\"NotebookEdit\"] occurs exactly 1 time. The tool registry array reads \"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",\"MultiEdit\",\"Write\",\"NotebookEdit\",\"Glob\",\"Grep\",\"LS\",\"TodoWrite\",\"TaskCreate\",\"TaskGet\",\"TaskList\",\"TaskUpdate\",\"TaskStop\",\"TaskOutput\",\"LSP\",\"ReadMcpResourceTool\",... and a second built-in-name array reads \"Bash\",\"Read\",\"Write\",\"Edit\",\"Glob\",\"Grep\",\"NotebookEdit\",\"WebFetch\",\"WebSearch\",\"Task\",\"TodoWrite\",... csift src/files/run.rs:183-186 matches the claimed snippet verbatim.",
          "rule": "One hit per array literal in the binary. The csift prefilter needles are read verbatim from the current file.",
          "note": "Confirmed, with one detail worth recording: the files prefilter array is now SIX needles, not the four the claim quotes - b\"Edit\", b\"Write\", b\"Bash\", b\"filePath\", b\"file-history-snapshot\" and b\"is_error\". The quoted four are still verbatim at the claimed lines and neither MultiEdit nor NotebookEdit is among the six, so the substring-dependence the claim warns about is intact."
        }
      ]
    },
    {
      "id": "REC-086",
      "area": "record-model",
      "behavior": "Claude Code labels an `Edit` whose `old_string` is the EMPTY string as a `Create` in its own UI (`if(e.old_string===\"\")return\"Create\"`), yet that op still emits the Edit-shaped result with no `type` key.",
      "depends": "csift `files` derives create-vs-edit ONLY from `toolUseResult.type == \"create\"`, so an empty-old_string Edit is reported `edit` with `is_create:false`; csift `recover` refuses the same op outright, because `apply_string_edit` returns `UnAnchorable` on an empty `old_string`.",
      "code": [
        {
          "path": "src/recover/buffer.rs",
          "lines": "306-313",
          "snippet": "    for h in hunks {\n        if h.old_string.is_empty() || !text.contains(&h.old_string) {\n            return EditOutcome::UnAnchorable;\n        }\n        if h.replace_all {\n            text = text.replace(&h.old_string, &h.new_string);\n        } else {\n            text = text.replacen(&h.old_string, &h.new_string, 1);"
        },
        {
          "path": "src/model/mutation.rs",
          "lines": "186-193",
          "snippet": "    pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n        let Some(probe) = self.tur_probe() else {\n            return Vec::new();\n        };\n        let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n            return Vec::new();\n        };\n        if file_path.is_empty() {"
        }
      ],
      "instrument": "strings -a <the Claude Code 2.1.258 binary> | grep -F 'if(e.old_string===\"\")return\"Create\"' | wc -l  # expect 1\nCounting rule: one `strings` output line contains the whole label function; read the surrounding `function cxe(e){...}` to confirm it is the Edit tool's display-label helper, not a result-shape branch.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -a <claude-code-2.1.258-binary> | grep -cF 'if(e.old_string===\"\"'   AND   strings -a <claude-code-2.1.258-binary> | grep -F 'if(e.old_string===\"\"'   AND   csift search '\"old_string\": *\"\"' --count-only ; csift search '\"oldString\":\"\"' --count-only   AND   sed -n '306,313p' src/recover/buffer.rs ; sed -n '180,215p' src/model/mutation.rs",
          "observed": "Exactly 1 matching strings line. The whole label helper is function cxe(e){if(!e)return\"Update\";if(e.file_path?.startsWith(Ea()))return\"Updated plan\";if(e.edits!=null)return\"Update\";if(e.old_string===\"\")return\"Create\";return\"Update\"}. On the same strings line the Edit result-echo schema is c({filePath:i(),oldString:i(),newString:i(),originalFile:i().nullable(),structuredPatch:R(...),userModified:M(),replaceAll:M(),gitDiff:...}) - it carries NO type key, while the Write result schema on another line is c({type:ee([\"create\",\"update\"]).describe(\"Whether a new file was created or an exis...\"). Corpus: csift search for an empty old_string returns 0 records and for an empty oldString 0 records. Code: src/recover/buffer.rs:306-313 and src/model/mutation.rs:186-193 match verbatim, and mutation.rs:196 is the single create test - probe.r#type ... == Some(\"create\").",
          "rule": "One strings line contains the whole label function, so grep -cF counts it once. The corpus figures count RECORDS whose raw bytes contain the literal; 0 means no transcript in ~/.claude/projects records an Edit with an empty old_string.",
          "note": "Both halves of the mechanism reproduce from the binary: the Create label branch is present exactly once, and the Edit result echo genuinely has no type field while the Write echo does - which is precisely why csift can only ever report such an op as an edit. Two additions worth carrying: the same helper has three other branches (a plan-directory path returns \"Updated plan\", and a call carrying edits returns \"Update\", so MultiEdit is never labelled Create), and the corpus contains zero empty-old_string Edits, so the csift consequence is derived rather than witnessed here."
        }
      ]
    },
    {
      "id": "REC-087",
      "area": "record-model",
      "behavior": "Claude Code writes a transcript by APPENDING whole lines and never rewrites, reorders, splits or deletes an already-written line: across two windows (20 s and 25 s) over the three most recently modified transcripts, every previously-read byte prefix re-hashed identically (6 of 6 observations) and no file ever shrank, while 2 of the 3 files in the 25 s window grew during it. The binary's file writers are appendFileSync (24 occurrences on 15 strings lines) with no in-place transcript rewrite path.",
      "depends": "Every `Lnnnn` header, every `csift show @<id> --line N` refetch, the show/verbatim continuation pointers and `recover --file-lines` treat a physical line number as a durable address; if a line were ever rewritten in place, every printed refetch would silently fetch a different record than the one that was matched.",
      "code": [
        {
          "path": "src/parse/lines.rs",
          "lines": "18-21",
          "snippet": "/// Open + memory-map a file read-only. Length is captured at open; a concurrently\n/// growing/shrinking live session is tolerated (we treat the length as fixed and\n/// skip any torn trailing fragment). Returns `Ok(None)` for an empty file.\npub(crate) fn mmap_file(path: &Path) -> Result<Option<Mmap>> {"
        }
      ],
      "instrument": "Python: take the 3 newest `~/.claude/projects/*/*.jsonl` by mtime, record `(size_n, md5(first size_n bytes))`, `time.sleep(20)`, re-read the first `size_n` bytes and re-hash. Expect prefix hashes byte-identical for every file and at least one file's size strictly larger. Counting rule: one observation per file; a prefix hash mismatch falsifies the claim.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: walk ~/.claude/projects for every *.jsonl, sort by mtime descending, take the 3 newest; record (size_n, md5 of the first size_n bytes); time.sleep(25); re-read the first size_n bytes and re-hash.   AND   strings -a <claude-code-2.1.258-binary> | grep -o appendFileSync | wc -l ; strings -a <claude-code-2.1.258-binary> | grep -c appendFileSync   AND   sed -n '18,21p' src/parse/lines.rs",
          "observed": "Run over the 3 newest transcripts: prefix md5 identical 3/3, size strictly grew 2/3 (201338->220620 and 140305->147933 bytes), shrank 0/3. An earlier 20 s run restricted to top-level transcripts: prefix md5 identical 3/3, grew 0/3, shrank 0/3. Binary: appendFileSync occurs 24 times on 15 distinct strings lines. csift src/parse/lines.rs:18-21 matches verbatim.",
          "rule": "One observation per file. A prefix-hash mismatch or a size decrease falsifies append-only; the number of files that GREW in the window depends on which sessions happen to be writing and is not a property of the format. Binary count: grep -o | wc -l counts occurrences (24); grep -c counts lines containing the token (15).",
          "note": "The append-only property reproduces and is if anything stronger than recorded, but two numbers needed fixing. The claim's 'one of the three grew' is a property of that particular window, not of the format - a re-run saw two of three grow and an earlier run saw none - so the behavior is restated with the growth count named as window-dependent. And the appendFileSync figure needs its counting rule attached: 24 is the occurrence count, while a stranger running the more natural grep -c would get 15 and wrongly read it as drift. An independent corroboration came from the compaction census in REC-088: one transcript carries 80 compaction boundaries and is still 123044 lines long, so 80 compaction events truncated nothing."
        }
      ]
    },
    {
      "id": "REC-088",
      "area": "record-model",
      "behavior": "A compaction does NOT rewrite the transcript: Claude Code appends a `type:\"system\"` `subtype:\"compact_boundary\"` record and a separate `type:\"user\"` `isCompactSummary:true` record after the existing lines, so every pre-compaction line number stays valid.",
      "depends": "This is what lets `verbatim` reconstruct the clipped turns from the SAME file and lets a refetch printed before a compaction still resolve afterwards; if compaction rewrote the file, every stored `Lnnnn` address in a prior answer would go stale.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "186-188",
          "snippet": "pub(crate) fn clone_origin(path: &Path, boundary_uuid: &str) -> Option<String> {\n    let dir = path.parent()?;\n    let finder = memchr::memmem::Finder::new(boundary_uuid.as_bytes());"
        }
      ],
      "instrument": "`csift search '' @<id> -t harness.compaction.boundary --format json` then `csift show @<id> --line <boundary line - 1>`: the record immediately before the boundary must be a pre-compaction record, proving the boundary was appended rather than inserted. Counting rule: one boundary per compaction event.",
      "located": {
        "claude_code": null,
        "csift": "0.9.4",
        "source": "AGENTS.md section 3.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' @<id> -t harness.compaction.boundary --format json --max-count 0   AND   csift show @<id> --line 1533..1538 --format json   AND   python3 over that one transcript: count lines with type==system and subtype==compact_boundary, count lines with type==user and isCompactSummary truthy, test whether each boundary line b is followed at b+1 by a summary, and resolve each boundary's logicalParentUuid to the line that first carries that uuid.   AND   sed -n '186,188p' src/session/summarize.rs",
          "observed": "In one top-level transcript of 123044 lines: 80 records with type==system and subtype==compact_boundary at strictly increasing lines 1537 ... 122592; 80 records with type==user and isCompactSummary true at lines 1538 ... 122593; 80 of 80 boundaries are followed at line b+1 by the summary. The nearest timestamped record before the first boundary is line 1533, type user, ts 2026-05-25T14:50:11.469Z, versus the boundary's 2026-05-25T14:52:40.365Z. For the first 10 boundaries, logicalParentUuid resolved to a STRICTLY EARLIER line in the same file 7 times (lines 1537->1533, 2801->2792, 3912->3899, 5801->5784, 7015->7002, 7877->7871, 10777->10761); every boundary's own parentUuid is null. csift show renders line 1537 as harness.compaction.boundary and 1538 as harness.compaction.summary. csift src/session/summarize.rs:186-188 matches verbatim.",
          "rule": "One boundary per compaction event. A boundary at line b is 'appended' if lines below b still hold pre-compaction records and b is greater than the previous boundary; a rewrite would show as a truncated file or a boundary at line 1. The logicalParentUuid resolution maps a uuid to the FIRST line carrying it.",
          "note": "Decisive. Eighty compactions in one transcript left it 123044 lines long with every boundary at a higher line number than the last, each immediately followed by its summary, and the record just below the first boundary is an ordinary user message stamped two and a half minutes earlier. The back-link is real too: seven of the first ten boundaries name a logicalParentUuid that resolves to a strictly earlier line in the same file. The other three resolve to no line in this file at all, which is consistent with a re-anchor onto a record that an earlier compaction lineage had already clipped - worth a follow-up but not a counterexample to appending."
        }
      ]
    },
    {
      "id": "REC-089",
      "area": "record-model",
      "behavior": "Record timestamp values are NOT monotone in file order: over the 14 newest-by-mtime top-level transcripts sized 200 KB to 40 MB (38 files are in that band), counting only the top-level timestamp of type user/assistant records and calling a backstep any record more than 1 ms earlier than the running maximum of its predecessors, there were 90 backsteps over 10906+ (measured 14508) records (0.62%), present in 7 of the 14 files, 59 of them larger than 60 s, worst 52904 s (about 14.7 hours).",
      "depends": "`list` derives a session's first activity from a HEAD read and its last from a TAIL read (first record = earliest, last = newest), and the all-projects flood-guard cap keeps the most-recently-active rows by that tail value - so a session whose tail carries a replayed or clone-copied older timestamp can be capped away or mis-sorted; `stats` is the surface that gets it right by taking a min/max over every timestamped record.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "240-247",
          "snippet": "        if let Some(ts) = rec.timestamp.as_deref() {\n            if out.first_utc.as_deref().is_none_or(|f| ts < f) {\n                out.first_utc = Some(ts.to_string());\n            }\n            if out.last_utc.as_deref().is_none_or(|l| ts > l) {\n                out.last_utc = Some(ts.to_string());\n            }\n        }"
        },
        {
          "path": "src/session/summarize.rs",
          "lines": "20-21",
          "snippet": "    let (head_skipped, head_consumed) =\n        head_records_prefiltered(path, line_is_list_candidate, |rec| {"
        }
      ],
      "instrument": "python3 over a DETERMINISTIC sample of ~/.claude/projects/*/*.jsonl: keep files sized 200 KB to 40 MB, sort by mtime descending, take 14. Per file walk lines in order, parse, keep records with type in {user, assistant} and a parseable ISO timestamp, and count a backstep whenever t < running_max - 0.001. Report backsteps, records, files-with-any, count over 60 s and the worst delta. Counting rule: one observation per qualifying record; ties and sub-millisecond jitter are not backsteps.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3: from ~/.claude/projects/*/*.jsonl keep files whose size is between 200 KB and 40 MB, sort by mtime descending, take the first 14; per file walk lines in order, parse, keep records whose type is user or assistant with a parseable ISO timestamp, and count a backstep whenever t < running_max - 0.001 s.   AND   sed -n '240,247p' src/stats.rs ; sed -n '20,21p' src/session/summarize.rs",
          "observed": "38 files sit in the size band; the 14 newest by mtime were sampled. 90 backsteps over 14508 user/assistant timestamped records = 0.62 percent, present in 7 of the 14 files, 59 of them larger than 60 s, worst delta 52904.2 s (14.7 hours). Per-file backstep counts: 30, 21, 15, 10, 10, 2, 2 and seven files with none. csift src/stats.rs:240-247 and src/session/summarize.rs:20-21 match verbatim.",
          "rule": "One observation per qualifying record. Ties and sub-millisecond jitter are not backsteps. Sample selection is deterministic and rerunnable: size band 200 KB to 40 MB, newest 14 by mtime, top-level transcripts only.",
          "note": "The phenomenon reproduces at the same order of magnitude but every number moved, because the original sample was drawn randomly with no recorded seed and cannot be rerun. The sampling rule is replaced with a deterministic one (newest 14 by mtime inside the same size band) and the figures are restated from it: 90 backsteps in 14508 records over 7 of 14 files, worst 14.7 hours rather than 10.3. The mechanism is now identified rather than guessed - the worst backstep sits exactly at the start of a whole-history replay block, the same resume replay characterised under REC-091, so the large deltas are replayed old records rather than clock skew."
        }
      ]
    },
    {
      "id": "REC-090",
      "area": "record-model",
      "behavior": "Because timestamps are not monotone in file order, Claude Code's file order and its time order are two DIFFERENT orderings of the same records - and csift's own windows split across them: `--turn` and `--line` window on file order while `--since`/`--until` and `agents --order-by` window on timestamps.",
      "depends": "A query that mixes the two (a `--turn` range plus a `--since`) can return a set that is neither a contiguous file range nor a contiguous time range, and `agents --order-by completion` sorts on a named instant that may precede an earlier-in-file sibling's instant.",
      "code": [
        {
          "path": "src/agents/run.rs",
          "lines": "68",
          "snippet": "        nodes.retain(|n| window_admits(n, &time_window, args.order_by));"
        }
      ],
      "instrument": "On a file measured to carry a backstep, run `csift search '' @<id> --turn -5.. --format json` and compare the emitted `ts_utc` sequence against sorted order; a non-sorted sequence in a file-order window is the observation. Counting rule: one comparison per emitted record pair.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/agents/run.rs:68"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift show @<id> --turn 15 --format json --max-count 0   AND   csift show @<id> --line 4804..4812 --format json, on a transcript first measured to carry a backstep at line 4808 with a 168.9 s delta; compare the emitted ts_utc sequence against its own sorted order.   AND   sed -n '68p' src/agents/run.rs",
          "observed": "--turn 15: 6 timestamped records spanning lines 4799-4810, file order does NOT equal time order, 2 adjacent descents and 5 pairwise inversions. --line 4804..4812: 7 timestamped records, 9 pairwise inversions; the rendered sequence is line 4806 at 14:06:13.786Z, 4807 at 14:06:12.863Z, 4808 at 14:03:23.968Z, 4809 at 14:03:23.968Z, 4810 at 14:06:13.885Z, 4811 at 14:06:12.868Z, 4812 at 14:06:13.355Z. csift src/agents/run.rs:68 matches verbatim.",
          "rule": "One comparison per emitted record pair. An inversion is a pair (i,j) with i before j in file order and ts_utc[i] greater than ts_utc[j]. A file-order window whose emitted timestamps are not sorted is the observation.",
          "note": "Confirmed by csift's own output, using the instrument the claim names. A single turn window - one turn, not a range - already emits six records whose timestamps run forward, back nearly three minutes, then forward again, so a file-order window is demonstrably not a time-order window. The inversions here are entirely inside one turn, which sharpens the claim: the two orderings can disagree without any turn boundary being crossed."
        }
      ]
    },
    {
      "id": "REC-091",
      "area": "record-model",
      "behavior": "A record's top-level uuid is NOT a primary key: over the 14 newest-by-mtime top-level transcripts sized 200 KB to 40 MB, counting lines that carry a top-level uuid and summing (occurrences - 1) per repeated value, 4 of the 14 files carried repeats totalling 1857 extra lines over 47696 uuid-bearing lines (3.9%). The dominant within-file cause is a RESUME REPLAY, not the compaction re-anchor: after an ai-title / mode / permission-mode line triple, Claude Code re-appends the whole prior history into the SAME file, and the replayed copy commonly differs from the original only by an added top-level slug key (844 of 1856 pairs are exactly original-plus-slug; only 49 of 1857 pairs, 2.6%, straddle a compact_boundary). Across files, a clone byte-copies records and shares uuids with its origin (measured: 558 shared uuids, 13.0% of the clone's distinct set); a subagent transcript does NOT - 0 of 1876 parent-child transcript pairs share a single top-level uuid.",
      "depends": "`search`'s `AddressSet` holds a SET of uuids and admits every record whose own uuid is in it, so all copies surface; any future join written as \"find THE record with this uuid\" would silently pick one of several, and the clone-origin probe already has to verify the uuid CARRIER rather than trust the uuid alone.",
      "code": [
        {
          "path": "src/search/scan.rs",
          "lines": "20-21",
          "snippet": "    let address = AddressSet { lines, uuids };\n    let use_address = !(address.lines.is_empty() && address.uuids.is_empty());"
        },
        {
          "path": "src/model/record.rs",
          "lines": "109",
          "snippet": "    pub logical_parent_uuid: Option<String>,"
        }
      ],
      "instrument": "python3 over a deterministic sample (files 200 KB to 40 MB, newest 14 by mtime): per file, Counter every top-level uuid string, then sum c-1 for every value with c>1. Report extras, total uuid-bearing lines and the number of files with any repeat. Then classify each repeat pair by whether a compact_boundary line lies strictly between the occurrences and by the key-set delta of the two raw lines. Counting rule: repeats counted per file, so cross-file duplication is NOT included in the number.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 over the same deterministic sample (files 200 KB to 40 MB, newest 14 by mtime): per file Counter every top-level uuid string, then sum c-1 for every value with c>1.   AND   per file, test for each repeat pair whether a compact_boundary line lies strictly between the two occurrences.   AND   per file, compare the two raw lines of each repeat pair byte-wise and by key set.   AND   a parent-versus-child pass: for every session dir with a subagents/ dir and a parent transcript under 30 MB, intersect the parent's uuid set with each child transcript's.   AND   csift list --max-count 0 --format json to find a row carrying clone_of, then intersect the clone's and origin's uuid sets.   AND   sed -n '20,21p' src/search/scan.rs ; sed -n '108p' src/model/record.rs",
          "observed": "Sample: 1857 extra uuid lines over 47696 uuid-bearing lines = 3.89 percent, in 4 of the 14 files (per-file extras 613, 615, 588, 41). Boundary test: only 49 of 1857 repeat pairs (2.6 percent) straddle a compact_boundary line - 7/41, 42/615, 0/613 and 0/588. Shape test on the 21.7 MB file: 613 repeat pairs, first occurrences spanning lines 25-3996 and second occurrences 4010-4626, immediately preceded by an ai-title / mode / permission-mode line triple at 4007-4009; 0 of the first 200 pairs are byte-identical and every one of those 200 differs from its original by exactly one added top-level key, slug. Across the four files, 844 of 1856 pairs are exactly first-copy-plus-slug and 124 are byte-identical. Parent-child pass: 16 session dirs, 1876 parent-child transcript pairs, 0 pairs sharing even one top-level uuid. Clone pass: exactly 1 row in the corpus carries clone_of; the 47.9 MB clone has 4293 distinct uuids and its 720.9 MB origin 70729, and 558 uuids (13.0 percent of the clone's) appear in BOTH files. csift src/search/scan.rs:20-21 and src/model/record.rs:108 match verbatim.",
          "rule": "Repeats counted per file, so cross-file duplication is excluded from the 1857/47696 figure. A repeat pair 'straddles' a boundary when a compact_boundary line number lies strictly between the two occurrence line numbers. Cross-file sharing is a set intersection of top-level uuid strings between two whole files.",
          "note": "The headline holds - a uuid identifies no single line - but both the numbers and the attributed mechanism needed correcting, and one half of the cross-file story is refuted outright. Only 2.6 percent of repeat pairs straddle a compaction boundary, so the compaction re-anchor is a minor contributor; the dominant shape is a resume that re-appends the entire prior history into the same file behind a session-state line triple, with the second copy identifiable by a newly added slug key. The clone half of the claim is confirmed by direct measurement on the corpus's single clone (558 uuids present in both files). The spawn-copy half is false here: across 1876 parent-child transcript pairs not one uuid is shared, so a subagent transcript mints fresh uuids rather than inheriting the spawning message's."
        }
      ]
    },
    {
      "id": "REC-092",
      "area": "record-model",
      "behavior": "Within ONE transcript a duplicated `uuid` has more than one physical line, and the copies are not interchangeable: measured over the 5 transcripts (of 39 sized 200KB-40MB) that carry intra-file repeats, 14 duplicated uuids are assistant records on both copies and 3 of those 14 carry a LATER copy whose four flat `message.usage` fields are all zero while the earlier copy carries the real numbers. Claude Code ships a zero-filled usage template that matches that shape exactly.",
      "depends": "The two `show` paths resolve a duplicated uuid DIFFERENTLY, and the difference is load-bearing. The RENDERED path (the default) treats `--uuid` as a membership test, so it emits EVERY copy - measured 2 record rows for a uuid the census says appears twice. The `--raw` path binds each requested uuid to the FIRST line whose OWN `uuid` field equals it (a body merely quoting the uuid does not satisfy the address) and therefore emits the earliest copy only - measured 1 line, the one carrying the real usage numbers.",
      "code": [
        {
          "path": "src/show/run.rs",
          "lines": "129-137",
          "snippet": "            for ((u, slot), finder) in uuid_line.iter_mut().zip(&uuid_finders) {\n                if slot.is_none() && finder.find(line).is_some() {\n                    // Confirm structurally: the record's OWN uuid must equal it (a body\n                    // merely quoting the uuid must not satisfy the address).\n                    if let Ok(Some(rec)) = crate::parse::parse_line(line) {\n                        if rec.uuid.as_deref() == Some(u.as_str()) {\n                            *slot = Some(line_no);\n                            want = true;\n                        }"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "543-549",
          "snippet": "/// Record-address selectors (`--line` / `--uuid`) parsed into membership sets - the \"fetch\n/// THESE records\" filter that turns `search` into the in-permission message-getter. Active when\n/// either set is non-empty; a record is addressed when its physical line OR uuid is in range.\npub(crate) struct AddressSet {\n    pub(crate) lines: BTreeSet<usize>,\n    pub(crate) uuids: BTreeSet<String>,\n}"
        }
      ],
      "instrument": "Find a duplicated uuid with the Python census, then run BOTH `csift show @<id> --uuid <uuid> --format json` (expect one row per copy) and `csift show @<id> --uuid <uuid> --raw` (expect exactly one line, the earliest copy). Counting rule: rows with kind==\"record\" in the rendered jsonl stream; non-blank stdout lines for `--raw`.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/show/run.rs:125-141"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 dupscan.py  (per file in ~/.claude/projects/*/*.jsonl sized 200KB-40MB: json-parse every line carrying \"uuid\", Counter the top-level `uuid`, keep values with count>1); then for one duplicated assistant uuid: `csift show @<id> --uuid <uuid> --format json` and `csift show @<id> --uuid <uuid> --raw`",
          "observed": "5 of 39 band files carry intra-file duplicate top-level uuids; the repeats are contiguous replay blocks (first file: lines 25->4010, 26->4011, 35->4012). Across those 5 files 14 duplicated uuids are assistant-on-both-copies; 3 of the 14 carry DIFFERING usage and in all 3 the later copy is all-zero, e.g. lines 2508/2601 -> (input,output,cache_read,cache_creation) = (2,4849,951431,16024) vs (0,0,0,0). `csift show --uuid <that uuid> --format json` emitted RECORD_ROWS=2 (line 2508 and line 2601, both label agent.tool.use); the same address with `--raw` emitted 1 line, the line-2508 copy carrying the real numbers. The zero shape is a literal in the harness binary: `var mm={output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\"}`",
          "rule": "One observation per (file, top-level uuid value) with count>1; usage compared as the 4-tuple of the flat token fields with a missing field read as 0. Row count = jsonl lines with kind==\"record\" (header and summary excluded) for the rendered path, non-blank stdout lines for `--raw`.",
          "note": "The claimed observable half is confirmed by instrument. The `depends` half was wrong for the command's DEFAULT path: the rendered path emitted 2 rows, not 1. First-wins is the `--raw` path's rule only. Both code snippets exist verbatim in the current tree at the lines given."
        }
      ]
    },
    {
      "id": "REC-093",
      "area": "record-model",
      "behavior": "An explicit `show` address that resolves to no record is a HARD failure, not an empty result: csift bails with `no such record(s): ` naming each miss (a single `--line` token with no `..` is an explicit address, a `..` token is a clamping range that must still yield at least one record).",
      "depends": "This is the durability guarantee's enforcement half - a stale refetch printed by an earlier search cannot come back as a silent empty; if Claude Code ever renumbered lines, every stale address would surface as a loud miss rather than as wrong content.",
      "code": [
        {
          "path": "src/show/run.rs",
          "lines": "164-166",
          "snippet": "    if !misses.is_empty() {\n        bail!(\"no such record(s): {}\", misses.join(\", \"));\n    }"
        },
        {
          "path": "src/show/run.rs",
          "lines": "251-253",
          "snippet": "    if !misses.is_empty() {\n        bail!(\n            \"no such record(s): {} — an explicit address renders message lines \\"
        }
      ],
      "instrument": "`csift show @<id> --line 999999 --raw; echo $?` -> stderr `csift: error: no such record(s): L999999 (file has N lines)` and exit 1. The DEFAULT (rendered) path bails from a different site and prints a different tail: `csift: error: no such record(s): L999999 - an explicit address renders message lines ...` with no `(file has N lines)` suffix. Counting rule: exit code non-zero and the literal `no such record(s): ` present; do not key on the suffix, it is path-dependent.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/show/run.rs:164-166"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift show @<id> --line 999999 --raw; echo $?   /   csift show @<id> --line 999999; echo $?   /   csift show @<id> --uuid 00000000-0000-0000-0000-000000000000; echo $?   /   csift show @<id> --line 999999..1000005 --raw; echo $?   /   csift show @<id> --line 159034..999999 --raw; echo $?",
          "observed": "raw single: `csift: error: no such record(s): L999999 (file has 159036 lines)` exit 1. raw range wholly beyond EOF: `csift: error: no such record(s): L999999-1000005 (file has 159036 lines)` exit 1. rendered single: `csift: error: no such record(s): L999999 - an explicit address renders message lines (role:user/role:assistant, superseded drafts included), attachment lines, and the promoted non-record lines (a queue-operation with text, turn_duration, away_summary, stop_hook_summary, file-history-snapshot/-delta); session-state cache lines (last-prompt, mode, ai-title, ...), a content-less queue dequeue, unpromoted system subtypes and torn lines are inspectable with `--raw`' exit 1. rendered uuid miss: same message with `uuid 00000000-...`, exit 1. Partially-beyond range clamped: 4 lines on stdout, exit 0.",
          "rule": "One observation per invocation; verdict = (exit code non-zero) AND (stderr contains the literal `no such record(s): `). A clamping range counts as a pass when it exits 0 with at least one stdout line.",
          "note": "Behaviour holds exactly - an unresolvable explicit address is a hard non-zero failure on both paths, never a silent empty, and a partially-beyond range still clamps and succeeds. Only the expected stderr literal in the instrument needed correcting: it describes the `--raw` site, while the default path bails from the second site with a different tail. Both bail sites and the `LineSpecs` snippet exist verbatim in the current tree."
        }
      ]
    },
    {
      "id": "REC-094",
      "area": "record-model",
      "behavior": "A line number is per-FILE, and a subagent transcript is a different file from its parent session, so an address is only meaningful paired with the transcript that owns it.",
      "depends": "csift's printed refetch is always addressed at the hit's OWN `session_id`, never the parent uuid - in text mode a subagent hit prints an extra `csift show @<agent-id> --line N` continuation - because pointing the parent at a subagent's line would silently fetch the WRONG record.",
      "code": [
        {
          "path": "src/search/render.rs",
          "lines": "398-400",
          "snippet": "/// The ready-to-run fetch command for a hit - `csift show` addressed at the transcript that\n/// OWNS the hit's line number (`session_id`, never the parent uuid: line numbers are\n/// per-file, so pointing the parent at a subagent's line would silently fetch the WRONG"
        }
      ],
      "instrument": "`csift search '<pattern>' <project> --format json | jq -r 'select(.kind==\"hit\") | .refetch'` on a scope containing subagent hits: every emitted command must name the hit's own `session_id`, and running one must return the same record the hit reported. Counting rule: one refetch per hit object.",
      "located": {
        "claude_code": null,
        "csift": "0.10.0",
        "source": "src/search/render.rs:398-410"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search 'clippy' . --max-count 400 --format json  (run from the csift repo root, so the project target is the repo's own transcripts), piped through a Python counter over every object carrying `hits`; then for 3 subagent hits, run the emitted `refetch` verbatim and compare, and run the SAME line number addressed at `parent_session_id`; plus `csift search 'clippy' . --max-count 60 | grep -c '↳ csift show @'`",
          "observed": "hits_with_refetch=1284, names_own_session_id=1284, mismatch=0, subagent_hits=431. Every subagent sample had parent_session_id != session_id. Round-trip: 3/3 own-refetch invocations returned exactly 1 record row whose `uuid` and `line` equalled the hit's. Counter-probe: 3/3 of the SAME line numbers addressed at the parent session returned a record whose uuid did NOT equal the hit's - the silent-wrong-record hazard, reproduced. Text mode printed 259 `↳ csift show @<agent-id> --line N` continuations in a 60-hit window.",
          "rule": "One observation per hit object carrying a non-null `refetch`; a refetch 'names its own id' iff it starts with the literal `csift show @` + that hit's `session_id` + a space. Round-trip pass iff exactly one kind==\"record\" row comes back with the hit's uuid and line.",
          "note": "Confirmed in both directions: the printed address is always the owning transcript's id (0/1284 exceptions), it round-trips to the identical record, and the parent-addressed variant of the same line number fetches a different record on every probe. The doc-comment snippet exists verbatim at src/search/render.rs:398-400."
        }
      ]
    },
    {
      "id": "REC-095",
      "area": "record-model",
      "behavior": "Claude Code's transcript writer emits non-ASCII text VERBATIM as UTF-8 and never as a `\\uXXXX` escape: over the whole live corpus of 67 top-level transcripts, inspecting the first 4,000 bytes of each of 804,007 non-blank lines, 178,918 lines (22.25%) carry a byte >= 0x80 and 0 lines carry a writer-emitted `\\uXXXX` escape for a codepoint >= U+0080 (130 regex hits were all a literal two-character backslash-u sequence inside quoted content, not an escape).",
      "depends": "This is exactly why csift's needle-safety predicate keeps non-ASCII needles prefilter-eligible, and why the product can promise regex over CJK at all; if the writer switched to ASCII-safe output, every multi-byte pattern would stop matching at the byte prefilter, before any counter could see it - the silent-drop failure `required_needles` exists to prevent.",
      "code": [
        {
          "path": "src/search/matcher.rs",
          "lines": "538-540",
          "snippet": "pub(crate) fn json_escapes_in_string(c: char) -> bool {\n    c == '\"' || (c as u32) < 0x20 || c == '\\u{7f}'\n}"
        }
      ],
      "instrument": "Python over a size-filtered random sample: per line take `raw[:4000]`, count the line if any byte >= 0x80, and separately count it if `re.search(rb'\\\\u00[89a-fA-F][0-9a-fA-F]|\\\\u0[1-9a-fA-F][0-9a-fA-F]{2}|\\\\u[1-9a-fA-F][0-9a-fA-F]{3}', head)` matches; for each escape hit check whether the raw bytes contain `\\\\\\\\u` (a literal backslash-u in content) and exclude it. Counting rule: one observation per line, prefix-bounded at 4,000 bytes.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py  (walks EVERY ~/.claude/projects/*/*.jsonl; per non-blank line take raw[:4000], count it if any byte >= 0x80, and separately count it if re.search(rb'\\\\u00[89a-fA-F][0-9a-fA-F]|\\\\u0[1-9a-fA-F][0-9a-fA-F]{2}|\\\\u[1-9a-fA-F][0-9a-fA-F]{3}') matches; a hit is excluded when the raw bytes contain a doubled backslash before the u)",
          "observed": "files_scanned=67, lines_total=804007, lines_nonascii=178918 (22.25%), esc_regex_hits=130, esc_hits_excluding_literal_backslash_u=0. Sampled excluded hits are all literal two-character backslash-u inside quoted content, e.g. `FIRST verify both respond \\\\u2014 ` and `<name>.md \\\\u2014 scratch markdown fixtur`.",
          "rule": "One observation per non-blank line, prefix-bounded at 4,000 bytes; denominator = all non-blank lines of all top-level transcripts.",
          "note": "Substance confirmed at 12x the claimed sample: still zero writer-emitted escapes, and the non-ASCII share is within 0.15 points of the claimed figure. Numbers restated on a whole-corpus rule a stranger can rerun without knowing which files were sampled. The `json_escapes_in_string` snippet exists verbatim at src/search/matcher.rs:537-539."
        }
      ]
    },
    {
      "id": "REC-096",
      "area": "record-model",
      "behavior": "An assistant record's `message.usage` object carries eleven keys - the same eleven the harness binary's zero-filled usage template enumerates - measured over the whole live corpus with one observation per assistant record carrying a dict `usage` (133,935 records): `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`, `service_tier`, `cache_creation`, `inference_geo` present on all 133,935; `iterations` on 133,800 (99.90%); `server_tool_use` and `speed` on 133,798 (99.90%); `output_tokens_details` on 22,044 (16.46%, of which 23 carry JSON null rather than an object).",
      "depends": "csift's `TokenUsage` probe deserializes exactly four of the eleven (`input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`), so `csift stats` reports no thinking-token split, no server-tool counts, no service tier and no inference geography; adding any of them means reading a key whose absence rate is already measured.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "270-275",
          "snippet": "pub struct TokenUsage {\n    pub input_tokens: Option<u64>,\n    pub output_tokens: Option<u64>,\n    pub cache_read_input_tokens: Option<u64>,\n    pub cache_creation_input_tokens: Option<u64>,\n}"
        }
      ],
      "instrument": "Python over a size-filtered random sample: for every line with `type == \"assistant\"` whose `message.usage` is a dict, increment a `Counter` over its keys; report each key's count against the record total. Counting rule: one observation per assistant RECORD (not per API message), so the per-block duplication is included by design.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py  (whole corpus; for every line with type==\"assistant\" whose message.usage is a dict, Counter over its keys)  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var mm=\\{output_tokens_details.{0,330}'",
          "observed": "usage_records=133935. Key counts: input_tokens / output_tokens / cache_read_input_tokens / cache_creation_input_tokens / service_tier / cache_creation / inference_geo = 133935 each (100%); iterations = 133800 (99.90%); server_tool_use = 133798 (99.90%); speed = 133798 (99.90%); output_tokens_details = 22044 (16.46%). Exactly 11 distinct keys, no twelfth. The harness binary carries a zero-filled template naming those same eleven: `var mm={output_tokens_details:{thinking_tokens:0},input_tokens:0,cache_creation_input_tokens:0,cache_read_input_tokens:0,output_tokens:0,server_tool_use:{web_search_requests:0,web_fetch_requests:0},service_tier:\"standard\",cache_creation:{ephemeral_1h_input_tokens:0,ephemeral_5m_input_tokens:0},inference_geo:\"\",iterations:[],speed:\"standard\"}`",
          "rule": "One observation per assistant RECORD whose message.usage is a dict (per-block duplication of one API message's usage is included by design); a key counts once per record it appears on, regardless of value.",
          "note": "The eleven-key set is confirmed twice over - by census and by a literal template in the 2.1.258 binary. Two numbers needed correcting: `iterations` is present on 2 more records than `server_tool_use`/`speed` (the claim grouped all three at one figure), and `output_tokens_details` runs at 16.46% here rather than 6.4%, so its absence rate is sample-sensitive and should not be treated as a constant. The TokenUsage snippet exists verbatim at src/model/record.rs:269-274 and still reads only four of the eleven."
        }
      ]
    },
    {
      "id": "REC-097",
      "area": "record-model",
      "behavior": "Three of the `message.usage` values are nested objects with fixed shapes: `cache_creation` carries `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens` (both present on all 133,935 records that carry a dict `cache_creation`), `server_tool_use` carries `web_search_requests` and `web_fetch_requests` (both on all 133,798), and `output_tokens_details` carries `thinking_tokens` (on all 22,021 records where it is an object). `output_tokens_details` is the one that is not always an object: of the 22,044 records carrying the key, 23 carry JSON `null`.",
      "depends": "csift reads none of the nested objects, so `stats` cannot report the 5m/1h cache split, web-tool call counts, or a thinking/non-thinking output split; a future addition must know these are nested, not flat.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "281-284",
          "snippet": "    pub fn token_usage(&self) -> Option<TokenUsage> {\n        let raw = self.usage.as_ref()?;\n        serde_json::from_str(raw.get()).ok()\n    }"
        }
      ],
      "instrument": "Same Python sweep, additionally iterating the keys of `usage['cache_creation']`, `usage['server_tool_use']` and `usage['output_tokens_details']` when each is a dict. Counting rule: one observation per (record, nested key) pair.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py  (whole corpus; iterate the keys of usage['cache_creation'], usage['server_tool_use'] and usage['output_tokens_details'] when each is a dict)  AND  python3 -c ... (classify every present output_tokens_details value as null / dict / other)  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'var mm=\\{output_tokens_details.{0,330}'",
          "observed": "cache_creation dict on 133935 records, subkeys ephemeral_1h_input_tokens=133935 and ephemeral_5m_input_tokens=133935 (both on every one). server_tool_use dict on 133798, subkeys web_search_requests=133798 and web_fetch_requests=133798 (both on every one). output_tokens_details present on 22044: dict on 22021 (subkey thinking_tokens on all 22021), JSON null on 23, other 0. The binary template shows the same three nested shapes.",
          "rule": "One observation per (record, nested key) pair; a nested value that is not a dict is counted separately rather than silently skipped.",
          "note": "Nested shapes confirmed by census and by the binary's zero-filled template. One addition the claim missed: `output_tokens_details` can be JSON null (23 of 22,044 records), so a future reader must handle null before indexing `thinking_tokens`. The `token_usage` snippet exists verbatim at src/model/record.rs:280-283."
        }
      ]
    },
    {
      "id": "REC-098",
      "area": "record-model",
      "behavior": "`cache_creation.ephemeral_5m_input_tokens + cache_creation.ephemeral_1h_input_tokens` equals the flat `cache_creation_input_tokens` on 133,798 of 133,935 whole-corpus records (99.898%) and disagrees on 137 (0.102%) - so the nested pair very nearly PARTITIONS the flat field but is not guaranteed to. Every disagreement is one-hour-cache-specific: all 137 have a zero 5m bucket and a non-zero 1h bucket, 101 of them with a flat field of 0 (the whole 1h amount missing from the flat field) and 36 with a non-zero flat field that still differs from the 1h amount.",
      "depends": "Summing the nested pair alongside the flat field would double-count cache-creation tokens in `csift stats` on 99.9% of records; a future implementation must pick one or the other. Claude Code's own resolution is worth copying rather than inventing: it takes the flat field when it is greater than zero and falls back to the nested sum otherwise, which is exactly what rescues the 101 flat-zero records.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "258-263",
          "snippet": "                let vals = [\n                    u.input_tokens.unwrap_or(0),\n                    u.output_tokens.unwrap_or(0),\n                    u.cache_read_input_tokens.unwrap_or(0),\n                    u.cache_creation_input_tokens.unwrap_or(0),\n                ];"
        }
      ],
      "instrument": "Python over a size-filtered random sample: for each assistant record with a dict `usage.cache_creation`, compare `(ephemeral_5m_input_tokens or 0) + (ephemeral_1h_input_tokens or 0)` against `(cache_creation_input_tokens or 0)`; report equal and unequal counts. Counting rule: one observation per record; missing sub-keys treated as 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py and python3 ccdetail.py  (whole corpus; for each assistant record with a dict usage.cache_creation compare (ephemeral_5m_input_tokens or 0)+(ephemeral_1h_input_tokens or 0) against (cache_creation_input_tokens or 0))  AND  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,90}cache_creation\\?\\.ephemeral_5m_input_tokens.{0,90}'",
          "observed": "partition_equal=133798, partition_unequal=137 of 133935 records (99.898% / 0.102%). Every one of the 137 disagreements has ephemeral_5m_input_tokens=0 and ephemeral_1h_input_tokens>0: in 101 the flat field is 0 while the nested sum is not (e.g. flat 0 vs sum 913; flat 0 vs sum 16249), and in 36 the flat field is non-zero but unequal (e.g. flat 565371 vs e1h 618002; flat 529421 vs e1h 803948). 132,587 records carry a non-zero ephemeral_1h_input_tokens and 132,450 of those agree. The harness binary resolves the conflict with an explicit preference: `_=r?.cache_creation_input_tokens??0,v=(r?.cache_creation?.ephemeral_5m_input_tokens??0)+(r?.cache_creation?.ephemeral_1h_input_tokens??0),C=_>0?_:v`",
          "rule": "One observation per assistant record with a dict cache_creation; missing sub-keys read as 0; equality is exact integer equality.",
          "note": "Substance holds; the agreement rate is 0.8 points higher than claimed on the whole corpus, and the disagreement has a shape the claim did not name - it is entirely a one-hour-cache accounting gap, and the harness itself carries the flat-else-sum fallback rule. The stats.rs four-value snippet exists verbatim at src/stats.rs:258-263."
        }
      ]
    },
    {
      "id": "REC-099",
      "area": "record-model",
      "behavior": "`output_tokens_details.thinking_tokens` is a SUBSET of `output_tokens` on every record that reports a non-zero `output_tokens`: over the whole corpus, 22,019 of 22,021 records carrying the key satisfy `thinking_tokens <= output_tokens` (99.991%). The 2 exceptions are not an accounting inversion but a zeroed-usage artifact - two per-block records of ONE API message whose four flat token fields are all 0 while the nested `thinking_tokens` (2218) survived the zeroing.",
      "depends": "If `csift stats` ever reports a thinking split it must SUBTRACT rather than add, otherwise the per-model output total would exceed what the API billed. The subtraction must also be floored: a record whose flat usage was zeroed while the nested thinking count survived would otherwise produce a negative non-thinking output figure.",
      "code": [
        {
          "path": "src/stats.rs",
          "lines": "307",
          "snippet": "    for ((model, _), vals) in usage_peak {"
        }
      ],
      "instrument": "Python over a size-filtered random sample: for each assistant record with a dict `usage.output_tokens_details` carrying `thinking_tokens`, compare it against `usage.output_tokens`; report the `<=` and `>` counts. Counting rule: one observation per record carrying the key.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py and python3 thinkviol.py  (whole corpus; for each assistant record with a dict usage.output_tokens_details carrying thinking_tokens, compare it against usage.output_tokens, then re-scan for the violating records and read their message.id, block types and flat usage fields)",
          "observed": "thinking_le_output=22019, thinking_gt_output=2 over 22,021 records carrying the key (99.991% / 0.009%). Both violations are in one transcript at adjacent lines 30 and 31, share one `message.id`, carry one content block each (thinking, then text), and have thinking_tokens=2218 with input_tokens=output_tokens=cache_read_input_tokens=cache_creation_input_tokens=0 - a fully zeroed flat usage whose nested thinking count survived. Restricting to records with output_tokens > 0 gives 22,019 observations and 0 violations.",
          "rule": "One observation per assistant record whose usage.output_tokens_details is a dict carrying thinking_tokens; missing output_tokens read as 0; violation iff thinking_tokens > output_tokens.",
          "note": "This is the one claim whose absolute was refuted. The claim asserted 0 violations over 456 sampled records; the whole corpus has 2 violations over 22,021. Both are explained - and the explanation strengthens the claim's own dependent, since the zeroed-usage shape is the same one that makes the per-field-MAX dedupe necessary - but the invariant must now be stated conditionally, not absolutely. The stats.rs line 307 snippet exists verbatim."
        }
      ]
    },
    {
      "id": "REC-100",
      "area": "record-model",
      "behavior": "A TOP-LEVEL transcript's filename basename equals the record-level `sessionId` on every observed file: over the whole live corpus, all 67 top-level transcripts carried a top-level `sessionId` on at least one record, 0 files carried a `sessionId` that differed from the basename, and 0 files carried more than one distinct `sessionId` value. The law does NOT extend to subagent transcripts: in a 150-file random sample of the 7,584 subagent transcripts, 150 of 150 carry the OWNING session's uuid as their `sessionId`, never their own basename.",
      "depends": "csift derives every row's id from the FILENAME and keeps the data-derived `sessionId` only as a fallback when the filename yields nothing - which is what makes subagent ids correct at all, since a subagent transcript's own records name the parent. The registry join in `status` matches a row by `sessionId`, so filename and data agreeing is what makes that join work on a top-level session; on a subagent transcript the data id would resolve to the parent instead.",
      "code": [
        {
          "path": "src/session/summarize.rs",
          "lines": "82-87",
          "snippet": "    // Prefer the filename-derived id; cross-check with the data id (§2.4 spirit).\n    let session_id = if session_id.is_empty() {\n        data_session_id.unwrap_or_default()\n    } else {\n        session_id\n    };"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "57-59",
          "snippet": "        if v.get(\"sessionId\").and_then(serde_json::Value::as_str) != Some(session_id) {\n            continue;\n        }"
        }
      ],
      "instrument": "Python over every `~/.claude/projects/*/*.jsonl`: collect the set of top-level `sessionId` string values per file, compare against the basename minus `.jsonl`, and report files whose basename is absent from the set plus files with more than one distinct value. Counting rule: one observation per file that carries at least one `sessionId`.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 sweep95_99.py  (over every ~/.claude/projects/*/*.jsonl: collect the set of top-level `sessionId` string values per file, compare against the basename minus .jsonl, report files whose basename is absent from the set and files with more than one distinct value)  AND  a second pass over subagent transcripts: 150 files randomly sampled (seed 11) from the 7,584 under ~/.claude/projects/*/*/subagents/**, excluding journal.jsonl, classifying each file's sessionId set as matching its own basename, matching the owning session uuid, or neither",
          "observed": "Top level: files_scanned=67, files_with_sessionId=67, basename_mismatch_count=0, multi_sessionid_count=0. Subagents: sample n=150 -> basename match 0, owning-session uuid 150, other 0, no sessionId 0, more than one distinct value 0. A 30-file probe at a different seed gave the same 30/30 split beforehand.",
          "rule": "One observation per file that carries at least one top-level `sessionId`; a file passes iff its basename minus `.jsonl` is a member of that file's sessionId set and the set has exactly one element.",
          "note": "Holds for top-level transcripts with a one-larger denominator (67 files, not 66 - consistent with one session created since the claim was written). Added the scope caveat the claim was missing: the identity is top-level-only, and on subagent transcripts the record-level sessionId is the owning session's uuid. Both code snippets exist verbatim, at src/session/summarize.rs:82-87 and src/live/registry.rs:57-59."
        }
      ]
    },
    {
      "id": "SUB-001",
      "area": "subagents",
      "behavior": "Claude Code writes subagent transcripts in exactly three on-disk shapes under a session's sidecar directory `<ENCODED>/<session-uuid>/`: a built-in-location transcript at `subagents/agent-<id>.jsonl` (which hosts BOTH plain Task/Agent subagents and teammates - 833 files), a workflow subagent at `subagents/workflows/wf_<id>/agent-<hex>.jsonl` (6664 files), and one `subagents/workflows/wf_<id>/journal.jsonl` per workflow run (160, exactly one per wf_* dir). The journal is an EVENT log, not a transcript: every line carries `{agentId, key, type}`, and a `result` line carries a fourth key `result` holding the return payload; no journal line has a `message` or a `role`.",
      "depends": "csift's `discover_subagents` enumerates only the two transcript shapes and hard-excludes `journal.jsonl` and every `.meta.json`, reading the journal only for completion status and the returned-message payload; listing the journal as a transcript would inject event objects into every spanning surface (`list`/`search`/`stats`/`files`/`recover`/`plan`/`image`/`status`).",
      "code": [
        {
          "path": "src/subagent/discover.rs",
          "lines": "18-21",
          "snippet": "/// Walks `<session-uuid>/subagents/` for built-in `agent-<hex>.jsonl` and\n/// `<session-uuid>/subagents/workflows/wf_*/agent-<hex>.jsonl` for workflow agents.\n/// **`journal.jsonl` is excluded** (it is an event log, not a transcript), as is any\n/// non-`agent-*.jsonl` or `.meta.json` file. Returns an empty vec when the session"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "110-113",
          "snippet": "/// True iff the workflow journal alongside a workflow subagent carries a `result`\n/// event for `agent_id` (the completion signal, §C). For a built-in subagent (no\n/// journal) this is always `false` - completion is inferred from the transcript.\npub(crate) fn journal_reports_completion(subagent: &Subagent, journals: &JournalCache) -> bool {"
        }
      ],
      "instrument": "`find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' | sed 's|.*/subagents/||;s|/[^/]*$||' | sort | uniq -c` - expect two prefixes only: an empty one (built-in, flat) and `workflows/wf_<id>`; `find ~/.claude/projects -name journal.jsonl | wc -l` must equal the number of `wf_*` dirs; then `csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .shape' | sort | uniq -c` and confirm no row is named `journal`. Counting rule: one row per file, journals excluded.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.1; AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/discover.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "find ~/.claude/projects -path '*/subagents/*' -name '*.jsonl' | sed 's|.*/subagents/||' | python3 -c \"import sys,re,collections; c=collections.Counter(); [c.update(['<flat>/agent-<hex>.jsonl' if '/' not in l.strip() else re.sub(r'wf_[^/]*','wf_<id>','/'.join(l.strip().split('/')[:-1]))+'/'+re.sub(r'^agent-.*\\\\.jsonl','agent-<hex>.jsonl',l.strip().split('/')[-1])]) for l in sys.stdin]; [print(v,k) for k,v in c.most_common()]\"  ;  find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -c 'keys' | sort | uniq -c  ;  find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -r 'select(has(\"message\") or has(\"role\"))' | wc -l  ;  for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.shape'; done | sort | uniq -c  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}journal\\.jsonl.{0,50}'",
          "observed": "Exactly three path shapes under subagents/: 6664 'workflows/wf_<id>/agent-<hex>.jsonl', 833 flat 'agent-<hex>.jsonl', 160 'workflows/wf_<id>/journal.jsonl'. journal count 160 == wf_* dir count 160. Journal line key sets, whole corpus: [\"agentId\",\"key\",\"type\"] x6665 and [\"agentId\",\"key\",\"result\",\"type\"] x6043 (12708 lines total); 0 lines carry a 'message' or 'role' key. csift agents shape values corpus-wide: builtin-task 669, teammate 164, workflow 6845 - no row named 'journal'. Binary 2.1.258 carries 'this.path=On(hH(n),\"journal.jsonl\")' and '\"subagents\",...n.agentRelPath,\"journal.jsonl\")' and 'Per-agent results: ${d}/journal.jsonl \\u2014 one {\"type\":\"result\",...} line per complet'.",
          "rule": "One row per file for the path-shape census (journals counted separately); one row per journal LINE for the key-set census; one row per csift kind==\"agent\" JSON row for the shape census, over every session directory that has a subagents/ dir.",
          "note": "Structure and exclusion both hold. Refined on two counts: the claim's journal key set is incomplete (a result event carries a fourth `result` key, 6043 of 12708 lines), and the flat built-in location is shared by two csift shapes (builtin-task and teammate), so 'built-in Task/Agent subagent' under-describes what lands there."
        }
      ]
    },
    {
      "id": "SUB-002",
      "area": "subagents",
      "behavior": "A subagent transcript's canonical agent id is the filename stem minus the on-disk `agent-` prefix, and it is byte-identical to the `agentId` Claude Code writes in the transcript records and in the workflow journal (7497/7497, 0 mismatches). That id is a BARE hex for 7333 of 7501 stems; the remaining 168 are the name-embedding teammate form `a<Name>-<16 hex>`. The owning session uuid is the directory component immediately before the `subagents` segment, and neither id form is itself a re-feedable top-level session uuid.",
      "depends": "csift derives every emitted row's id trio (`session_id`, `is_subagent`, `parent_session_id`) through the `subagent` helpers rather than by reading the path stem: the stem yields an `agent-`-prefixed id that joins to nothing and resolves through no `@` target, and only the parent uuid is re-feedable for a downstream fetch.",
      "code": [
        {
          "path": "src/subagent/ids.rs",
          "lines": "5-12",
          "snippet": "/// Strip the on-disk `agent-` filename prefix to the bare-hex canonical agent id (the\n/// value the transcript record's `agentId` field AND the workflow journal carry). The\n/// single source of truth for this rule - used by `make_subagent` and by the\n/// `recover` / `session` / `files` subcommands so a subagent row's printed `session_id`\n/// is the SAME bare hex `agents` prints, hence joinable across surfaces.\n#[must_use]\npub fn bare_agent_id(stem: &str) -> &str {\n    stem.strip_prefix(\"agent-\").unwrap_or(stem)"
        },
        {
          "path": "src/subagent/ids.rs",
          "lines": "32-36",
          "snippet": "/// The re-feedable PARENT session uuid for a transcript path, or `None` when the path is a\n/// top-level `<uuid>.jsonl` (which IS its own session). A subagent transcript lives at\n/// `…/<PARENT-UUID>/subagents/[workflows/wf_*/]agent-<hex>.jsonl`, so the parent uuid is the\n/// directory component immediately BEFORE the `subagents` segment. This is what makes a\n/// search/files subagent match re-feedable: its bare-hex `session_id` is NOT a re-feedable"
        },
        {
          "path": "src/subagent/ids.rs",
          "lines": "10-45",
          "snippet": "#[must_use]\npub fn parent_session_id_from_path(path: &Path) -> Option<String> {\n    let mut prev: Option<&str> = None;\n    for comp in path.components() {\n        let c = comp.as_os_str().to_str()?;\n        if c == \"subagents\" {\n            // The component just before `subagents` is the parent-session dir name.\n            return prev.map(str::to_string);"
        }
      ],
      "instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .agent_id' | head -1`, then `rg -m1 -o '\"agentId\":\"[^\"]*\"' <that agent's jsonl>` - the two must be string-equal and must equal the filename stem minus the leading `agent-`; feeding the printed id back as `csift show @<id> --turn -1` must fetch rather than error. Counting rule: one equality test per subagent file; every id csift prints must round-trip through the @-grammar.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 1; src/subagent/meta.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,os,re,pathlib; files=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/agent-*.jsonl'),recursive=True); eq=neq=noid=0\\nfor p in files:\\n    bare=os.path.basename(p)[6:-6]; found=None\\n    for line in open(p,errors='replace'):\\n        m=re.search(r'\\\"agentId\\\":\\\"([^\\\"]*)\\\"',line)\\n        if m: found=m.group(1); break\\n    eq+= found==bare; neq+= found is not None and found!=bare; noid+= found is None\\nprint(len(files),eq,neq,noid)\"  ;  ok=0; fail=0; for d in $(find ~/.claude/projects -type d -name subagents | head -8); do u=$(basename \"$(dirname \"$d\")\"); for a in $(csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.agent_id' | head -8); do csift show \"@$a\" --turn -1 >/dev/null 2>&1 && ok=$((ok+1)) || fail=$((fail+1)); done; done; echo \"$ok $fail\"  ;  f=$(find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' | head -1); csift show \"$f\" --line 1 --format json | jq -c 'select(.kind==\"header\")|{is_subagent}'",
          "observed": "7497 of 7497 subagent transcripts: filename stem minus the `agent-` prefix == the FIRST in-record `agentId`; 0 mismatches, 0 files with no agentId. 160 of 160 workflow journals: the wf_* dir's agent file stems are a subset of that journal's agentId values. 48 of 48 agent ids printed by `csift agents` fetched successfully through `csift show @<id> --turn -1` (exit 0). csift's printed parent_session_id equals the directory component immediately before `subagents`. Stem-shape census over 7501 stems: 7333 match `^[0-9a-f]+$` (all length 17), 168 match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$`.",
          "rule": "One equality test per subagent transcript file (stem minus `agent-` vs the first `\"agentId\":\"...\"` byte match in that file); one fetch attempt per id printed by `csift agents`, counted as OK on exit 0.",
          "note": "The equality and the parent-uuid derivation both hold exactly. Refined because 'the canonical agent id is the BARE <hex>' is false for 168 of 7501 stems (the teammate form); the correct invariant is 'the stem minus the agent- prefix', which holds for all of them and round-trips as an @-target in 48 of 48 attempts."
        }
      ]
    },
    {
      "id": "SUB-003",
      "area": "subagents",
      "behavior": "Subagent transcripts are stored FLAT under one session directory at every depth: a subagent spawned by another subagent lands in the SAME `subagents/` dir, so nesting is never encoded in the path and exists only as the child's `spawn_tool_use_id` joined to the issuing `Task`/`Agent`/`Workflow` tool_use, which is recorded in the SPAWNING agent's transcript rather than the main one (117 subagent transcripts carry such a tool_use, all at the flat built-in location, 0 under `workflows/`). On-disk nesting is still zero (0 `subagents/*/subagents/*` files across 7501 subagent transcripts), but LOGICAL depth is no longer uniform: 138 nodes resolve to depth 1, 32 to depth 2 and 20 to depth 3, all with a parent distinct from themselves.",
      "depends": "csift rebuilds the tree logically: parent_agent_id comes from the meta's own parentAgentId when the harness wrote one, else from a GLOBAL spawn index over the main transcript plus every subagent transcript, in both cases rejecting a value that names the node itself (a /fork child's cloned transcript carries its own spawning tool_use, so the graph join self-names); assign_depths then walks the id-to-parent chain (cycle-guarded) for depth. A path-derived tree would report every agent at depth 0, and the defensive recursion into nested subagents/ dirs is insurance for a future layout, not a present-data fix. The global spawn index folds the main transcript first and the subagent locals in discovery order with FIRST-wins on every id-keyed map (v0.10.2): a clone's copy of a sibling's spawn record never displaces the original issuer.",
      "code": [
        {
          "path": "src/subagent/topology.rs",
          "lines": "7",
          "snippet": "/// returned message) and the per-node files-changed list. `children` is the tool_use-graph"
        },
        {
          "path": "src/subagent/discover.rs",
          "lines": "69-72",
          "snippet": "    // (C) DEFENSIVE recursion (insurance, not a present-data fix). Verified 2026-06-07:\n    // across all 2348 subagent transcripts on disk there are ZERO sub-sub-agents - the\n    // real layout is single-level FLAT (a child of a general-purpose subagent would land\n    // flat in this SAME `subagents/` dir, already covered by (A)). But if a FUTURE Claude"
        },
        {
          "path": "src/subagent/topology.rs",
          "lines": "5-9",
          "snippet": "/// One fully-linked subagent node in the topology (§new-model). Carries the flat\n/// lifecycle facts PLUS the toolUseId-linked spawn linkage (trigger time, parent agent,\n/// returned message) and the per-node files-changed list. `children` is the tool_use-graph\n/// nesting (real multi-level chains exist since CC 2.1.25x: measured 190 nodes at depth\n/// 1-3 with a distinct parent across the corpus)."
        },
        {
          "path": "src/subagent/topology.rs",
          "lines": "346-355",
          "snippet": "    // Parent agent: the meta's own `parentAgentId` when the harness wrote one, else the\n    // tool_use-graph join - and NEVER the node itself. A `/fork` child is a clone of its\n    // parent's transcript, so the spawning tool_use sits in the child's own file and the\n    // graph join names the child as its own parent (measured: every depth-65 node was\n    // such a self-cycle before v0.10.1).\n    let not_self = |p: String| (p != subagent.agent_id).then_some(p);\n    let parent_agent_id = subagent\n        .meta_parent_agent_id\n        .clone()\n        .and_then(not_self)"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "73-77",
          "snippet": "    /// `parentAgentId` - the harness's own word on the spawning agent (written since CC\n    /// 2.1.25x; 92 metas in the reference corpus). Load-bearing for a `/fork` child: its\n    /// transcript is a CLONE of the parent's and therefore carries the spawning tool_use\n    /// itself, so the tool_use-graph join names the child as its own parent. This field\n    /// breaks that self-cycle (v0.10.1)."
        },
        {
          "path": "src/subagent/spawn.rs",
          "lines": "101-110",
          "snippet": "    pub(crate) fn merge(&mut self, other: ParentSpawnIndex) {\n        for (k, v) in other.spawns {\n            self.spawns.entry(k).or_insert(v);\n        }\n        for (k, v) in other.tool_results {\n            self.tool_results.entry(k).or_insert(v);\n        }\n        for (k, v) in other.issuer {\n            self.issuer.entry(k).or_insert(v);\n        }"
        }
      ],
      "instrument": "`find ~/.claude/projects -path '*/subagents/*/subagents/*' -name '*.jsonl' | wc -l` must be 0; `csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .depth' | sort | uniq -c` - any value above 1 would be the first observed nesting. Counting rule: one row per subagent transcript file; one depth per node.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6.5; SPEC.md section 10.3; src/subagent/discover.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "find ~/.claude/projects -path '*/subagents/*/subagents/*' -name '*.jsonl' | wc -l  ;  for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\")|.depth'; done | sort -n | uniq -c  ;  for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --format json | jq -r 'select(.kind==\"agent\" and .depth>0)|[.depth,(if .parent_agent_id==.agent_id then \"SELF\" else \"OTHER\" end)]|@tsv'; done | sort | uniq -c  ;  find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' -print0 | xargs -0 rg -l '\"name\":\"(Task|Agent|Workflow)\"' | wc -l",
          "observed": "0 nested `subagents/*/subagents/*` transcripts (the flat layout holds). Corpus is now 7501 subagent transcripts, not 2348. csift depth census: 7456 at depth 0, 138 at depth 1, 32 at depth 2, 20 at depth 3, 32 at depth 65. Of the 222 depth>0 rows, 190 have a parent_agent_id DIFFERENT from their own agent_id (genuine multi-level chains) and 32 have parent_agent_id == agent_id (a self-parent, all with agent_type 'fork'). 117 subagent transcripts carry a `Task|Agent|Workflow` tool_use, every one of them at the flat built-in location and 0 under `workflows/`.",
          "rule": "One row per csift kind==\"agent\" JSON row, over every session directory that has a subagents/ dir; one file per `rg -l` hit for the spawn-tool-use probe; nested-directory count is one row per file.",
          "note": "Drifted on the measured half. Two separate findings. (1) Genuine multi-level nesting now exists: 190 nodes at depth 1-3 with a real distinct parent, so 'depth is uniformly 1' and 'children is empty on all current data' are stale in three csift comments. (2) A defect this exposes: all 32 nodes at depth 65 are `/fork` children whose parent_agent_id equals their own agent_id - a self-parent cycle that runs csift's `assign_depths` guard to its `guard > 64` cap (src/subagent/topology.rs:261), so `agents` reports depth 65 for every forked lane. Claude Code writes `parentAgentId` into 92 subagent meta.json files in 2.1.258, which is the field that would resolve a fork child's real parent; csift reads none of it (`rg -n 'parentAgentId' src/` is empty)."
        },
        {
          "claude_code": "2.1.258",
          "csift": "0.10.2",
          "date": "2026-09-03",
          "verdict": "drifted",
          "instrument": "meta.json census over the corpus's subagent trees (agentType fork or isFork) counting parentAgentId presence, plus the spawn-index merge unit test with a repeated tool_use id",
          "observed": "33 fork metas, 0 carry parentAgentId; the 0.10.1 merge was later-wins on issuer/spawns/tool_results, so a clone's copy of a sibling's spawn record would re-parent that sibling onto the clone; 0.10.2 keeps the first issuer",
          "rule": "one meta.json = one fork child; a repeated tool_use id across two locals is the clone shape",
          "note": "the meta-first path is the mechanism only when the harness writes parentAgentId (none of the 33 here do); the effective guard for today's clones is the self-link drop plus the first-wins fold (spawn.rs merge); unit spawn_index_merge_keeps_the_first_issuer_for_a_repeated_tool_use_id"
        }
      ]
    },
    {
      "id": "SUB-004",
      "area": "subagents",
      "behavior": "A subagent transcript's FIRST record is an `isSidechain:true` `type:\"user\"` seed carrying the spawn prompt the parent delivered - exhaustively 7467 of 7500 subagent transcripts. The one exception is a `/fork` child (33 of 7500), whose line 1 is the timestampless `fork-context-ref` record and whose next record is an `isSidechain:true` ASSISTANT record in 32 of 33 cases, so a fork child has no user seed at its head. A sidechain record does not occur in any top-level transcript: 0 lines across all 64 top-level transcripts.",
      "depends": "csift deliberately does NOT gate `isSidechain` out of `is_genuine_user`, so `list`'s per-subagent preview can treat that seed as the subagent's first user message; the seed nevertheless classifies `agent.communication.inbox` with direction parent to self rather than `user.message`.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "48-52",
          "snippet": "        // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n        // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n        // per-subagent preview legitimately treats that seed as the subagent's \"first\n        // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n        // any real corpus."
        },
        {
          "path": "src/model/classify.rs",
          "lines": "330-335",
          "snippet": "        // The spawn-prompt seed of a subagent transcript is an inbound comm (parent ⇨ self),\n        // not the operator (GOLD §3) - unchanged, regardless of isMeta.\n        if ctx.is_subagent && ctx.is_transcript_opener {\n            push_unique(out, Class::CommInbox);\n            return;\n        }"
        }
      ],
      "instrument": "The claim's `csift list @<agent-id> --format json | jq -r .first_user` returns an OBJECT, not a string: its keys are `{excerpt, ts_local, ts_utc}`. Read `.first_user.excerpt`, and expect it EMPTY for a teammate seed (a `<teammate-message>` seed is a peer message, so `is_genuine_user` excludes it): 26 of 40 sampled subagent rows had a non-empty excerpt.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 1; src/model/predicates.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,json,pathlib,collections; files=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/agent-*.jsonl'),recursive=True); c=collections.Counter()\\nfor p in files:\\n    v=json.loads(open(p,errors='replace').readline()); m=v.get('message'); c[(v.get('type'),v.get('isSidechain'),(m or {}).get('role') if isinstance(m,dict) else None)]+=1\\nprint(len(files)); [print(n,k) for k,n in c.most_common()]\"  ;  python3 -c \"import glob,pathlib; tops=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*.jsonl')); print(len(tops), sum(1 for p in tops for l in open(p,errors='replace') if '\\\"isSidechain\\\":true' in l))\"  ;  for f in $(find ~/.claude/projects -path '*/subagents/*' -name 'agent-*.jsonl' | head -20) $(find ~/.claude/projects -path '*/subagents/workflows/*' -name 'agent-*.jsonl' | head -20); do csift show \"$f\" --line 1 --format json | jq -r 'select(.kind==\"record\")|.label'; done | sort | uniq -c",
          "observed": "7500 subagent transcripts: 7467 open with ('user', isSidechain=true, message.role='user'); the other 33 open with a 'fork-context-ref' record. Top-level transcripts: 64 files, 0 lines matching \"isSidechain\":true. 40 of 40 sampled subagent line-1 records classify `agent.communication.inbox` under `csift show --line 1`, one showing `\"from\":\"team-lead\"`. Of the 33 fork children, line 2 is an ('assistant', isSidechain=true) record in 32 cases and a user sidechain seed in 1.",
          "rule": "One line-1 record per subagent transcript file (exhaustive, not sampled); one grep hit per line for the top-level isSidechain scan; one label per csift kind==\"record\" row over a 40-file sample split 20 flat / 20 workflow.",
          "note": "The behaviour holds with an exception the claim did not carry, and the exhaustive count (7467/7500) replaces the sampled assertion. The stated instrument is wrong twice over - `.first_user` is an object, and even read correctly it is empty for teammate lanes - so a stranger running it as written would read the claim as failing."
        }
      ]
    },
    {
      "id": "SUB-005",
      "area": "subagents",
      "behavior": "A transcript created by `/fork` opens with a TIMESTAMPLESS `type:\"fork-context-ref\"` record on LINE 1, carrying `{agentId, parentSessionId, parentLastUuid, contextLength}` - the parent's last record uuid at fork time and the number of messages carried into the fork - before any timestamped record; the field pair appears nowhere else.",
      "depends": "csift's `agents` head walk captures the fork provenance for free while looking for the first timestamped record, surfacing `fork_parent_last_uuid` / `fork_context_length` and `--agent-type fork`; a timestamp-first head scan would skip line 1 and the lane's `started_utc` would be wrong.",
      "code": [
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "18-20",
          "snippet": "    // A `/fork` child's LINE 1 is a timestampless `fork-context-ref` record carrying\n    // the parent's last uuid at fork time + the carried context length; it precedes\n    // the first timestamped record, so the same head walk captures it for free."
        },
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "23-27",
          "snippet": "    let (head_skipped, head_consumed) = head_records(&subagent.path, |rec| {\n        if rec.is_type(\"fork-context-ref\") {\n            fork_parent_last_uuid = rec.parent_last_uuid.clone();\n            fork_context_length = rec.context_length;\n        }"
        },
        {
          "path": "src/subagent/types.rs",
          "lines": "103-107",
          "snippet": "    /// Fork provenance from a head `fork-context-ref` record (a `/fork` child):\n    /// the parent's last record uuid at fork time. `None` for a non-fork agent.\n    pub fork_parent_last_uuid: Option<String>,\n    /// The context length carried into the fork, from the same record.\n    pub fork_context_length: Option<u64>,"
        }
      ],
      "instrument": "`rg -l '\"type\":\"fork-context-ref\"' ~/.claude/projects --glob '*.jsonl' | while read f; do head -1 \"$f\" | jq -c '{type,parentLastUuid,contextLength,timestamp}'; done` - every hit must be line 1 with a null timestamp; then `csift agents @<session> --agent-type fork --format json | jq '{agent_id, fork_parent_last_uuid, fork_context_length}'` and `csift show @<session> --uuid <fork_parent_last_uuid>`. Counting rule: one record per forked transcript.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.8.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6 v0.8.1 ledger; CHANGELOG 0.8.1; src/model/record.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg -l --glob '*.jsonl' '\"type\":\"fork-context-ref\"' ~/.claude/projects | wc -l  ;  python3 -c \"import subprocess,json,pathlib; out=subprocess.run(['rg','-l','--glob','*.jsonl','\\\"type\\\":\\\"fork-context-ref\\\"',str(pathlib.Path.home()/'.claude/projects')],capture_output=True,text=True).stdout.split()\\nline1=ts=0; keys=set(); loc={'sub':0,'top':0}\\nfor p in out:\\n    loc['sub' if '/subagents/' in p else 'top']+=1\\n    lines=open(p,errors='replace').readlines(); hits=[i for i,l in enumerate(lines) if '\\\"type\\\":\\\"fork-context-ref\\\"' in l]\\n    line1+= hits==[0]; v=json.loads(lines[0]); keys.add(tuple(sorted(v.keys()))); ts+= v.get('timestamp') is not None\\nprint(len(out),line1,ts,loc,keys)\"  ;  rg --glob '*.jsonl' '\"parentLastUuid\"' ~/.claude/projects | rg -v 'fork-context-ref' | wc -l  ;  rg --glob '*.jsonl' '\"contextLength\"' ~/.claude/projects | rg -v 'fork-context-ref' | wc -l  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}fork-context-ref.{0,60}'  ;  for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --agent-type fork --format json | jq -r 'select(.kind==\"agent\")|[(if .fork_parent_last_uuid then \"HAS\" else \"NULL\" end),(.fork_context_length//\"null\"|tostring)]|@tsv'; done | sort | uniq -c",
          "observed": "33 files carry a fork-context-ref record; all 33 are under `subagents/`, 0 top-level. In all 33 the record is on line 1 and ONLY line 1, and in all 33 its `timestamp` is absent. Key set is identical across all 33: ('agentId','contextLength','parentLastUuid','parentSessionId','type'). `parentLastUuid` occurs on exactly 33 lines corpus-wide and 0 of them outside a fork-context-ref line; same for `contextLength` (0 outside). Binary 2.1.258: 'async function x7n(e,n){await Fi().appendEntry({type:\"fork-context-ref\",...e},void 0,void 0,n)}' and '[fork-context-ref] parent uuid ${e.parentLastUuid} not found in ${d}'. Every csift `--agent-type fork` node reported a non-null fork_parent_last_uuid and a numeric fork_context_length (observed values 31, 117, 143, 200, 251, 344, 383, 2513, 3060, 4229, ...).",
          "rule": "One record per forked transcript; the field-pair uniqueness is one grep hit per line over every *.jsonl under ~/.claude/projects, subtracting lines that also contain the fork-context-ref type literal.",
          "note": "Every element of the claim was instrumented and agreed: line-1-only, timestampless, exact four-field payload, and the field pair appearing nowhere else in the corpus. The binary independently confirms the writer (`appendEntry({type:\"fork-context-ref\",...})`) and that `parentLastUuid` is looked up against the parent's record store."
        }
      ]
    },
    {
      "id": "SUB-006",
      "area": "subagents",
      "behavior": "A subagent's `agent-<id>.meta.json` companion (written by replacing the transcript's `.jsonl` suffix) carries a KIND-DEPENDENT field set. A workflow agent meta carries `{agentType}` alone (2105 of 6669) or `{agentType, spawnDepth}` (4564 of 6669). A built-in meta carries `{agentType, description, toolUseId}` plus, in current builds, some of `{spawnDepth, name, model, parentAgentId, isFork}`. A TEAMMATE meta carries `{agentType, description, name, taskKind, teamName, color, model, permissionMode, planModeRequired, spawnDepth}` (+`parentAgentId` on 8 of 164) and NO `toolUseId`, and its `agentType` is OVERLOADED with the teammate NAME rather than the real subagent type (164 of 164), which lives on the spawning `Agent` tool_use as `input.subagent_type`. Corpus key vocabulary over 7502 metas: agentType 7502, spawnDepth 5047, description 833, toolUseId 668, model 261, name 246, taskKind/teamName/color/planModeRequired/permissionMode 164 each, parentAgentId 92, isFork 33, customAgentType 1, stoppedByUser 1.",
      "depends": "csift reads the meta keys tolerantly (a malformed, missing or key-absent meta yields all-`None`, never an error, and the lifecycle still resolves from the transcript) and, for a teammate only, prefers the spawn tool_use's `subagent_type` over the meta `agentType`; without that preference `agents` prints the teammate's name where its type belongs, and without a `toolUseId` the id-join spawn linkage is null.",
      "code": [
        {
          "path": "src/subagent/meta.rs",
          "lines": "54-58",
          "snippet": "/// The fields csift reads from a subagent's `meta.json`. A built-in meta carries\n/// `{agentType, description, toolUseId}` (+ often `name`); a workflow agent meta carries only\n/// `{agentType}`; a TEAMMATE meta carries `{agentType, description, name, taskKind, teamName,\n/// color, model, …}` and NO `toolUseId`. All are optional - a malformed / missing / key-absent\n/// meta yields all-`None` (never an error; the lifecycle still resolves from the transcript)."
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "63-65",
          "snippet": "    /// The spawning parent `Task`/`Agent` tool_use id (built-in only; the topology join\n    /// key). Captured here so the previously-dropped `toolUseId` reaches the topology.\n    pub tool_use_id: Option<String>,"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "67-72",
          "snippet": "    /// `taskKind` - `\"in_process_teammate\"` marks a teammate (the only way to distinguish it\n    /// from a built-in Task subagent, since both share the on-disk location). `None`/other for\n    /// a plain built-in or workflow agent.\n    pub task_kind: Option<String>,\n    /// `teamName` - the team a teammate belongs to (teammate metas only).\n    pub team_name: Option<String>,"
        },
        {
          "path": "src/subagent/spawn.rs",
          "lines": "20-22",
          "snippet": "    /// `input.subagent_type` on the spawning tool_use - the richer agent-type label used\n    /// as a fallback when the built-in meta.json's `agentType` is absent.\n    pub subagent_type: Option<String>,"
        }
      ],
      "instrument": "`for f in ~/.claude/projects/*/*/subagents/*.meta.json; do jq -c 'keys' \"$f\"; done | sort | uniq -c` - expect the three key sets above; `jq -r 'select(.taskKind==\"in_process_teammate\") | [.agentType,.name] | @tsv'` over the same files must show agentType equal to name; then `csift agents @<session> --format json | jq -r 'select(.shape==\"teammate\") | [.agent_type, .name] | @tsv'` must report a real subagent type, not the name. Counting rule: one key set per meta file.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.3.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/meta.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,json,pathlib,collections; metas=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True)\\nflat=[p for p in metas if '/workflows/' not in p]; wf=[p for p in metas if '/workflows/' in p]\\nfor label,ps in (('flat',flat),('workflows',wf)):\\n    c=collections.Counter(); tm=eq=0\\n    for p in ps:\\n        v=json.load(open(p)); c[tuple(sorted(v.keys()))]+=1\\n        if v.get('taskKind')=='in_process_teammate': tm+=1; eq+= v.get('agentType')==v.get('name')\\n    print(label,len(ps)); [print(' ',n,list(k)) for k,n in c.most_common(8)]; print('  teammates',tm,'agentType==name',eq)\"  ;  for d in $(find ~/.claude/projects -type d -name subagents); do u=$(basename \"$(dirname \"$d\")\"); csift agents \"@$u\" --shape teammate --format json | jq -r 'select(.kind==\"agent\")|[(if .agent_type==.name then \"SAME\" else \"DIFF\" end),(if .spawn_tool_use_id then \"SPAWN\" else \"NOSPAWN\" end)]|@tsv'; done | sort | uniq -c  ;  rg -n 'parentAgentId|spawnDepth|isFork' src/",
          "observed": "7502 meta files: 833 at the flat built-in location, 6669 under workflows/. Workflow metas now carry TWO key sets: ['agentType','spawnDepth'] x4564 and ['agentType'] x2105. Flat metas: ['agentType','description','toolUseId'] x294, +spawnDepth x198, ['agentType','color','description','model','name','permissionMode','planModeRequired','spawnDepth','taskKind','teamName'] x155, ['agentType','description','model','parentAgentId','spawnDepth','toolUseId'] x79, ['agentType','description','name','toolUseId'] x56, isFork forms x14 and x11, teammate+parentAgentId x8. Teammates: 164, agentType == name in 164 of 164, and NONE carries toolUseId. csift teammate rows: 146 report an agent_type DIFFERENT from the name with a recovered spawn id, 9 SAME with a spawn id, 9 SAME without one. `rg -n 'parentAgentId|spawnDepth|isFork' src/` returns nothing.",
          "rule": "One key set per meta file, partitioned by whether the path contains /workflows/; one row per csift kind==\"agent\" teammate row for the type-recovery census.",
          "note": "The kind-dependence and the teammate agentType overload both hold exactly (164/164). Refined because current Claude Code writes more than the claim lists: a workflow meta now usually carries `spawnDepth` too (4564 of 6669, so 'only {agentType}' covers just 32% of them), and four fields csift reads none of - `spawnDepth` (5047 metas), `parentAgentId` (92), `isFork` (33), `model` (261). csift's type-preference works: 146 of 164 teammate rows report a real subagent type distinct from the name; the 18 that fall back to the name are lanes where the name-join found no spawn record or the spawn carried the same string."
        }
      ]
    },
    {
      "id": "SUB-007",
      "area": "subagents",
      "behavior": "A subagent's `meta.json` carries NO `status` field (0 of 7502 metas, up from the 400 originally sampled) and is close to write-once: the only post-spawn mutation observed on one is a `\"stoppedByUser\":true` write (1 of 7502), and the workflow RESULT manifests under the session's `workflows/` dir are terminal-only (1 live run dir had no manifest at scan time).",
      "depends": "csift never derives child liveness from meta or from a result file - `status`/`wait` read each child's own transcript tail plus the incremental workflow journal - so a status field appearing in meta would be a cheaper instrument csift is currently forgoing, and any code assuming one would read `None` forever.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "3-6",
          "snippet": "//! `subagents/*.meta.json` has NO status field (child liveness can never come from\n//! meta), and workflow RESULT files are terminal-only - but `journal.jsonl` inside each\n//! `wf_*` dir is written INCREMENTALLY (`{type:\"started\",agentId}` at spawn,\n//! `{type:\"result\",agentId}` at return), so `started - result` = workflow agents in"
        }
      ],
      "instrument": "`python3 -c \"import json,glob,pathlib;ps=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True);print(sum('status' in json.load(open(p)) for p in ps), len(ps))\"` - expect 0 of N; flattening the keys the same way shows `stoppedByUser` on a rare few. Counting rule: one presence test per meta file.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6.13; src/live/children.rs module doc; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,json,pathlib,collections; ps=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/**/*.meta.json'),recursive=True); keys=collections.Counter()\\nst=sb=0\\nfor p in ps:\\n    v=json.load(open(p)); keys.update(v.keys()); st+= 'status' in v; sb+= 'stoppedByUser' in v\\nprint(len(ps),st,sb); print(dict(keys.most_common()))\"  ;  python3 -c \"import glob,json,os,pathlib; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); m=d=0\\nfor sd in sess:\\n    man={os.path.basename(p)[:-5] for p in glob.glob(os.path.join(sd,'workflows','wf_*.json'))}; dirs={os.path.basename(p) for p in glob.glob(os.path.join(sd,'subagents','workflows','wf_*'))}; m+=len(man-dirs); d+=len(dirs-man)\\nprint(m,d)\"  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}stoppedByUser.{0,40}'",
          "observed": "7502 meta files: 0 carry a `status` key; 1 carries `stoppedByUser`. Full key vocabulary contains no status-like field other than that single `stoppedByUser`. Run-manifest side: of 163 workflow run manifests, 4 have no corresponding `subagents/workflows/wf_<id>/` agent dir and 1 agent dir has no manifest - consistent with a manifest written only at run end. Binary 2.1.258 carries the meta-writer fragment '...d?.stoppedByUser&&{stoppedByUser:!0},...d?.parentAgentId' and a separate liveness test 'e.stoppedByUser!==!0&&(e.status===\"running\"||e.status===' on a different record type.",
          "rule": "One presence test per meta file for each key; one set-difference row per workflow run id for the manifest/agent-dir comparison.",
          "note": "The load-bearing half - no status field in meta, so child liveness can never come from it - holds at 18x the original sample size (0 of 7502). Refined on the numbers and on 'effectively write-once': the 2.1.258 meta writer merges `stoppedByUser` and `parentAgentId` conditionally into the object it writes, and 92 metas carry `parentAgentId`, so a second mutable field exists even though the corpus cannot distinguish a spawn-time write from a later one. A status-bearing sibling record does exist in the binary (`e.status===\"running\"`), but on a different object than the subagent meta."
        }
      ]
    },
    {
      "id": "SUB-008",
      "area": "subagents",
      "behavior": "Each workflow run's `journal.jsonl` is written INCREMENTALLY, one line per lifecycle event - `{type:\"started\", key, agentId}` at spawn and `{type:\"result\", key, agentId, result}` at return - so `started` minus `result` approximates that run's agents in flight; the two counts do not balance while agents are running (36 of 160 journals measured with started != result), which a terminal dump could never produce. The event-type set is OPEN, not a pair: a third terminal type `failed` (same key shape as `started`) also occurs (1 of 12718 events), so `started - result` overstates in-flight by the number of failed agents.",
      "depends": "csift's `JournalCache` parses each journal once per topology build and keys completion on the presence of a `result` event for an `agentId` (first event per agent wins), feeding `agents`' workflow status and returned message and `status`'s `journal_in_flight`, which is what produces a waiting-children verdict for a fan-out whose child transcripts already look settled.",
      "code": [
        {
          "path": "src/live/children.rs",
          "lines": "122-132",
          "snippet": "            for line in raw.lines() {\n                let Ok(v) = serde_json::from_str::<serde_json::Value>(line) else {\n                    continue;\n                };\n                match v.get(\"type\").and_then(serde_json::Value::as_str) {\n                    Some(\"started\") => started += 1,\n                    Some(\"result\") => resulted += 1,\n                    _ => {}\n                }\n            }\n            report.journal_in_flight += started.saturating_sub(resulted);"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "166-177",
          "snippet": "                if v.get(\"type\").and_then(serde_json::Value::as_str) != Some(\"result\") {\n                    continue;\n                }\n                let Some(agent) = v.get(\"agentId\").and_then(serde_json::Value::as_str) else {\n                    continue;\n                };\n                let payload = match v.get(\"result\") {\n                    Some(serde_json::Value::String(s)) => Some(s.clone()),\n                    Some(other) => Some(other.to_string()),\n                    None => None,\n                };\n                // FIRST event per agent wins - the former scans returned on first match."
        }
      ],
      "instrument": "`for j in ~/.claude/projects/*/*/subagents/workflows/*/journal.jsonl; do s=$(grep -c '\"type\":\"started\"' \"$j\"); r=$(grep -c '\"type\":\"result\"' \"$j\"); echo \"$s $r\"; done | awk '$1!=$2' | wc -l` - expect a nonzero minority; compare the same difference with `csift status @<session> --format json | jq '.evidence[] | select(.surface==\"children\")'` during a live fan-out. Counting rule: whole-line type matches per journal file, started minus result per run.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6.13; src/live/children.rs module doc; CHANGELOG 0.9.0; dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,json,pathlib,collections; js=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/subagents/workflows/wf_*/journal.jsonl')); types=collections.Counter(); sk=collections.Counter(); rk=collections.Counter(); imb=bal=ts=tr=0\\nfor p in js:\\n    s=r=0\\n    for line in open(p,errors='replace'):\\n        v=json.loads(line); t=v.get('type'); types[t]+=1\\n        if t=='started': s+=1; sk[tuple(sorted(v.keys()))]+=1\\n        elif t=='result': r+=1; rk[tuple(sorted(v.keys()))]+=1\\n    ts+=s; tr+=r; imb+= s!=r; bal+= s==r\\nprint(len(js),bal,imb,ts,tr,dict(types),dict(sk),dict(rk))\"  ;  find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 rg -l '\"type\":\"failed\"' | while read j; do echo \"started=$(rg -c '\\\"type\\\":\\\"started\\\"' \"$j\") result=$(rg -c '\\\"type\\\":\\\"result\\\"' \"$j\") failed=$(rg -c '\\\"type\\\":\\\"failed\\\"' \"$j\")\"; done  ;  strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,50}journal\\.jsonl.{0,60}'",
          "observed": "160 journals; 124 with started == result, 36 with started != result. Totals: 6669 started, 6048 result, and a THIRD event type not in the claim - 1 'failed'. started key set is uniformly ('agentId','key','type'); result key set is uniformly ('agentId','key','result','type'). The single journal carrying a failed event reads started=107 result=106 failed=1, so its entire imbalance is that one terminal failure. Binary 2.1.258: 'Per-agent results: ${d}/journal.jsonl \\u2014 one {\"type\":\"result\",...} line per complet' and 'Read <transcriptDir>/journal.jsonl \\u2014 it records each agent's actual return valu'.",
          "rule": "Whole-line JSON parse per journal line, bucketed by the `type` value; started minus result computed per run file; one imbalance row per journal file.",
          "note": "Incremental writing and the live imbalance both hold, at 160 journals rather than 153. Refined because the event vocabulary is open: a `failed` event terminates an agent without a `result` line, and csift's counter (src/live/children.rs:126-129) matches only \"started\" and \"result\" with a catch-all `_ => {}`, so a failed workflow agent inflates `journal_in_flight` by 1 permanently. The one journal on disk carrying a failed event is exactly the case: started 107, result 106, failed 1."
        }
      ]
    },
    {
      "id": "SUB-009",
      "area": "subagents",
      "behavior": "Workflow RUN manifests are written at the TOP level of the session sidecar as `<session-uuid>/workflows/wf_<id>.json` - a different directory from `<session-uuid>/subagents/workflows/wf_<id>/`, which holds no `wf_*.json` at all - carrying 18 to 20 keys: `{runId, taskId, workflowName, status, agentCount, durationMs, totalTokens, totalToolCalls, defaultModel, startTime, timestamp, script, scriptPath, logs, phases, result, summary, workflowProgress}` plus `args` and `error` when present. There is no `startedAt` field; the run's start instant is `startTime`.",
      "depends": "csift's `discover_workflow_runs` reads those manifests as the `kind:\"run\"` rows of `agents` and parents each `wf_<id>` agent under its run by `workflow_id == runId`; looking under `subagents/workflows/` alone would find agent dirs and no run metadata, and the `workflows/scripts/` subdir plus any non-`wf_*.json` entry are ignored.",
      "code": [
        {
          "path": "src/subagent/topology.rs",
          "lines": "157-160",
          "snippet": "/// Read every top-level `<session>/workflows/wf_*.json` manifest (§5) as a [`WorkflowRun`].\n/// Returns an empty vec when the session has no sidecar / no `workflows/` dir (never an\n/// error for the common no-workflow case). The `workflows/scripts/` subdir and any\n/// non-`wf_*.json` entry are ignored."
        },
        {
          "path": "src/subagent/tests.rs",
          "lines": "49",
          "snippet": "/// A top-level `workflows/wf_abc.json` manifest is also written (NOT under"
        }
      ],
      "instrument": "`csift agents ... | jq 'select(.kind==\"run\")'` does NOT emit one row per manifest. A row is emitted only for a run that has a manifest AND at least one in-scope agent node, and csift synthesizes an extra row with `status: null` for a `subagents/workflows/wf_<id>/` dir that has no manifest yet. Measured: 163 manifests, 4 with no agent dir, 1 agent dir with no manifest, and 163 - 4 + 1 = 160 rows emitted - an exact reconciliation. Target each session by its explicit `<uuid>.jsonl` path; an `@<uuid>` target can resolve to two project dirs and double-count.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.2.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "find ~/.claude/projects -path '*/workflows/*' -name 'wf_*.json' ! -path '*/subagents/*' | wc -l  ;  find ~/.claude/projects -path '*/subagents/workflows/*' -name 'wf_*.json' | wc -l  ;  python3 -c \"import glob,json,pathlib,collections; ms=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json')); c=collections.Counter(); [c.update([tuple(sorted(json.load(open(p)).keys()))]) for p in ms]; print(len(ms)); [print(n,list(k)) for k,n in c.most_common()]\"  ;  python3 -c \"import glob,json,os,pathlib,subprocess,collections; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); disk=collections.Counter(); csi=collections.Counter()\\nfor sd in sess:\\n    for p in glob.glob(os.path.join(sd,'workflows','wf_*.json')): disk[json.load(open(p)).get('status')]+=1\\n    r=subprocess.run(['csift','agents',sd+'.jsonl','--format','json'],capture_output=True,text=True)\\n    for line in r.stdout.splitlines():\\n        v=json.loads(line)\\n        if v.get('kind')=='run': csi[v.get('status')]+=1\\nprint(dict(disk),dict(csi))\"",
          "observed": "163 manifests at `<session-uuid>/workflows/wf_<id>.json`; 0 `wf_*.json` files anywhere under `subagents/workflows/`. Manifest key sets are 18 to 20 keys, never 10: the majority (111 files) is ['agentCount','defaultModel','durationMs','logs','phases','result','runId','script','scriptPath','startTime','status','summary','taskId','timestamp','totalTokens','totalToolCalls','workflowName','workflowProgress'], plus 'args' (43 files) and plus 'error' (9 files). There is NO `startedAt` key. The same `workflows/` dir also holds a `scripts/` subdir (14 sessions) and per-run `.js` files whose names do not start with `wf_`. csift emits a 13-key `kind:\"run\"` row: ['agent_count','default_model','duration_ms','kind','run_id','session_id','started_local','started_utc','status','task_id','total_tokens','total_tool_calls','workflow_name'], 160 rows against 163 manifests.",
          "rule": "One key set per manifest file; one `kind:\"run\"` row per csift invocation, targeting each session by its explicit transcript path so a uuid cannot resolve to two project dirs.",
          "note": "The location claim - the discriminating fact - holds exactly (163 manifests at the top-level workflows dir, 0 under subagents/workflows). Refined on the field list: one named field (`startedAt`) does not exist and the manifest is roughly twice as wide as listed. The stated instrument also needed correcting; run through it as written, a stranger would see 160 rows against 163 manifests and read that as a defect rather than the documented run-must-have-agents filter at src/agents/json.rs:39-47 plus the synthesized stand-in at src/agents/run.rs:109-131."
        }
      ]
    },
    {
      "id": "SUB-010",
      "area": "subagents",
      "behavior": "A workflow RUN's `status` is written verbatim by the workflow runtime into the run MANIFEST and is an OPEN set, not a closed enum: `completed`, `killed` and `failed` all observed (154 / 4 / 5 over 163 manifests). It is a different axis from the per-agent lifecycle status csift computes from a transcript tail. The journal does NOT carry a run status - a journal line's only discriminator is `type`, whose observed values are `started`, `result` and `failed`.",
      "depends": "csift reads the value with a plain string field read and prints it unchanged on `kind:\"run\"` rows; tightening it into a closed set would drop or mislabel a run state the runtime later adds.",
      "code": [
        {
          "path": "src/subagent/meta.rs",
          "lines": "113-117",
          "snippet": "pub(crate) fn journal_reports_completion(subagent: &Subagent, journals: &JournalCache) -> bool {\n    journals\n        .events_for(subagent)\n        .is_some_and(|data| data.results.contains_key(&subagent.agent_id))\n}"
        }
      ],
      "instrument": "The manifest status census and the csift run-row census agree only over runs that are both manifest-backed and agent-bearing; compare them after subtracting manifests with no `subagents/workflows/wf_<id>/` dir (4) and adding csift's synthesized null-status rows for agent dirs with no manifest (1). Target each session by explicit transcript path, not `@<uuid>`.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 -c \"import glob,json,pathlib,collections; ms=glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json')); c=collections.Counter(json.load(open(p)).get('status') for p in ms); print(len(ms),dict(c))\"  ;  python3 -c \"import glob,json,os,pathlib,subprocess,collections; sess=sorted({os.path.dirname(os.path.dirname(p)) for p in glob.glob(str(pathlib.Path.home()/'.claude/projects/*/*/workflows/wf_*.json'))}); csi=collections.Counter()\\nfor sd in sess:\\n    r=subprocess.run(['csift','agents',sd+'.jsonl','--format','json'],capture_output=True,text=True)\\n    for line in r.stdout.splitlines():\\n        v=json.loads(line)\\n        if v.get('kind')=='run': csi[v.get('status')]+=1\\nprint(dict(csi))\"  ;  find ~/.claude/projects -path '*/subagents/workflows/*' -name journal.jsonl -print0 | xargs -0 cat | jq -c 'keys' | sort | uniq -c",
          "observed": "163 manifests: status completed 154, killed 4, failed 5 - three distinct values, no fourth. csift `kind:\"run\"` rows over the same 15 sessions: completed 153, killed 4, failed 2, null 1 (160 rows), which reconciles exactly against the 4 manifests with no agent dir and the 1 agent dir with no manifest. Journal lines carry only two key sets, ['agentId','key','type'] and ['agentId','key','result','type'] - there is no `status` key anywhere in any journal.",
          "rule": "One status per manifest file; one status per csift kind==\"run\" row, sessions targeted by explicit transcript path; one key set per journal line for the journal-status check.",
          "note": "The open-set finding holds and the same three values recur at a larger N (154/4/5 over 163, against the ledger's 148/5/4 over 157) - nothing tightened, nothing new appeared. Refined on two points: the status is a MANIFEST field only (the journal has no status key at all, so 'in the run manifest and the journal' is wrong), and the stated instrument's 'the two must agree' needs the manifest/agent-dir reconciliation or it reads as a mismatch."
        }
      ]
    },
    {
      "id": "SUB-011",
      "area": "subagents",
      "behavior": "The tools that spawn a subagent are named exactly `Task`, `Agent` and `Workflow` in a tool_use `name` field - all three are registered tool names in Claude Code 2.1.258. `Agent` and `Workflow` are the spellings a real corpus carries (corpus-wide tool-axis census: Agent 1832 records, Workflow 356 records, across 35 distinct tool keys); `Task` is matched defensively and occurred zero times in the corpus measured here.",
      "depends": "csift's global spawn index and the self-to-child comm direction gate on that three-name set; matching only `Task` leaves every child unlinked and collapses the `agents` tree to a flat list, and a fourth spawn tool name would leave its children parentless and unlabelled under `agent.communication.signal`.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "262-264",
          "snippet": "pub(crate) fn is_spawn_tool_name(name: &str) -> bool {\n    matches!(name, \"Task\" | \"Agent\" | \"Workflow\")\n}"
        },
        {
          "path": "src/subagent/spawn.rs",
          "lines": "250-252",
          "snippet": "pub(crate) fn is_spawn_tool(name: &str) -> bool {\n    matches!(name, \"Agent\" | \"Task\" | \"Workflow\")\n}"
        }
      ],
      "instrument": "`csift search '' @<session> -t agent.tool.use --count-by tool --format json | jq -r 'select(.key==\"Agent\" or .key==\"Task\" or .key==\"Workflow\") | [.key,.records] | @tsv'`. Counting rule: one record per tool_use record on the tool axis, deduped per record by the census.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 10.3"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -oN '\"Bash\",\"BashOutput\"[^\\]]*'   AND   csift search '' --count-by tool --format json   AND   csift search '' -t agent.communication.sent --count-by tool --format json",
          "observed": "Binary tool registry string (verbatim excerpt): \"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",\"Edit\",...,\"Agent\",\"Task\",\"Workflow\",\"Skill\",... - all three spawn names are registered tools at 2.1.258. Corpus tool-axis census over 653288 matched records / 35 distinct tool keys: Agent 1832, Workflow 356, SendMessage 1581, TaskCreate 2222, TaskStop 182; the key `Task` is ABSENT (zero records). Restricted to the sent-comm view: Agent 916, Workflow 178, SendMessage 725, no Task.",
          "rule": "One census record per record on the tool axis (a tool_use and its paired tool_result each count once, hence the exact 2x between the unfiltered and the single-view counts); a tool name absent from the 35 keys occurred zero times in the whole projects root.",
          "note": "The three-name set is confirmed twice over: at the binary level (all three appear in 2.1.258's tool registry string) and at the corpus level (Agent and Workflow both occur; Task does not). The claim's per-transcript figures (151 Agent, 22 Workflow in 'one representative parent transcript') are not a reproducible counting rule - they name no transcript and transcripts grow, so they were replaced with a corpus-wide census a stranger can rerun. csift's defensive match on `Task` is the right call: the name is live in the harness even though this corpus never exercised it."
        }
      ]
    },
    {
      "id": "SUB-012",
      "area": "subagents",
      "behavior": "A TEAMMATE subagent (Claude Code's persistent, directly addressable team-member agent) lands at the BUILT-IN on-disk location `subagents/agent-<id>.jsonl`, so location alone cannot classify it; the sole discriminator is its `meta.json` `\"taskKind\":\"in_process_teammate\"`, which workflow agents never carry.",
      "depends": "csift upgrades the location-derived `BuiltinTask` kind to `Teammate` on that literal, and the upgrade is what `agents --shape teammate`, the teammate control hint, the name-join spawn linkage and the real-agent-type preference all key on.",
      "code": [
        {
          "path": "src/subagent/types.rs",
          "lines": "13-16",
          "snippet": "    /// A \"teammate\" (`taskKind:\"in_process_teammate\"`) - Claude Code's persistent, directly\n    /// addressable team-member agent. It lands at the built-in on-disk LOCATION\n    /// (`subagents/agent-<id>.jsonl`), so location alone can't tell it apart; the discriminator\n    /// is the meta.json `taskKind`. Its canonical id embeds the teammate name"
        },
        {
          "path": "src/subagent/meta.rs",
          "lines": "28-35",
          "snippet": "    // A built-in-LOCATION agent whose meta declares `taskKind:\"in_process_teammate\"` is a\n    // teammate, not a plain Task subagent - the only way to tell them apart (both sit at\n    // `subagents/agent-<id>.jsonl`). Workflow agents never carry this taskKind, so the upgrade\n    // only ever fires from BuiltinTask.\n    let kind = if kind == SubagentKind::BuiltinTask\n        && meta.task_kind.as_deref() == Some(\"in_process_teammate\")\n    {\n        SubagentKind::Teammate"
        }
      ],
      "instrument": "`rg -l '\"taskKind\":\"in_process_teammate\"' ~/.claude/projects/*/*/subagents/*.meta.json | wc -l`, confirming each such meta sits directly under `subagents/` and never under `workflows/`; the same set must be what `csift agents @<session> --shape teammate --format json` lists. Counting rule: one row per meta file carrying the literal.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.3.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5; src/subagent/meta.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'in_process_teammate'   AND   a python3 pass over the 7500 files matching ~/.claude/projects/*/*/subagents/*.meta.json and ~/.claude/projects/*/*/subagents/workflows/*/*.meta.json, tallying json['taskKind'] and the key union",
          "observed": "Binary carries the literal `in_process_teammate` at 2.1.258. Of 7500 meta.json files: taskKind = {'<absent>': 7336, 'in_process_teammate': 164}. All 164 teammate metas sit directly under `subagents/`; a repeat of the same tally restricted to the `subagents/workflows/*/` glob returned 0. Teammate meta key set: agentType, color, description, model, name, permissionMode, planModeRequired, spawnDepth, taskKind, teamName - and 0 of 164 carry `toolUseId`.",
          "rule": "One row per meta.json file; a file counts as a teammate iff its parsed `taskKind` is exactly `in_process_teammate`. Location is decided by which glob matched the file.",
          "note": "Fully confirmed, and the location-ambiguity premise is confirmed too: all 164 teammate metas share the built-in location with the 669 non-teammate built-in metas, so nothing but `taskKind` separates them. The corpus also confirms the companion fact csift relies on downstream - `toolUseId` is absent on 164/164 teammate metas, which is why the id-join cannot reach a teammate's spawn (see SUB-014)."
        }
      ]
    },
    {
      "id": "SUB-013",
      "area": "subagents",
      "behavior": "A teammate's canonical agent id EMBEDS its name rather than being a bare hex: the shape is `a<Name>-<16 hex>`, and the name half may itself contain dashes.",
      "depends": "csift's `@<agent-id>` entry gate is `is_bare_subagent_hex || is_teammate_agent_id` (with an explicit uuid guard so an `a`-led uuid cannot slip in), so every id `agents` prints round-trips as a target; a hex-only gate sent teammate ids down the project-dir branch, where they failed.",
      "code": [
        {
          "path": "src/path/ids.rs",
          "lines": "45-57",
          "snippet": "pub(crate) fn is_teammate_agent_id(s: &str) -> bool {\n    if is_uuid(s) {\n        return false;\n    }\n    let Some((head, tail)) = s.rsplit_once('-') else {\n        return false;\n    };\n    head.len() >= 2\n        && head.starts_with('a')\n        && head.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')\n        && tail.len() >= 12\n        && tail.bytes().all(|b| b.is_ascii_hexdigit())\n}"
        }
      ],
      "instrument": "`ls ~/.claude/projects/*/*/subagents/agent-a*-*.jsonl | sed 's|.*/agent-||;s|.jsonl||'` - every id must match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` and its sibling meta must carry the teammate taskKind; then feed each id printed by `csift agents @<session> --shape teammate --format json | jq -r .agent_id` back as `csift show @<id> --turn -1` and require a fetch. Counting rule: teammate nodes that round-trip as targets over teammate nodes listed.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.3.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 1; SPEC.md section 6.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 regex check `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` over the basenames of every ~/.claude/projects/*/*/subagents/agent-*.meta.json whose taskKind is in_process_teammate   AND   csift agents @<session-uuid> --shape teammate --format json | jq -r 'select(.kind==\"agent\").agent_id'   piped one id at a time into   csift show @<agent-id> --turn -1",
          "observed": "164/164 teammate ids match `^a[A-Za-z0-9-]+-[0-9a-f]{16}$`; hex-tail length is 16 on 164/164; 135/164 have at least one dash inside the NAME half; the name half equals the meta `name` on 164/164. Round-trip on one session's 49 teammate ids: ok=49 fail=0 (every `csift show @<agent-id> --turn -1` exited 0 and fetched records).",
          "rule": "One row per teammate meta file for the shape test; one attempt per agent_id csift printed for the round-trip, counting a non-zero exit or empty fetch as a failure.",
          "note": "Both halves of the claim measured: the shape (`a` + name + `-` + 16 hex, name half dash-tolerant) and the consequence csift's `is_teammate_agent_id` gate exists for (every printed id is a usable `@` target). The dash-in-name case is not an edge case here - it is the majority, 135 of 164."
        }
      ]
    },
    {
      "id": "SUB-014",
      "area": "subagents",
      "behavior": "A teammate is spawned by an `Agent` tool_use whose `input.name` equals the teammate name and whose `input.subagent_type` carries the real agent type (distinct from the meta's `agentType`, which is overloaded with the teammate NAME - measured equal to `name` on 164/164 teammate metas). Because the teammate meta carries no `toolUseId` (0/164), that NAME is the only join back to the spawning record - but the join is not always available: in the measured session 41 of 49 teammates joined a spawn by name (8 teammate names never appear as a spawn `input.name`), and 3 of 52 `Agent` spawns carry neither `name` nor `subagent_type`. The team lead subsequently addresses the teammate through `<teammate-message>` blocks and the SendMessage tool.",
      "depends": "csift keeps a `spawn input.name -> [(trigger_utc, tool_use_id)]` index and disambiguates a recurring name by trigger time, recovering the spawn tool_use id, the true trigger instant, the parent agent and the real subagent type; without the name-join a teammate node has null spawn linkage, no trigger timestamp and floats at depth 0.",
      "code": [
        {
          "path": "src/subagent/spawn.rs",
          "lines": "38-42",
          "snippet": "    /// `spawn input.name → [(trigger_utc, tool_use_id)]` for every spawn tool_use that named\n    /// its agent. The NAME-join fallback for a TEAMMATE, whose meta carries no `toolUseId` (so\n    /// the usual id-join can't reach its spawning `Agent` tool_use). Keyed by the `Agent` tool's\n    /// `name` param (== the teammate's meta `name`). A name may recur across a session, so the\n    /// values are a list disambiguated by trigger time in [`Self::spawn_id_for_name`]."
        }
      ],
      "instrument": "`csift search '\"name\":\"Agent\"' @<session> -t agent.tool.use --raw | jq -c '.message.content[]?|select(.name==\"Agent\")|.input|{name,subagent_type}'` - `input.name` must equal a teammate meta's `name` while `input.subagent_type` differs from that meta's `agentType`; `csift agents @<session> --shape teammate --format json | jq '{agent_id, agent_type, name, team_name, spawn_tool_use_id}'` must show the recovered linkage. Counting rule: one row per `Agent` tool_use block.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.5.0",
        "source": "AGENTS.md section 3.7; SPEC.md section 6.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 pass over ONE main transcript (~/.claude/projects/<encoded>/<session-uuid>.jsonl) collecting every tool_use block with name==\"Agent\" and its input.name / input.subagent_type   AND   a python3 pass over that session's teammate metas for (name, agentType)   AND   csift agents @<session-uuid> --shape teammate --format json",
          "observed": "52 `Agent` tool_use blocks in that transcript; 49 carry input.name, and all 49 of those also carry input.subagent_type (values: claude 48, opus 1; 3 blocks carry neither). The session's 49 teammate metas have agentType == their own `name` on 49/49, while the spawn's subagent_type is `claude`/`opus` - so the two really are different strings. Meta names present as a spawn input.name: 41/49. csift's recovered nodes: 49 teammate rows, spawn_tool_use_id non-null on 41/49, trigger_utc non-null on 49/49, agent_type rendered as `claude` (the spawn's subagent_type) rather than the meta's name-overloaded agentType.",
          "rule": "One row per `Agent` tool_use block for the spawn side; one row per teammate meta file for the meta side; a name-join counts as recovered iff csift's node emits a non-null spawn_tool_use_id.",
          "note": "The mechanism is confirmed; the coverage is the correction. A teammate whose name never appears in a spawn `input.name` gets a null spawn_tool_use_id, so a consumer must not treat spawn linkage as guaranteed. csift still dates all 49 nodes (trigger_utc non-null on 49/49), so the missing join costs the spawn id, not the timeline. Same session shows 116 `<teammate-message>` occurrences and 43 SendMessage tool_use blocks, so the addressing half of the claim is exercised there too."
        }
      ]
    },
    {
      "id": "SUB-015",
      "area": "subagents",
      "behavior": "A PERSISTENT teammate spawn's tool_result is an immediate acknowledgement carrying `toolUseResult.status == \"teammate_spawned\"`; the teammate's actual work returns later as inbound `<teammate-message>` records, never through that tool_result.",
      "depends": "the ack shares the spawn `tool_use_id`, so the spawn lookup would otherwise resolve it as a return - csift labels it `agent.tool.result` ONLY, never `agent.communication.inbox`, and gives it no comm direction, so a count of inbound replies counts real replies rather than launch confirmations.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "62-68",
          "snippet": "    pub(crate) fn is_teammate_spawn_ack(&self) -> bool {\n        self.tur_probe()\n            .as_ref()\n            .and_then(|p| p.status.as_ref())\n            .and_then(serde_json::Value::as_str)\n            == Some(\"teammate_spawned\")\n    }"
        }
      ],
      "instrument": "The ledger's instrument does not work: `csift search 'teammate_spawned' <project dir> --raw` matches RENDERED record text, and `toolUseResult` is never rendered, so the pattern only finds prose that mentions the literal (corpus-wide it returned 86 records, 36 with a dict toolUseResult, and `.toolUseResult.status` was null on all 36). Use a direct pass over the transcript instead: for each line, parse `toolUseResult` and keep those whose `status` is `teammate_spawned`, then join `tool_use_id` back to the spawning tool_use in the same file. csift's labelling half of the check works as written (`csift show @<session-uuid> --line <n> --format json`).",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.3a; src/model/classify.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 pass over all 11 main transcripts that own at least one teammate meta, joining every tool_result whose toolUseResult.status == \"teammate_spawned\" back to the tool_use with the same id in the same file   AND   csift show @<session-uuid> --line <n> --format json | jq -r '.labels[]'  on two such records   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'teammate_spawned'",
          "observed": "156 acks across 11 transcripts; toolUseResult.status == 'teammate_spawned' on 156/156; 156/156 join a tool_use named `Agent` in the same file, 0 join any other tool. Ack toolUseResult key set (156 each): status, prompt, teammate_id, agent_id, model, name, color, tmux_session_name, tmux_window_name, tmux_pane_id, team_name, is_splitpane, plan_mode_required (agent_type on 146). The carriers are type:\"user\"/role:\"user\" records. csift labels on two sampled acks: ['agent.tool.result'] - single label, direction null. Binary carries the literal `teammate_spawned` at 2.1.258.",
          "rule": "One row per tool_result carrier whose toolUseResult.status is exactly `teammate_spawned`; a join counts only when the ack's tool_use_id matches a tool_use id in the SAME transcript.",
          "note": "The behavior is confirmed exactly as stated, including the consequence csift depends on: the ack shares the spawn's tool_use_id (156/156) yet carries only `agent.tool.result`, never `agent.communication.inbox`, and no direction - so counting inbound replies does not count launch confirmations. Only the ledger's suggested instrument needed replacing. Incidental observation worth recording: every ack in this corpus carries tmux_* fields with is_splitpane false, i.e. the pane id names the orchestrator's own pane, not a pane of the teammate's own."
        }
      ]
    },
    {
      "id": "SUB-016",
      "area": "subagents",
      "behavior": "An ASYNC / background `Agent` spawn's tool_result is an immediate LAUNCH ACK rather than the child's work: it carries `toolUseResult` {isAsync:true, status:\"async_launched\", agentId, description, outputFile} and a text body opening `Async agent launched successfully`, and it shares the spawn's `tool_use_id`. At 2.1.258 that opening sentence continues with a harness instruction not to quote the tool result or the agentId into a user-facing reply, so the ack must be matched as a PREFIX, never as a whole-string equality.",
      "depends": "csift keeps the ack labelled `agent.tool.result` only (never `agent.communication.inbox`) and redirects returned-message resolution to the child transcript tail; without that, `agents` reports the launch confirmation as the agent's report.",
      "code": [
        {
          "path": "src/model/classify.rs",
          "lines": "82-88",
          "snippet": "        if let Some(probe) = self.tur_probe() {\n            if probe.status.as_ref().and_then(serde_json::Value::as_str) == Some(\"async_launched\")\n                || probe.is_async.as_ref().and_then(serde_json::Value::as_bool) == Some(true)\n            {\n                return true;\n            }\n        }"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "185",
          "snippet": "pub const ASYNC_LAUNCH_ACK_PREFIX: &str = \"Async agent launched successfully\";"
        }
      ],
      "instrument": "Match the rendered prefix, not the structured field: `csift search 'Async agent launched' --raw --max-count 0` reaches these records (search matches rendered text, and the ack's prefix IS the rendered tool_result body). Searching `async_launched` does NOT work - that string lives only in `toolUseResult`, which is never rendered, so the search returns only prose mentions.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'Async agent launched' --raw --max-count 0  piped to a python3 pass checking the tool_result text prefix and the toolUseResult field set   AND   a python3 pass over every ~/.claude/projects/*/*.jsonl joining each async ack's tool_use_id to a tool_use in the same file   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'Async agent launched'",
          "observed": "906 raw records matched; 297 are tool_results whose text STARTS WITH `Async agent launched successfully`, and on those the toolUseResult carries isAsync 297/297, status 297/297, agentId 297/297, description 297/297, outputFile 297/297, with (isAsync==true AND status=='async_launched' AND agentId present) on 297/297. The other 347 text occurrences are prose mentions, not tool_results. Separate join pass: 272 async_launched acks in main transcripts, and their tool_use_id joins a tool_use named `Agent` on 272/272, 0 other tools. Binary 2.1.258 string (verbatim): `Async agent launched successfully. (This tool result is internal metadata \\u2014 never quote or paste any part of it, including the agentId below, into a user-facing reply.)`",
          "rule": "One row per tool_result block; the prefix test is on the joined text of the tool_result content; the field test requires the key to be present in the parsed toolUseResult object.",
          "note": "Every element of the claim is confirmed at the measured scale, and the field set is perfectly co-occurring (297/297 on all five keys) rather than merely usually present. The refinement is that 2.1.258 extends the ack sentence with an internal-metadata warning after the matched prefix; csift's ASYNC_LAUNCH_ACK_PREFIX is a `starts_with` test (src/model/classify.rs:89-92) so it is unaffected, but a consumer matching the full sentence would now miss."
        }
      ]
    },
    {
      "id": "SUB-017",
      "area": "subagents",
      "behavior": "A `SendMessage` whose `input` (or nested `input.message`) carries a `type` other than `message`/`direct` is a control SIGNAL rather than prose. Claude Code 2.1.258 declares nine typed payload kinds in one registry: plan_approval_request, plan_approval_response, shutdown_request, shutdown_approved, shutdown_rejected, task_assignment, task_completed, teammate_terminated, idle_notification. Which side carries which matters: on the SENT side (SendMessage input.type) the corpus shows only `message` (711), `shutdown_response` (41) and `shutdown_request` (38); `shutdown_approved` and `teammate_terminated` are INBOUND `<teammate-message>` payload types, alongside `idle_notification` and `task_assignment`. An absent type would mean a plain message, but `type` was present on 790/790 SendMessage blocks measured.",
      "depends": "csift routes those to `agent.communication.signal` instead of `agent.communication.sent`, so a count of sent messages counts real messages and a shutdown handshake stays separately countable.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "266-269",
          "snippet": "/// True when a `SendMessage` `input` is a control SIGNAL rather than a prose message (GOLD\n/// §3): the top-level `type` (or a nested `message.type`) is present and is NOT `message`/\n/// `direct` (e.g. `shutdown_request`/`shutdown_response`/…). Absent type ⇒ a plain message.\npub(crate) fn send_message_is_signal(input: Option<&serde_json::Value>) -> bool {"
        }
      ],
      "instrument": "`csift search SendMessage <project dir> --raw | jq -c '.message.content[]?|select(.name==\"SendMessage\")|.input|.type // .message.type' | sort | uniq -c` beside `csift search '' <project dir> --count-by label | grep agent.communication`. Counting rule: one row per SendMessage tool_use block.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.6.0",
        "source": "AGENTS.md section 3.3a"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'SendMessage' -t agent.communication --raw --max-count 0  piped to a python3 tally of input.type / input.message.type   AND   csift search 'teammate-message' --raw --max-count 0  piped to a python3 regex+json parse of each `<teammate-message>` payload's `type`   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'PTr=new Map'  parsed for the map's message-type keys",
          "observed": "SENT side, 790 SendMessage tool_use blocks corpus-wide: message 711, shutdown_response 41, shutdown_request 38; type absent on 0/790. `shutdown_approved` and `teammate_terminated` never appear as a SendMessage input.type. RECEIVED side, 1177 parsed `<teammate-message>` payloads: <prose, no json payload> 708, idle_notification 334, task_assignment 49, shutdown_request 36, teammate_terminated 25, shutdown_approved 25. Binary 2.1.258 declares a typed-payload registry with NINE keys, in map order: plan_approval_request, plan_approval_response, shutdown_request, shutdown_approved, shutdown_rejected, task_assignment, task_completed, teammate_terminated, idle_notification. The literal \"direct\" also occurs 37x in the binary.",
          "rule": "One row per SendMessage tool_use block for the sent side; one row per `<teammate-message>` element for the received side, its type read from the payload's top-level `type` when the body parses as JSON, else counted as prose. Binary keys counted once each from the map literal.",
          "note": "csift's rule (anything not message/direct is a signal) classifies all nine binary-declared kinds correctly, so the mechanism holds. Two corrections: (1) the claim's 'observed set' mixes the sent and received directions - shutdown_approved and teammate_terminated were never sent through SendMessage in this corpus, they arrive as inbound teammate-message payloads; (2) the set is larger than four - the binary declares nine, of which four the ledger never listed (plan_approval_request, plan_approval_response, shutdown_rejected, task_completed) and one observed on disk is NOT in the binary registry (`shutdown_response`, 41 sends, though the literal does appear 11x in the binary). Note also `idle_notification` is by far the commonest inbound signal (334 of 469 typed payloads), so a consumer that only models shutdown traffic will misread most teammate signalling."
        }
      ]
    },
    {
      "id": "SUB-018",
      "area": "subagents",
      "behavior": "A subagent's returned message reaches its parent by one of three carriers, not one: the synchronous tool_result text, the child transcript's tail assistant text when the parent result was the async launch ack, or the workflow journal's `result` event payload; a node that resolves through none of the three carries a null source and no returned message. Measured over 27 sessions / 7678 subagent nodes: workflow-journal 6224, async-child-tail 436, sync-tool-result 396, unresolved 622 - exactly one source per node.",
      "depends": "csift implements all three paths and records which one answered as `returned_message_source`; dropping the async-child-tail fallback loses the report for every background-launched agent, and dropping the journal path loses it for the dominant workflow kind.",
      "code": [
        {
          "path": "src/subagent/spawn.rs",
          "lines": "264-271",
          "snippet": "pub enum ReturnedMsgSource {\n    /// A synchronous built-in: the parent tool_result text IS the returned message.\n    SyncToolResult,\n    /// An async built-in (`Async agent launched …` sentinel): the message is the child\n    /// transcript's tail assistant text.\n    AsyncChildTail,\n    /// A workflow agent: the message is the `journal.jsonl` `result` event payload.\n    WorkflowJournal,"
        }
      ],
      "instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | .returned_message_source' | sort | uniq -c`. Counting rule: one node per subagent transcript, exactly one source per node.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.7",
        "source": "SPEC.md section 10.3; SPEC.md section 6 v0.6.7 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift agents @<session-uuid> --returned-message --format json | jq -r 'select(.kind==\"agent\").returned_message_source' | sort | uniq -c   run first on one 579-node session and then looped over every session dir holding subagent transcripts",
          "observed": "One 579-node session: workflow-journal 402, async-child-tail 91, sync-tool-result 53, null 33 (402+91+53+33 = 579 exactly, so exactly one source per node and no node carries two). Looped over 27 sessions / 7678 agent nodes: workflow-journal 6224 (81.1%), null 622 (8.1%), async-child-tail 436 (5.7%), sync-tool-result 396 (5.2%). Nodes with a null source are exactly the nodes with no returned_message (33/33 on the single session).",
          "rule": "One node per subagent transcript; source read from the node's `returned_message_source`; the four buckets must sum to the node count, which is the check that a node never carries two sources.",
          "note": "All three carriers are exercised, and the structural invariant the claim rests on (exactly one source per node) is verified by the bucket sum on a 579-node session. The claim's specific counts (147 / 6 / 403 / 1) do not reproduce anywhere and cannot: they are a snapshot of one unnamed session at csift 0.6.7, and transcripts grow. Replaced with a corpus-scale distribution carrying its counting rule. The 'unresolved' share is materially larger than the claim implies - 8.1% corpus-wide, not ~0.2% - so a consumer must handle a null source as a normal outcome rather than an anomaly. The 'zero linkage mismatches' assertion has no instrument behind it here: nothing in the output distinguishes a correct join from a wrong one, so it stays an assertion."
        }
      ]
    },
    {
      "id": "SUB-019",
      "area": "subagents",
      "behavior": "The parent's record of a subagent return is not the child's own conclusion, but not because of truncation: Claude Code 2.1.258 APPENDS a continuation footer `agentId: <id> (use SendMessage with to: '<id>', summary: '<5-10 word recap>' to continue this agent)` to the subagent tool_result (present on 813 of 919 tool_results carrying `agentId: `). When the child's last assistant message is a terse sign-off - `Done.`, `Complete.`, `Standing by.` - the parent's return reads as a bare wrapper, but the terse text is the child's own final words, not a harness truncation of a longer answer; the substantive report sits in an earlier message of the same child transcript. The operational rule is unchanged: read the child's own words with `csift show @<agent-id> --turn -1..`.",
      "depends": "csift tags every return with its resolution source (returned_message_source: sync-tool-result, async-child-tail or workflow-journal) and points at the child's own final words (show @<agent-id> --turn -1..); a sync return is the ORCHESTRATOR's record - the child's own terse sign-off plus the harness's appended continuation footer - so reading returned_message as the agent's conclusion misreads a sign-off, not a truncation.",
      "code": [
        {
          "path": "src/subagent/spawn.rs",
          "lines": "254-256",
          "snippet": "/// The synthesized prefix Claude Code writes into a tool_result when a subagent is\n/// launched ASYNCHRONOUSLY (run_in_background) - the real returned message is then NOT in\n/// the parent tool_result but in the child transcript tail."
        },
        {
          "path": "src/cli/agents_args.rs",
          "lines": "123",
          "snippet": "\\\"what did the agent conclude\\\": a `sync-tool-result` source faithfully reports \\"
        },
        {
          "path": "src/subagent/spawn.rs",
          "lines": "260-266",
          "snippet": "/// Where a subagent's returned message was resolved FROM (§3) - surfaced so a consumer\n/// knows whether it read the parent tool_result, the child transcript tail, or the\n/// workflow journal.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ReturnedMsgSource {\n    /// A synchronous built-in: the parent tool_result text IS the returned message.\n    SyncToolResult,"
        },
        {
          "path": "src/cli/agents_args.rs",
          "lines": "122-128",
          "snippet": "        SEMANTICS: it answers \\\"what did the ORCHESTRATOR record as the return\\\", not \\\n        \\\"what did the agent conclude\\\": a `sync-tool-result` source faithfully reports \\\n        the parent's tool_result even when it is a terse sign-off (`Done.`, `Complete.`) \\\n        plus the harness's appended continuation footer (`agentId: <id> (use SendMessage \\\n        with to: '<id>', summary: '<5-10 word recap>' to continue this agent)` - the harness \\\n        appends, it never truncates); the child's own final words are always \\\n        `csift show @<agent-id> --turn -1..`). \\"
        }
      ],
      "instrument": "`csift agents @<session> --returned-message --format json | jq -r 'select(.returned_message_source==\"sync-tool-result\") | .returned_message' | rg '^Done\\. agentId'`, then compare each hit with `csift show @<agent-id> --turn -1`. Counting rule: wrapper-shaped returns over sync returns.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.6.7",
        "source": "SPEC.md section 6 v0.6.7 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "csift search 'Done\\. agentId' --raw --max-count 0  piped to a python3 pass counting tool_results whose text starts with `Done. agentId:`   AND   csift search 'agentId: ' --raw --max-count 0  piped to a python3 pass splitting each tool_result at the harness footer regex `agentId: [0-9a-f]{8,}[^\\n]*(use SendMessage with to:|do not mention to user)`   AND   csift show @<agent-id> --turn -1  on six terse-return children   AND   strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -cN 'Done\\. agentId'",
          "observed": "Tool_results whose text starts with `Done. agentId:`: 0 corpus-wide. Binary occurrences of `Done. agentId`: 0 (the only `Done.` strings in 2.1.258 are prompt instructions telling agents NOT to emit confirmations like \"Done.\"). What the corpus actually holds: 919 tool_results containing `agentId: `, of which 813 carry the harness footer, whose 2.1.258 template is verbatim `agentId: ${e.agentId} (use SendMessage with to: '${e.agentId}', summary: '<5-10 word recap>' to continue this agent)`. Splitting at that footer: 641 have a long head, 172 a head under 40 chars - the short heads are `Async agent launched successfully.` 103, `Complete.` 15, `Done.` 14, `Complete. No further action.` 4, `DONE` 3, `Standing by.` 3, and a long tail of similar sign-offs. Fetching six of those children's own last turn showed the child's final assistant message IS the terse text (e.g. a child whose parent return read `Done.` ends L16 `Done.` L17 `Done.`; another ends `Complete.`), with the substantive report several messages EARLIER in the same tail.",
          "rule": "One row per tool_result block. A record counts as wrapper-shaped only if its joined tool_result text starts with the literal `Done. agentId:`. For the footer split, the head is everything before the first footer-regex match.",
          "note": "The mechanism named in the claim is not observable in Claude Code 2.1.258: there is no truncation-to-wrapper. The harness appends a continuation footer and, separately, splits result content into harness notes / body / tail sections verified by a content hash (the 2.1.258 helper takes a note count, a tail count and a section hash and, on hash mismatch, returns the body untouched rather than trimming it). Zero tool_results in the corpus start with `Done. agentId:` and the string does not exist in the binary. The consequence csift documents is still true and still worth documenting - the orchestrator's record of a return is often uninformative - so the fix is a wording correction at three doc sites (one of which is `--help`, hence a versioned surface change), not a removal."
        }
      ]
    },
    {
      "id": "SUB-020",
      "area": "subagents",
      "behavior": "A teammate is an in-process Agent subagent that holds no background-task id: Claude Code 2.1.258 resolves `TaskStop` against its task registry and answers `No task found with ID: <id>` on a miss, and the on-disk registry under ~/.claude/tasks holds 0 entries of the teammate id shape among 517 entries - so every id form csift can print (the name, the `Name@team` form, the `a<Name>-<hash>` agent id) misses. It also has no separate OS process (spawn acks carry is_splitpane false with the orchestrator's own tmux pane id). The working control path is `SendMessage` addressed by name; 2.1.258 declares `shutdown_request` as a typed payload kind alongside `shutdown_approved`, `shutdown_rejected` and `teammate_terminated`, and the corpus records the full handshake completing.",
      "depends": "csift is read-only, so naming the correct control tool is the whole remedy: `agents` prints a fact-led two-line hint as a text footer whenever a teammate is in scope and the same pointer as each teammate node's JSON `control_hint`. Without it a CORRECT teammate id reads as a wrong id when the wrong tool rejects it - a real session spent about 30 minutes on that mistake.",
      "code": [
        {
          "path": "src/agents/run.rs",
          "lines": "230-234",
          "snippet": "pub(crate) const TEAMMATE_CONTROL_HINT_L1: &str = \"note: teammate rows are in-process Agent subagents — address one BY NAME (the `(@name)` shown) \\\nvia SendMessage to steer it, and `message:{\\\"type\\\":\\\"shutdown_request\\\"}` to terminate it.\";\npub(crate) const TEAMMATE_CONTROL_HINT_L2: &str =\n    \"      A teammate is NOT a background task (TaskStop / a `task_id` will not find it) and has no \\\nseparate OS process (it shares the orchestrator PID — `pkill` won't help).\";"
        }
      ],
      "instrument": "In a live session holding a teammate, call TaskStop with the exact agent id `csift agents` printed and read the rejection text, then send SendMessage `{type:\"shutdown_request\"}` addressed to the teammate name and observe the lane stop; `csift agents @<session> --shape teammate --format json | jq -r .control_hint` must carry the same pointer. Counting rule: one attempt per control tool. The tool contract itself is re-verifiable only against a live SendMessage schema, not from a static corpus.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.3.0",
        "source": "AGENTS.md section 3.7 control hint; SPEC.md section 6.5; SKILL.md Hook 2"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -N 'No task found with ID'   AND   strings -n 6 ... | rg -N 'PTr=new Map' (the SendMessage typed-payload registry)   AND   find ~/.claude/tasks -maxdepth 2 -mindepth 2 | sed 's|.*/||;s|\\.json$||' | rg -c '^a[A-Za-z0-9-]+-[0-9a-f]{16}$'   AND   csift search 'No task found with ID' --max-count 0   AND   a python3 pass over the teammate-owning transcripts pairing SendMessage input.type==shutdown_request with inbound `<teammate-message>` payload types   AND   csift agents @<session-uuid> --shape teammate  (text footer + JSON control_hint)",
          "observed": "Binary 2.1.258 generates the rejection from a task-registry lookup: `function SOn(e,n){return \\`No task found with ID: ${e}${h$e(n.taskRegistry,n.getAppState,xde(n))}\\`}`. On-disk task registry: 517 entries under ~/.claude/tasks, of which 0 match the teammate id shape `^a[A-Za-z0-9-]+-[0-9a-f]{16}$` (ids there are small integers under per-session dirs). Corpus holds the actual recorded rejections as tool_result text - `<tool_use_error>No task found with ID: <teammate name></tool_use_error>`, the same for the `<name>@session-<id>` form, and the same for the exact `a<Name>-<16 hex>` agent id - three id forms, all rejected. Teammate spawn acks: is_splitpane false on 156/156 with a non-empty tmux_pane_id (the orchestrator's own pane). Shutdown path: SendMessage input.type==shutdown_request sent 38 times; 2 of 4 teammate-bearing transcript groups show a shutdown_request sent AND a shutdown_approved / teammate_terminated coming back; the binary declares shutdown_request, shutdown_approved, shutdown_rejected and teammate_terminated as typed payload kinds. csift emits the two-line hint as a text footer and control_hint on 49/49 teammate rows.",
          "rule": "One attempt per control tool per id form for the rejection side; one row per registry entry file for the task-registry check; one row per SendMessage tool_use block and one per `<teammate-message>` payload for the handshake pairing.",
          "note": "The rejection text itself is a historical recorded observation (the three rejected id forms are preserved verbatim in a transcript tool_result from an earlier Claude Code), not a fresh live call at 2.1.258. It is re-confirmed indirectly at 2.1.258 in two independent ways: the rejection is emitted from a taskRegistry lookup in the current binary, and the current on-disk task registry contains no teammate-shaped id at all (0 of 517). What would settle it directly: calling TaskStop with a running teammate's exact agent id in a live 2.1.258 session and reading the error, which needs a live teammate this verification pass did not hold. The SendMessage half is better evidenced than the claim states - both the typed-payload registry in the binary and two transcripts recording a sent shutdown_request answered by shutdown_approved / teammate_terminated."
        }
      ]
    },
    {
      "id": "SUB-021",
      "area": "subagents",
      "behavior": "Claude Code exports `CLAUDE_CODE_SESSION_ID` into the Bash tool environment and its value equals the session's own jsonl basename exactly - but it names the TOP-LEVEL session in EVERY lane: a subagent (an Agent-tool subagent and a workflow agent alike) is handed the PARENT session uuid and its own id is withheld from the Bash env, given only to hooks. Older builds handed a built-in Task subagent its own id.",
      "depends": "csift's `whoami` uses that variable and nothing else - never a process-tree walk, never most-recent-mtime, which is a false-positive trap under concurrent sessions - refuses to guess when it is absent, and reports `is_subagent`/`parent_session_id`/`depth` as NULL with a stderr lane note on the env form rather than fabricating false/0. `@trap:<marker>` exists as the env-independent self-resolution path precisely because a running subagent cannot name itself from the env.",
      "code": [
        {
          "path": "src/whoami.rs",
          "lines": "3-7",
          "snippet": "//! ## Detection (verified empirically inside a live Claude Code Bash tool, 2026-06-07)\n//!\n//! Claude Code exports `CLAUDE_CODE_SESSION_ID` into its Bash tool environment.\n//! It was confirmed to equal exactly the session's own jsonl filename:\n//!"
        },
        {
          "path": "src/whoami.rs",
          "lines": "33",
          "snippet": "const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";"
        },
        {
          "path": "src/whoami.rs",
          "lines": "98-100",
          "snippet": "    // C-16 lane honesty: the env names the TOP-LEVEL session in EVERY lane (current CC\n    // hands a subagent its parent's id), so env-only resolution cannot know whether the\n    // CALLER is that session. Say so on stderr; the unknowable JSON fields are null."
        }
      ],
      "instrument": "In a top-level Bash call, `echo $CLAUDE_CODE_SESSION_ID` then `ls ~/.claude/projects/*/$CLAUDE_CODE_SESSION_ID.jsonl` must match exactly one file; from inside a subagent, the same echo prints the PARENT uuid while `csift whoami @trap:<a fresh three-CamelCase-word plus four-digit marker>` prints the subagent's own ancestry chain, and `csift whoami --format json` reports the lane fields null. Counting rule: one env value per lane, one string comparison of the two ids. Requires a live subagent lane; it cannot be reproduced from a static corpus.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.8; SPEC.md sections 6.3 and 6.3a; CHANGELOG 0.8.2 (lane honesty); src/whoami.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "Run from inside a LIVE workflow-subagent lane: (1) `echo \"LEN=${#CLAUDE_CODE_SESSION_ID}\"; ls ~/.claude/projects/*/\"$CLAUDE_CODE_SESSION_ID\".jsonl | wc -l`; (2) `csift whoami @trap:<fresh three-CamelCase-word + four-digit marker> --format json`; (3) python string comparison of the env value against the trap-resolved own agent id and against the trap-resolved parent_session_id; (4) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'CLAUDE_CODE_SESSION_ID'` plus a raw-byte context dump around each of its 13 occurrences.",
          "observed": "env value length 36, uuid-shaped, matched exactly 1 file under ~/.claude/projects/*/<value>.jsonl. `whoami @trap:` emitted 2 identity rows: the caller's own transcript (a .../subagents/workflows/wf_*/agent-*.jsonl path, is_subagent true, a 17-char bare-hex agent id) and its top-level ancestor. String comparison: env == the resolved parent_session_id -> True; env == the caller's own agent id -> False. Binary (2.1.258), child-env builder: `function OOe(e){let n={CLAUDECODE:\"1\",CLAUDE_CODE_SESSION_ID:e.sessionId,CLAUDE_CODE_CHILD_SESSION:\"1\",CLAUDE_PID:String(process.pid)};if(e.source==\"agent\")n.AI_AGENT=...}` and the shell call sites pass `OOe({sessionId:Q(),effortLevel:z,source:\"agent\"})`. Binary hook-input schema: `agent_id:i().optional().describe(\"Subagent identifier. Present only when the hook fires from within a subagent (e.g., a tool called by an AgentTool worker). Absent for the main thread, even in --agent sessions. Use this field (not agent_type) to distinguish subagent calls from main-thread calls.\")`. Live shell env also carries CLAUDE_CODE_CHILD_SESSION=1 and CLAUDE_PID alongside the session id, exactly as that builder writes them.",
          "rule": "One env value read per lane; one glob count of top-level transcripts whose basename equals it (expect exactly 1); two string equality tests (env vs own id, env vs parent id) against the ids the env-independent @trap path resolved in the same lane.",
          "note": "Every code site is verbatim in the current tree: src/whoami.rs:3-7 (module doc), :33 (`const SESSION_ID_ENV: &str = \"CLAUDE_CODE_SESSION_ID\";`), :98-100 (the C-16 lane-honesty comment, confirmed by `grep -n \"C-16 lane honesty\" src/whoami.rs` -> 98). One clause of the claim is not decidable on this machine: 'older builds handed a built-in Task subagent its own id' is a statement about superseded Claude Code versions - deciding it would need a live Bash call inside a built-in Task subagent under one of those older binaries. Everything about 2.1.258 checked out: the id a subagent is handed is the top-level parent's, and the subagent's own id reaches only hooks (documented `agent_id` field), which is why an env-independent self-resolution path is needed at all."
        }
      ]
    },
    {
      "id": "SUB-022",
      "area": "subagents",
      "behavior": "A SUBAGENT transcript flushes per content block as the block closes, so its launching tool_use record is on disk while that very command runs (verified: a first-use @trap resolves, exit 0). The MAIN conversation instead writes an async flush of the completed assistant message - every block of the message lands at one wall-clock instant - and the tool_use record was measured landing 0.10-2.80 s (n=17, p50 0.57 s) after its own timestamp, not the earlier-stated 1-3.4 s band. It is a race, not a wait: csift's own trap scan finishes in 0.05-0.06 s, well inside the window, so a main-lane first use still normally misses.",
      "depends": "csift's `@trap:<marker>` self-resolution therefore hits on the FIRST try from a subagent lane but normally misses on a top-level first use and resolves on a re-run of the SAME marker (a second attempt inside ONE script runs in that same unlanded window); the no-match error routes `@main` first, then the literal-marker check, then retry-as-lane-confirmation. The same window makes a UserPromptSubmit hook asking for the PREVIOUS prompt exclude hits younger than now minus three seconds and take the newest survivor.",
      "code": [
        {
          "path": "src/path/trap.rs",
          "lines": "12-19",
          "snippet": "/// Resolve `@trap:<marker>` to the CALLING agent/session by finding the transcript whose Bash\n/// `tool_use` command carries the (unique, literal) marker AND the literal `csift` - i.e. the\n/// very command that launched this run. Mechanism + TIMING (subagent verified live 2026-07-12;\n/// main-lane mechanism re-measured 2026-08-29): a SUBAGENT's transcript flushes per content\n/// block as it closes, so its launching tool_use is on disk at dispatch and a first try\n/// resolves. The MAIN conversation writes an async flush of the COMPLETED assistant message\n/// that lands ~1-3.4s AFTER the tool was dispatched - a RACE, not a wait (a 263s command was\n/// observed with its unpaired tool_use already on disk 39s in). csift finishes well inside"
        },
        {
          "path": "src/path/trap.rs",
          "lines": "302-308",
          "snippet": "pub(crate) fn bash_command_carries_trap(path: &Path, marker: &str) -> bool {\n    let Ok(Some(mmap)) = crate::parse::mmap_bytes(path) else {\n        return false;\n    };\n    let bytes: &[u8] = &mmap;\n    if memchr::memmem::find(bytes, marker.as_bytes()).is_none() {\n        return false;"
        }
      ],
      "instrument": "From a main-thread Bash call run `csift whoami @trap:<a fresh three-CamelCase-word plus four-digit marker>` and then re-run the IDENTICAL command as a SECOND, SEPARATE invocation: expect a no-match error followed by a resolution; from inside a subagent the first try must resolve (exit 0). Counting rule: one attempt per separate shell-tool invocation, exit code recorded; a second attempt inside one script does not count. Measure the window as the difference between the tool_use record's `timestamp` and the wall-clock dispatch time.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.4.0",
        "source": "AGENTS.md section 2 TIMING; SPEC.md section 6.3a; CHANGELOG 0.8.2 (@trap timing corrected); src/path/trap.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) Subagent half: `csift whoami @trap:<fresh marker> --format json; echo EXIT=$?` as the FIRST use of that marker, from inside a live workflow-subagent lane. (b) Main-lane half: an inline python poller against the parent's main transcript - stat the file every 0.15 s, read only the newly appended bytes, and for each new record print (wall-clock time the bytes were first seen) minus (the record's own `timestamp` field); ran 240 s. (c) `/usr/bin/time -p csift whoami @trap:<marker>` x3 to size csift's own runtime against that window. (d) `csift agents @<session> --format json` filtered to rows carrying pending_tool_use_id.",
          "observed": "(a) EXIT=0 on the first invocation - the launching Bash tool_use was already on disk while its own command was still running (a later 3-run timing loop with a second fresh marker also resolved every time). (b) 240 s of main-lane appends: assistant records carrying a tool_use block n=17, landing lag min 0.10 s, p50 0.57 s, max 2.80 s; assistant thinking-block records n=18, lag min 0.17 s, p50 3.01 s, max 18.67 s; user tool_result records n=17, min 0.12 / p50 0.51 / max 1.29; the blocks of one assistant message appear at the SAME wall-clock instant, so the earliest block of a message carries the largest lag - the async-flush-of-the-completed-message mechanism. (c) csift trap resolution wall time 0.05, 0.05, 0.06 s. (d) 3 lanes were live with an unreturned tool_use at that instant.",
          "rule": "One @trap attempt per SEPARATE shell-tool invocation, exit code recorded. Flush lag = (wall clock when the appended bytes were first observed, 0.15 s poll resolution) - (that record's own ISO `timestamp`); one measurement per newly appended record, bucketed by record type and block type.",
          "note": "Code sites verbatim: src/path/trap.rs:12-19 (the TIMING doc comment, `grep -n` -> 12 and 19) and src/path/trap.rs:302-308 (`pub(crate) fn bash_command_carries_trap`, at 302). The subagent half is directly instrumented and holds. The main-lane half was measured from the receiving end (record appearance vs the record's own timestamp) rather than from a main-thread dispatch clock, because a subagent cannot issue a main-thread shell call; that measurement puts the floor below the claimed 1 s and the ceiling below the claimed 3.4 s, but leaves the mechanism and the race conclusion intact. The main-lane 'first try misses, a re-run of the same marker resolves' pair itself was not executed here - it needs two separate shell-tool invocations from the top-level lane."
        }
      ]
    },
    {
      "id": "SUB-023",
      "area": "subagents",
      "behavior": "A subagent's own first-record timestamp lags the parent Task/Agent/Workflow tool_use timestamp by a median of 0.20 s (n=772 nodes over 20 sessions), with half the population below 0.2 s, a p90 of 0.73 s, a p99 of 3.35 s and a long tail out to 104.7 s - too coarse, and too heavy-tailed, to order the siblings of a parallel fan-out.",
      "depends": "csift makes the parent tool_use instant the default `--order-by trigger` axis and keeps the child's `started_utc` as a secondary timestamp; ordering by start would scramble a parallel fan-out.",
      "code": [
        {
          "path": "src/subagent/types.rs",
          "lines": "49-50",
          "snippet": "    /// built-in `meta.json` `toolUseId` (on disk for every built-in subagent; `None` for\n    /// workflow agents, whose meta carries only `agentType`). This is the join key into"
        }
      ],
      "instrument": "`csift agents @<session> --format json | jq -r 'select(.kind==\"agent\") | [.trigger_utc, .started_utc] | @tsv'` and difference each pair. Counting rule: one delta per agent node carrying both instants.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "SPEC.md section 6.5"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "For every session root on the machine that has a subagents/ directory (25 found by `ls ~/.claude/projects/*/*/subagents`), run `csift agents @<session> --format json`, keep `kind==\"agent\"` rows that carry a non-null spawn_tool_use_id AND both instants, drop rows where trigger_utc == started_utc verbatim (those are the documented fallback where trigger could not be joined and is copied from the child head), and difference each remaining pair. Separately, chain those rows into fan-out batches (consecutive spawns <= 5.0 s apart, size >= 2) and count sibling pairs whose start order inverts their trigger order.",
          "observed": "20 sessions contributed 772 agent rows with a real parent-tool_use join and distinct instants (plus 32 rows dropped as verbatim-equal fallbacks). Lag seconds: min 0.071, p10 0.165, p50 0.200, p90 0.733, p99 3.352, max 104.685. 384 of 772 rows (50%) are BELOW 0.2 s; only 3 rows exceed 4.7 s. By shape: builtin-task n=627 min 0.071 p50 0.195 max 104.685; teammate n=145 min 0.170 p50 0.227 max 4.002. Fan-out check: 15 batches, 17 sibling pairs, 1 start-order inversion (5.9%).",
          "rule": "One delta per agent node that carries BOTH a real spawn join (spawn_tool_use_id non-null) and two distinct instants; verbatim-equal pairs excluded as fallback artifacts, not as zero-lag measurements. Inversion = a sibling pair (a,b) in one batch with trigger_a < trigger_b but started_a > started_b.",
          "note": "The stated 0.2-4.7 s range turns out to be roughly the p50-to-p99 band, not the range: the measured floor is 0.071 s and the measured ceiling 104.685 s (one builtin-task node; the next largest are 4.87 and 4.71 s). The design consequence is unchanged and is now directly supported rather than inferred - 1 of 17 sibling pairs in a fan-out chain starts out of trigger order, so ordering by start would scramble a fan-out. Code site verbatim: src/subagent/types.rs:49-50 (the spawn_tool_use_id doc comment, `grep -n` -> 49)."
        }
      ]
    },
    {
      "id": "SUB-024",
      "area": "subagents",
      "behavior": "A permission escalation still leaves NO jsonl trace and no control-file trace: a tool_use block carries only {type,id,name,input,caller} (5753/5753 blocks), no permission-field name occurs in any transcript of the sampled project dir, and the session registry records only busy/idle/shell. On disk the blocked lane is an assistant tool_use record standing as the LAST record with no tool_result for its id - but `stop_reason:\"tool_use\"` is only reliably part of that signature in a MAIN transcript (5768/5768 there); in a subagent transcript the per-block flush writes the record before the message closes, so 1578 of 3352 subagent tool_use records carry `stop_reason:null`. csift's frozen verdict keys on the record shape, not on stop_reason, so it is unaffected.",
      "depends": "csift's `lifecycle` reads the frozen verdict from the NEWEST meaningful record (the first non-metadata record from EOF) and forces status Running, because the older tail walk-back found the assistant text PRECEDING the frozen call and reported a blocked lane as completed; it then classifies escalation-blocked only when the ported dangerous-command classifier would hoist that command, and otherwise says awaiting-execution rather than pretending to tell slow from wedged.",
      "code": [
        {
          "path": "src/subagent/lifecycle.rs",
          "lines": "35-40",
          "snippet": "    // TAIL: last record's timestamp == completion (best-effort), whether the transcript\n    // terminates with a visible assistant message (a clean finish), AND whether the lane is\n    // FROZEN at an unreturned tool_use. The frozen verdict comes from the NEWEST meaningful\n    // record only (the first non-metadata record from EOF): if it is an assistant tool_use, no\n    // tool_result followed it (it IS the last record) ⇒ the lane is blocked there, NOT done. The\n    // terminal_agent_msg walk-back is UNCHANGED for every non-frozen lane."
        }
      ],
      "instrument": "Trigger a permission prompt and, while it waits, run `csift agents @<session> --format json | jq 'select(.pending_tool_use_id) | {pending_classification, pending_since_utc, status}'` - the row must be pending and never `completed`; confirm the transcript's final line is a `tool_use` whose id has no later `tool_result`. Counting rule: the last record's shape at that instant, one pending row per frozen lane.",
      "located": {
        "claude_code": "2.1.220",
        "csift": "0.6.x",
        "source": "AGENTS.md section 3.9; src/subagent/lifecycle.rs comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(1) Key census: parse every `tool_use` block in the 4 largest top-level transcripts of one project dir and print the union of block keys with counts. (2) `rg -c 'permissionPromptStartTimeMs|\"isEscalated\"|\"requires_approval\"|\"permission_request\"|\"pendingPermission\"' *.jsonl` across the 23 top-level transcripts of that project dir. (3) Registry: `python -c` over ~/.claude/sessions/*.json printing the observed `status` and `kind` values and the key set of one file. (4) Live frozen-lane specimen: read the last line of a currently-executing lane's transcript, check the block types and whether the tool_use id appears on any later line; and `csift agents @<session> --format json` filtered to rows with pending_tool_use_id. (5) `strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'permission[A-Za-z_]{0,24}'` plus raw-byte context around `hook_event_name`.",
          "observed": "(1) 5753 tool_use blocks; key union EXACTLY {type:5753, id:5753, name:5753, input:5753, caller:5753} - no permission, isEscalated or requires_approval key on any block. (2) zero matching lines across 23 top-level transcripts. (3) 7 live registry files, keys [bridgeSessionId, cwd, entrypoint, kind, messagingSocketPath, name, nameSince, nameSource, peerFeatures, peerProtocol, pid, pidDomain, procStart, sessionId, startedAt, status, statusUpdatedAt, updatedAt, version]; status values observed {busy:2, idle:4, shell:1}, kind {interactive:7} - no permission-pending state. (4) The live lane's last record: type=assistant, blocks=['tool_use'], its tool_use id appears on exactly 1 line of the whole file (no tool_result), block keys ['caller','id','input','name','type']; `csift agents` reported 3 rows with status=running, pending_tool_name=Bash, pending_classification=awaiting-execution, completed_utc=None. (5) The binary carries a first-class hook event: `hook_event_name:x(\"PermissionRequest\"),tool_name:i(),tool_input:de(),permission_suggestions:R(_i()).optional()`, and permission requests travel the in-process messaging inbox (`[InboxPoller] Dropping permission request `, `: agent_id `, ` does not match sender `), never a transcript record.",
          "rule": "One key-union census over every tool_use block in the 4 largest top-level transcripts of one project dir; one grep count of permission-field names over that dir's 23 top-level transcripts (expect 0); the last record's shape at one instant, one pending row per frozen lane.",
          "note": "Code site verbatim: src/subagent/lifecycle.rs:35-40 (the TAIL comment, `grep -n` -> 35). What an instrument here could and could not decide: the absence of a trace and the shared signature were measured, and the in-flight branch of the signature was captured live (3 lanes frozen at an unreturned Bash tool_use, reported running/awaiting-execution and never completed). The escalation-blocked branch itself was NOT produced - triggering a real permission prompt from this lane would block the session on a human, so 'the same signature also covers an escalation-blocked lane' rests on the absence of any distinguishing field rather than on a captured specimen. One thing has moved since the claim was written: 2.1.258 ships a dedicated `PermissionRequest` hook event, so the higher-fidelity live signal the claim describes as needing a Notification or PreToolUse hook now has a purpose-built event to hang a sidecar on."
        }
      ]
    },
    {
      "id": "SUB-025",
      "area": "subagents",
      "behavior": "Background-shell and subagent completion notifications land in a session's MAIN transcript only: across 8 session roots holding 372 subagent transcripts, all 180 classified `<task-notification>` carriers sit in the main file and 0 sit in any subagent transcript.",
      "depends": "csift scopes `wait --until notification` to the main lane on that basis and reads background carriers only from the main file; a subagent-lane scan for shell completions would find nothing to join.",
      "code": [
        {
          "path": "src/live/wait.rs",
          "lines": "157-161",
          "snippet": "                for (raw, cond) in &conds {\n                    if record_matches(cond, &rec, cur.is_main) {\n                        return finish(args, raw, &main, &lens, &activity, start.elapsed());\n                    }\n                }"
        }
      ],
      "instrument": "`rg -c '<task-notification>'` is not a sound counting rule for this claim - in this corpus 661 subagent lines contain the literal tag as prose (relayed instructions and documentation quoting it) while 0 are carriers. Count carriers with `csift search '' -t harness.notification <target> --max-count 0`, or by requiring the record's message text to START with the tag.",
      "located": {
        "claude_code": "2.1.237",
        "csift": "0.9.0",
        "source": "dev session 2026-08-30"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "Per session root in one project dir: `csift search '' -t harness.notification @<session> --max-count 0 --format json`, counting hits by their own is_subagent flag and by label. Cross-check of the claim's own instrument: `rg -c '<task-notification>' <main>.jsonl` beside a recursive count over that session's subagents/*.jsonl, then a python pass over every subagent transcript in the project dir classifying each matching line as carrier (its message text STARTS with the tag) or prose. Corpus-wide caveat probe: `csift search '' -t harness.notification.monitor --max-count 0 --format json`, and `rg -c --glob '*/subagents/**/*.jsonl' '\"name\":\"Monitor\"'` under ~/.claude/projects.",
          "observed": "8 session roots with subagents (372 subagent transcripts between them, largest 255): classified notification carriers main=3/4/108/0/1/0/42/22, subagent=0 in every one - 180 main-lane carriers, 0 subagent-lane carriers. Labels present: harness.notification.background-command 128, .subagent 46, .workflow 2. The raw literal count over the same subagent transcripts was 661 lines, of which 0 are carriers (all prose mentions of the tag). Corpus-wide monitor notifications: 946 records over 4 sessions, main=946 subagent=0; and `\"name\":\"Monitor\"` appears on 0 lines of any subagent transcript on this machine.",
          "rule": "One carrier per record whose message text STARTS with `<task-notification>` (equivalently: one csift hit under `-t harness.notification`), bucketed by the hit's own is_subagent flag. Lines that merely contain the literal string are NOT carriers and must not be counted.",
          "note": "Code site verbatim: src/live/wait.rs:154-158 (the condition loop; note the same 5 lines also appear at 166-170, so a stranger anchoring by text alone will find two hits). Two numeric drifts in the claim's specimen sentence: one specimen still reads exactly 3 carriers over 9 subagent transcripts, but the 42-carrier specimen now has 255 subagent transcripts rather than 54 (the session kept running). The stated caveat - 'a Monitor armed by a subagent notifies into that subagent's own lane' - is untested here rather than confirmed: all 946 monitor notifications on this machine landed in main lanes, and no subagent transcript in the corpus contains a Monitor tool_use at all, so no subagent ever armed one. Deciding that caveat needs a session where a subagent itself arms a Monitor and the resulting pulse is located."
        }
      ]
    },
    {
      "id": "SUB-026",
      "area": "subagents",
      "behavior": "A WORKFLOW-agent transcript (`<uuid>/subagents/workflows/wf_<id>/agent-<hex>.jsonl`) never persists the structured `toolUseResult` echo for a LANDED file tool: the tool_result block carries only the bare model-facing string (`File created successfully at: <path>` / `The file <path> has been updated`), so the `{type, filePath, content, structuredPatch, originalFile}` object a top-level session writes does not exist in that lane at all.",
      "depends": "csift `recover` reconstructs content from that echo, so it carries an input-side fallback rebuilding the mutation from the tool_use INPUT (`Write.content`, `Edit.old_string`/`new_string`, `MultiEdit.edits[]`) gated on the ABSENCE of a carrier (`ids_with_result`); without it every file written by a workflow agent reports `no recoverable history` even though `files` and `search` see the write, because those read the tool_use input directly.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "35-41",
          "snippet": "/// WHY: a subagent (built-in Task/Agent-tool) and a workflow-agent transcript record the\n/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with\n/// NO structured `toolUseResult` echo - unlike a top-level session, whose carrier carries\n/// `{type:create, filePath, content, …}`. `extract_from_tool_use_result` reads that echo,\n/// so without this fallback a file WRITTEN BY A SUBAGENT is invisible to `recover`\n/// (`no recoverable history`) even though `files`/`search` see it (they read the tool_use\n/// input directly). The authoritative content IS in the input - `Write.content`,"
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "400-406",
          "snippet": "        // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n        // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n        // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n        // `\"File created successfully…\"` string), so those ids are absent here and the\n        // input-side fallback below supplies their content (§ subagent recover).\n        let mut ids_with_result: std::collections::HashSet<String> =\n            std::collections::HashSet::new();"
        }
      ],
      "instrument": "python3 walk over ~/.claude/projects (journal.jsonl excluded), lane by path component, one count per tool_result BLOCK bucketed by whether the carrying record's toolUseResult is a dict. Observed 2026-09-02 at Claude Code 2.1.258: workflow lanes 1788 create results / 0 with the echo and 2734 update results / 0 with the echo, and 2594 of 2594 non-null workflow toolUseResult values string-typed (zero dict-valued). The earlier '1746' create count and the '600 most-recently-modified transcripts' second pass are superseded; only the zero is stable.",
      "located": {
        "claude_code": "2.1.177",
        "csift": "0.2.0",
        "source": "src/recover/events.rs:32-46 doc comment; dev session 2026-08-23; proportions measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'  # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY   ;   python3 - <<'PY'  # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY",
          "observed": "Workflow lanes: 1788 tool_result blocks whose text holds 'File created successfully at:', 0 of them on a record with a dict-valued toolUseResult; 2734 blocks whose text starts 'The file ' and holds ' has been updated', again 0 with a dict-valued toolUseResult. Stronger form from the workflow-only pass: across every workflow lane the toolUseResult field is present on exactly 2594 records and is string-typed on 2594 of 2594, so a dict-valued toolUseResult does not occur in that lane at all. Same pass, top level for contrast: 1824 create blocks, 1824 with the dict echo.",
          "rule": "One count per tool_result BLOCK whose rendered text holds the marker (a string content is the text; an array content contributes its joined text blocks). Lane is decided by path components only. journal.jsonl excluded. The corpus is live and append-only, so the absolute counts grow between runs; the load-bearing number is the zero.",
          "note": "Both code sites exist verbatim in the current tree: src/recover/events.rs lines 35-41 and src/recover/scan.rs lines 400-406 match the quoted snippets character for character. The update-side measurement is new here and makes the claim stronger than it was written: the omission is not specific to creates, it covers every landed file tool in the lane."
        }
      ]
    },
    {
      "id": "SUB-027",
      "area": "subagents",
      "behavior": "The 2.1.191 floor holds: across 645 landed file results recorded below 2.1.191 in the built-in lane (49 creates + 596 updates) not one carries the structured toolUseResult echo. The 'onward' half is not absolute: 57 landed file results at 2.1.191 or later carry no echo (1 create at 2.1.199, 3 creates and 54 updates at 2.1.231), and they are interleaved with echo-bearing results inside the same four transcripts - so echo presence is a per-operation property, not a per-lane or per-version one.",
      "depends": "csift `recover` gates its input-side fallback per tool_use ID rather than per lane, so a built-in subagent transcript reconstructs from the carrier exactly like a top-level session and never double-emits; a lane-level gate would double-count every built-in subagent write.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "400-404",
          "snippet": "        // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n        // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n        // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n        // `\"File created successfully…\"` string), so those ids are absent here and the\n        // input-side fallback below supplies their content (§ subagent recover)."
        }
      ],
      "instrument": "The phrase 'all at version >= 2.1.191 bar 4 strays' is inverted. No echo-bearing block sits below 2.1.191 at all; the 4 create-side strays are echo-LESS blocks at 2.1.191 or later (1 at 2.1.199, 3 at 2.1.231).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.2.0",
        "source": "measured 2026-09-02; dev session 2026-08-23"
      },
      "first_seen_claude_code": "2.1.191",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'  # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY  (restricted to '<uuid>/subagents/<name>.jsonl', i.e. no 'workflows' component, and bucketed additionally by the record's 'version' field and by the sibling '<stem>.meta.json' taskKind)",
          "observed": "Built-in-location create results: 853 blocks, 800 with the dict echo and 53 without - both totals reproduce the claim exactly. Version split of the 800 echo-bearing blocks: 2.1.191 24, 2.1.196 37, 2.1.199 32, 2.1.208 18, 2.1.210 18, 2.1.211 4, 2.1.217 247, 2.1.219 126, 2.1.231 235, 2.1.233 7, 2.1.251 50, 2.1.252 1, 2.1.257 1 - every one at 2.1.191 or later, none below. The 53 echo-less: 2.1.156 2, 2.1.159 3, 2.1.177 44, 2.1.199 1, 2.1.231 3. Extension to update results in the same lane (teammate transcripts excluded): below 2.1.191 596 blocks, 596 echo-less (2.1.159 14, 2.1.177 582); at 2.1.191 or later 638 with the echo but 54 without, all at 2.1.231 and confined to 4 built-in transcripts that also hold echo-bearing results - in one of them the 12 echo-bearing update results run 13:12 to 14:11 and the 10 echo-less ones run 14:47 to 14:51, same transcript, same record version.",
          "rule": "One count per tool_result BLOCK whose text holds the create or the update marker, bucketed by the carrying record's own 'version' string and by whether its toolUseResult is a dict. Built-in lane = a 'subagents' path component with no 'workflows' component; teammate transcripts (sibling meta taskKind in_process_teammate) excluded from the update extension so the two claims stay separable.",
          "note": "src/recover/scan.rs lines 400-404 match the quoted snippet verbatim. The interleaving is direct evidence for the claim's own 'depends': a lane-level gate would be wrong in both directions here, while the per-tool_use-id gate csift actually uses handles a transcript that mixes both shapes."
        }
      ]
    },
    {
      "id": "SUB-028",
      "area": "subagents",
      "behavior": "A TEAMMATE lane (a built-in-location transcript whose `*.meta.json` carries `taskKind:\"in_process_teammate\"`) always carries the structured `toolUseResult` echo on a file write - 491 of 491 create results in the corpus.",
      "depends": "csift attributes teammate file mutations through the same carrier path as a top-level session, so `agents --with-files` and `files` report a teammate's creates with an accurate `is_create` without ever reaching the input-side fallback.",
      "code": [
        {
          "path": "src/model/mutation.rs",
          "lines": "186-192",
          "snippet": "    pub fn carrier_create_paths(&self) -> Vec<(String, String, bool)> {\n        let Some(probe) = self.tur_probe() else {\n            return Vec::new();\n        };\n        let Some(file_path) = probe.file_path.as_ref().and_then(serde_json::Value::as_str) else {\n            return Vec::new();\n        };"
        }
      ],
      "instrument": "python3 over `~/.claude/projects/**/subagents/*.jsonl`: read the sibling `<stem>.meta.json`, take its `taskKind`, and bucket each `File created successfully at:` tool_result by (taskKind, echo-present). Counting rule: one count per matching tool_result BLOCK. Observed `('in_process_teammate', True)` 491 and `('in_process_teammate', False)` 0.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "python3 - <<'PY'  # os.walk ~/.claude/projects, skip journal.jsonl; lane from path components (has 'workflows' => workflow, has 'subagents' only => built-in, neither => top level); parse a line only when it holds the marker bytes; for every tool_result BLOCK whose rendered text holds the marker, tally (lane, isinstance(rec['toolUseResult'], dict)) PY  (built-in-location transcripts only; taskKind read from the sibling '<stem>.meta.json'; bucket every create and every update tool_result by (taskKind, dict-valued toolUseResult))",
          "observed": "Teammate lanes (sibling meta taskKind == in_process_teammate): 491 'File created successfully at:' blocks, 491 with a dict-valued toolUseResult, 0 without - the claim's 491/491 reproduces exactly. Extension not in the claim: 1357 update results in the same lanes, 1357 with the echo, 0 without. 1848 landed file results, 0 counterexamples.",
          "rule": "One count per tool_result BLOCK whose text holds the create or update marker; taskKind is read once per transcript from the sibling meta file; a transcript with no meta or no taskKind is bucketed separately and is not counted here.",
          "note": "src/model/mutation.rs lines 186-192 match the quoted snippet verbatim. The teammate lane is the only subagent-located lane in the corpus with a perfect echo record; the general-purpose built-in lane has 57 exceptions (see SUB-027) and the workflow lane has none at all (SUB-026)."
        }
      ]
    },
    {
      "id": "SUB-029",
      "area": "subagents",
      "behavior": "A file READ performed inside a subagent lane is usually persisted as a bare rendered `tool_result` string with NO `toolUseResult` field on the record at all, whereas a top-level session always writes the structured echo: pairing every `Read` tool_use with its later same-id tool_result across the corpus gives 6071 top-level pairs carrying a `file` echo, 84 other and 0 missing, against 3129 subagent pairs carrying a `file` echo, 626 other and 23183 missing.",
      "depends": "csift `recover` reconstructs content only from the structured echo plus an input-side fallback covering Write/Edit/MultiEdit, so a Read inside a subagent lane contributes no content anchor and `recover --file` reports far less coverage than the transcript actually holds.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "35-41",
          "snippet": "/// WHY: a subagent (built-in Task/Agent-tool) and a workflow-agent transcript record the\n/// tool RESULT as a bare `tool_result` string (`\"File created successfully at: …\"`) with\n/// NO structured `toolUseResult` echo - unlike a top-level session, whose carrier carries\n/// `{type:create, filePath, content, …}`. `extract_from_tool_use_result` reads that echo,\n/// so without this fallback a file WRITTEN BY A SUBAGENT is invisible to `recover`\n/// (`no recoverable history`) even though `files`/`search` see it (they read the tool_use\n/// input directly). The authoritative content IS in the input - `Write.content`,"
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "400-406",
          "snippet": "        // tool_use_ids whose result carrier carries the structured `toolUseResult` echo -\n        // i.e. the ops `extract_from_tool_use_result` can reconstruct from. SUBAGENT and\n        // workflow-agent transcripts OMIT `toolUseResult` (the tool_result is just a\n        // `\"File created successfully…\"` string), so those ids are absent here and the\n        // input-side fallback below supplies their content (§ subagent recover).\n        let mut ids_with_result: std::collections::HashSet<String> =\n            std::collections::HashSet::new();"
        }
      ],
      "instrument": "Re-measured 2026-09-02 at Claude Code 2.1.258: top level 6037 with a file echo, 82 other, 0 missing; subagent 3129 with a file echo, 626 other, 23230 missing. The subagent 'file' and 'other' buckets reproduce the claim's 3129 and 626 exactly, so the pairing rule is the same one; 'missing' grew 23183 -> 23230 with new activity. The top-level buckets came in LOWER than the claim's 6071 / 84, so top-level counts are not monotone - a top-level transcript can leave the corpus - and only the 0-missing result is stable.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": "2.1.156",
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'  # single forward pass per jsonl under ~/.claude/projects: collect ids of tool_use blocks named Read, then for the later tool_result block carrying that id classify the CARRYING record as 'file' when toolUseResult.file is an object, 'other' when toolUseResult is present in another shape, 'missing' when the field is absent or null; bucket by whether the path has a 'subagents' component; also tally the carrying record's version PY",
          "observed": "Top level 6037 'file' / 82 'other' / 0 'missing'. Subagent 3129 'file' / 626 'other' / 23230 'missing'. Restricted to records stamped version 2.1.258 (the current release): top level 103 'file' and 0 'missing'; subagent 18 'file' and 248 'missing'. The subagent 'missing' bucket spans record versions 2.1.156 through 2.1.258, so it is current behavior.",
          "rule": "One observation per (Read tool_use, first later tool_result with the same id) pair inside one file; an unreturned Read contributes nothing; scope is the path test alone.",
          "note": "The 82 top-level 'other' pairs are Reads whose echo is not a file object (an image read, for one). The claim's 'always writes the structured echo' survives in the strong form that matters: 0 top-level Read pairs of 6119 have no toolUseResult at all, at every version from 2.1.150 to 2.1.258."
        }
      ]
    },
    {
      "id": "SUB-030",
      "area": "subagents",
      "behavior": "A workflow-agent transcript writes a `toolUseResult` field ONLY for a FAILED tool call, and always as a plain JSON STRING (the error text), never the structured object: across every workflow lane in the corpus `toolUseResult` is present on exactly 2592 records, all string-typed, and every one of those records also carries a `tool_result` block with `is_error:true` - a 1:1 correspondence.",
      "depends": "csift derives `ids_with_result` from `rec.tool_use_result.is_some()`, which is true for those string-valued error carriers too, so the input-side fallback would skip them; they are also in `failed_ids`, so the two gates agree and a failed workflow-lane Edit is never replayed as a phantom mutation.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "417-424",
          "snippet": "            let has_structured_result = rec.tool_use_result.is_some();\n            if let Some(blocks) = rec.blocks() {\n                for b in blocks {\n                    if let Block::ToolResult {\n                        tool_use_id: Some(id),\n                        is_error,\n                        ..\n                    } = b"
        }
      ],
      "instrument": "Re-measured 2026-09-02: 2594 records, 2594 string-typed, 2594 with an errored tool_result block, and the converse 2594 of 2594 - the claim's 2592 was the same measurement a few hours earlier on a live corpus.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'  # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY",
          "observed": "Workflow lanes: 2594 records carry a non-null toolUseResult; the JSON type is string on 2594 of 2594 and object on 0; all 2594 also carry a tool_result block with is_error true. Converse direction, which the claim asserts but did not measure: of the workflow records carrying at least one errored tool_result block, 2594 of 2594 carry a toolUseResult - so the correspondence is 1:1 in both directions.",
          "rule": "One count per RECORD. A record qualifies for the first tally when 'toolUseResult' is present and not null; for the second when any of its message.content blocks has type tool_result and is_error true.",
          "note": "src/recover/scan.rs lines 417-424 match the quoted snippet verbatim. Because the string-valued error carrier still makes rec.tool_use_result.is_some() true, the id lands in ids_with_result AND in failed_ids, so the two gates agree exactly as the claim's 'depends' says."
        }
      ]
    },
    {
      "id": "SUB-031",
      "area": "subagents",
      "behavior": "The first half holds: 116528 of 119112 workflow tool_result blocks carry a bare string (97.8%). The second half is backwards. The 2584 array-valued blocks are not the errored calls - none of them is errored. The array form marks a non-text payload (2447 deferred tool-schema fetches carrying 'tool_reference' blocks, 133 image reads carrying 'image' blocks, 4 text), while an ERROR always arrives as a string: 2594 of 2594 errored workflow tool_results are string-valued.",
      "depends": "csift normalises both shapes through `model::tool_result_content_text` before matching, so the integrity-error classifier and `search`'s tool-result rendering behave identically in either lane; a surface that assumed the array form would see no text at all in workflow transcripts.",
      "code": [
        {
          "path": "src/recover/carriers.rs",
          "lines": "284-285",
          "snippet": "pub(crate) fn classify_integrity_error(content: &serde_json::Value) -> Option<IntegrityKind> {\n    let text = crate::model::tool_result_content_text(content);"
        }
      ],
      "instrument": "Add the is_error cross-tab, otherwise the two totals (2584 arrays vs 2594 errors) invite the coincidence reading that this measurement refutes.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "python3 - <<'PY'  # os.walk ~/.claude/projects restricted to paths containing a 'workflows' component, skip journal.jsonl; for every record holding b'\"tool_result\"' tally (block['is_error'] is True, json type of block['content']) per BLOCK and (record has an errored tool_result, record['toolUseResult'] is not None) per RECORD PY   ;   plus a second workflow pass joining each tool_result back to its tool_use id to name the originating tool for the array-valued blocks",
          "observed": "Workflow tool_result content types: 116477 string / 2584 array on the first pass and 116528 string / 2584 array eight minutes later (the corpus is live; the array count did not move). Cross-tab with is_error: (is_error false, array) 2584, (is_error false, string) 113934, (is_error true, string) 2594, (is_error true, array) 0. Every errored workflow tool_result carries a STRING content and every array-valued content is non-errored. What the arrays actually are, by the name of the tool_use they join back to: 2447 carrying only 'tool_reference' blocks (deferred tool-schema fetch results), 133 carrying only 'image' blocks (image reads), 4 carrying 'text' (MCP tool results). 2447 + 133 + 4 = 2584.",
          "rule": "One count per tool_result BLOCK in a workflow-lane transcript; content type is the JSON type of block['content']; the originating tool name comes from the tool_use with the same id earlier in the same file.",
          "note": "src/recover/carriers.rs lines 284-285 match the quoted snippet verbatim, and the normalisation argument the claim rests on is unaffected - a surface that assumed the array form would miss 97.8% of workflow tool results, and a surface that assumed the string form would miss every image and tool-schema payload."
        }
      ]
    },
    {
      "id": "SUB-032",
      "area": "subagents",
      "behavior": "Claude Code REJECTS a `Write` issued from inside ANY agent lane when the basename matches the report-file pattern - the guard is an `agentId` present together with `/^(REPORT|SUMMARY|FINDINGS|ANALYSIS).*\\.md$/i` - with errorCode 5 and the message `Subagents should return findings as text, not write report files. Include this content in your final response instead.`, delivered as a `<tool_use_error>`-wrapped tool_result with `is_error:true`.",
      "depends": "csift's `failed_ids` gate is what keeps that rejected Write out of both surfaces: without it the input-side fallback would replay the report's full content as a created file and `files` would report a write that `recover` correctly finds no history for - a forensic false positive on 'did this agent write X?'.",
      "code": [
        {
          "path": "src/recover/events.rs",
          "lines": "67-76",
          "snippet": "        // Skip when this op already has a `toolUseResult` carrier to reconstruct from, OR\n        // when its result was an ERROR (a failed Edit/Write never mutated the file, so its\n        // input is a phantom - `is_error:true` covers both \"String to replace not found\"\n        // and the Edit-before-Read \"File has not been read yet\" wall, incl. the\n        // Bash-created-then-directly-Edited and the must-re-Read-a-plan cases).\n        if let Some(id) = id {\n            if ids_with_result.contains(id) || failed_ids.contains(id) {\n                continue;\n            }\n        }"
        }
      ],
      "instrument": "The bare phrase search is no longer a clean counting rule: 12 tool_result blocks corpus-wide hold the phrase today because the message has since been quoted in prose and in binary dumps. Anchor on the '<tool_use_error>' prefix plus is_error true, which still gives exactly 4 - all workflow lanes, all at record version 2.1.231.",
      "located": {
        "claude_code": "2.1.231",
        "csift": "0.2.0",
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'Subagents should return findings as text' -t agent.tool.result --format json   ;   python3 - <<'PY'  # walk ~/.claude/projects, print lane, record version and block is_error for every tool_result whose text holds the phrase PY   ;   python3 -c \"b=open('~/.local/share/claude/versions/2.1.258'.replace('~',__import__('os').path.expanduser('~')),'rb').read(); print(b.count(b'tengu_subagent_md_report_blocked'), b.count(b'REPORT|SUMMARY|FINDINGS|ANALYSIS'))\"",
          "observed": "python pass: 12 tool_result blocks corpus-wide now hold the phrase, of which exactly 4 are rejections (text begins '<tool_use_error>Subagents should return findings as text'), all with is_error true, all in workflow lanes, all on records stamped 2.1.231; the other 8 are later transcripts quoting the message as prose or dumping it out of a binary. csift agrees independently: matched 9, and exactly 4 hits render the '<tool_use_error>' head with is_error true. Binary 2.1.258 holds the guard once, inside a Write tool validateInput: 'if(n.agentId&&/^(REPORT|SUMMARY|FINDINGS|ANALYSIS).*\\.md$/i.test(...))return s(\"tengu_subagent_md_report_blocked\",{contentBytes:Buffer.byteLength(o)}),{result:!1,message:\"Subagents should return findings as text, not write report files. Include this content in your final response instead.\",errorCode:5}' - the literals 'tengu_subagent_md_report_blocked' and 'REPORT|SUMMARY|FINDINGS|ANALYSIS' each occur twice in the binary (once in the string table, once in the code).",
          "rule": "One count per tool_result BLOCK. A block counts as a REJECTION only when its text starts with '<tool_use_error>' followed by the message and is_error is true; a block that merely contains the phrase does not.",
          "note": "src/recover/events.rs lines 67-76 match the quoted snippet verbatim. The guard is CURRENT, not a fossil of 2.1.231: the 2.1.258 binary still carries it, gated on n.agentId being set, testing the basename with the case-insensitive regex, and returning errorCode 5. The corpus has no rejection after 2.1.231 simply because no agent has attempted such a filename since."
        }
      ]
    },
    {
      "id": "SUB-033",
      "area": "subagents",
      "behavior": "Claude Code itself reconstructs raw file content from the RENDERED side when it rebuilds read state out of a transcript: it strips every `<system-reminder>...</system-reminder>` block, maps each line through a gutter stripper matching leading digits followed by a tab, colon or U+2192 arrow, rejoins with newlines and then trims the result.",
      "depends": "that is the exact inverse csift would need for the echo-less subagent Reads, and the trailing trim is why such a reconstruction is not byte-exact - a csift implementation must decide whether to copy the trim or diverge; csift's own gutter stripper already implements the per-line half.",
      "code": [
        {
          "path": "src/recover/diff.rs",
          "lines": "193-198",
          "snippet": "        // Find the gutter separator: a tab or the U+2192 arrow after leading digits.\n        let trimmed = line.trim_start();\n        let digits: String = trimmed.chars().take_while(char::is_ascii_digit).collect();\n        if digits.is_empty() {\n            continue;\n        }"
        }
      ],
      "instrument": "Seek the literal `content.replace(/<system-reminder>` in a `strings -a` dump of the 2.1.258 Claude Code binary; the same expression continues with a split on newlines, a map through the gutter-stripping helper, a rejoin and a trim. The helper itself is a one-line function returning the first capture of `/^\\s*\\d+[\\u2192\\t:](.*)$/` or the line unchanged, findable by the same seek. Counting rule: one `strings` output line holds each minified expression; expect exactly one construction site.",
      "located": {
        "claude_code": "2.1.258",
        "csift": null,
        "source": "measured 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'content\\.replace\\(/<system-reminder>.{0,220}'   ;   python3 -c \"import os;b=open(os.path.expanduser('~/.local/share/claude/versions/2.1.258'),'rb').read();n=b'content.replace(/<system-reminder>';print(b.count(n));i=b.find(n);print(b[i-260:i+320]);m=b'function adr(';print(b.count(m));j=b.find(m);print(b[j:j+80])\"",
          "observed": "Exactly one occurrence of 'content.replace(/<system-reminder>' in the 2.1.258 binary, reading in full: I.content.replace(/<system-reminder>[\\s\\S]*?<\\/system-reminder>/g,\"\").split(`\\n`).map(adr).join(`\\n`).trim(). Exactly one occurrence of 'function adr(', reading in full: function adr(e){return e.match(/^\\s*\\d+[\\u2192\\t:](.*)$/)?.[1]??e}. The enclosing gate is I.type===\"tool_result\"&&I.tool_use_id joined to a Read entry, I.is_error!==!0 and typeof I.content===\"string\"; the reconstructed text is then stored per filePath together with an isPartialView flag set when C.toolUseResult?.file?.truncatedByTokenCap===!0.",
          "rule": "Byte count of each literal in the binary; the claim predicts exactly one construction site and exactly one helper, and both counts are 1.",
          "note": "src/recover/diff.rs lines 193-198 match the quoted snippet verbatim, and csift's strip_gutter accepts the tab and the U+2192 arrow but not the colon separator the harness regex also allows - a deliberate narrowing, worth knowing if csift ever implements the full inverse. This site is also the mechanical link to SUB-029: it reads toolUseResult?.file when present and otherwise reconstructs from the rendered string, which is exactly the situation of an echo-less subagent Read."
        }
      ]
    },
    {
      "id": "QT-SYS-001",
      "area": "queue-and-telemetry",
      "behavior": "Claude Code writes `type:\"system\"` records for its own UI that carry a `subtype` outside the four csift models separately (`compact_boundary`, `turn_duration`, `away_summary`, `stop_hook_summary`): observed subtypes are `informational` (a notice with a `level` such as `warning` or `notice` and a string `content`, e.g. the Remote Control disconnect warning written when the signed-in account changed in another session), `agents_killed`, `scheduled_task_fire`, `model_refusal_fallback`, `model_refusal_no_fallback`, `api_error` and `local_command`; every one carries `uuid`, `timestamp` and `parentUuid`, `isMeta:false`, and NO `message{}` field.",
      "depends": "`search -t harness.meta.system` (the v0.10.1 catch-all leaf, gated) classifies any system subtype other than the four modeled ones and renders `[<subtype> <level>] <content>`; `show --line` renders such a line flag-free; if the harness started sending these records to the model (a `message{}` field) or moved a subtype into a differently-typed record, the leaf's visibility law and its candidate needle would be wrong.",
      "code": [
        {
          "path": "src/model/classify_promoted.rs",
          "lines": "24-25",
          "snippet": "\"compact_boundary\" => None,\n                _ => Some(Class::MetaSystem),"
        },
        {
          "path": "src/search/record_text.rs",
          "lines": "245",
          "snippet": "let subtype = rec.subtype.as_deref().unwrap_or(\"system\");"
        },
        {
          "path": "src/search/scan.rs",
          "lines": "377",
          "snippet": "std::sync::LazyLock::new(|| memmem::Finder::new(b\"\\\"subtype\\\"\"));"
        }
      ],
      "instrument": "Corpus: rg --no-filename -o '\"type\":\"system\",\"subtype\":\"[a-z_]+\"' -- <project-dir>/*.jsonl | sort | uniq -c (count every subtype; the four modeled ones aside, every other value must classify under harness.meta.system: csift search '' <target> -t harness.meta.system --count-by label). Binary: strings -n 6 <claude-binary> | rg -c 'subtype:\"informational\"|\"informational\"' (and the same for each other subtype) plus rg -o 'subtype:\"informational\"[^}]{0,120}' to read the writer's field set (content, level). Visibility: none of these records carries a message{} field (jq 'select(.type==\"system\") | has(\"message\")' over the transcript).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "SPEC.md v0.10.1 ledger entry 9; AGENTS.md section 3.3a gated-promoted bullet; measured now"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "rg --no-filename -o '\"type\":\"system\",\"subtype\":\"[a-z_]+\"' -- <csift project dir>/*.jsonl | sort | uniq -c; jq on the live informational record (type, subtype, level, content, isMeta, keys); strings -n 6 <claude 2.1.258 binary> | rg -c '\"<subtype>\"' per subtype and rg -o 'subtype:\"informational\"[^}]{0,120}'; ./target/debug/csift search '' @<session> --no-subagents -t harness.meta.system",
          "observed": "Project-dir census: stop_hook_summary 412, turn_duration 286, away_summary 147, compact_boundary 17, agents_killed 3, scheduled_task_fire 1, model_refusal_fallback 1, informational 1. The live informational record: type system, subtype informational, level warning, isMeta false, string content (the Remote Control disconnect notice), keys content,cwd,entrypoint,gitBranch,isMeta,isSidechain,level,parentUuid,sessionId,slug,subtype,timestamp,type,userType,uuid,version - no message key. Binary literal counts: informational 17, api_error 29, model_refusal_fallback 19, model_refusal_no_fallback 7, agents_killed 4, local_command 11, scheduled_task_fire 9; two informational writer sites read subtype:\"informational\",content:<fn>(e),level:\"notice\". csift search -t harness.meta.system on the live session: 2 hits ([agents_killed] and [informational warning] Remote Control disconnected ...), matched 2 exchanges.",
          "rule": "One count per raw line matching the exact type+subtype pair over the project directory's top-level transcripts (subagent lanes excluded); binary counts are occurrences of the quoted subtype string among strings of length >= 6; the visibility fact is the key set of the one live record.",
          "note": "The four modeled subtypes keep their own leaves; a system line with no subtype stays unlabeled by design. level values seen: warning (live record), notice (binary writer sites)."
        }
      ]
    },
    {
      "id": "TURN-001",
      "area": "turn-boundary",
      "behavior": "A `type:\"user\"` record is not always a human turn: `tool_result` blocks ride on `role:\"user\"` records too, so most user records are tool-result CARRIERS rather than operator prose. Re-measured 2026-09-02 over 64 top-level transcripts: 59,005 of 67,758 `role:\"user\"` records are carriers (87.1%) against 3,289 that pass the genuine shape gate - a 20.6x corpus-wide overcount for a bare `role:\"user\"` filter, rising to 119.6x on the most agent-heavy session. On one 47,602-line transcript the naive filter reports 3,604 where csift reports 77 `user.message` records (46.8x).",
      "depends": "`Record::is_genuine_user` gates on content SHAPE - a bare string, or blocks carrying a `text` block and NO `tool_result` block - and is the discriminator behind the `user.message` label, turn delimiting, `list` previews and `verbatim` reconstruction. A bare `role:\"user\"` filter overcounts human turns about 20x corpus-wide and about 120x on the carrier-heaviest session.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "37-43",
          "snippet": "    pub fn is_genuine_user(&self) -> bool {\n        if !self.is_type(\"user\") {\n            return false;\n        }\n        if self.is_compact_summary.unwrap_or(false) {\n            return false;\n        }"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "68-75",
          "snippet": "        match &msg.content {\n            Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n            Some(Content::Blocks(blocks)) => {\n                let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n                let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n                if !has_text || has_tool_result {\n                    return false;\n                }"
        }
      ],
      "instrument": "`rg -c '\"role\":\"user\"' <transcript>` (the carrier-inflated raw count) versus `csift search '' @<id> -t user.message -c` (the genuine count); counting rule: one per physical line for rg, one per record passing `is_genuine_user` for csift. Split the raw side by shape with `jq -r 'select(.type==\"user\") | (if (.message.content|type)==\"string\" then \"string\" elif ([.message.content[]?.type]|index(\"tool_result\")) then \"tool_result\" else \"text\" end)' <transcript> | sort | uniq -c`.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.1; SKILL.md wrong-assumption table"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) csift search '' --count-by label --no-subagents ; (b) rg -c '\"role\":\"user\"' ~/.claude/projects/<proj>/<id>.jsonl and csift search '' @<id> --no-subagents --count-by label ; (c) python3 over every top-level transcript: for each line with '\"role\"', json.loads, keep message.role=='user', tally (i) all such records, (ii) those whose message.content is a list containing a tool_result block, (iii) those passing the is_genuine_user shape gate re-implemented in python (not isMeta, not isCompactSummary, string content OR blocks with a text block and no tool_result block, minus the interrupt/local-command-stdout/slash-wrapper/task-notification markers and the peer wrappers)",
          "observed": "Corpus (64 top-level transcripts): 67758 role:user records; 59005 of them are tool_result carriers = 87.1%; 3289 pass the genuine shape gate (2642 string-content + 647 text-block). Naive-filter overcount factor = 67758/3289 = 20.60x corpus-wide; the worst single session (>=20 genuine) is 6580 role:user vs 55 genuine = 119.6x, and the largest session is 13265 role:user vs 112 genuine = 118.4x with 91.0% carriers. csift's own top-level census reports 2468 user.message + 848 user.unsent = 3316 records, which brackets the python gate's 3289 (csift additionally splits out superseded drafts and the agents-stopped notice). Single-transcript run of the ledger's own instrument: rg -c '\"role\":\"user\"' = 3604 on a 47602-line transcript, against csift user.message = 77 on the same file, i.e. 46.8x.",
          "rule": "One per JSON record (line). 'carrier' = message.role=='user' AND message.content is an array containing a block with type=='tool_result'. 'genuine' = the is_genuine_user shape gate above. csift's user.message count is one per classified record. Top-level transcripts only (subagent files excluded) so the rg and csift sides address the same bytes.",
          "note": "The trap is confirmed and has grown considerably sharper than the recorded figures - the direction, the mechanism and the shape gate are all intact, only the magnitudes were stale. The claim's per-session example pairs (393/1619 and 177/8675) could not be matched to specific transcripts in the present corpus, so they were replaced with freshly measured pairs carrying their counting rule. Code sites src/model/predicates.rs:37-43 and 68-75 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-002",
      "area": "turn-boundary",
      "behavior": "Some human turns carry no `text` block at all - the operator's words ride inside a `tool_result` payload (an AskUserQuestion answer, a tool-use rejection with a typed instruction). Re-measured 2026-09-02 over 64 top-level transcripts: 126 of 2,594 human turn openers (4.9%) extract as ZERO characters when only `text` blocks are read. A reader that walks only `content[].type==\"text\"` blocks additionally loses the 2,642 of 3,289 genuine openers (80%) whose content is a bare string rather than a block array.",
      "depends": "csift recovers those turns through `user.answer` / `user.rejection` (each dual-labeled with `agent.tool.result` and deduped to the richer user view) and renders the opener body through `Record::reconstructed_user_text`, so a text-block-only reader is the thing that loses the words, not csift.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "235-250",
          "snippet": "    pub fn reconstructed_user_text(&self, plan_index: Option<&PlanIndex>) -> Option<String> {\n        if let Some(text) = self.genuine_user_text() {\n            return Some(text);\n        }\n        if let Some(unit) = self.auq_exchange() {\n            return Some(normalize_line(&unit));\n        }\n        if let Some((rejected_id, msg)) = self.plan_rejection_message() {\n            let mut out = normalize_line(&msg);\n            if let (Some(idx), Some(id)) = (plan_index, rejected_id.as_deref()) {\n                if let Some(path) = idx.plan_path(id) {\n                    out.push_str(&format!(\" [plan: {path}]\"));\n                }\n            }\n            return Some(out);\n        }"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "68-75",
          "snippet": "        match &msg.content {\n            Some(Content::Text(s)) => !is_synthetic_user_marker(s) && !is_peer_message(s),\n            Some(Content::Blocks(blocks)) => {\n                let has_tool_result = blocks.iter().any(|b| matches!(b, Block::ToolResult { .. }));\n                let has_text = blocks.iter().any(|b| matches!(b, Block::Text { .. }));\n                if !has_text || has_tool_result {\n                    return false;\n                }"
        }
      ],
      "instrument": "`csift search '' @<id> --count-by label` and compare `user.answer` + `user.rejection` against `user.message`; counting rule: one per record, richest-view deduped. Cross-check by extracting only `text` blocks from each turn opener with jq and counting the empty results.",
      "located": {
        "claude_code": null,
        "csift": "0.8.2",
        "source": "SKILL.md wrong-assumption table; AGENTS.md section 3.3"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' --count-by label --no-subagents (whole-scope record census, reading the user.message / user.answer / user.rejection keys), cross-checked against the python genuine-shape census from TURN-001 which additionally splits genuine openers into string-content and text-block-content",
          "observed": "Top-level census: user.message 2468, user.answer 116, user.rejection 10 records. Human turn openers = 2468+116+10 = 2594; the openers whose words ride inside a tool_result payload and so yield ZERO characters to a text-block-only reader = 116+10 = 126, i.e. 4.86%. Separately, of the 3289 genuine openers found by the python shape gate, 2642 (80.3%) carry STRING content rather than a `text` block, so a reader that literally walks only `content[].type==\"text\"` loses those as well.",
          "rule": "One per classified record. 'human turn opener' = the three human opener leaves user.message + user.answer + user.rejection (peer-message and notification openers excluded - they are not the operator). 'zero characters from text blocks' = user.answer + user.rejection, which ride tool_result carriers and carry no text block by construction. Top-level transcripts only.",
          "note": "The mechanism holds - the words really do hide in tool_result payloads and csift really does recover them through user.answer / user.rejection - but the recorded 197/1191 = 16.5% is stale by a factor of three; the present corpus reads 126/2594 = 4.9%. The share is corpus-dependent (it tracks how much a corpus uses AskUserQuestion), so the counting rule matters more than the percentage. Worth recording alongside it: the string-content share is the larger hazard for a blocks-only reader, and the claim did not mention it. Code sites src/model/exchange.rs:235-250 and src/model/predicates.rs:68-75 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-003",
      "area": "turn-boundary",
      "behavior": "A turn opens on any of FOUR record shapes, not just genuine human prose: a genuine user record, an ANSWERED AskUserQuestion whose answer rides a non-errored `tool_result` carrier, a tool-use rejection carrying a typed instruction, and an inbound peer message.",
      "depends": "`Record::opens_turn` is the single delimiter every csift surface keys on (`search` exchange reconstruction, `show --turn`, `stats` turns, `files` attribution, `verbatim`); keying on `is_genuine_user` alone drops three of the four shapes and renumbers every `tN` header, `show --turn` address and `--turn` window in the crate.",
      "code": [
        {
          "path": "src/model/exchange.rs",
          "lines": "201-205",
          "snippet": "    /// The single boundary predicate (§6.4): this record opens a new turn iff it is a\n    /// genuine human message, an ANSWERED AskUserQuestion (the answer is the user's\n    /// message), a tool-use rejection carrying a typed user instruction, OR an inbound\n    /// teammate/peer message. Every surface (turns / search / recover / files) keys turn\n    /// delimiting on THIS predicate so they never drift."
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "214-219",
          "snippet": "    pub fn opens_turn(&self) -> bool {\n        self.is_genuine_user()\n            || self.is_auq_answer_boundary()\n            || self.is_plan_rejection_boundary()\n            || self.is_peer_message_record()\n    }"
        }
      ],
      "instrument": "`csift show @<id> --turn -3..` on a session that used AskUserQuestion or a plan rejection - the fetched turns must start at those record shapes - then `csift search '' @<id> -t user.answer -c` and `-t user.rejection -c` for the non-genuine openers. Counting rule: one turn per opening record, superseded drafts excluded from numbering.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.2.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.1; SKILL.md Labels section"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' -t <leaf> --no-subagents --max-count 0 --format json | jq -rc 'select(.kind==\"exchange\")|[.session_id,.turn_index]|@tsv' to locate a turn opened by each non-genuine shape, then csift show @<id> --turn <N> to read the turn back and inspect which record is first",
          "observed": "All four opener shapes were found opening real turns. (1) genuine: 2468 user.message records top-level. (2) answered AskUserQuestion: turn 35 of one transcript renders its FIRST record as `t35 ... 0x25C2 user.answer L4102 [AskUserQuestion . 2 questions] Q1 ... A1: ...` - the answer carrier is the opener. (3) rejection with a typed instruction: turn 21 of another transcript renders its FIRST record as `t21 ... 0x25C2 user.rejection L1686 The user wants to clarify these questions. ...`. (4) inbound peer message: turn 33 of a third transcript renders its FIRST record as `t33 ... 0x25B8 agent.communication.inbox <name> => self L6568 <teammate-message teammate_id=...>`. Corpus-wide top-level counts for the three non-genuine shapes: user.answer 116, user.rejection 10, agent.communication.inbox 749 records.",
          "rule": "One turn per opening record. A shape counts as confirmed only when `csift show --turn N` prints that record as the FIRST line of turn N, which is exactly what `opens_turn` decides; superseded drafts are outside turn numbering and were not counted.",
          "note": "Confirmed by direct observation of all four shapes in live 2.1.258-era data, not by reading the predicate. The rejection specimen is worth recording: on an AskUserQuestion the harness synthesizes a clarify-request body ('The user wants to clarify these questions...') and delivers it behind the same typed-instruction delimiter, so the rejection arm fires for AskUserQuestion clarifications as well as plan kick-backs. Code sites src/model/exchange.rs:201-205 and 214-219 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-004",
      "area": "turn-boundary",
      "behavior": "`isMeta:true` on a `type:\"user\"` record is an AUTHORSHIP flag: the string content LOOKS human but is harness-injected. Re-measured 2026-09-02 over 64 top-level transcripts: 1,409 such records, led by `[Image: source: ...]` (628), `<local-command-caveat>...` (161), a skill base-directory preamble (41), `Stop hook feedback: ...` (30) and `Continue from where you left off.` (6). Claude Code 2.1.258 ships the continuation marker in three lengths - the bare `Continue from where you left off.` plus a process-restart variant and a runner-moved variant that both continue past the full stop - so the marker is a PREFIX, not always the whole content.",
      "depends": "csift gates `isMeta` out of `is_genuine_user` before any content inspection, so a meta pseudo-turn never opens a turn and never enters the `user.message` census - verified directly: zero of the corpus's `user.message` records carry `isMeta:true`. The recognized markers reparent to `harness.schedule.*` / `harness.meta.*`; the continuation test is `trim_start().starts_with(...)`, which is what makes it survive the two longer 2.1.258 variants. An `isMeta` record matching no known marker classifies EMPTY - such a record is not merely unlabeled but invisible to a scan (40 of 41 skill-preamble records return no hit at all), so a novel hook wrapper can never be mistaken for the operator.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "70-75",
          "snippet": "    /// System-injected pseudo-turn marker (§4.2). `true` ⇒ a `type:\"user\"` record\n    /// whose string/text content LOOKS human (\"Continue from where you left off.\",\n    /// loop ticks, stop-hook feedback, `<local-command-caveat>…`) but is machine-\n    /// generated - must be excluded from genuine-user and from turn-delimiting.\n    #[serde(default, rename = \"isMeta\")]\n    pub is_meta: Option<bool>,"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "44-47",
          "snippet": "        // §4.2 TRAP: isMeta:true user records look human but are system-injected.\n        if self.is_meta.unwrap_or(false) {\n            return false;\n        }"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "187-190",
          "snippet": "/// The fixed harness-injected continuation marker (GOLD §5) - `harness.schedule.continuation`.\n/// A `type:\"user\"` (`isMeta`) record CC injects to resume a session from where it left off.\n/// Verified across real `~/.claude/projects` data (522 occurrences), exact content.\npub const SCHEDULE_CONTINUATION_MARKER: &str = \"Continue from where you left off.\";"
        }
      ],
      "instrument": "`rg -c '\"isMeta\":true' <transcript>` against `csift search '' @<id> --count-by label` (no `user.message` key may cover them), and `rg -c 'Continue from where you left off\\.' ~/.claude/projects/*/*.jsonl` against `csift search 'Continue from where you left off' -t harness.schedule.continuation -c`. Counting rule: one per raw line for rg, one per classified record for csift.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.2; src/model/record.rs isMeta doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) csift search '' -t user.message --no-subagents --raw | rg -c '\"isMeta\":true' ; (b) python3 over every top-level transcript tallying type=='user' records with isMeta true, bucketed by the first 46 characters of their content; (c) csift search '<content head>' --count-by label --no-subagents for each named example; (d) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Continue from where you left off\\.[^\"\\\\`]{0,140}' | sort -u",
          "observed": "(a) ZERO: not one user.message record in the whole top-level corpus carries isMeta:true. (b) 1409 isMeta:true user records exist top-level; the largest content heads are '[Image: source: ...]' 628, '<local-command-caveat>Caveat: The messages bel...' 161, a skill base-directory preamble 41, and assorted injected monitoring prompts. (c) 'Continue from where you left off.' -> 6 records, all isMeta, all classified harness.schedule.continuation, none user.message; 'Stop hook feedback' -> 30 isMeta records; '# Autonomous loop tick' -> 0 records in this corpus; the 41 skill-preamble isMeta records return only 1 matched record from search, i.e. 40 of them classify to no label at all and are invisible to a scan. (d) 2.1.258 ships THREE continuation strings, not one: 'Continue from where you left off.', 'Continue from where you left off. Note: this session was automatically restarted after its process exited unexpectedly; the user has not sent a new message since the restart', and 'Continue from where you left off. This session moved to a new runner; files you created earlier may no longer exist, so verify the working directory state before relying on'.",
          "rule": "One per JSON record for the disk tallies; one per classified record for the csift censuses; one per distinct string for the binary. A named example counts as present only when a record's content STARTS with it (a mid-prose mention does not count).",
          "note": "The core assertion is confirmed by the strongest available instrument - a raw sweep of every user.message record in the corpus finds no isMeta among them. Three corrections were needed. The '522 occurrences, exact content' figure is stale twice over: this corpus holds 6, and 2.1.258 emits two longer variants that make 'exact content' wrong as a description (csift's starts_with test already handles them, so no code change follows). '# Autonomous loop tick' does not occur in this corpus and was dropped from the example list. Code sites src/model/record.rs:69-74, src/model/predicates.rs:44-47 and src/model/markers.rs:174-177 all match verbatim."
        }
      ]
    },
    {
      "id": "TURN-005",
      "area": "turn-boundary",
      "behavior": "When the user interrupts, Claude Code writes a `type:\"user\"` text-block record whose content is EXACTLY one of two synthesized strings: `[Request interrupted by user]` or `[Request interrupted by user for tool use]`. Both strings are still present in the 2.1.258 binary. Re-measured 2026-09-02: 264 and 24 occurrences across 64 top-level transcripts (324 and 41 counting subagent transcripts too), all non-`isMeta`, all arriving as a text block, none carrying any extra prose and no third variant anywhere in the corpus.",
      "depends": "csift matches them by exact `==` (never `contains`, so a real message quoting the phrase stays genuine), excludes them from `is_genuine_user` and from turn opening, and classifies them `harness.interrupt.user` / `harness.interrupt.tool`. Because no occurrence carries extra prose, dropping them as boundaries loses zero user content; counting them inflates every turn index downstream of an interrupt.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "81-90",
          "snippet": "/// The two exact-content synthesized strings Claude Code writes when the user\n/// interrupts (§4.2.1). They are a `type:\"user\"` `text`-block record whose content is\n/// EXACTLY one of these - a machine-synthesized interrupt marker, NOT a human turn.\n/// Verified across real `~/.claude/projects` data: 116 + 21 occurrences, all\n/// non-`isMeta`, none carrying any extra prose (dropping them as turn boundaries loses\n/// zero user content).\npub const INTERRUPT_MARKERS: &[&str] = &[\n    \"[Request interrupted by user]\",\n    \"[Request interrupted by user for tool use]\",\n];"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "47-52",
          "snippet": "pub fn is_synthetic_user_marker(content: &str) -> bool {\n    INTERRUPT_MARKERS.contains(&content)\n        || content.starts_with(LOCAL_COMMAND_STDOUT_PREFIX)\n        || is_slash_command_wrapper(content)\n        || is_agents_stopped_notice(content)\n}"
        }
      ],
      "instrument": "`rg -oNI '\\[Request interrupted by user[^]]*\\]' ~/.claude/projects -g '*.jsonl' | sort | uniq -c` - expect exactly the two strings and no third variant - then `csift search 'Request interrupted by user' --count-by label`, where every hit must key under `harness.interrupt.*` and none under `user.message`. Counting rule: one per record whose content EQUALS the marker exactly (a mid-prose quote does not count).",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.2.1; src/model/markers.rs INTERRUPT_MARKERS doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'Request interrupted by user[^\"\\\\]*' | sort | uniq -c ; (b) python3 over every top-level transcript: for each type=='user' record join its string-or-text-block content and, when it starts with '[Request interrupted by user', bucket it by (exact-marker-or-NON-EXACT, isMeta, content shape) ; (c) csift search 'Request interrupted by user' --count-by label",
          "observed": "(a) both strings are live in the 2.1.258 binary: 'Request interrupted by user]' and 'Request interrupted by user for tool use]'. (b) 264 records whose content is EXACTLY '[Request interrupted by user]' and 24 whose content is EXACTLY '[Request interrupted by user for tool use]'; ZERO records in the NON-EXACT bucket, i.e. no third variant and no occurrence carrying extra prose; all 288 are non-isMeta and all arrive as a text block rather than a bare string. (c) corpus-wide including subagent transcripts the label census reads harness.interrupt.user 324 and harness.interrupt.tool 41, with NO user.message key present among the 12 keys the pattern touches.",
          "rule": "One per record whose joined content EQUALS the marker exactly - a mid-prose quote does not count, which is why the raw rg line count over the same dir (388 + 197) is far higher and unusable: this corpus's own dev sessions discuss the markers in prose and code.",
          "note": "Every structural half of this claim survived an attempt to break it: the exact-match set is closed at two, the NON-EXACT bucket is empty, and no interrupt record classifies as the operator. Only the occurrence counts were stale (116/21 -> 264/24 top-level). The gap between the exact-content count and a raw regex count over the same directory (288 vs 585) is itself the argument for the exact-`==` test the code uses. Code sites src/model/markers.rs:76-85 and 42-47 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-006",
      "area": "turn-boundary",
      "behavior": "Local-command OUTPUT arrives as a non-`isMeta` `type:\"user\"` record whose string content STARTS WITH `<local-command-stdout>` (154 occurrences measured 2026-09-02 across 64 top-level transcripts), while its sibling wrapper `<local-command-caveat>` does carry `isMeta` (161 occurrences). The split is total in both directions - no stdout record carries the flag and no caveat record lacks it. Claude Code 2.1.258's tag registry also declares a third sibling, `local-command-stderr`, which produced no records in this corpus.",
      "depends": "The `isMeta` gate alone does not catch the stdout form, so csift excludes it by content prefix and classifies it `harness.command.stdout`; without the explicit prefix test the harness's own command output opens a turn and is injected into a reconstructed turn as operator prose.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "92-96",
          "snippet": "/// Prefix of a `<local-command-stdout>…` user record (§4.2.2) - local-command OUTPUT\n/// (machine), not the user's prose. Non-`isMeta` string content (its sibling\n/// `<local-command-caveat>` carries `isMeta` and is already excluded). Must NOT open a\n/// turn.\npub const LOCAL_COMMAND_STDOUT_PREFIX: &str = \"<local-command-stdout>\";"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "273-276",
          "snippet": "        if s.starts_with(LOCAL_COMMAND_STDOUT_PREFIX) {\n            push_unique(out, Class::CommandStdout);\n            return;\n        }"
        }
      ],
      "instrument": "`rg -c '\"content\":\"<local-command-stdout>' ~/.claude/projects/*/*.jsonl` and confirm none of those lines carry `\"isMeta\":true` while `<local-command-caveat>` lines do; then `csift search 'local-command-stdout' --count-by label`, which must key only under `harness.command.stdout`. Counting rule: one per record whose content string begins with the tag.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.2.2; src/model/markers.rs LOCAL_COMMAND_STDOUT_PREFIX doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) python3 over every top-level transcript: for each type=='user' record with string content, bucket by which of '<local-command-stdout>' / '<local-command-stderr>' / '<local-command-caveat>' it starts with, crossed with isMeta ; (b) csift search 'local-command-stdout' --count-by label and csift search '' -t harness.command.stdout --no-subagents --count-by label ; (c) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '<local-command-(stdout|caveat)>' and the surrounding tag registry",
          "observed": "(a) 154 records begin with `<local-command-stdout>`, every one of them non-isMeta and string-content; 161 records begin with `<local-command-caveat>`, every one of them isMeta:true; ZERO records begin with `<local-command-stderr>`. The split is total - no stdout record carries isMeta and no caveat record lacks it. (b) csift's top-level census reports harness.command.stdout = 154, an exact match with the disk count, and the pattern census shows no user.message key. (c) `<local-command-stdout>` appears 16 times in the 2.1.258 binary and the emitting site is visible as um(`<local-command-stdout>${...}</local-command-stdout>`); the tag registry in the same binary reads wf=\"command-name\", Vp=\"command-message\", Wq=\"command-args\", Vd=\"local-command-stdout\", q_=\"local-command-stderr\", _B=\"local-command-caveat\".",
          "rule": "One per record whose content string begins with the tag. csift side: one per classified record, top-level transcripts only so the two counts address the same bytes.",
          "note": "The structural claim - the isMeta gate alone does not catch the stdout form, so a content-prefix test is required - is confirmed exactly, and csift's harness.command.stdout census agrees with the on-disk count to the record (154 = 154), with nothing keyed under user.message. Only the occurrence count was stale (52 -> 154). One thing the claim does not mention and a future reader should: 2.1.258 declares `local-command-stderr` beside the two known tags. It is unexercised here, so csift's marker set has no record to be wrong about yet, but a stderr-bearing record would today fall through the prefix test and read as the operator. Code sites src/model/markers.rs:87-91 and src/model/classify.rs:273-276 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-007",
      "area": "turn-boundary",
      "behavior": "A slash-command invocation is stored as a machine-templated wrapper record in TWO tag orders that coexist on disk: the older `<command-name>/x</command-name>...` leading form and the current `<command-message>...</command-message>\\n<command-name>/x</command-name>\\n<command-args>...` form with the message tag FIRST. Both tags still ship in Claude Code 2.1.258. Re-measured 2026-09-02 across 64 top-level transcripts: 44 files carry an old-order wrapper and 29 carry a new-order one, 20 files carry BOTH, and the record split is 161 old-order against 37 new-order.",
      "depends": "`is_slash_command_wrapper` accepts EITHER leading tag, so a new-order wrapper is neither surfaced as genuine prose nor allowed to open a turn. Anchoring on `<command-name>` alone silently reclassified every new-order record as GENUINE user prose - raw wrapper XML surfaced as `user.message`, a no-args wrapper opened a turn, and turn NUMBERING shifted on every transcript carrying new-order wrappers.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "105-112",
          "snippet": "/// The SAME slash-command wrapper in the NEWER tag order: current CC emits\n/// `<command-message>…</command-message>\\n<command-name>/x</command-name>\\n<command-args>…`\n/// -- the message tag FIRST (verified live 2026-07: 14 sessions new-order vs 35 old-order\n/// in one real corpus; both orders coexist). Detection must accept EITHER leading tag:\n/// anchoring on `<command-name>` alone silently reclassified every new-order record as\n/// GENUINE user prose - raw wrapper XML surfaced as `user.message`, and a no-args\n/// wrapper opened a turn.\npub const COMMAND_MESSAGE_PREFIX: &str = \"<command-message>\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "114-118",
          "snippet": "/// True when `content` is a slash-command wrapper (§4.2.3) in EITHER tag order.\n#[must_use]\npub fn is_slash_command_wrapper(content: &str) -> bool {\n    content.starts_with(COMMAND_NAME_PREFIX) || content.starts_with(COMMAND_MESSAGE_PREFIX)\n}"
        }
      ],
      "instrument": "`rg -l '\"<command-message>' ~/.claude/projects -g '*.jsonl' | wc -l` versus `rg -l '\"<command-name>' ... | wc -l` (counting rule: distinct transcript FILES, each counted under the order its wrappers open with; a file spanning a version transition carries both), then `csift search '' @<id> --count-by label | grep harness.command.invocation`.",
      "located": {
        "claude_code": "2.1.150",
        "csift": "0.5.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.2.3 and the v0.5.0 ledger; CHANGELOG 0.5.0"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "(a) python3 over every top-level transcript: for each type=='user' record with string content starting with `<command-name>` or `<command-message>`, record the file under the order its content OPENS with and tally the records ; (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '</?command-(name|message|args)>' | sort | uniq -c ; (c) csift show @<id> --line <n> --raw against the flag-free csift show @<id> --line <n> on a new-order record",
          "observed": "(a) 44 files contain at least one old-order (`<command-name>` leading) wrapper and 29 files contain at least one new-order (`<command-message>` leading) wrapper, with 20 files carrying BOTH orders - direct evidence that the two coexist and that files span the transition. Record split: 161 old-order + 37 new-order = 198 wrapper records, which matches csift's harness.command.invocation census of 198 exactly. (b) 2.1.258 still ships all three tags: `<command-name>` 13, `</command-name>` 4, `<command-message>` 2, `<command-args>` 2, `</command-args>` 2. (c) a live new-order record's raw bytes read \"content\":\"<command-message>csift</command-message>\\n<command-name>/csift</command-name>\\n<command-args>what is the id of this session?</command-args>\" and csift renders it `user.message ... /csift what is the id of this session?` - recognized, not surfaced as prose.",
          "rule": "Counting rule differs from the claim's and the difference is the point: a file is counted under EVERY order it contains, so the two file counts overlap by the 20 transition-spanning files (the claim counted each file under one order only, which cannot show the overlap). Records: one per JSON record whose content string begins with either tag.",
          "note": "Both orders are alive in current data and in the current binary, and the 20 files carrying both are the cleanest possible demonstration that anchoring on a single leading tag would misread part of a single transcript. The recorded 14-vs-35 file figures are stale and were measured under a one-order-per-file rule that hides the overlap, so the counting rule is corrected alongside the numbers. Code sites src/model/markers.rs:100-107 and 109-113 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-008",
      "area": "turn-boundary",
      "behavior": "Genuine prose the user typed after a slash command is not in the record's visible content: it lives in the wrapper's `<command-args>...</command-args>` body, with the invoked command name in `<command-name>...</command-name>`.",
      "depends": "csift recovers it with `Record::slash_command_args`, so a WITH-args wrapper carries BOTH labels `user.message` (first, richest-view law) and `harness.command.invocation` and its unfiltered render is the extracted `/name args` rather than wrapper XML; a NO-args wrapper is `harness.command.invocation` only. Without the extraction the typed instruction is unsearchable and buried in XML.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "100-105",
          "snippet": "        const OPEN: &str = \"<command-args>\";\n        const CLOSE: &str = \"</command-args>\";\n        let start = s.find(OPEN)? + OPEN.len();\n        let end = s[start..].find(CLOSE).map_or(s.len(), |rel| start + rel);\n        let args = s[start..end].trim();\n        if args.is_empty() {"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "277-288",
          "snippet": "        if is_slash_command_wrapper(s) {\n            // Prose typed after the slash command (`<command-args>`) IS genuine user\n            // input - and the RICHER view (richest-view law: the prose beats the\n            // wrapper), so it is pushed FIRST: the unfiltered record-text emission\n            // renders `/name args`, never the wrapper XML. An explicit\n            // `-t harness.command.invocation` still reaches the wrapper form.\n            if self.slash_command_args().is_some() {\n                push_unique(out, Class::UserMessage);\n            }\n            push_unique(out, Class::CommandInvocation);\n            return;\n        }"
        }
      ],
      "instrument": "`csift search '<text typed after a slash command>' @<id>` must match, and `csift show @<id> --line <that line>` must render `/name args` rather than the raw XML; `csift search '' @<id> -t harness.command.invocation --format json | jq -r '.labels'` shows both labels on a with-args wrapper. Counting rule: records carrying the invocation leaf, split by whether `labels` also contains `user.message`.",
      "located": {
        "claude_code": null,
        "csift": "0.5.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 4.2.3; src/model/classify.rs slash-wrapper arm"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' -t harness.command.invocation --no-subagents --max-count 0 --format json | jq -rc 'select(.kind==\"exchange\")|.hits[]|select(.label==\"harness.command.invocation\")|[(.labels|join(\"+\")),(.excerpt|test(\"<command-args></command-args>\")|tostring)]|@tsv' | sort | uniq -c ; then csift show @<id> --line <n> (flag-free) against csift show @<id> --line <n> --raw on one with-args record",
          "observed": "198 wrapper records split exactly three ways: 157 labelled `harness.command.invocation` alone with an empty `<command-args></command-args>`, 4 labelled `harness.command.invocation` alone without that literal empty-args excerpt, and 37 labelled `user.message+harness.command.invocation` - user.message FIRST in every one of the 37. So 37 with-args wrappers carry both labels and 161 no-args wrappers carry the invocation leaf alone; the 37 agrees to the record with the independent python count of with-args wrappers (33 new-order + 4 old-order). The render law was checked on a live record whose raw bytes are `<command-message>csift</command-message>\\n<command-name>/csift</command-name>\\n<command-args>what is the id of this session?</command-args>`: flag-free `show` prints `0x25C2 user.message L<n> /csift what is the id of this session?`, the extracted `/name args`, while an explicit `-t harness.command.invocation` selector renders the raw wrapper XML instead.",
          "rule": "One per classified record, top-level transcripts only. 'with-args' = the `<command-args>...</command-args>` body is non-empty after trimming; the label ORDER is read from the JSON `labels` array, whose first element is the richest view.",
          "note": "Confirmed on every axis the claim asserts, by instrument rather than by reading the classifier: the dual label, the user.message-first ordering, the invocation-only label on no-args wrappers, and the `/name args` render replacing the XML. The 37/161 split reconciles exactly with the independent on-disk wrapper census from TURN-007 (198 total), so the two instruments cross-check each other. Code sites src/model/predicates.rs:100-105 and src/model/classify.rs:277-288 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-009",
      "area": "turn-boundary",
      "behavior": "Claude Code 2.1.258 synthesizes the AskUserQuestion answer written into the answering `tool_result` from a THREE-branch conditional, and the set has turned over since this claim was recorded. Live branches: `Your questions have been answered: \"<q>\"=\"<a>\". You can now continue with these answers in mind.` (100 carriers on disk), `The user answered: \"<q>\"=\"<a>\". Read the answers carefully - they may request clarification, changes, or that you not proceed - and follow what they actually say.` (16 carriers), and `The user did not answer the questions.` for the unanswered branch (0 carriers here). The historical `User has answered your questions: ...` phrasing is absent from the 2.1.258 binary and has zero live carriers in the current corpus, surviving only as quoted text inside other records.",
      "depends": "csift's AUQ_ANSWER_MARKERS carries all three shipped phrasings since v0.10.1 (`User has answered your questions`, `Your questions have been answered`, `The user answered:`) and the search prefilter's verifiable synth-marker set carries the same three, so a marker-only carrier under any of them opens a turn and classifies user.answer; the unanswered branch (`The user did not answer the questions.`) is deliberately excluded so it never opens one. The START-anchored trim_start + starts_with design remains necessary - the retired phrasing draws 313 quotation matches corpus-wide that a contains check would false-positive on.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "3-9",
          "snippet": "/// The synthesized prefixes Claude Code writes into the `tool_result` answering an\n/// `AskUserQuestion` (§4.4). CC has shipped THREE phrasings for the same synthesized\n/// answer - verified across real `~/.claude/projects` data and the 2.1.258 binary:\n///\n/// - `\"User has answered your questions: \\\"<q>\\\"=\\\"<a>\\\". …\"`\n/// - `\"Your questions have been answered: \\\"<q>\\\"=\\\"<a>\\\". …\"`  (the dominant form\n///   in older data; a single hardcoded marker missed it entirely)"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "15",
          "snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &["
        },
        {
          "path": "src/model/markers.rs",
          "lines": "35-38",
          "snippet": "pub fn is_auq_answer_text(text: &str) -> bool {\n    let head = text.trim_start();\n    AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "15-22",
          "snippet": "pub const AUQ_ANSWER_MARKERS: &[&str] = &[\n    \"User has answered your questions\",\n    \"Your questions have been answered\",\n    // CC 2.1.258's third branch (`The user answered: \"<q>\"=\"<a>\". Read the answers\n    // carefully ...`); the fourth branch `The user did not answer the questions.` is\n    // the UNANSWERED case and must never open a turn, so it stays out.\n    \"The user answered:\",\n];"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "34-38",
          "snippet": "#[must_use]\npub fn is_auq_answer_text(text: &str) -> bool {\n    let head = text.trim_start();\n    AUQ_ANSWER_MARKERS.iter().any(|m| head.starts_with(m))\n}"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "487-495",
          "snippet": "    let mut verifiable: Vec<&[u8]> = vec![\n        b\"<task-notification>\",\n        br#\"\"answers\"\"#,\n        b\"User has answered your questions\",\n        b\"Your questions have been answered\",\n        b\"The user answered:\",\n        // The agents-stopped kill notice renders a fabricated `[subagent stopped]` head.\n        b\"stopped by the user\",\n    ];"
        }
      ],
      "instrument": "`rg -c 'User has answered your questions' ~/.claude/projects -g '*.jsonl'` versus `rg -c 'Your questions have been answered' ...` - both non-zero over a corpus spanning versions, a file carrying both spans a transition - then `csift search '' @<id> -t user.answer -c`. Counting rule: one per tool_result carrier whose content STARTS with the marker.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.1.0",
        "source": "SPEC.md section 4.4; src/model/markers.rs AUQ_ANSWER_MARKERS doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "(a) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -c 'User has answered' ; (b) strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{0,60}Your questions have been answered.{0,420}' ; (c) python3 over every top-level transcript: for each type=='user' record carrying a tool_result block, take the block's rendered content and count it when it STARTS WITH one of the candidate phrasings ; (d) csift search '<phrasing>' --count-by label for each phrasing",
          "observed": "(a) 'User has answered' returns ZERO hits in the 2.1.258 binary - the older phrasing is no longer shipped. (b) the surviving site is a THREE-branch conditional, quoted verbatim from the binary: `Your questions have been answered: ${_}. You can now continue with these answers in mind.`:`The user answered: ${_}. Read the answers carefully \\u2014 they may request clarification, changes, or that you not proceed \\u2014 and follow what they actually say.`;else v=\"The user did not answer the questions.\" (c) on disk: 100 carriers START with 'Your questions have been answered', 16 START with 'The user answered: ', ZERO with 'User has answered your questions', ZERO with 'The user did not answer the questions'. (d) the pattern 'User has answered your questions' still matches 313 records corpus-wide but every one is a quotation inside prose, a tool result or an attachment - none is a carrier and none keys under user.answer.",
          "rule": "One per tool_result carrier whose rendered content STARTS with the phrasing (trim_start then starts_with, matching the code's own test); a mid-content quotation does not count, which is what separates the 313 pattern matches from the 0 real carriers.",
          "note": "This is a genuine behavioural drift, found in the binary and then confirmed on disk. Claude Code has rotated the phrasing set: one of the two recorded markers is retired, and a third has shipped that csift does not know. The live blast radius is currently nil because the marker test is only the FALLBACK - all 16 `The user answered:` carriers also carry the structured `toolUseResult.answers` object, so `is_auq_answer_boundary` still opens their turns and they still classify `user.answer` (see TURN-010). The exposure is latent and one-sided: any carrier that ever arrives WITHOUT `toolUseResult` under the new phrasing would silently stop opening a turn. Adding `The user answered:` to AUQ_ANSWER_MARKERS closes it; the fourth string `The user did not answer the questions.` should stay OUT, since that branch is the unanswered case and must not open a turn. Code sites src/model/markers.rs:3-9, 14-17 and 30-33 all match verbatim - the code is exactly as the claim describes it, which is precisely why the drift shows up as a missing marker rather than a wrong one."
        }
      ]
    },
    {
      "id": "TURN-010",
      "area": "turn-boundary",
      "behavior": "An ANSWERED AskUserQuestion carrier is a `type:\"user\"` record with a non-errored `tool_result` block AND a non-empty structured `toolUseResult.answers` object. Re-measured 2026-09-02 across 64 top-level transcripts: all 95 distinct answered carriers carry a non-empty `answers` object and none carries `is_error:true`, but only 79 of the 95 (83%) also carry a marker string csift recognises - the other 16 lead with Claude Code 2.1.258's newer `The user answered: ...` phrasing, which is not in `AUQ_ANSWER_MARKERS` (see TURN-009).",
      "depends": "`is_auq_answer_boundary` opens a turn on the structured signal, with the marker string as the fallback for an older record that has no `toolUseResult`; the answer is the user's message, so the carrier is a turn opener classified `user.answer` (deduped from its `agent.tool.result` twin by the richest-view law). The structured-signal-first ordering is what keeps the 16 new-phrasing carriers working, so the primary/fallback split is doing real load-bearing work rather than being belt-and-braces.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "240-252",
          "snippet": "    /// True when this record is an ANSWERED AskUserQuestion carrier that should open a\n    /// turn (§4.4 / §6.4): a `type:\"user\"` record carrying a `tool_result` block whose\n    /// `is_error` is not true, AND it is a real answer - signalled by a non-empty\n    /// `toolUseResult.answers` object (the clean, structured source) OR, as a fallback\n    /// for an older record without `toolUseResult`, the synthesized AUQ-answer marker in\n    /// the tool_result content. The answer is a genuine USER message (the user's\n    /// selection + prose reasoning), so it is a turn boundary.\n    ///\n    /// A CANCELLED / rejected / validation-errored AUQ (no `answers`, `is_error:true`,\n    /// or a `Cancelled…` / `<tool_use_error>…` body) is NOT a boundary - those carry no\n    /// typed user message. Verified on real data: all 81 answered carriers have\n    /// non-empty `toolUseResult.answers`, the marker string, and `is_error` false; the\n    /// rejection/cancel carriers have none of the three."
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "254-262",
          "snippet": "    pub fn is_auq_answer_boundary(&self) -> bool {\n        if !self.is_type(\"user\") {\n            return false;\n        }\n        // The carrier must ride on a non-errored tool_result block.\n        let Some(blocks) = self.blocks() else {\n            return false;\n        };\n        let mut has_non_errored_tool_result = false;"
        }
      ],
      "instrument": "`csift search '' @<id> -t user.answer --format json | jq -r .line` and check each line carries `\"answers\":{` with at least one key and no `\"is_error\":true`. Counting rule: AUQ tool_result carriers, split by the three signals.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3 case 2; src/model/predicates.rs is_auq_answer_boundary doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t user.answer --no-subagents --max-count 0 --raw > /tmp/ua.jsonl ; then jq over that file for (i) records with a non-empty object at .toolUseResult.answers, (ii) records where any tool_result block has is_error true, (iii) the first 34 characters of the tool_result content ; cross-checked with csift search '' -t user.answer --no-subagents --count-by label and with the independent python carrier scan from TURN-009",
          "observed": "95 distinct records by uuid (116 record-instances in the census; the 21-instance gap is clone-inherited duplicate uuids across forked transcripts, which is documented behaviour, not a discrepancy - and 100-79=21 on the phrasing split confirms the duplicates are all of the dominant phrasing). Of the 95 distinct carriers: 95 carry a non-empty `toolUseResult.answers` object (95/95), 0 carry `is_error:true` on any tool_result block (0/95), and the leading text splits 79 `Your questions have been answered:` against 16 `The user answered: \"...`. So two of the three signals are universal and the third is not: only 79 of 95 (83%) carry a marker string csift recognises.",
          "rule": "One per distinct AUQ tool_result carrier record, keyed by uuid to collapse clone-inherited duplicates; the census's 116 counts record-instances instead. Split by the three signals independently: structured non-empty `.toolUseResult.answers`, absence of `is_error:true`, and a recognised leading marker string.",
          "note": "The claim's structural core survives intact and was confirmed by sweeping every user.answer record in the corpus: 95/95 on the structured answers object, 0/95 on is_error. What no longer holds is the 'all three signals' sub-assertion - 16 of 95 carriers now carry only two, because Claude Code changed the phrasing out from under the marker list. That the boundary still fires for all of them is a direct vindication of preferring the structured signal over the string, and it is the reason TURN-009's drift is currently latent rather than live. The 81 -> 95 count change also carries a counting-rule correction: uuid-distinct carriers are 95 while raw record-instances are 116, the difference being clone-inherited copies. Code sites src/model/predicates.rs:240-252 and 254-262 both match verbatim."
        }
      ]
    },
    {
      "id": "TURN-011",
      "area": "turn-boundary",
      "behavior": "A cancelled, rejected or validation-errored AskUserQuestion lands as a tool_result with is_error:true and no toolUseResult object at all (so no answers map). Two body shapes are observed, not one: the generic tool-rejection text 'The user doesn't want to proceed with this tool use...' (9 of 15) and '<tool_use_error>InputValidationError...' (6 of 15); no 'Cancelled...' body occurs in the corpus. A THIRD non-answer shape exists that the claim does not mention and that is NOT an error: when the picker times out, Claude Code 2.1.258 writes an is_error-absent carrier whose body is 'No response after 60s - the user may be away' with an EMPTY toolUseResult.answers map (3 specimens). csift rejects it for the right reason anyway - the empty map fails has_auq_answers and the body matches no AUQ-answer marker - so it opens no turn and never labels user.answer.",
      "depends": "`is_auq_answer_boundary` returns false on the FIRST errored `tool_result` block, so a cancelled question never opens a turn and never counts as `user.answer`; admitting the errored form fabricates a user turn out of an abandoned question.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "263-273",
          "snippet": "        for b in blocks {\n            if let Block::ToolResult { is_error, .. } = b {\n                if is_error.unwrap_or(false) {\n                    return false; // an errored AUQ result (cancel/reject) is never a boundary\n                }\n                has_non_errored_tool_result = true;\n            }\n        }\n        if !has_non_errored_tool_result {\n            return false;\n        }"
        }
      ],
      "instrument": "`csift search '' @<id> --count-by result` splits tool results `ok` / `error`; then fetch an `error` AUQ carrier with `csift show @<id> --line <n>` and confirm it carries no `user.answer` label. Counting rule: one per AUQ tool_result carrier record.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3 case 2; SPEC.md section 4.4; src/model/predicates.rs is_auq_answer_boundary body"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search 'AskUserQuestion' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"AskUserQuestion\")|.tool_use_id' | sort -u > IDS ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | .message.content[]? | select(.type==\"tool_result\") | . as $b | [$b.tool_use_id, ($b.is_error // false), (($r.toolUseResult // null) | if type==\"object\" then ((.answers // null) | if type==\"object\" then (keys|length) else -1 end) else -2 end), ((if ($b.content|type)==\"string\" then $b.content else \"\" end)[0:42])] | @tsv' | grep -F -f IDS ; csift search 'InputValidationError' --count-by label ; csift search 'No response after 60s' --count-by label ; csift --claude-home $FX search LIMA --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "134 AUQ carriers joined by tool_use_id. is_error:true = 15, split 9 bodies opening 'The user doesn't want to proceed with this tool use' and 6 opening '<tool_use_error>InputValidationError'; all 15 carry NO toolUseResult object at all, so no answers map. is_error:false = 119, of which 116 carry a non-empty toolUseResult.answers map (1-4 keys) and 3 carry an EMPTY answers map with the body 'No response after 60s - the user may be away'. Label census: 'InputValidationError' -> 119 agent.tool.result, 0 user.answer; 'No response after 60s' -> 7 agent.tool.result, 0 user.answer. Fixture through csift 0.10.0 (four records, one genuine opener first): the is_error:true InputValidationError carrier and the is_error-absent empty-answers carrier both stay in turn 0 labelled agent.tool.result, while the non-empty-answers carrier opens turn 1 as user.answer.",
          "rule": "One row per tool_result block whose tool_use_id joins to an AskUserQuestion tool_use block anywhere in the corpus (7,762 .jsonl files under ~/.claude/projects). In the answers column -2 means the record carries no toolUseResult object at all and 0 means the map is present but empty.",
          "note": "Holds, with the body taxonomy corrected and one shape added. The claim's 'indistinguishable from an ordinary tool error' is exactly right for the 15 errored carriers: their bodies are the generic rejection text and a generic validation error, and nothing on the record names AskUserQuestion. csift's guard is stronger than the claim describes, because the empty-answers timeout carrier is is_error-absent yet still correctly refused. Code site src/model/predicates.rs 263-273 verified verbatim in the current file."
        }
      ]
    },
    {
      "id": "TURN-012",
      "area": "turn-boundary",
      "behavior": "Only RESOLVED AskUserQuestion invocations reach the transcript, not only answered ones. Claude Code 2.1.258 resolves the tool on four terminal branches and writes a tool_result for each: the two answered phrasings, an explicit 'The user did not answer the questions.', and an away/timeout path ('No response after 60s - the user may be away', 3 corpus specimens; the binary also carries 'Before going idle the user had selected: ${_}.'). So an abandoned picker can end up on disk as a harness-resolved result rather than as nothing at all.",
      "depends": "csift therefore treats the ANSWERING carrier as the turn opener and renders the whole question-plus-options-plus-answer unit from it; a blocked-on-human lane can only be seen through the elicitation sidecar a hook writes, never through the native transcript.",
      "code": [
        {
          "path": "src/elicitation.rs",
          "lines": "4-7",
          "snippet": "//! Three Claude Code elicitations stall a session on a human yet are invisible / ambiguous\n//! in the native jsonl while pending: **AskUserQuestion** and **ExitPlanMode** (CC buffers\n//! the whole assistant turn until answered - nothing on disk during the wait, see §3.4) and\n//! an **MCP Elicitation** (the inner request lives in memory). A Claude Code hook records"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "240-246",
          "snippet": "    /// True when this record is an ANSWERED AskUserQuestion carrier that should open a\n    /// turn (§4.4 / §6.4): a `type:\"user\"` record carrying a `tool_result` block whose\n    /// `is_error` is not true, AND it is a real answer - signalled by a non-empty\n    /// `toolUseResult.answers` object (the clean, structured source) OR, as a fallback\n    /// for an older record without `toolUseResult`, the synthesized AUQ-answer marker in\n    /// the tool_result content. The answer is a genuine USER message (the user's\n    /// selection + prose reasoning), so it is a turn boundary."
        }
      ],
      "instrument": "While an AskUserQuestion picker is open in a live session, `rg -c AskUserQuestion` over that session's own transcript returns only previously answered ones; counting rule: one per `tool_use` block naming the tool. Only a live session can show it.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.1.0",
        "source": "SPEC.md section 4.4; AGENTS.md sections 3.4 and 3.10"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' -t agent.tool.use --count-by pairing ; csift search 'AskUserQuestion' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"AskUserQuestion\")|.pairing' | sort | uniq -c ; csift search 'ExitPlanMode' -t agent.tool.use --format json | jq -r '.hits[]?|select(.tool_name==\"ExitPlanMode\")|.pairing' | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Your questions have been answered'",
          "observed": "Corpus-wide 210,896 tool_use records: 210,865 paired, 31 pending. Every one of the 31 pending is a Bash (4) or SendMessage (27) call. AskUserQuestion: 134 of 134 paired, 0 pending. ExitPlanMode: 16 of 16 paired, 0 pending. The 2.1.258 binary builds the AUQ tool_result in one place and returns a result on every branch: '...`Your questions have been answered: ${_}. You can now continue with these answers in mind.`:`The user answered: ${_}. Read the answers carefully ... and follow what they actually say.`;else v=\"The user did not answer the questions.\";return{type:\"tool_result\",content:v,tool_use_id:f}'. On-disk elicitation sidecars written by a hook: 20 elicitations.jsonl files carrying 124 pending AskUserQuestion and 12 pending ExitPlanMode markers; all 112 distinct sidecar pending keys later resolve to a native tool_use record.",
          "rule": "One per tool_use block; pairing='pending' means no tool_result with that tool_use_id exists anywhere in scope. Interactive-elicitation tools are compared against the whole-corpus pending rate (31 of 210,896 = 0.015%) as the control.",
          "note": "Holds on the pairing instrument, with the 'only answered ones appear' wording corrected to 'only resolved ones'. One residual: a post-hoc corpus cannot watch the transient state, so this is a differential (0 pending out of 150 interactive-elicitation tool_uses against 31 pending elsewhere), not a direct observation of an open picker. A timestamp probe was tried and is NOT evidence of buffering: joining the sidecar pending markers to their native records gives native_ts minus sidecar_pending_ts of n=112, median 0.4s, max 0.9s, i.e. the assistant record carries the GENERATION instant, so once written it reveals nothing about how long the human took. What would settle it: attach to a live session with an open AskUserQuestion picker and scan its own transcript for that tool_use id before answering. Code sites src/elicitation.rs 4-7 and src/model/predicates.rs 240-246 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-013",
      "area": "turn-boundary",
      "behavior": "Same two sub-shapes, current counts: 70 rejection carriers corpus-wide, 10 with the typed-instruction delimiter and 60 bare. The claim's 31 and 36 are stale measurements; the bare form now outnumbers the typed one 6 to 1.",
      "depends": "Only the delimiter-bearing shape opens a turn (`is_plan_rejection_boundary`, classified `user.rejection`), because everything AFTER the delimiter is the genuine typed message; a bare rejection carries no typed message, so admitting it fabricates an empty human turn out of every rejected Edit, and concatenating the boilerplate as the operator's words corrupted 51 records. The delimiter is registered as a CONSERVATIVE synth-prefilter marker because the `[plan: <path>]` pointer its render resolves comes from a DIFFERENT record.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "129-134",
          "snippet": "/// The synthesized marker Claude Code writes into the `tool_result` when the user\n/// REJECTS a tool use (§4.2.4) - fires for ANY rejected tool_use (ExitPlanMode plan\n/// kick-backs AND rejected AskUserQuestion / Edit / etc.). On its own it is NOT a user\n/// turn; it becomes one only when followed by the [`PLAN_REJECTION_USER_PREFIX`] tail\n/// (a real typed user instruction).\npub const PLAN_REJECTION_MARKER: &str = \"The user doesn't want to proceed with this tool use\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "136-140",
          "snippet": "/// The fixed ASCII delimiter that precedes the user's typed instruction in a\n/// rejection-with-message (§4.2.4): everything AFTER it is the genuine user message.\n/// A rejection WITHOUT this delimiter (the `STOP what you are doing and wait…` form)\n/// carries no typed message and must NOT open a turn.\npub const PLAN_REJECTION_USER_PREFIX: &str = \"To tell you how to proceed, the user said:\\n\";"
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "193-199",
          "snippet": "    /// True when this record is a tool-use rejection carrying a typed user instruction\n    /// (§4.2.4) and so should open a turn. A rejection without a typed message is NOT a\n    /// boundary (see [`Record::plan_rejection_message`]).\n    #[must_use]\n    pub fn is_plan_rejection_boundary(&self) -> bool {\n        self.plan_rejection_message().is_some()\n    }"
        },
        {
          "path": "src/search/matcher.rs",
          "lines": "521-522",
          "snippet": "    // CONSERVATIVE (needs cross-record / external data - force the full scan).\n    let mut conservative: Vec<&[u8]> = vec![b\"To tell you how to proceed\"];"
        }
      ],
      "instrument": "`rg -c \"The user doesn't want to proceed with this tool use\" ~/.claude/projects -g '*.jsonl'` for the total against `rg -c 'To tell you how to proceed, the user said' ...` for the typed subset (the second must be a strict subset; the difference is the bare form), then `csift search '' -t user.rejection -c`, which should equal the second count. Counting rule: one per record carrying the marker.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3 case 3; SPEC.md section 4.2.4; SKILL.md wrong-assumption table"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg \"doesn't want to proceed with this tool use\" ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | .message.content[]? | select(.type==\"tool_result\") | (if (.content|type)==\"string\" then .content else ((.content // []) | map(.text // \"\") | join(\" \")) end) | select(startswith(\"The user doesn'\\''t want to proceed with this tool use\")) | if test(\"To tell you how to proceed, the user said:\\n\") then \"typed\" else \"bare\" end' | sort | uniq -c ; csift search '' -t user.rejection --count-by label ; csift --claude-home $FX search MIKE --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ; csift --claude-home $FX search FOXTROT --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "The binary carries both templates as adjacent literals: 'The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.' and the same sentence ending 'To tell you how to proceed, the user said:'. Corpus: 70 tool_result bodies START with the rejection marker, split 10 typed (carrying the delimiter) and 60 bare. csift reports exactly 10 user.rejection records corpus-wide - an exact match with the typed count. Fixture through csift 0.10.0: a typed-tail rejection opens its own turn as user.rejection; a bare 'STOP what you are doing' rejection stays inside the preceding turn as agent.tool.result.",
          "rule": "One per tool_result block whose flattened text STARTS with the rejection marker (a mid-prose quotation does not count), split by whether the body contains the exact delimiter 'To tell you how to proceed, the user said:' followed by a newline. Corpus is 7,762 .jsonl files under ~/.claude/projects.",
          "note": "Structure holds exactly and the delimiter is byte-identical in 2.1.258. Only the counts moved. The csift-side cross-check is unusually tight: the structural typed count (10) and the user.rejection label count (10) agree to the record. Code sites src/model/markers.rs 124-129 and 131-135, src/model/exchange.rs 193-199, src/search/matcher.rs 512-513 all verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-014",
      "area": "turn-boundary",
      "behavior": "The plan APPROVAL carrier is structurally distinct from every rejection shape: it reads `User has approved your plan...`, carries no `is_error` and no typed message - it is the harness greenlight, not something the operator wrote.",
      "depends": "csift never treats an approval as a user message or a turn boundary: it matches no rejection delimiter and, carrying no `answers` map and no AUQ marker, it is not an answered-AUQ boundary either, so `opens_turn` stays false and the human-turn count does not grow by one per approved plan.",
      "code": [
        {
          "path": "src/model/tests/boundaries.rs",
          "lines": "333-335",
          "snippet": "fn plan_approval_is_not_a_boundary() {\n    // The approval path is the harness greenlight (no typed message, no is_error) -\n    // must NOT become a turn boundary."
        },
        {
          "path": "src/model/exchange.rs",
          "lines": "214-219",
          "snippet": "    pub fn opens_turn(&self) -> bool {\n        self.is_genuine_user()\n            || self.is_auq_answer_boundary()\n            || self.is_plan_rejection_boundary()\n            || self.is_peer_message_record()\n    }"
        }
      ],
      "instrument": "`rg -c 'User has approved your plan' ~/.claude/projects -g '*.jsonl'` for the raw count, then confirm `csift search '' @<that session> -t user.message -c` does not grow by it. Counting rule: one per raw occurrence for rg, one per classified record for csift.",
      "located": {
        "claude_code": null,
        "csift": null,
        "source": "SPEC.md section 4.2.4; src/model/tests/boundaries.rs plan_approval_is_not_a_boundary"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'User has approved your plan' ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | .message.content[]? | select(.type==\"tool_result\") | . as $b | (if ($b.content|type)==\"string\" then $b.content else ((($b.content // []) | map(.text // \"\") | join(\" \"))) end) as $t | select($t | startswith(\"User has approved your plan\")) | [($b|has(\"is_error\")), (($r.toolUseResult // {}) | keys | join(\",\")), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; csift search 'User has approved your plan' --count-by label ; csift --claude-home $FX search ECHO --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "Binary literal: 'User has approved your plan. You can now start coding. Start with updating your todo list if applicable'. Corpus: 12 approval carriers. All 12 have has(\"is_error\") false - the key is ABSENT, not false - and isMeta absent. toolUseResult keys are filePath,hasTaskTool,isAgent,plan on 7 and the same plus planWasEdited on 5; no answers key on any. csift label census over the approval text: 51 agent.tool.result, 15 agent.tool.use, 8 agent.thinking, 4 agent.message, 2 agent.communication.inbox and ZERO keys under any user.* leaf. Fixture through csift 0.10.0: an approval carrier placed after a genuine opener stays inside that turn (turn index unchanged) and labels agent.tool.result.",
          "rule": "One per tool_result block whose flattened text starts with 'User has approved your plan' across 7,762 .jsonl files under ~/.claude/projects; the csift side counts one per classified record under every surviving leaf.",
          "note": "Holds without correction. Worth recording as extra structure: the approval carrier is positively distinguishable from an answered AskUserQuestion not only by the missing answers map but by carrying a plan-shaped toolUseResult (filePath, plan, isAgent, hasTaskTool, optional planWasEdited) instead. Code sites src/model/tests/boundaries.rs 324-326 and src/model/exchange.rs 214-219 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-015",
      "area": "turn-boundary",
      "behavior": "An inbound peer message is still a type:user, role:user, STRING-content record excluded from is_genuine_user and kept as a turn opener, but its shape is no longer always the relay wrapper. Current corpus: 464 of 854 teammate-bearing records begin with the bare '<teammate-message' tag and carry no preamble; 390 begin with 'Another Claude session sent a message:'. Claude Code 2.1.258 additionally ships two preamble variants the claim does not list: 'Another Claude session sent a message while you were working:' and 'A peer session sent a message while you were working:'.",
      "depends": "csift excludes peer messages from `is_genuine_user` while keeping them turn OPENERS via `is_peer_message_record`, so turn segmentation stays byte-stable where peers already opened turns while the `user` mislabel is removed; the record classifies `agent.communication.inbox` with a `teammate_id` to `self` direction, and its body renders through `inbound_comm_preview` / `record_text_sections`, never as raw XML.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "45-52",
          "snippet": "/// True when `content` is an inbound teammate/peer message (GOLD §5) - it carries a\n/// [`TEAMMATE_MESSAGE_OPEN`] tag at a section BOUNDARY (FINDING-1). Real data (edge-fixtures scout):\n/// the real shape is ALWAYS the relayed wrapper `Another Claude session sent a message:\\n\n/// <teammate-message …>\\n<BODY>\\n</teammate-message>\\n\\n<security footer>` (126 of 126), so the\n/// boundary is the content start, just after the relay preamble, or right after a prior section's\n/// close tag. A tag merely QUOTED mid-prose (a genuine user message that mentions the literal tag -\n/// common in csift's OWN dev sessions) is NOT a boundary, so the record stays `user.message` rather\n/// than being mislabeled `agent.communication.inbox` (the FINDING-1 fix)."
        },
        {
          "path": "src/model/markers.rs",
          "lines": "148",
          "snippet": "pub const TEAMMATE_MESSAGE_OPEN: &str = \"<teammate-message\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "151",
          "snippet": "/// `Another Claude session sent a message:\\n<teammate-message …>`. A peer tag IMMEDIATELY after"
        },
        {
          "path": "src/model/classify.rs",
          "lines": "14-21",
          "snippet": "    /// True when this record is ANY inbound PEER message (GOLD §1 + FINDING-2) - a\n    /// `<teammate-message>` OR `<agent-message>` at a section boundary. The predicate\n    /// [`Record::is_genuine_user`] EXCLUDES and [`Record::opens_turn`] INCLUDES (a peer message is\n    /// not the operator, but it still delimits a turn). Reads the raw (un-normalized) message text so\n    /// the relay preamble's `\\n` survives; gated to `type:\"user\"` (the only place a peer message\n    /// arrives). The body render for a peer-opened turn comes from\n    /// [`Record::inbound_comm_preview`] (`turns`/`list`) / `record_text_sections` (`search`).\n    #[must_use]"
        }
      ],
      "instrument": "`rg -c '<teammate-message' ~/.claude/projects/*/*.jsonl` and `rg -c 'Another Claude session sent a message:' ...` against `csift search '' @<id> --count-by label`, where the traffic must key under `agent.communication.inbox` and none of it under `user.message`. Counting rule: one per record carrying the open tag at a section boundary (csift's number is the lower, correct one).",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.2.0",
        "source": "AGENTS.md section 3.3; SPEC.md sections 4.1 and 5.3; src/model/peer.rs doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c | test(\"<teammate-message\")) or ($c | test(\"<agent-message\"))) | [(if ($c | startswith(\"Another Claude session sent a message:\")) then \"relay-preamble\" elif ($c | startswith(\"<teammate-message\")) then \"bare-teammate-start\" elif ($c | startswith(\"<agent-message\")) then \"bare-agent-start\" else \"other-position\" end), (if ($c | test(\"<teammate-message\")) then \"T\" else \"-\" end) + (if ($c | test(\"<agent-message\")) then \"A\" else \"-\" end), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Another Claude session sent a message' ; csift --claude-home $FX search NEEDLEBARE --count-by label ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "909 type:user string-content records carry a peer open tag. Of the 854 teammate-bearing ones, 464 START with the bare '<teammate-message' tag and 390 start with the relay preamble 'Another Claude session sent a message:' - so the preamble is present on 46%, not 100%. The 2.1.258 binary defines three preamble forms in one place: 'var QY=\"Another Claude session sent a message\",pe=`${QY} while you were working:`,me=`${QY}:`,gt=\"A peer session sent a message while you were working:\"'. Fixture through csift 0.10.0: a bare '<teammate-message teammate_id=...>' at content start labels agent.communication.inbox, so csift handles the preamble-less majority correctly.",
          "rule": "One per type:user record whose message.content is a STRING containing a peer open tag, bucketed by what the content STARTS with, over 7,762 .jsonl files under ~/.claude/projects.",
          "note": "Holds structurally; the '126 of 126 relay wrapper' quantifier is stale and should not be relied on. The same stale sentence is copied into the doc comment at src/model/peer.rs 45-52, which is worth correcting in place. csift's behaviour is unaffected for the bare and colon-preamble forms because content start is itself a section boundary; the two 'while you were working' variants are the real problem and are recorded under TURN-016. Code sites src/model/peer.rs 45-52, src/model/markers.rs 143 and 145-149, src/model/classify.rs 14-21 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-016",
      "area": "turn-boundary",
      "behavior": "The `<agent-message from=\"...\">` peer form is delivered as a type:\"user\", role:\"user\", STRING-content record and is isMeta-carrying (47 of 47 corpus records have isMeta:true). Claude Code 2.1.258 relays it under one of THREE preambles the binary defines together in one literal and strips as one list: `Another Claude session sent a message:`, the same head plus ` while you were working:`, and `A peer session sent a message while you were working:`. The mid-turn `while you were working` form carried 29 of those 47 records, so preamble recognition, not the tag alone, decides whether a relay is seen.",
      "depends": "csift folds the form into `is_peer_message` on CONTENT shape rather than on `isMeta`, and anchors it at a section boundary that accepts ALL THREE relay preambles (`PEER_MESSAGE_PREAMBLES`, the same set the binary strips), so an isMeta agent-message still opens a turn and classifies `agent.communication.inbox` with the `from` attribute as the direction source. Keying peer detection off `isMeta` drops the form entirely; carrying only the colon preamble drops every mid-turn relay into the empty label vector - no turn, no leaf, no census row, no rendered `show` address, only `show --raw`.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "59-62",
          "snippet": "/// True when `content` is an inbound `<agent-message from=\"…\">` peer message (P1c M1 / FINDING-2) at\n/// a section BOUNDARY - the DISTINCT peer form from [`is_teammate_message`]. Like a teammate message\n/// it classifies `agent.communication.inbox`, is excluded from [`Record::is_genuine_user`], yet\n/// still opens a turn. Boundary-anchored (FINDING-1) for the same reason - a quoted tag is not it."
        },
        {
          "path": "src/model/peer.rs",
          "lines": "22-75",
          "snippet": "#[must_use]\npub fn is_peer_message(content: &str) -> bool {\n    is_teammate_message(content) || is_agent_message(content)\n}"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "169",
          "snippet": "pub const AGENT_MESSAGE_OPEN: &str = \"<agent-message\";"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "158-162",
          "snippet": "pub const PEER_MESSAGE_PREAMBLES: &[&str] = &[\n    \"Another Claude session sent a message:\",\n    \"Another Claude session sent a message while you were working:\",\n    \"A peer session sent a message while you were working:\",\n];"
        },
        {
          "path": "src/model/peer.rs",
          "lines": "120-127",
          "snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {\n    let t = prefix.trim_end();\n    t.is_empty()\n        || PEER_MESSAGE_PREAMBLES.iter().any(|p| t.ends_with(p))\n        || t.ends_with(TASK_NOTIFICATION_CLOSE)\n        || t.ends_with(TEAMMATE_MESSAGE_CLOSE)\n        || t.ends_with(AGENT_MESSAGE_CLOSE)\n}"
        }
      ],
      "instrument": "`rg -c '<agent-message ' ~/.claude/projects/*/*.jsonl`, then `csift search '<agent-message from=' --count-by label` and `--raw | jq -r .isMeta | sort | uniq -c`. Counting rule: one per record carrying the open tag at a section boundary.",
      "located": {
        "claude_code": "2.1.191",
        "csift": "0.4.0",
        "source": "AGENTS.md section 3.3; src/model/peer.rs is_agent_message doc comment; dev session 2026-09-01"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'def rtrim: sub(\"[[:space:]]+$\";\"\"); def isbnd($c;$open): ((($c | indices($open)) // []) | map(. as $i | ($c[0:$i] | rtrim) as $t | ($t==\"\") or ($t|endswith(\"Another Claude session sent a message:\")) or ($t|endswith(\"</task-notification>\")) or ($t|endswith(\"</teammate-message>\")) or ($t|endswith(\"</agent-message>\"))) | any); select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c|test(\"<teammate-message\")) or ($c|test(\"<agent-message\"))) | [(if isbnd($c;\"<teammate-message\") or isbnd($c;\"<agent-message\") then \"boundary\" else \"quoted-only\" end), ($r.isMeta // false)] | @tsv' | sort | uniq -c ; csift search 'sent a message while you were working' --count-by label ; csift --claude-home $FX search NEEDLENEW --count-by label ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "47 type:user string-content records carry '<agent-message', all with isMeta:true. Re-running csift's own is_section_boundary rule in jq splits them 18 boundary / 29 quoted-only, and the 29 quoted-only ones are exactly the records whose preamble is 'Another Claude session sent a message while you were working:' - a form csift's PEER_MESSAGE_PREAMBLE does not contain. Consequence measured three ways: (1) csift search 'sent a message while you were working' --count-by label returns 6 records, all agent.tool.result / agent.thinking / agent.tool.use prose from same-day sessions, and ZERO under agent.communication.inbox; (2) on a live specimen csift show <target> --line N fails with 'no such record(s)' while csift show <target> --line N --raw prints the line, so the record is present on disk but absent from the rendered record set; (3) a controlled fixture through csift 0.10.0 with four peer records gives: old colon preamble -> 1 agent.communication.inbox, 'while you were working' preamble -> 0 records, 'A peer session sent a message while you were working:' -> 0 records, bare '<teammate-message' at content start -> 1 agent.communication.inbox.",
          "rule": "One per type:user record whose message.content is a STRING containing '<agent-message', over 7,762 .jsonl files under ~/.claude/projects; boundary-ness computed by reimplementing csift's is_section_boundary (content start, or the prefix right-trimmed ending in the colon preamble or one of the three section close tags).",
          "note": "Drifted, and this is the load-bearing finding of the batch. The fix is to widen PEER_MESSAGE_PREAMBLE from a single constant to the three forms Claude Code 2.1.258 defines together in one binary literal ('var QY=\"Another Claude session sent a message\",pe=`${QY} while you were working:`,me=`${QY}:`,gt=\"A peer session sent a message while you were working:\"'), matching on the QY prefix rather than the exact colon form. Note the silence is total rather than partial: an unrecognised isMeta peer record produces no label at all, so no census or -t selector discloses it. Code sites src/model/peer.rs 59-62 and 72-75 and src/model/markers.rs 156 verified verbatim; the defect is in the boundary predicate they depend on, not in these lines."
        }
      ]
    },
    {
      "id": "TURN-017",
      "area": "turn-boundary",
      "behavior": "The mechanism is exactly as described, but 'immediately after the relay preamble' should read 'immediately after THE colon-form relay preamble'. Claude Code 2.1.258 ships three relay preambles and csift's is_section_boundary lists only one of them, so the boundary set is complete for close tags and content start but incomplete for preambles (see TURN-016).",
      "depends": "csift recognises a tag only at a SECTION BOUNDARY - the content start, immediately after the relay preamble, or right after a prior section's close tag - so a batched record still yields every section while a quoting message stays `user.message` instead of being reparented to `agent.communication.inbox` or a harness notification.",
      "code": [
        {
          "path": "src/model/peer.rs",
          "lines": "33-43",
          "snippet": "pub(crate) fn has_boundary_section(content: &str, open: &str) -> bool {\n    let mut idx = 0;\n    while let Some(rel) = content[idx..].find(open) {\n        let start = idx + rel;\n        if is_section_boundary(&content[..start]) {\n            return true;\n        }\n        idx = start + open.len();\n    }\n    false\n}"
        },
        {
          "path": "src/model/peer.rs",
          "lines": "120",
          "snippet": "pub(crate) fn is_section_boundary(prefix: &str) -> bool {"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "171-178",
          "snippet": "/// Section CLOSE tags (FINDING-1). A peer / `<task-notification>` open tag that sits right after\n/// one of these (modulo whitespace) is at a section BOUNDARY ([`is_section_boundary`]), so a\n/// BATCHED record's later sections are still recognized - while a tag QUOTED mid-prose (a genuine\n/// user message that merely mentions the literal tag, common in csift's OWN dev sessions) is NOT a\n/// boundary and never starts a section. Kept beside their open-tag constants so the pair never drift.\npub(crate) const TASK_NOTIFICATION_CLOSE: &str = \"</task-notification>\";\npub(crate) const TEAMMATE_MESSAGE_CLOSE: &str = \"</teammate-message>\";\npub(crate) const AGENT_MESSAGE_CLOSE: &str = \"</agent-message>\";"
        }
      ],
      "instrument": "In a session whose prose quotes the tag, `csift search '<teammate-message' @<id> --count-by label`: the quoting records must count under `user.message` and the relayed ones under `agent.communication.inbox`, with no record in both. Counting rule: one section per boundary-anchored open tag; records carrying the literal tag split by whether the first occurrence sits at a boundary.",
      "located": {
        "claude_code": null,
        "csift": "0.2.0",
        "source": "AGENTS.md section 3.3 (FINDING-1); src/model/peer.rs is_section_boundary doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift --claude-home $FX2 search BATCHONE --count-by label ; csift --claude-home $FX2 search BATCHTWO --count-by label ; csift --claude-home $FX2 search BATCHQUOTE --count-by label ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | (.message.content // null) as $c | select(($c|type)==\"string\") | [(if ($c | test(\"</task-notification>[[:space:]]*<(task-notification|teammate-message|agent-message)\")) then \"batch-after-taskn\" else empty end), (if ($c | test(\"</teammate-message>[[:space:]]*<(task-notification|teammate-message|agent-message)\")) then \"batch-after-teammate\" else empty end)] | select(length>0) | .[]' | sort | uniq -c ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "Fixture through csift 0.10.0: one record holding a '<task-notification>...</task-notification>' section followed by a '<teammate-message ...>' section carries BOTH harness.notification.task and agent.communication.inbox, and a needle in either section finds the record under both leaves; a separate record whose prose merely quotes '<teammate-message teammate_id=\"x\">' mid-sentence labels user.message ONLY, with no inbox or notification leaf. Corpus: 125 records carry a section open tag immediately after a close tag - 123 after '</teammate-message>' and 2 after '</task-notification>' - so batching is real, not hypothetical. The boundary reimplementation over the 909 peer-tag records finds 8 non-isMeta records where no occurrence sits at a boundary, i.e. genuine messages that only quote the tag.",
          "rule": "One section per boundary-anchored open tag; a record counts as batched when a regex finds a section open tag separated from a preceding close tag by whitespace only. Corpus is 7,762 .jsonl files under ~/.claude/projects.",
          "note": "Holds on both halves that were testable: batched records do yield every section, and a quoting message does stay user.message. Refined only because the singular 'the relay preamble' is now an incomplete description of Claude Code's behaviour. Code sites src/model/peer.rs 33-43 and 120-127 and src/model/markers.rs 158-165 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-018",
      "area": "turn-boundary",
      "behavior": "Claude Code injects automation completions as a `type:\"user\"`, non-`isMeta`, STRING-content record wrapped in `<task-notification>...</task-notification>`, carrying the inner tags `<task-id>`, `<status>`, `<summary>`, `<event>`, `<tool-use-id>` and optionally `<output-file>` / `<result>`. It passes every genuine-user gate, so it DOES open a turn.",
      "depends": "csift keeps it as a turn opener (a real delivered trigger) but reparents its label to `harness.notification.<kind>` and renders `[<kind> <task-id> <status>] <summary>` via `automation_label`, so machine pulses are neither counted as operator prose nor dumped as raw XML into a reconstructed turn.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "120-127",
          "snippet": "/// Prefix of a `<task-notification>…</task-notification>` user record - a MACHINE-INJECTED\n/// automation trigger (a background-command / workflow / spawned-task completion notice CC\n/// inserts as a `type:\"user\"`, non-`isMeta`, STRING-content record). It LOOKS like a human\n/// turn to [`Record::is_genuine_user`] (it passes every gate), so it DOES open a turn - but\n/// it is an automation pulse, not the operator's prose. [`Record::automation_trigger`]\n/// classifies it so surfaces can LABEL the segment (`[workflow <id> completed] <summary>`)\n/// instead of dumping the raw `<task-id>`/`<output-file>`/`<status>` XML wrapper.\npub const TASK_NOTIFICATION_PREFIX: &str = \"<task-notification>\";"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "133-141",
          "snippet": "    /// Classify this record as a MACHINE-INJECTED automation trigger, if it is one.\n    ///\n    /// A `<task-notification>` record is a `type:\"user\"`, non-`isMeta`, STRING-content\n    /// record CC inserts when a background command / spawned task / workflow completes. It\n    /// passes every [`Record::is_genuine_user`] gate (so it opens a turn like a human\n    /// message), but it is an automation pulse - surfacing its raw `<task-id>` /\n    /// `<output-file>` / `<status>` XML as \"user prose\" is noise. This parser extracts the\n    /// stable inner tags so a surface can render `[workflow <task-id> completed] <summary>`\n    /// instead. Returns `None` for any non-`<task-notification>` record."
        }
      ],
      "instrument": "`rg -c '<task-notification>' <transcript>` against `csift search '' @<id> --count-by label | rg harness.notification`. Counting rule: one per boundary-anchored notification SECTION (a batched record yields several) for csift, one per line for rg.",
      "located": {
        "claude_code": null,
        "csift": "0.4.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 5.1; src/model/markers.rs TASK_NOTIFICATION_PREFIX doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select($c | startswith(\"<task-notification>\")) | [($r.isMeta // false), ($c|test(\"<task-id>\")), ($c|test(\"<status>\")), ($c|test(\"<summary>\")), ($c|test(\"<event>\")), ($c|test(\"<tool-use-id>\")), ($c|test(\"<output-file>\")), ($c|test(\"<result>\"))] | @tsv' | sort | uniq -c ; csift search '' -t harness.notification --count-by label ; csift --claude-home $FX3 search CHARLIE --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "2,799 records whose type:user STRING content starts with '<task-notification>'. isMeta is false on all 2,799. Inner tags: <task-id> and <summary> on all 2,799; <status> on 1,962 and <event> on 837, and those two are mutually exclusive; <tool-use-id> and <output-file> on 1,957 each; <result> on 325. csift label census: 2,811 records across harness.notification.background-command 1,518, monitor 948, subagent 198, workflow 142, task 5. Fixture through csift 0.10.0: a task-notification record placed after a genuine opener and a compaction summary advances the turn index from 0 to 1 and labels harness.notification.task, so it does open a turn and is not counted as operator prose.",
          "rule": "One per type:user record whose message.content is a STRING starting with '<task-notification>', over 7,762 .jsonl files under ~/.claude/projects; the csift side counts one per classified record per surviving leaf, which is why the notification total slightly exceeds the raw count (the agents-stopped notices of TURN-019 also key under harness.notification.subagent).",
          "note": "Holds without correction, including the optional tags. Two details worth pinning for a future reader: <status> and <event> never co-occur (a monitor or scheduled pulse carries <event> and no <status>, which is why the renderer falls back to the event), and <result> appears on 325 of 2,799, matching the background-agent report shape that also earns agent.communication.inbox. Code sites src/model/markers.rs 115-122 and src/model/predicates.rs 133-141 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-019",
      "area": "turn-boundary",
      "behavior": "Same two templates, current count 10 specimens rather than 9. One dead branch to record: Claude Code 2.1.258 selects the plural template whenever the stopped count is not exactly 1, so the singular-COUNT phrasing '1 background agent was stopped by the user' is never emitted; csift's is_agents_stopped_notice accepts it anyway, which is harmless tolerance rather than a needed arm. The quoted items are the agents' descriptions, each an ellipsis-truncated prompt prefix averaging 54-55 characters.",
      "depends": "Because it is a plain non-`isMeta` string that matches no other marker, it reads as a genuine human turn unless explicitly excluded: csift treats it as a synthetic user marker (never genuine, never a turn opener), classifies it `harness.notification.subagent` and renders `[subagent stopped] ...`; the dual-arm predicate is required because the singular template has no leading count. 9 corpus specimens were previously read as `user.message`.",
      "code": [
        {
          "path": "src/model/markers.rs",
          "lines": "54-58",
          "snippet": "/// The harness's \"N background agent(s) were stopped by the user: ...\" notice (v0.10.0):\n/// a plain-string `type:\"user\"` record Claude Code writes when async agents are killed\n/// from the UI. It names a count and truncated prompt prefixes, never an id, and it\n/// triggers no generation - the model sees it only alongside the next real prompt. Not\n/// the operator, never a turn opener; classifies `harness.notification.subagent`."
        },
        {
          "path": "src/model/markers.rs",
          "lines": "60-75",
          "snippet": "pub fn is_agents_stopped_notice(content: &str) -> bool {\n    let s = content.trim_start();\n    // The singular template names the agent: `Background agent \"<desc>\" was stopped by\n    // the user.` (no count).\n    if s.starts_with(\"Background agent \\\"\") && s.contains(\" was stopped by the user\") {\n        return true;\n    }\n    let digits = s.bytes().take_while(u8::is_ascii_digit).count();\n    if digits == 0 {\n        return false;\n    }\n    let rest = &s[digits..];\n    (rest.starts_with(\" background agent was stopped by the user\")\n        || rest.starts_with(\" background agents were stopped by the user\"))\n        && rest.contains(AGENTS_STOPPED_MARKER)\n}"
        },
        {
          "path": "src/model/markers.rs",
          "lines": "47-52",
          "snippet": "pub fn is_synthetic_user_marker(content: &str) -> bool {\n    INTERRUPT_MARKERS.contains(&content)\n        || content.starts_with(LOCAL_COMMAND_STDOUT_PREFIX)\n        || is_slash_command_wrapper(content)\n        || is_agents_stopped_notice(content)\n}"
        }
      ],
      "instrument": "`rg -oNI 'background agents? (was|were) stopped by the user' ~/.claude/projects -g '*.jsonl' | sort | uniq -c` plus the singular `Background agent \"` form (both templates must be present for the dual arm to stay necessary), then `csift search 'stopped by the user' --count-by label`, where every hit keys under `harness.notification.subagent` and none under `user.message`. Counting rule: one per raw match for rg, one per classified record for csift.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.0",
        "source": "AGENTS.md section 3.3a; SPEC.md section 5.1 and the v0.10.0 ledger; src/model/markers.rs is_agents_stopped_notice doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'stopped by the user' ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"user\") | . as $r | (.message.content // null) as $c | select(($c|type)==\"string\") | select(($c|test(\"^Background agent \\\"\")) or ($c|test(\"^[0-9]+ background agents? (was|were) stopped by the user\"))) | [($r.isMeta // false), (if ($c|test(\"^Background agent \\\"\")) then \"singular\" else \"plural\" end), ($c | gsub(\"\\\"[^\\\"]*\\\"\";\"\\\"<desc>\\\"\") | .[0:90])] | @tsv' | sort | uniq -c ; csift search 'stopped by the user' --count-by label ; csift --claude-home $FX3 search DELTA --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "The 2.1.258 binary picks the template by count in one expression: 'let Fo=kn.length===1?`Background agent \"${kn[0]}\" was stopped by the user.`:`${kn.length} background agents were stopped by the user: ${kn.map((jn)=>`\"${jn}\"`).join(\", \")}.`;return Ye.enqueuePendingNotification({agentId:Ze(),value:Fo,mode:\"task-notification\",skipAttachments:!0})' - the value is a bare string, not the structured notification payload the other call sites pass. Corpus: 10 specimens, 1 singular and 9 plural with counts 2, 3, 5, 10 and 26; isMeta absent on all 10; the quoted item count always equals the leading number; every specimen contains '...' and the quoted descriptions average 54-55 characters. csift label census over 'stopped by the user': 10 harness.notification.subagent and ZERO under any user.* leaf. Fixture through csift 0.10.0: a plural notice placed mid-turn does NOT advance the turn index and labels harness.notification.subagent.",
          "rule": "One per type:user record whose STRING content matches '^Background agent \"' or '^[0-9]+ background agents? (was|were) stopped by the user', over 7,762 .jsonl files under ~/.claude/projects.",
          "note": "Holds; only the specimen count and one never-taken predicate arm needed correction. The bare-string value is what makes the exclusion necessary: unlike every other task-notification call site, this one enqueues raw prose with no XML wrapper and no isMeta, so without the explicit marker it would pass every genuine-user gate. Code sites src/model/markers.rs 49-53, 55-70 and 42-47 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-020",
      "area": "turn-boundary",
      "behavior": "A compaction SUMMARY is a `type:\"user\"` record carrying `isCompactSummary:true` with STRING content - not a `type:\"summary\"` record - so on the `type` field alone it looks like a human turn.",
      "depends": "csift gates `isCompactSummary` out of `is_genuine_user` before any content inspection, so a summary never opens a turn and its machine recap never enters the operator census; it stays a turn MEMBER classified `harness.compaction.summary`, which is what lets `verbatim` walk backwards THROUGH a compaction rather than stopping at it.",
      "code": [
        {
          "path": "src/model/record.rs",
          "lines": "66-68",
          "snippet": "    /// Compaction summary marker - when true, this user record is NOT a human turn.\n    #[serde(default, rename = \"isCompactSummary\")]\n    pub is_compact_summary: Option<bool>,"
        },
        {
          "path": "src/model/predicates.rs",
          "lines": "37-43",
          "snippet": "    pub fn is_genuine_user(&self) -> bool {\n        if !self.is_type(\"user\") {\n            return false;\n        }\n        if self.is_compact_summary.unwrap_or(false) {\n            return false;\n        }"
        }
      ],
      "instrument": "`rg -c '\"isCompactSummary\":true' <transcript>` against `csift stats @<id> --format json | jq .compactions`, and confirm `csift search '' @<id> --count-by label` keys them under `harness.compaction.summary`, never `user.message`. Counting rule: one per record carrying the flag.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md sections 3.3 and 3.5; SPEC.md section 4.7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.isCompactSummary == true) | [.type, (.message.role // \"-\"), ((.message.content // null)|type), (.isMeta // false), (.isVisibleInTranscriptOnly // false)] | @tsv' | sort | uniq -c ; find ~/.claude/projects -name '*.jsonl' | xargs -n 40 jq -rc 'select(.type==\"summary\") | .type' | sort | uniq -c ; csift search '' -t harness.compaction --count-by label ; csift --claude-home $FX3 search BRAVO --format json | jq -r '[.turn_index, (.hits[]|.label)] | @tsv' ($FX* is a throwaway --claude-home tree holding only the synthetic fixture transcript described under observed: <root>/projects/<encoded-dir>/<uuid>.jsonl)",
          "observed": "232 records carry isCompactSummary:true. All 232 are type:user with message.role user and STRING content, isMeta absent, isVisibleInTranscriptOnly true - a single uniform row, no variants. Zero records of type 'summary' exist anywhere in the corpus. csift label census: 232 harness.compaction.summary and 232 harness.compaction.boundary, a one-to-one pairing. Fixture through csift 0.10.0: a compaction summary placed after a genuine opener keeps turn index 0 - it does not open a turn - and labels harness.compaction.summary only, with no user.message leaf.",
          "rule": "One per record carrying isCompactSummary:true, over 7,762 .jsonl files under ~/.claude/projects; the type:'summary' probe counts one per record of that type and returns nothing.",
          "note": "Holds without correction, and the negative half of the claim is now measured rather than assumed: there is not one type:'summary' record in the corpus, so a reader keying on .type alone really does see only 'user'. The 232 summaries pair exactly one-to-one with 232 compact_boundary system records, which is a useful invariant for anyone auditing compaction points. Code sites src/model/record.rs 65-67 and src/model/predicates.rs 37-43 verified verbatim."
        }
      ]
    },
    {
      "id": "TURN-021",
      "area": "turn-boundary",
      "behavior": "A subagent transcript's FIRST record is an `isSidechain:true` `type:\"user\"` seed carrying the spawn prompt for a Task/Agent/teammate child; a `/fork` child leads with a `type:\"fork-context-ref\"` line (agentId, parentSessionId, parentLastUuid, contextLength) and its seed follows. No top-level transcript starts with a sidechain seed.",
      "depends": "csift deliberately does NOT gate `isSidechain` out of `is_genuine_user`, so `list`'s per-subagent preview can treat that seed as the subagent's first user message (gating it would silently blank every subagent preview for zero real benefit); the seed classifies `agent.communication.inbox` with a parent-to-self direction, and the per-surface scan owns the subagent-versus-parent context instead.",
      "code": [
        {
          "path": "src/model/predicates.rs",
          "lines": "48-53",
          "snippet": "        // NOTE on `isSidechain`: a subagent transcript's FIRST record is an\n        // `isSidechain:true` user seed. It is NOT gated out here on purpose - `list`'s\n        // per-subagent preview legitimately treats that seed as the subagent's \"first\n        // user message\", and in TOP-LEVEL transcripts a sidechain seed does not occur in\n        // any real corpus. Gating it would silently blank the subagent preview for zero\n        // real benefit; the per-surface scan owns subagent-vs-parent context instead."
        }
      ],
      "instrument": "`head -1 <a subagent transcript> | jq -r '[.isSidechain, .message.role] | @tsv'` must print `true` then `user`, `rg -c '\"isSidechain\":true' <a top-level transcript>` must be 0, and `csift list @<agent-id> --format json | jq -r .first_user` must be non-empty. Counting rule: one seed per subagent transcript.",
      "located": {
        "claude_code": null,
        "csift": "0.1.0",
        "source": "AGENTS.md section 3.3; SPEC.md section 1; src/model/predicates.rs isSidechain note"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "for each of three subagent lanes of one live session: csift show @<agent-id> --line 1 --raw | jq '{type,isSidechain,role:.message.role,agentId}'; for every top-level transcript: head -c 400 <file> | rg -q '\"isSidechain\":true' (count the files whose FIRST line carries the seed)",
          "observed": "The three sampled lanes were /fork children and their first line is a `type:\"fork-context-ref\"` record (agentId set, no isSidechain, no message), so the sidechain seed is the SECOND record on a fork child; 0 of 67 top-level transcripts start with an isSidechain:true seed.",
          "rule": "Line 1 of each transcript file only; a top-level file counts once regardless of later sidechain records.",
          "note": "Refined for the fork shape; the top-level half holds exactly (0 of 67)."
        }
      ]
    },
    {
      "id": "TURN-022",
      "area": "turn-boundary",
      "behavior": "When the user recalls a sent message with escape, edits it and re-sends, Claude Code leaves the ORIGINAL on disk: the draft and the delivered resend are separate `type:\"user\"` records sharing ONE non-null `parentUuid`, and only the last in file order was delivered. The shared parent is usually a turn-end anchor record - measured parents were a `stop_hook_summary` system record 133 times, `turn_duration` 83, `away_summary` 80, an interrupt marker 19 and an assistant record once.",
      "depends": "`superseded_draft_indices` keeps the LAST opener per `parentUuid` and marks the earlier siblings superseded, and `group_turn_indices_deduped` drops them entirely from turn numbering, so an edited-away message can never resurface as a phantom turn or leak its abandoned text into a neighbour. The signal is parent-uuid IDENTITY, not text similarity, which would miss the common prepend/insert edit; distinct real turns never share a parentUuid, and a record with a null/empty parent is never grouped.",
      "code": [
        {
          "path": "src/model/grouping.rs",
          "lines": "88-93",
          "snippet": "/// Indices of turn-opening records that are SUPERSEDED DRAFTS - an earlier sibling of a\n/// later turn-opener sharing the SAME non-null `parentUuid` (§6.4.1). This is the on-disk\n/// shape of the \"type a message, ESC-cancel / edit, resend\" loop (and any rewind that\n/// re-opens a turn from the same point): Claude Code appends every draft as its own\n/// `type:\"user\"` record, yet only ONE - the last in file order - was actually delivered to\n/// the model. The earlier siblings are abandoned drafts."
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "95-101",
          "snippet": "/// WHY last-in-file is the survivor (verified on real `~/.claude/projects` data): distinct\n/// real turns never share a `parentUuid` (each user turn is parented to the assistant\n/// message that preceded it), so same-parent openers are ALWAYS alternative versions of one\n/// logical turn; and across the corpus the last sibling's subtree is the one that reaches\n/// furthest toward the leaf (the live branch). A content-similarity heuristic would miss the\n/// common case where the user *prepended/inserted* text on the edit (`look…` → `take a closer look…`),\n/// so the parent-uuid identity - not text - is the load-bearing signal."
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "114-117",
          "snippet": "pub fn superseded_draft_indices<T>(\n    records: &[T],\n    rec: impl Fn(&T) -> &Record,\n) -> std::collections::HashSet<usize> {"
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "131-135",
          "snippet": "        // Keep the LAST opener per parent: when a new sibling appears, the previously-seen\n        // one for that parent becomes a superseded draft.\n        if let Some(prev) = latest.insert(parent, i) {\n            superseded.insert(prev);\n        }"
        },
        {
          "path": "src/model/grouping.rs",
          "lines": "140-153",
          "snippet": "/// [`group_turn_indices`] with esc-cancel / edit-resend DRAFT SUPPRESSION (§6.4.1): a\n/// superseded draft ([`superseded_draft_indices`]) is dropped ENTIRELY - it neither opens a\n/// turn nor folds in as a member - so a message the user edited away before sending can\n/// never resurface as a phantom turn (nor leak its abandoned text into a neighbour). This is\n/// the delimiter every session-operating surface (`turns` / `search` / `files` / `recover`)\n/// uses, so they stay byte-consistent on what counts as a turn.\n#[must_use]\npub fn group_turn_indices_deduped<T>(\n    records: &[T],\n    rec: impl Fn(&T) -> &Record,\n) -> Vec<Vec<usize>> {\n    let skip = superseded_draft_indices(records, |x| rec(x));\n    group_turn_indices_core(records, |x| rec(x).opens_turn(), &skip)\n}"
        }
      ],
      "instrument": "Group all turn-opening user records of one transcript by `parentUuid` and count groups of size greater than one (counting rule: drafts = group size minus one, one per group), then cross-validate against csift's own `N superseded draft(s)` footer. Measured draft rates over four sessions: 11% (62 genuine / 7 drafts), 26% (1,216 / 317), 6% (86 / 5), 3% (32 / 1).",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.8.2",
        "source": "AGENTS.md section 3.3; SPEC.md section 6.4.1; src/model/grouping.rs doc comment; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "csift search '' @<session> --no-subagents -t user.unsent --format json | jq 'select(.superseded_draft==true) | .hits[0].line' (first draft line), then sed -n '<line>p' <transcript> | jq -r .parentUuid and rg -c '\"parentUuid\":\"<that uuid>\"' <transcript>",
          "observed": "The first superseded draft of the live session (L7292) has parentUuid facd56a1... and exactly 2 records share that parentUuid: the draft and its delivered resend; the draft renders with superseded_draft:true and turn_index null.",
          "rule": "One draft record followed to its parent; sibling count = raw lines carrying the identical parentUuid value.",
          "note": "The draft/resend pair sharing one parentUuid is the C-18 shape csift labels user.unsent."
        }
      ]
    },
    {
      "id": "TURN-023",
      "area": "turn-boundary",
      "behavior": "Draft groups are n-way, not pairs, and their content shape varies: in one 317-draft sample 230 drafts carried STRING content and 87 carried text-BLOCK content, and up to 8 drafts shared a single `parentUuid` (group sizes reaching 9).",
      "depends": "csift's draft rule handles both content shapes and n-way groups rather than assuming a pair, and assigns the SINGLE leaf `user.unsent` at the SCAN layer - a pure per-record classify cannot see the LATER same-parent sibling that makes a record a draft - so `user.message` counts stay pure while the draft is still searchable, censusable, suppressed under a `--turn` window and always reachable by `show --line` / `--uuid` (JSON `superseded_draft:true` with a null `turn_index`).",
      "code": [
        {
          "path": "src/search/hits.rs",
          "lines": "172-176",
          "snippet": "    let labels = if superseded {\n        vec![Class::UserUnsent]\n    } else {\n        rec.classify(ctx)\n    };"
        },
        {
          "path": "src/model/taxonomy.rs",
          "lines": "44-51",
          "snippet": "    /// `user.unsent` - a SUPERSEDED turn-opener draft: sent, esc-recalled into the input\n    /// box, edited and re-sent, leaving the original on disk sharing the resend's\n    /// parentUuid. Assigned at the SCAN layer (the superseded set needs a LATER sibling,\n    /// which a pure per-record classify cannot see); outside turn numbering; 99% never\n    /// drew a reply. A recalled-then-ABANDONED message has no sibling and is\n    /// structurally undetectable; a QUEUED text edited before dispatch never becomes a\n    /// user record at all (it survives only in `queue-operation` lines).\n    UserUnsent,"
        }
      ],
      "instrument": "`csift search '' @<id> -t user.unsent --format json | jq -r '[.label, .turn_index] | @tsv'` - every row must read `user.unsent` with a null turn index - and record for each draft whether `message.content` is a JSON string or an array. Counting rule: one per superseded opener record, identified by a shared `parentUuid` with a later opener in the same file.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.9.2",
        "source": "AGENTS.md section 3.3; SPEC.md section 5.1; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' <project-dir> -t user.unsent --count-by label (draft count) and sed -n '<draft line>p' <transcript> | jq -r '.message.content|type' (content shape of a sampled draft)",
          "observed": "28 user.unsent records across the csift project dir; the sampled draft (L7292 of the live session) carries block-array content (an `array`), i.e. the text-block shape the claim lists beside the string shape.",
          "rule": "Draft count = records the scan-layer superseded-draft rule labels user.unsent over the project's top-level and subagent lanes; shape read off one sampled draft.",
          "note": "The 317-draft string-vs-block split (230/87, up to 8 per parent) is the 2026-09-01 measurement on another session and was not re-counted here; this check re-verified that both shapes occur (one block-shaped specimen) and the mechanism."
        }
      ]
    },
    {
      "id": "TURN-024",
      "area": "turn-boundary",
      "behavior": "Superseded drafts are overwhelmingly abandoned and are not in the surviving conversation: 99.1% never drew a reply (3 of 317 measured did), and Claude Code's own `compactMetadata.preservedMessages` accounting excludes every draft uuid (0 of 772 measured) while the conversation DAG threads through the resend sibling, never the draft.",
      "depends": "That exclusion is the measured instrument behind classifying `user.unsent` LLM-invisible, and it is also the wording law: csift says 'not in the surviving conversation', never 'the model never saw it', because a few drafts did draw real replies before the retraction. csift also refuses to use `preservedMessages` membership as a general visibility test.",
      "code": [
        {
          "path": "src/model/taxonomy.rs",
          "lines": "160-165",
          "snippet": "    /// - `user.unsent`: a superseded draft is NOT in the surviving conversation -\n    ///   Claude Code's own `compactMetadata.preservedMessages` accounting excludes\n    ///   every draft uuid (0 of 772 measured), and the conversation DAG threads\n    ///   through the resend sibling, never the draft. (Wording law: \"not in the\n    ///   surviving conversation\", never \"the model never saw it\" - a few drafts\n    ///   drew real replies before the retraction.)"
        }
      ],
      "instrument": "Take the draft uuids from `csift search '' @<id> -t user.unsent --format json` and test membership in the compaction boundary's `preservedMessages` array read via `csift show @<id> --uuid <boundary> --raw`. Counting rule: one membership test per draft uuid (expect 0 of N); reply rate = drafts with at least one assistant child over drafts tested.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.2",
        "source": "SPEC.md sections 5 and 5.1; src/model/taxonomy.rs llm_visible doc comment"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "csift search '' @<session> --no-subagents -t user.unsent --format json | jq '{superseded_draft, turn_index}' per hit (a draft is outside turn numbering); the 99.1% never-replied ratio needs the per-draft reply scan over the 317-draft session (rg for the draft uuid as a later parentUuid)",
          "observed": "Every draft hit in the live session carries superseded_draft:true and turn_index:null (3 of 3 sampled), i.e. outside the numbered conversation as the claim states; the never-replied ratio was not re-measured in this audit.",
          "rule": "Sampled hits of one session; the ratio stands as the 2026-09-01 measurement (3 of 317 drew a reply).",
          "note": "Mechanism re-verified; the percentage is carried forward from the prior measurement and marked as such."
        }
      ]
    },
    {
      "id": "TURN-025",
      "area": "turn-boundary",
      "behavior": "Two neighbouring shapes leave NO recoverable record. A message esc-recalled and never resent has no later same-parent sibling, so it is structurally undetectable. A queued text edited before dispatch never becomes a user record at all - it survives only in `queue-operation` lines: over human prose enqueued into the queue (n=782, enqueue lines only, whitespace-normalized text join) 72% match a later user record exactly and 81% match by prefix, so roughly 19-28% of typed queue text never becomes a user record.",
      "depends": "csift's `user.unsent` covers only SUPERSEDED drafts, never 'every unsent message'; the queue-only class is a separate, larger set reachable solely under the gated `user.queued` leaf, and csift documents the join rule and its counting basis instead of asserting a bare dispatched flag (no join key exists on the queue line).",
      "code": [
        {
          "path": "src/model/grouping.rs",
          "lines": "120-130",
          "snippet": "    for (i, item) in records.iter().enumerate() {\n        let r = rec(item);\n        if !r.opens_turn() {\n            continue;\n        }\n        let Some(parent) = r.parent_uuid.as_deref() else {\n            continue; // null parent: never grouped (would merge unrelated records)\n        };\n        if parent.is_empty() {\n            continue;\n        }"
        }
      ],
      "instrument": "Extract every `queue-operation` enqueue `content` and test whether the whitespace-normalized text appears as a later `type:\"user\"` record's string content or `text` block - `csift search '' @<id> -t user.queued --format json` against `-t user.message`. Counting rule: enqueue lines only, human prose only, one comparison per enqueued text; 'exact' = full-string equality, 'prefix' = the first 200 characters as a substring. An earlier figure of about 61% never-dispatched (54 of 88 texts, three-session sample) was measured over ALL queue operations and is superseded by the human-prose-only figures.",
      "located": {
        "claude_code": "2.1.252",
        "csift": "0.9.2",
        "source": "AGENTS.md section 3.3; SPEC.md section 5.1 and the v0.10.0 ledger; dev session 2026-08-31"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "queue half: csift search '' @<session> --no-subagents -t user.queued | rg -c 'popAll' (texts recalled into the input box); esc-recall-without-resend half: a live trial (send, esc, never resend) followed by a diff of the transcript before/after - structurally no record can appear",
          "observed": "8 popAll recalls in the live session's queue lines (texts that returned to the input box; a popAll'd text that was then edited never becomes a user record); the esc-recall-without-resend half was not trialed in this audit and cannot be observed from the corpus by construction.",
          "rule": "popAll count = queue-operation lines with operation popAll in one session; the negative half is a design statement, decidable only by a live trial.",
          "note": "One half observed (8 specimens), the other half is unobservable from disk by construction and is stated as such."
        }
      ]
    },
    {
      "id": "WIN-001",
      "area": "windows",
      "behavior": "A Windows session's projects-directory basename is letter-led rather than dash-led: the sanitizer applies no slash, case or colon pre-normalization, so the cwd `C:\\Users\\x` encodes to `C--Users-x` (the drive letter passes through; `:` and `\\` each become exactly one dash) and a UNC root `\\\\server\\share` encodes to `--server-share`.",
      "depends": "csift's `@`-token grammar has to recognize both shapes: `is_drive_encoded_token` accepts `<letter>--...` as an encoded project dir (bare or `@`-prefixed) while the UNC form is reachable only as `@--server-...`, because a bare `--`-leading token is reserved for the mistyped-flag guard; a drive-shaped token that matches no directory falls through to real-path resolution, since it can also be a genuine relative path. Without those arms a Windows project directory is unreachable by any target form.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "9-13",
          "snippet": "/// `replace(/[^a-zA-Z0-9]/g,\"-\")` runs per UTF-16 CODE UNIT: every unit outside ASCII\n/// alphanumerics becomes ONE `-` - so an astral char (two surrogate units) yields TWO\n/// dashes, and a Windows `C:\\Users\\x` yields `C--Users-x` (`:` and `\\` are one unit\n/// each). No dash collapsing, no case folding. A char-wise or byte-wise replacement\n/// DIVERGES from CC on any non-ASCII cwd and resolves the wrong dir."
        },
        {
          "path": "src/path/home.rs",
          "lines": "147-154",
          "snippet": "/// The Windows drive-letter encoded shape: `<letter>--…` (`C:\\Users\\x` → `C--Users-x`).\n/// Distinct from every other token class: a Unix encoded dir leads with `-`, an id is\n/// hex/uuid-shaped, and a real RELATIVE path named like this is disambiguated by the\n/// caller (encoded-dir lookup first, real-path fallthrough on a miss).\npub(crate) fn is_drive_encoded_token(token: &str) -> bool {\n    let b = token.as_bytes();\n    b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b'-' && b[2] == b'-'\n}"
        },
        {
          "path": "src/path/resolver.rs",
          "lines": "182-186",
          "snippet": "                // `@-Users-…` / `@C--Users-…` → an encoded project-dir name (a Unix cwd's\n                // leading `/` encodes to `-`; a Windows cwd's `C:\\` encodes to `C--`).\n                _ if id.starts_with('-') || is_drive_encoded_token(id) => {\n                    explicit_dirs.push(resolve_target(Path::new(id))?);\n                    session_target = true;"
        }
      ],
      "instrument": "Only a Claude Code session started on Windows produces the directory: run one and list `~/.claude/projects` - the basename must lead with the drive letter. Platform-independently the encoder is pinned by the csift unit tests in `src/path/tests/encode.rs` (`C--Users-dev-proj` from the drive form, plus the `--server-share-proj` UNC token) and the target grammar by `csift list @C--Users-dev-proj --claude-home <fixture>`. Counting rule: one output dash per non-alphanumeric UTF-16 code unit, so `C:\\` yields `C--`.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.3",
        "source": "SPEC.md section 2.1; AGENTS.md section 3.1"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{140}replace\\(/\\[\\^a-zA-Z0-9\\]/g,\"-\"\\).{0,200}'  +  node -e on the extracted regex  +  csift list @C--Users-dev-proj --claude-home <fixture>",
          "observed": "binary carries the sanitizer verbatim: 'var IL=200; function k(e){return e.replace(/[^a-zA-Z0-9]/g,\"-\")} function KA(e){let n=k(e); if(n.length<=IL) return n; return `${n.slice(0,IL)}-${be(e)}`}', exported as 'IL as MAX_SANITIZED_LENGTH'; running that exact regex gives C:\\Users\\x -> C--Users-x (10 chars), \\\\server\\share -> --server-share (14), /a/b/c_d -> -a-b-c-d, /a/.claude/x -> -a--claude-x; csift on a fixture home resolved 'C--Users-dev-proj' bare AND as '@C--Users-dev-proj' (both printed the session with cwd C:\\Users\\dev\\proj), resolved '@--server-share-proj', rejected a bare '--server-share-proj' with \"not a project target; did you mistype a flag? (a UNC-encoded dir is targeted as '@--server-share-proj')\", and fell through to real-path resolution for 'Z--nope-nope'",
          "rule": "one output dash per non-alphanumeric UTF-16 code unit, no collapsing, no case folding; one target form per csift invocation, 5 forms exercised",
          "note": "The encoder is decided without a Windows session because it is a pure function of the extracted regex: no slash, colon or case pre-normalization, and the drive letter survives because it is alphanumeric. The 200-char cap plus a hash suffix is present in the same function. All three csift code sites match verbatim at the claimed lines (src/path/home.rs 9-13 and 147-154, src/path/resolver.rs 182-186)."
        }
      ]
    },
    {
      "id": "WIN-002",
      "area": "windows",
      "behavior": "Claude Code places its `.claude` config home under the home directory resolved with Node `os.homedir()` semantics: `$HOME` on Unix, but ONLY `%USERPROFILE%` on Windows - the Windows branch never consults `HOME`, even though Git-Bash and MSYS shells export one, often a POSIX-style spelling a native process cannot use.",
      "depends": "csift's `home_dir()` is cfg-split the same way (Unix reads `HOME`, Windows reads `USERPROFILE`, with the standard library's home lookup as the fallback), so a stray Git-Bash `HOME` never sends csift to a `.claude` directory Claude Code does not write; honoring it would make every subcommand read an empty or nonexistent projects root.",
      "code": [
        {
          "path": "src/path/home.rs",
          "lines": "29-35",
          "snippet": "/// The user's home directory, resolved the way Claude Code itself resolves it (Node's\n/// `os.homedir()`): `$HOME` on Unix, `%USERPROFILE%` on Windows. The per-platform split is\n/// load-bearing on Windows - CC never consults `HOME` there, but Git-Bash/MSYS shells\n/// export one (often a POSIX-style `/c/Users/...` a native process cannot use), and\n/// honoring it would point csift at a `.claude` dir CC never writes. The conventional env\n/// var is read first so a test harness can relocate home per-subprocess; `std::env::home_dir`\n/// (un-deprecated, Windows-correct since Rust 1.85 - MSRV is above both) is the fallback."
        },
        {
          "path": "src/path/home.rs",
          "lines": "36-48",
          "snippet": "pub(crate) fn home_dir() -> Result<PathBuf> {\n    #[cfg(not(windows))]\n    if let Some(h) = std::env::var_os(\"HOME\") {\n        if !h.is_empty() {\n            return Ok(PathBuf::from(h));\n        }\n    }\n    #[cfg(windows)]\n    if let Some(h) = std::env::var_os(\"USERPROFILE\") {\n        if !h.is_empty() {\n            return Ok(PathBuf::from(h));\n        }\n    }"
        }
      ],
      "instrument": "In a Git-Bash shell on Windows with `HOME` set to a POSIX-style path, print both `HOME` and `USERPROFILE` and compare them against where `~/.claude/projects` actually exists; `csift list` must still read the profile-directory `.claude`. Only a Windows Git-Bash shell exposes the divergence. Counting rule: one resolution per shell.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.1",
        "source": "SPEC.md section 2.1; AGENTS.md section 7"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "unverifiable-here",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function s\\(\\)\\{return process\\.env\\.CLAUDE_CONFIG_DIR\\}var Se=.{0,80}'  +  HOME=<tmp> node -e 'console.log(require(\"os\").homedir())'  +  env -u CLAUDE_CONFIG_DIR HOME=<fixture> csift list",
          "observed": "binary: 'function s(){return process.env.CLAUDE_CONFIG_DIR}var Se=Zo(()=>(s()??i(R(),\".claude\")).normalize(\"NFC\"),s)' with 'import{homedir as R}' - so the config home is CLAUDE_CONFIG_DIR, else join(os.homedir(), \".claude\"), NFC-normalized; on this unix host os.homedir() returned the overridden HOME, and csift with HOME pointed at a fixture listed that fixture's session",
          "rule": "one home resolution per process; the divergence needs a shell where HOME and USERPROFILE differ, which only exists on Windows",
          "note": "The unix half is confirmed (Claude Code joins '.claude' onto os.homedir(); os.homedir() honors HOME on unix; csift agrees). The load-bearing half - that the Windows branch consults only %USERPROFILE% and never HOME - cannot be decided on this host: this binary is a macOS build and os.homedir() is native code. What would decide it: a Windows Git-Bash shell with HOME set to a POSIX-style spelling different from %USERPROFILE%; print both, then check which one ~/.claude/projects actually lives under and whether csift list reads it. Both csift code sites match verbatim (src/path/home.rs 29-35 and 36-48)."
        }
      ]
    },
    {
      "id": "WIN-003",
      "area": "windows",
      "behavior": "On Windows Claude Code ships a SEPARATE first-class tool literally named `PowerShell` beside `Bash`, `BashOutput` and `KillShell` in its tool registry; it carries the invocation in the same `input.command` field and is described as executing a given PowerShell command with an optional timeout.",
      "depends": "csift must treat it as a shell tool wherever a shell tool matters: `@trap` self-identification matches both names (a Bash-only gate left `@trap` blind exactly in the bashless Windows fallback), the background-task scan ingests `run_in_background` launches from both, and `--count-by tool` reports `PowerShell` as its own census key.",
      "code": [
        {
          "path": "src/path/trap.rs",
          "lines": "324-331",
          "snippet": "                        // Both SHELL tools carry the invocation in `input.command`: `Bash`\n                        // everywhere, and Windows' SEPARATE `PowerShell` tool (CC 2.1.228:\n                        // the fallback when Git-for-Windows bash is absent, or the gated\n                        // preference - same `command` field, verbatim from the binary's\n                        // tool registry). A Bash-only gate left @trap blind exactly on the\n                        // mandatory Windows fallback.\n                        if (n == \"Bash\" || n == \"PowerShell\")\n                            && inp"
        },
        {
          "path": "src/live/background_scan.rs",
          "lines": "52-57",
          "snippet": "                input: Some(input),\n                ..\n            } if (matches!(name.as_str(), \"Bash\" | \"PowerShell\")\n                && input\n                    .get(\"run_in_background\")\n                    .and_then(serde_json::Value::as_bool)"
        }
      ],
      "instrument": "Run `strings` over the installed Claude Code binary for the version under test and look for the tool-registry array holding `Bash`, `BashOutput`, `KillShell` and `PowerShell`, and for the tool description text `Executes a given PowerShell command`. On a Windows session, `csift search '' @<session> --count-by tool` lists `PowerShell` as its own key. Counting rule: one census key per tool `name`, counted over tool_use blocks.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.4",
        "source": "SPEC.md section 6 v0.7.4 ledger; AGENTS.md section 3.9; CHANGELOG 0.7.4"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg 'Executes a given PowerShell command'  and  rg '\"BashOutput\",\"KillShell\"'  +  csift search '' @<fixture-session> --claude-home <fixture> --count-by tool  +  csift whoami @trap:<marker> --claude-home <fixture>",
          "observed": "tool registry verbatim: 'var ymo=[\"Bash\",\"BashOutput\",\"KillShell\",\"PowerShell\",\"Tmux\",\"Monitor\",\"REPL\",\"Read\",...]'; description verbatim: 'Executes a given PowerShell command with optional timeout. Working directory persists between commands; shell state (variables, functions) does not.'; on a synthetic Windows-shaped transcript csift's census printed '2 Bash / 2 PowerShell / 2 Write' - PowerShell is its own key - and @trap resolved through a PowerShell tool_use whose input.command carried the marker",
          "rule": "one registry entry per tool name; one census key per tool name counted over tool_use blocks; one trap resolution per marker",
          "note": "PowerShell sits between KillShell and Tmux in the registry array, i.e. it is a first-class tool and not an alias. The same input.command field is asserted by the description text and by csift's trap resolving on it. Both csift code sites match verbatim (src/path/trap.rs 324-331, src/live/background_scan.rs 52-57)."
        }
      ]
    },
    {
      "id": "WIN-004",
      "area": "windows",
      "behavior": "The Windows shell tool is selected in a fixed order: CLAUDE_CODE_USE_POWERSHELL_TOOL as an explicit override (any defined value wins, not just a truthy one), else PowerShell is FORCED ON when the Git-for-Windows bash lookup returns null, else a named feature gate decides. The bash lookup itself has FOUR steps, not three: CLAUDE_CODE_GIT_BASH_PATH (accepted only when its basename is bash.exe/sh.exe/bash/sh AND the file exists, otherwise a warning is logged and auto-detection continues), then C:\\Program Files\\Git\\bin\\bash.exe, then the (x86) sibling, then a bash.exe derived from a git executable found on PATH (join(gitdir,'..','..','bin','bash.exe')).",
      "depends": "csift cannot assume a Windows transcript uses `Bash`: the bashless fallback is mandatory rather than optional, so every shell-tool consumer must accept both names or go blind on exactly the machines that have no bash installed.",
      "code": [
        {
          "path": "src/path/trap.rs",
          "lines": "322-331",
          "snippet": "                    } = b\n                    {\n                        // Both SHELL tools carry the invocation in `input.command`: `Bash`\n                        // everywhere, and Windows' SEPARATE `PowerShell` tool (CC 2.1.228:\n                        // the fallback when Git-for-Windows bash is absent, or the gated\n                        // preference - same `command` field, verbatim from the binary's\n                        // tool registry). A Bash-only gate left @trap blind exactly on the\n                        // mandatory Windows fallback.\n                        if (n == \"Bash\" || n == \"PowerShell\")\n                            && inp"
        }
      ],
      "instrument": "strings -n 6 <claude-versions-dir>/<version> and read the selection function and the bash-discovery function whole, rather than only checking that both env names appear; the fourth probe and the env-override validation are only visible in the full function body.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.4",
        "source": "SPEC.md section 6 v0.7.4 ledger; AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function Zk\\(\\)\\{.{0,200}'  and  rg -o 'function de\\(\\)\\{let\\{existsSync:e\\}=ce\\(\\).{0,900}'",
          "observed": "selection verbatim: 'function Zk(){let e=a.CLAUDE_CODE_USE_POWERSHELL_TOOL; if(D()!==\"windows\")return e===!0; if(e!==void 0)return e; if(B1()===null)return!0; return P(\"tengu_cobalt_ridge\",!1)}' and 'function cs(){if(D()!==\"windows\")return!0; return B1()!==null}' / 'function HD(){return cs()?\"bash\":\"powershell\"}'; bash discovery verbatim: env override validated by 'L.basename(...).toLowerCase()' being in ['bash.exe','sh.exe','bash','sh'] AND existsSync, else a warn 'CLAUDE_CODE_GIT_BASH_PATH \"...\" not found / is not a bash/sh binary; falling back to auto-detection'; then r=['C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe','C:\\\\Program Files (x86)\\\\Git\\\\bin\\\\bash.exe']; then a which-style lookup of 'git' joined as join(n,'..','..','bin','bash.exe'); returns null if none exist",
          "rule": "one probe per candidate path, tried in source order; presence of both env names on the selection path",
          "note": "The three-step selection order in the claim is exactly right. Two refinements: (1) the env override is honored whenever it is DEFINED (the code returns its value, so an explicit falsey value forces the tool OFF, it does not fall through); (2) the git-bash probe has a fourth step - deriving bash.exe from a git on PATH - and the env path is validated by basename and existence with a warn-and-continue, so a bad CLAUDE_CODE_GIT_BASH_PATH does not by itself force PowerShell on. The forced-on branch still cannot be executed here; it needs a Windows host with no Git-for-Windows bash. The csift code site matches verbatim (src/path/trap.rs 322-331)."
        }
      ]
    },
    {
      "id": "WIN-005",
      "area": "windows",
      "behavior": "The Windows `Bash` tool runs the real Git-for-Windows (MSYS2) bash, so the command text on those records is POSIX syntax; the `PowerShell` tool's command text is not, and no lexical bash analysis is valid on it.",
      "depends": "csift's bash-lexical layers stay valid on Windows `Bash` records but deliberately do NOT run on `PowerShell` records: the dangerous-rm escalation classifier and the bash mutation attribution both skip them, so a pending PowerShell lane classifies `awaiting-execution` rather than escalation-blocked, and PowerShell shell-side file mutations surface as opaque rows instead of attributed ones. Structured Read/Write/Edit attribution is unaffected.",
      "code": [
        {
          "path": "src/bash_danger.rs",
          "lines": "84-91",
          "snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
        },
        {
          "path": "src/recover/scan.rs",
          "lines": "366-374",
          "snippet": "                } else if name == \"PowerShell\" && cmd.is_some() {\n                    out.push(OpaqueCommand {\n                        session_id: session_id.to_string(),\n                        line_no,\n                        turn_index,\n                        timestamp_utc: rec.timestamp.clone(),\n                        marker: \"powershell\".to_string(),\n                    });\n                }"
        }
      ],
      "instrument": "On a Windows session, `csift files @<session> --by timeline` shows structured Read/Write/Edit rows and zero bash-heuristic rows for PowerShell commands, while `csift recover` reports those commands in its opaque per-window accounting under the `powershell` marker. Counting rule: one opaque row per PowerShell tool_use carrying a command.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.4",
        "source": "AGENTS.md section 3.9; SPEC.md section 6 v0.7.4 ledger"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "holds",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o '.{40}Using bash path.{0,200}'  +  csift agents @<fixture-session> --claude-home <fixture> --format json  +  csift files @<fixture-session> --by timeline  +  csift recover @<fixture-session> --file 'C:\\Users\\dev\\proj\\src\\app.ts' --coverage",
          "observed": "binary verbatim: 'function tkt(){if(D()===\"windows\"){let e=B1(); if(e)process.env.SHELL=e,t(`Using bash path: \"${e}\"`); else t(\"Git Bash not found; BashTool will be unavailable\")}}' - the Windows Bash tool's shell IS the discovered Git-for-Windows bash, and without it the tool is withdrawn. On two sibling subagent lanes each ending in an unreturned shell tool_use, csift classified {'pending_tool_name':'PowerShell','pending_classification':'awaiting-execution'} and {'pending_tool_name':'Bash','pending_classification':'escalation-blocked'} for the equivalent recursive-force delete. files --by timeline listed exactly 1 mutation (the structured Write) and zero rows for the PowerShell Set-Content. recover --coverage printed 'opaque in window: 1 PowerShell command(s), never parsed' with the row 'L2  turn 0  ...  powershell'.",
          "rule": "one pending classification per frozen lane; one mutation row per attributed operand; one opaque row per PowerShell tool_use carrying a command",
          "note": "The lexical-bash split is instrumented directly rather than argued: the SAME shape of destructive command classifies escalation-blocked under Bash and awaiting-execution under PowerShell, and a PowerShell write idiom that the bash heuristics would have attributed instead surfaces as an opaque 'powershell' row. Both csift code sites match verbatim (src/bash_danger.rs 84-91, src/recover/scan.rs 366-374)."
        }
      ]
    },
    {
      "id": "WIN-006",
      "area": "windows",
      "behavior": "Claude Code writes Windows paths into structured tool fields in native form (`C:\\...`), so on a Windows transcript the absolute path shapes are a leading `/`, a `<letter>:` drive prefix, or a `\\\\server\\...` UNC prefix - and a Windows `Bash` record's POSIX-style command text is therefore not joinable with the native `filePath` values of the same session.",
      "depends": "csift's `is_absolute_shell_path` accepts all three shapes before deciding whether to join an operand to the carrying record's `cwd` (treating `C:\\...` as relative would fabricate a path in every `files`/`recover` row), and `join_shell_path` joins in the BASE's separator family - a Windows-family base with `\\`, a unix base with `/` - so host path semantics never leak into the analysis and a transcript from either platform is readable on either platform.",
      "code": [
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "206-215",
          "snippet": "/// True for a path the shell treats as absolute: unix-rooted, a Windows drive form\n/// (`C:\\` or `C:/`), or a UNC `\\\\server\\...` prefix.\n#[must_use]\npub fn is_absolute_shell_path(p: &str) -> bool {\n    if p.starts_with('/') || p.starts_with(\"\\\\\\\\\") {\n        return true;\n    }\n    let b = p.as_bytes();\n    b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && matches!(b[2], b'/' | b'\\\\')\n}"
        },
        {
          "path": "src/bash_mutations/cwd.rs",
          "lines": "217-221",
          "snippet": "/// Join path fragments onto an absolute base and normalize `.`/`..` lexically, as pure\n/// string work in the BASE's separator family. Transcripts are analyzed on any host, so\n/// host `Path` semantics must never leak in: a Windows-family base joins with `\\` and\n/// converts `/` in the fragments; a unix base joins with `/`. `..` never pops past the\n/// root (the lexical-normalize convention used elsewhere in the crate)."
        }
      ],
      "instrument": "The unit test `is_absolute_shell_path_covers_all_three_families` in `src/bash_mutations/tests/cwd.rs` pins the drive, UNC and unix families (and rejects a driveless `C:relative`); on a real Windows session, `csift files @<session> --format json` rows must carry drive-led paths with an explicit resolution class and no cwd-joined fabrication. Counting rule: one resolution class per mutation operand.",
      "located": {
        "claude_code": "2.1.228",
        "csift": "0.7.4",
        "source": "AGENTS.md section 3.9; dev session measurement of structured path shapes"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "unverifiable-here",
          "instrument": "csift files @<fixture-session> --claude-home <fixture> --by timeline --format json, run twice against two fixtures identical except for the record cwd (C:\\Users\\dev\\proj vs /home/dev/proj), each with the same relative bash redirect (a printf into src/out.txt)  +  ls ~/.claude/projects | grep -cE '^[A-Za-z]--'",
          "observed": "windows-family base produced {'path':'C:\\\\Users\\\\dev\\\\proj\\\\src\\\\out.txt','op':'bash','resolution':'cwd-joined'} and the unix base {'path':'/home/dev/proj/src/out.txt','op':'bash','resolution':'cwd-joined'} - same macOS host, separator family taken from the base, not the host; a structured Write of C:\\Users\\dev\\proj\\src\\app.ts passed through verbatim with resolution null (no cwd join, no fabrication). Corpus: 0 of 15 project directories are drive-letter-led, so no real Windows transcript exists here.",
          "rule": "one resolution class per mutation operand; one directory per listing entry",
          "note": "The csift half is instrumented and behaves as claimed. The Claude Code half - that a Windows session writes native C:\\... into the record cwd and into structured tool fields such as toolUseResult.filePath, and that the Windows Bash tool's POSIX command text is therefore not joinable with them - cannot be decided on this host: there is no Windows transcript in the corpus and the fixtures above are hand-written, so they prove csift's handling, not Claude Code's wire shape. What would decide it: one transcript under a drive-letter-led project directory; read its top-level cwd field and one Bash record's input.command side by side. Both csift code sites match verbatim (src/bash_mutations/cwd.rs 206-215 and 217-221), and the named unit test is present in src/bash_mutations/tests/cwd.rs."
        }
      ]
    },
    {
      "id": "WIN-007",
      "area": "windows",
      "behavior": "A session registry row carries the owner process's creation instant under one of TWO optional keys: `procStart` (an asctime-like UTC string, `Sun Aug 16 09:04:23 2026`) or `procStartFt` (a FILETIME integer - 100-nanosecond ticks since 1601-01-01, e.g. `134328101803820142`). The row schema declares both optional, a platform-conditional splitter writes the value to exactly one of them, and every reader in the binary resolves `procStartFt ?? procStart`. The rendering is not pinned to the key: unix rows carry the asctime form under `procStart`, and a Windows 11 ARM64 session measured at 2.1.258 wrote its FILETIME under `procStart` as well.",
      "depends": "csift reads `procStartFt` first and falls back to `procStart`, then parses EITHER rendering into an instant before comparing it with the probed process start (an all-digits value is the FILETIME form, otherwise the asctime parse), so the pid-reuse guard works whichever key and whichever rendering a row carries; reading a single key, or parsing a single format, would silently degrade every Windows row to a disclosed pid-only probe.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "13-21",
          "snippet": "//! `procStart` is the OWNER PROCESS's creation instant in a PLATFORM-SPECIFIC rendering:\n//! on unix an asctime string in UTC (`Sun Aug 16 09:04:23 2026`), on Windows a FILETIME\n//! integer (100ns ticks since 1601-01-01, e.g. `134328101803820142`). `pidDomain` names\n//! the pid space the row was written in (`darwin`, `linux`, or `win32:<hostname>`); a row\n//! from another domain cannot be probed here and the verdict says so. `ps lstart` renders\n//! in the LOCAL zone - a naive string/local comparison flags pid reuse on EVERY row.\n//! Parse both sides to instants and compare with a small tolerance; when either side is\n//! absent or unparseable, degrade to a pid-only probe AND say so in the evidence (the\n//! reuse guard was skipped, honest, never silent)."
        },
        {
          "path": "src/live/registry.rs",
          "lines": "175-188",
          "snippet": "/// Parse the registry's `procStart`: an asctime-like UTC string (`Sun Aug 16 09:04:23\n/// 2026`, unix) or a FILETIME integer (`134328101803820142`, Windows: 100ns ticks since\n/// 1601). `None` on any mismatch - the caller degrades to pid-only + a note.\npub(crate) fn parse_registry_proc_start(s: &str) -> Option<jiff::Timestamp> {\n    let s = s.trim();\n    if !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) {\n        return filetime_to_timestamp(s.parse::<u64>().ok()?);\n    }\n    let bd = jiff::fmt::strtime::parse(\"%a %b %e %H:%M:%S %Y\", s).ok()?;\n    let dt = bd.to_datetime().ok()?;\n    dt.to_zoned(jiff::tz::TimeZone::UTC)\n        .ok()\n        .map(|z| z.timestamp())\n}"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "190-199",
          "snippet": "/// A Windows FILETIME (100ns ticks since 1601-01-01 UTC) as an instant; `None` when the\n/// value cannot be a real creation time (before the unix epoch or absurdly far out).\npub(crate) fn filetime_to_timestamp(ticks: u64) -> Option<jiff::Timestamp> {\n    let secs = i64::try_from(ticks / 10_000_000).ok()? - FILETIME_UNIX_OFFSET_SECS;\n    if secs < 0 {\n        return None;\n    }\n    let nanos = i32::try_from((ticks % 10_000_000) * 100).ok()?;\n    jiff::Timestamp::new(secs, nanos).ok()\n}"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "72-77",
          "snippet": "            // The 2.1.258 schema carries TWO keys and every reader in the binary is\n            // `procStartFt ?? procStart` (a platform splitter writes exactly one of\n            // them; the Windows session measured live wrote the FILETIME under\n            // `procStart`, so both spellings are read and both renderings parse).\n            proc_start: str_field(\"procStartFt\").or_else(|| str_field(\"procStart\")),\n            pid_domain: str_field(\"pidDomain\"),"
        }
      ],
      "instrument": "Read `procStart` from every row under `~/.claude/sessions/*.json` and classify it: an all-ASCII-digit value is the Windows FILETIME rendering, anything else is the unix asctime form. Measured 2026-09-02 on this machine: 9 rows, 9 asctime, 0 FILETIME (a unix host). Counting rule: one classification per row file; a FILETIME row requires a registry written by a Windows session.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "src/live/registry.rs module doc; SKILL.md status/wait section"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "drifted",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function OU\\(e\\)\\{.{0,120}' ; rg -o 'async function m\\(e,t\\)\\{let n=t===void 0.{0,900}' ; rg -c 'ToFileTimeUtc|CreationDate|Get-CimInstance'  +  python3 json read of procStart across ~/.claude/sessions/*.json",
          "observed": "the persisted schema carries TWO keys: '{peerToken:..., procStart: optional string, procStartFt: optional string, pidDomain: optional string}', split by 'function OU(e){return c()?{procStart:void 0,procStartFt:e}:{procStart:e,procStartFt:void 0}}' and read back everywhere as 'r.procStartFt ?? r.procStart'; the <pid>.json row reader accepts both ('procStart: typeof o.procStart===\"string\"?o.procStart:void 0, ...typeof o.procStartFt===\"string\"&&{procStartFt:o.procStartFt}'). The ONLY proc-start producer in the binary is ps: two call sites, both 'ps -o lstart= -p <pid>' with env LC_ALL=C, TZ=UTC. ToFileTimeUtc 0 hits, CreationDate 0 hits, Get-CimInstance 0 hits; the 4 Get-Process and 6 tasklist hits are a PowerShell alias table, a permission-prompt placeholder, an IDE-detection command and a command safe-flag allowlist - none is a process-start probe. On disk: 8 rows, 8 asctime procStart values, 0 all-digit values, 0 rows carrying procStartFt.",
          "rule": "one classification per row file (all-ASCII-digit = FILETIME rendering, else asctime); one producer per distinct proc-start call site in the binary",
          "note": "Current Claude Code does not put a FILETIME under 'procStart'. It carries a SECOND key, 'procStartFt', beside it, and a platform-conditional splitter writes the value to exactly one of them - on the FILETIME platform the value goes to procStartFt and procStart is left undefined, and the paired reader explicitly REFUSES a procStart value there ('if(c())return e.procStart!==void 0?void 0:e.procStartFt'). Every reader in the binary is 'procStartFt ?? procStart'. csift reads only 'procStart' (src/live/registry.rs registry_row_for), so its all-digit FILETIME branch in parse_registry_proc_start cannot fire on a current Windows row and the reuse guard would silently degrade to pid-only there. Separately, no FILETIME producer exists anywhere in this build - the only acquisition is ps -o lstart= - so the FILETIME rendering may now be reachable only on code paths this macOS build does not contain; that half stays undecided here. All three csift code sites still match verbatim (src/live/registry.rs 13-21, 171-184, 186-195), so the drift is in the module doc's field name and platform story, not in a moved snippet. What would settle the remainder: one ~/.claude/sessions/<pid>.json written by a Windows session - read whether it carries procStart, procStartFt, or neither."
        }
      ]
    },
    {
      "id": "WIN-008",
      "area": "windows",
      "behavior": "A session registry row names the pid space it was written in via pidDomain, whose head is the platform token and whose tail disambiguates the pid space: bare 'darwin' on macOS (and on any host that is neither linux, wsl nor windows), 'win32:<hostname lowercased>' on Windows, and 'linux:<contents of /etc/machine-id>:<readlink of /proc/self/ns/pid>' on linux and wsl. The field is absent on older rows.",
      "depends": "csift compares the row's domain head against the local one and returns a foreign-domain verdict WITHOUT probing when they differ - a pid from another machine or operating system means nothing locally, so `stale-dead` is undecidable and the evidence row says exactly that instead of guessing.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "109-122",
          "snippet": "/// The pid domain this csift runs in, in the registry's own vocabulary (the prefix\n/// before any `:<hostname>` suffix).\n#[must_use]\npub(crate) fn local_pid_domain() -> &'static str {\n    if cfg!(target_os = \"macos\") {\n        \"darwin\"\n    } else if cfg!(target_os = \"linux\") {\n        \"linux\"\n    } else if cfg!(windows) {\n        \"win32\"\n    } else {\n        \"unknown\"\n    }\n}"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "134-139",
          "snippet": "    if let Some(d) = pid_domain {\n        let head = d.split(':').next().unwrap_or(d);\n        if head != local_pid_domain() {\n            return PidLiveness::ForeignDomain(d.to_string());\n        }\n    }"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "205-212",
          "snippet": "            PidLiveness::ForeignDomain(d) => (\n                format!(\"row from another pid domain ({d})\"),\n                Some(\n                    \"the registry row was written in another pid domain (another machine \\\n                     or OS): its pid means nothing here - stale-dead is undecidable\"\n                        .to_string(),\n                ),\n            ),"
        }
      ],
      "instrument": "Read pidDomain from every row under ~/.claude/sessions/*.json (measured 2026-09-02: 8 rows, 5 'darwin', 3 absent), and read the producer out of the binary for the platform branches this host cannot write. Counting rule: one value per row file; a win32 or linux value requires a row written on that platform.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.10.1",
        "source": "src/live/registry.rs module doc; SKILL.md status/wait section"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 | rg -o 'function YG\\(\\)\\{return wie\\.pidDomain.{0,420}' ; rg -o '[^.a-zA-Z]process\\.platform' | wc -l ; rg -o 'process\\.env\\.' | wc -l  +  python3 json read of pidDomain across ~/.claude/sessions/*.json",
          "observed": "producer verbatim: 'function YG(){return wie.pidDomain??=(async()=>{let n=D(); if(n===\"windows\")return `darwin:${e().toLowerCase()}`; if(n!==\"linux\"&&n!==\"wsl\")return\"darwin\"; let[t,r]=await Promise.all([a(\"/etc/machine-id\",\"utf8\").then((i)=>i.trim(),()=>\"\"),u(\"/proc/self/ns/pid\").catch(()=>\"\")]); return `darwin:${t}:${r}`})()...}' with 'import{hostname as e}from\"os\"'. The literal 'darwin' is the build-inlined platform constant: this build has 0 bare process.platform and 0 bare process.arch occurrences in code against 836 process.env. occurrences, while two globalThis.process.platform member expressions survive - the signature of a targeted build-time define on the bare identifier only. On disk: 8 rows, 5 'darwin', 3 with no pidDomain field.",
          "rule": "one value per row file; one branch per platform in the producer; the define is established by counting bare vs globalThis-qualified process.platform occurrences",
          "note": "The claim's Windows form is right, but its vocabulary list is wrong for linux: a linux or wsl row is NOT bare 'linux', it is a three-segment 'linux:<machine-id>:<pid-namespace>'. This does not break csift, which compares only the head before ':' (local_pid_domain plus the split in src/live/registry.rs 130-135), but the module doc and the SKILL wording should say so. The row counts also moved since the ledger was written (9 rows / 6 darwin then, 8 rows / 5 darwin now - registry rows are pid-keyed and get swept). The win32 head itself is inferred from the build-define rather than read off a Windows row; what would settle it directly is one ~/.claude/sessions/<pid>.json written by a Windows session. All three csift code sites match verbatim (src/live/registry.rs 105-118 and 130-135, src/live/verdict.rs 205-212)."
        }
      ]
    },
    {
      "id": "WIN-009",
      "area": "windows",
      "behavior": "Process liveness has no portable probe. On unix `ps -p PID -o lstart=` answers both existence and start time, except that busybox ps rejects `-p`/`lstart` outright and so fails even for a LIVE pid, where Linux `/proc/<pid>` answers liveness directly. Windows has no such `ps` at all: existence plus the creation FILETIME come from a PowerShell `Get-Process -Id` query, and `tasklist` answers existence alone.",
      "depends": "csift's probe is a platform chain that never fabricates a verdict: unix tries `ps` then `/proc`; Windows tries PowerShell `Get-Process` (start time via `ToFileTimeUtc`, a `NOSTART` sentinel when another user's process hides it) then `tasklist`; and when no probe tool exists - including on a host that is neither unix nor Windows - the verdict is `Unavailable`, stating that pid liveness cannot be checked here rather than reporting a live session `stale-dead`.",
      "code": [
        {
          "path": "src/live/registry.rs",
          "lines": "97-99",
          "snippet": "    /// No process probe exists on this host (neither `ps`, `/proc`, PowerShell nor\n    /// `tasklist` answered): the verdict must say so.\n    Unavailable,"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "213-222",
          "snippet": "    if !out.status.success() || text.is_empty() {\n        // busybox ps (Alpine and friends) rejects `-p`/`lstart` outright, so the probe\n        // fails for a LIVE pid too. On Linux `/proc/<pid>` answers liveness directly:\n        // present = alive with the start time unknown (the reuse-guard skip is\n        // disclosed); absent (or no /proc at all, as on macOS where the ps form is\n        // reliable) = the no-such-process verdict stands.\n        if std::path::Path::new(&format!(\"/proc/{pid}\")).is_dir() {\n            return PsProbe::Alive(None);\n        }\n        return PsProbe::NoProcess;"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "237-244",
          "snippet": "/// Windows: one PowerShell call answers both questions - `Get-Process -Id` fails (exit\n/// 3 by our script) for a missing pid, and `StartTime.ToFileTimeUtc()` yields the\n/// creation FILETIME (the registry's own rendering) when the process is ours to inspect;\n/// `NOSTART` when the start time is not readable (another user's process) = alive, guard\n/// skipped. When PowerShell cannot be spawned, `tasklist` answers liveness alone; when\n/// neither tool exists the probe is unavailable and the verdict says so.\n#[cfg(windows)]\npub(crate) fn ps_probe(pid: u32) -> PsProbe {"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "264-270",
          "snippet": "    // PowerShell missing or broken: tasklist answers liveness (no start time).\n    let Ok(out) = std::process::Command::new(\"tasklist\")\n        .args([\"/FI\", &format!(\"PID eq {pid}\"), \"/NH\", \"/FO\", \"CSV\"])\n        .output()\n    else {\n        return PsProbe::Unavailable;\n    };"
        },
        {
          "path": "src/live/registry.rs",
          "lines": "279-282",
          "snippet": "#[cfg(not(any(unix, windows)))]\npub(crate) fn ps_probe(_pid: u32) -> PsProbe {\n    PsProbe::Unavailable\n}"
        },
        {
          "path": "src/live/verdict.rs",
          "lines": "213-216",
          "snippet": "            PidLiveness::Unavailable => (\n                \"probe unavailable on this host\".to_string(),\n                Some(\"pid liveness cannot be checked here - stale-dead is undecidable\".to_string()),\n            ),"
        }
      ],
      "instrument": "On macOS ps -p $$ -o lstart= prints the LOCAL-zone instant in the '%a %e %b %H:%M:%S %Y' field order (day of month before month), which is csift's SECOND parse format - the registry's own procStart, written with TZ=UTC, uses the '%a %b %e ...' order instead. A probe test that pins only one order will pass on one platform and silently fall back to Alive(None) on the other. A missing pid is reported by a non-zero ps exit, not an empty stdout.",
      "located": {
        "claude_code": null,
        "csift": "0.10.1",
        "source": "src/live/registry.rs probe comments; SKILL.md status/wait section"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "ps -p $$ -o lstart= ; test -d /proc ; ps -p 999999 -o lstart= ; echo $?  +  csift status @main (reading the pid evidence row)",
          "observed": "ps -p $$ -o lstart= printed 'Wed  2 Sep 19:59:08 2026' and exited 0; /proc is absent on this host; ps -p 999999 -o lstart= printed 'ps: process id too large: 999999' and exited 1, so the probe reaches csift's no-such-process arm via the non-zero exit rather than an empty success. csift status @main printed 'verdict running / registry status busy (pid <n>) / pid alive (start-time guard matched)' - the unix ps arm ran end to end and its local-zone reading matched the registry's UTC asctime procStart.",
          "rule": "one probe outcome per platform; the pid evidence row names exactly one outcome and one guard state per status run",
          "note": "The unix arm is instrumented end to end, including the cross-rendering comparison the module doc describes (local-zone ps output against a UTC-rendered registry value, guard reported as matched). Two arms were NOT executed on this host and remain code-only: the busybox fallback (needs a shell whose ps rejects -p/lstart, with /proc present) and the whole Windows chain (PowerShell Get-Process with the NOSTART sentinel, then tasklist, then Unavailable). All six csift code sites match verbatim (src/live/registry.rs 93-95, 209-218, 233-240, 260-266, 275-278 and src/live/verdict.rs 213-216)."
        }
      ]
    },
    {
      "id": "WIN-010",
      "area": "windows",
      "behavior": "No Windows Claude Code session exists in this development corpus: a scan finds ZERO project directories with a drive-letter-led encoded name and ZERO transcripts carrying a `PowerShell` tool_use, so every PowerShell-side wire shape csift handles is derived from the Claude Code binary rather than observed on disk.",
      "depends": "csift's PowerShell handling is honest-skip by construction - a pending PowerShell lane classifies `awaiting-execution` and its command text is never lexically parsed, surfacing instead as an opaque `powershell` marker in recover's per-window accounting - but the background-launch grammar it assumes for PowerShell (the same `run_in_background` input flag and result text as Bash) is untested against real data.",
      "code": [
        {
          "path": "src/recover/scan.rs",
          "lines": "366-374",
          "snippet": "                } else if name == \"PowerShell\" && cmd.is_some() {\n                    out.push(OpaqueCommand {\n                        session_id: session_id.to_string(),\n                        line_no,\n                        turn_index,\n                        timestamp_utc: rec.timestamp.clone(),\n                        marker: \"powershell\".to_string(),\n                    });\n                }"
        }
      ],
      "instrument": "ls ~/.claude/projects | grep -cE '^[A-Za-z]--' and csift search PowerShell -t agent.tool.use --count-by tool. Re-measured 2026-09-02 with csift 0.10.0: 0 drive-letter-led directories out of 15, and 0 PowerShell census keys among 219 matched records across 11 tool keys, over 7603 transcripts in scope (66 top-level, 7537 subagent). Counting rule: one directory per listing entry; one census key per tool name.",
      "located": {
        "claude_code": "2.1.258",
        "csift": "0.7.4",
        "source": "dev session 2026-09-02"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "ls ~/.claude/projects | grep -cE '^[A-Za-z]--' ; ls ~/.claude/projects | wc -l  +  csift search PowerShell -t agent.tool.use --count-by tool  +  csift list --format json (header row)",
          "observed": "0 drive-letter-led directories out of 15 project directories; the census printed 91 Bash / 63 Edit / 23 Write / 22 StructuredOutput / 7 WebFetch / 4 WebSearch / 2 Agent / 2 SendMessage / 2 TaskCreate / 2 Workflow / 1 Monitor and the footer '219 matched record(s) across 11 tool key(s)' - no PowerShell key, so every hit is prose mentioning the word while riding some other tool; list reported sessions_in_scope 7603 (66 top-level, 7537 subagent)",
          "rule": "one directory per listing entry; one census key per tool name, counted over tool_use blocks in records matching the pattern",
          "note": "The finding stands - no Windows Claude Code session exists in this corpus, so every PowerShell-side wire shape csift handles is still derived from the binary rather than observed on disk. Only the numbers moved: 219 matched records rather than 190 (same 11 tool keys), because the corpus grew during the day the ledger was written. The corpus size is worth carrying with the claim so a later re-run can tell growth from a real Windows session appearing. The csift code site matches verbatim (src/recover/scan.rs 366-374)."
        }
      ]
    },
    {
      "id": "WIN-011",
      "area": "windows",
      "behavior": "Claude Code 2.1.258's dangerous-rm classifier strips `$(...)` and non-`$` `(...)` groups to a FIXPOINT and, on a separate tree-sitter pass, refuses to analyze a command carrying more than 64 substitution nodes: when the command text also matches `/\\brm(?:dir)?\\b/` it returns `behavior:\"ask\"` with `classifierApprovable:false` and `circuitBreaker:\"dangerousRemoval\"` (explicit approval, not bypassable), and otherwise returns no verdict. The counted node set is command_substitution plus process_substitution plus `${ ... }` funsub-shaped expansion/ERROR nodes, so the user-facing count labelled `command substitutions` is broader than `$( ... )` alone.",
      "depends": "Only the >64 bail is a real port divergence: csift's `dangerous_rm` has no substitution-count gate, so on a command with more than 64 substitutions and an `rm`/`rmdir` word but no bare `$VAR/...` target, Claude Code hoists to explicit approval while csift predicts `awaiting-execution` rather than `escalation-blocked`. The fixpoint strip is NOT a divergence -- the port implements the same fixpoint loop. Both facts are disjoint from the PowerShell lane, on which this lexical-bash classifier never runs.",
      "code": [
        {
          "path": "src/bash_danger.rs",
          "lines": "84-91",
          "snippet": "/// STALENESS NOTE (binary evidence, 2026-08-12): CC 2.1.228's classifier (`aLa`) has\n/// EVOLVED past the 2.1.x generation this port mirrors - it strips `$(…)` groups to a\n/// FIXPOINT (this port is single-pass), and a tree-sitter pass bails to explicit approval\n/// when a command carries >64 command substitutions. csift's escalation-blocked prediction\n/// can therefore diverge from current CC on those shapes; a port refresh is a recorded\n/// follow-up, not silent drift. (CC also ships a separate Windows `PowerShell` tool; this\n/// lexical-bash classifier deliberately does NOT run on PowerShell commands - a pending\n/// PowerShell lane classifies awaiting-execution.)"
        },
        {
          "path": "src/bash_danger.rs",
          "lines": "155-166",
          "snippet": "fn strip_paren_groups(s: &str) -> String {\n    let mut n = s.to_string();\n    loop {\n        let prev = n.clone();\n        n = DOLLAR_PAREN.replace_all(&n, \" \").into_owned();\n        n = remove_plain_groups(&n);\n        if n == prev {\n            break;\n        }\n    }\n    n\n}"
        }
      ],
      "instrument": "Run `strings` over the installed Claude Code binary, locate the classifier's command-substitution handling, and compare it against `src/bash_danger.rs`: the fixpoint strip loop and the 64-substitution bail are present in the binary and absent from the port. Counting rule: one comparison per classifier generation.",
      "located": {
        "claude_code": "2.1.228",
        "csift": null,
        "source": "src/bash_danger.rs header comment; AGENTS.md section 3.9"
      },
      "first_seen_claude_code": null,
      "checks": [
        {
          "claude_code": "2.1.258",
          "csift": "0.10.1",
          "date": "2026-09-02",
          "verdict": "refined",
          "instrument": "strings -n 6 ~/.local/share/claude/versions/2.1.258 > /tmp/cc258.strings.txt; rg -o 'function hnt\\(e\\)\\{if\\(!e\\.includes.{0,700}' /tmp/cc258.strings.txt; rg -o 'if\\(d\\(e\\),o\\.length>64\\)\\{if\\(/\\\\brm\\(\\?:dir\\)\\?\\\\b/\\.test\\(e\\.text\\)\\).{0,120}' /tmp/cc258.strings.txt; rg -o 'function sF\\(e,n,r\\)\\{return\\{behavior.{0,220}' /tmp/cc258.strings.txt; rg -o 'yto=/\\^.{0,120}' /tmp/cc258.strings.txt; strings -n 6 ~/.local/share/claude/versions/2.1.229 | rg -c 'too many to analyze for catastrophic removals'; rg -c '64' src/bash_danger.rs; sed -n '84,91p;155,166p' src/bash_danger.rs",
          "observed": "2.1.258 binary, classifier `hnt`: fixpoint strip present verbatim -- `for(let o=\"\";o!==r;)o=r,r=r.replace(/\\$\\([^()]*\\)/g,\" \").replace(/(?<!\\$)\\([^()]*\\)/g,\" \");`. Substitution-count bail present verbatim -- `if(d(e),o.length>64){if(/\\brm(?:dir)?\\b/.test(e.text))return sF(\"rm\",`This command contains ${o.length} command substitutions - too many to analyze for catastrophic removals. This requires explicit approval.`,`- too many command substitutions to analyze (${o.length})`);return null}`. The bail's return builder is `function sF(e,n,r){return{behavior:\"ask\",message:n,decisionReason:{type:\"safetyCheck\",reason:`Dangerous ${e} operation ${r}`,classifierApprovable:!1,circuitBreaker:\"dangerousRemoval\"},suggestions:[]}}`. The two ported regexes are byte-identical in 2.1.258: `_to=/^(?:[A-Za-z_][A-Za-z0-9_]*\\+?=[^\\s]*\\s+)*\\\\?(?:[^\\s=]*\\/)?(rm|rmdir)(?:\\s|$)/` and `yto=/^\"?\\$(?:\\{[A-Za-z_][A-Za-z0-9_]*\\}|[A-Za-z_][A-Za-z0-9_]*)\"?\\/(?:\\*|\\$|\\/|[\"']|$)/`. The 2.1.229 binary carries the same bail message (1 match), so both mechanisms predate 2.1.258. csift side: `rg -c '64' src/bash_danger.rs` = 1, and that single occurrence is inside the staleness comment on line 87 -- the port has NO substitution-count gate. But `strip_paren_groups` at src/bash_danger.rs:155-166 IS a fixpoint (`loop { let prev = n.clone(); ...; if n == prev { break; } }`), present since the file's first commit.",
          "rule": "Two mechanisms x two sides = four cells. A mechanism is present in Claude Code iff its minified source substring matches at least once in the `strings -n 6` output of the version binary; present in the csift port iff the corresponding Rust construct exists in src/bash_danger.rs. Cells observed: fixpoint-strip present in CC = yes, present in port = yes; >64-substitution bail present in CC = yes, present in port = no.",
          "note": "Both Claude Code mechanisms hold in 2.1.258 and are not new (the bail message also matches once in the 2.1.229 binary), so nothing drifted on the Claude Code side. Two wording corrections were needed. First, the parenthetical \"(this port is single-pass)\" is false and was false when written: `strip_paren_groups` has been a fixpoint loop since the file's first commit, structurally identical to the binary's `for(let o=\"\";o!==r;)` loop, so the fixpoint is a point of agreement, not a divergence. Second, the >64 bail is narrower than stated: it fires only when the command text also matches `/\\brm(?:dir)?\\b/` (otherwise the pass returns null and analysis continues), and the counted set includes process substitutions and `${ … }` funsubs alongside `$( … )`, so the count in the user-facing message is broader than the phrase \"command substitutions\" suggests. The residual divergence the claim is really about is genuine and confirmed by grep: `64` occurs exactly once in src/bash_danger.rs and only inside the comment, so the port has no count gate and will predict awaiting-execution where Claude Code hoists to explicit approval. The claim's regex-core assumption also re-verified: the binary's `_to` and `yto` are byte-identical to the port's EGP and ZHP in 2.1.258, so the port is not stale in its lexical core."
        }
      ]
    }
  ]
}