towl 0.3.7

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


        <!-- Custom HTML head -->

        <meta name="description" content="Documentation for towl - a fast CLI tool to scan codebases for TODO comments">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="theme-color" content="#ffffff">

        <link rel="icon" href="favicon.svg">
        <link rel="shortcut icon" href="favicon.png">
        <link rel="stylesheet" href="css/variables.css">
        <link rel="stylesheet" href="css/general.css">
        <link rel="stylesheet" href="css/chrome.css">
        <link rel="stylesheet" href="css/print.css" media="print">

        <!-- Fonts -->
        <link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
        <link rel="stylesheet" href="fonts/fonts.css">

        <!-- Highlight.js Stylesheets -->
        <link rel="stylesheet" id="highlight-css" href="highlight.css">
        <link rel="stylesheet" id="tomorrow-night-css" href="tomorrow-night.css">
        <link rel="stylesheet" id="ayu-highlight-css" href="ayu-highlight.css">

        <!-- Custom theme stylesheets -->


        <!-- Provide site root and default themes to javascript -->
        <script>
            const path_to_root = "";
            const default_light_theme = "navy";
            const default_dark_theme = "navy";
            window.path_to_searchindex_js = "searchindex.js";
        </script>
        <!-- Start loading toc.js asap -->
        <script src="toc.js"></script>
    </head>
    <body>
    <div id="mdbook-help-container">
        <div id="mdbook-help-popup">
            <h2 class="mdbook-help-title">Keyboard shortcuts</h2>
            <div>
                <p>Press <kbd></kbd> or <kbd></kbd> to navigate between chapters</p>
                <p>Press <kbd>S</kbd> or <kbd>/</kbd> to search in the book</p>
                <p>Press <kbd>?</kbd> to show this help</p>
                <p>Press <kbd>Esc</kbd> to hide this help</p>
            </div>
        </div>
    </div>
    <div id="body-container">
        <!-- Work around some values being stored in localStorage wrapped in quotes -->
        <script>
            try {
                let theme = localStorage.getItem('mdbook-theme');
                let sidebar = localStorage.getItem('mdbook-sidebar');

                if (theme.startsWith('"') && theme.endsWith('"')) {
                    localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
                }

                if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
                    localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
                }
            } catch (e) { }
        </script>

        <!-- Set the theme before any content is loaded, prevents flash -->
        <script>
            const default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? default_dark_theme : default_light_theme;
            let theme;
            try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
            if (theme === null || theme === undefined) { theme = default_theme; }
            const html = document.documentElement;
            html.classList.remove('navy')
            html.classList.add(theme);
            html.classList.add("js");
        </script>

        <input type="checkbox" id="sidebar-toggle-anchor" class="hidden">

        <!-- Hide / unhide sidebar before it is displayed -->
        <script>
            let sidebar = null;
            const sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
            if (document.body.clientWidth >= 1080) {
                try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
                sidebar = sidebar || 'visible';
            } else {
                sidebar = 'hidden';
                sidebar_toggle.checked = false;
            }
            if (sidebar === 'visible') {
                sidebar_toggle.checked = true;
            } else {
                html.classList.remove('sidebar-visible');
            }
        </script>

        <nav id="sidebar" class="sidebar" aria-label="Table of contents">
            <!-- populated by js -->
            <mdbook-sidebar-scrollbox class="sidebar-scrollbox"></mdbook-sidebar-scrollbox>
            <noscript>
                <iframe class="sidebar-iframe-outer" src="toc.html"></iframe>
            </noscript>
            <div id="sidebar-resize-handle" class="sidebar-resize-handle">
                <div class="sidebar-resize-indicator"></div>
            </div>
        </nav>

        <div id="page-wrapper" class="page-wrapper">

            <div class="page">
                <div id="menu-bar-hover-placeholder"></div>
                <div id="menu-bar" class="menu-bar sticky">
                    <div class="left-buttons">
                        <label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
                            <i class="fa fa-bars"></i>
                        </label>
                        <button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
                            <i class="fa fa-paint-brush"></i>
                        </button>
                        <ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
                            <li role="none"><button role="menuitem" class="theme" id="default_theme">Auto</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
                            <li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
                        </ul>
                        <button id="search-toggle" class="icon-button" type="button" title="Search (`/`)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="/ s" aria-controls="searchbar">
                            <i class="fa fa-search"></i>
                        </button>
                    </div>

                    <h1 class="menu-title">towl Documentation</h1>

                    <div class="right-buttons">
                        <a href="print.html" title="Print this book" aria-label="Print this book">
                            <i id="print-button" class="fa fa-print"></i>
                        </a>
                        <a href="https://github.com/glottologist/towl" title="Git repository" aria-label="Git repository">
                            <i id="git-repository-button" class="fa fa-github"></i>
                        </a>

                    </div>
                </div>

                <div id="search-wrapper" class="hidden">
                    <form id="searchbar-outer" class="searchbar-outer">
                        <div class="search-wrapper">
                            <input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
                            <div class="spinner-wrapper">
                                <i class="fa fa-spinner fa-spin"></i>
                            </div>
                        </div>
                    </form>
                    <div id="searchresults-outer" class="searchresults-outer hidden">
                        <div id="searchresults-header" class="searchresults-header"></div>
                        <ul id="searchresults">
                        </ul>
                    </div>
                </div>

                <!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
                <script>
                    document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
                    document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
                    Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
                        link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
                    });
                </script>

                <div id="content" class="content">
                    <main>
                        <h1 id="introduction"><a class="header" href="#introduction">Introduction</a></h1>
<p><strong>towl</strong> is a fast command-line tool built in Rust that scans codebases for TODO comments. It provides an interactive TUI for browsing and managing TODOs, can create GitHub issues from them, and supports multiple output formats for CI/scripting. It detects TODO, FIXME, HACK, NOTE, and BUG comments across many languages, with configurable patterns, context-aware output, and robust resource limits.</p>
<h2 id="key-features"><a class="header" href="#key-features">Key Features</a></h2>
<ul>
<li><strong>Interactive TUI</strong> -- Browse, filter, sort, and peek at TODOs in a full-screen terminal interface powered by ratatui</li>
<li><strong>GitHub integration</strong> -- Create GitHub issues from selected TODOs and automatically replace comments with issue links</li>
<li><strong>Multi-language support</strong> -- Scans Rust, Python, JavaScript, Go, Shell, and more via configurable comment prefixes and function patterns</li>
<li><strong>Multiple output formats</strong> -- JSON, CSV, Markdown, TOML, and terminal table (non-interactive mode)</li>
<li><strong>Type filtering &amp; sorting</strong> -- Filter results by TODO type; sort by file, line, type, or priority</li>
<li><strong>Context-aware</strong> -- Captures surrounding code lines and enclosing function names</li>
<li><strong>Configurable</strong> -- Customise file extensions, exclude patterns, comment prefixes, and TODO patterns via <code>.towl.toml</code> (override with <code>--config</code> or <code>TOWL_CONFIG</code> env var)</li>
<li><strong>AI validation</strong> -- LLM-powered TODO analysis using Claude, OpenAI, or local CLI agents (Claude Code, Codex)</li>
<li><strong>Safe by design</strong> -- Path traversal protection, resource limits, symlink resolution, and secret handling for tokens</li>
<li><strong>Fast</strong> -- Concurrent file scanning, async I/O with tokio, compiled regex patterns, and static enum dispatch</li>
</ul>
<h2 id="how-it-works"><a class="header" href="#how-it-works">How It Works</a></h2>
<pre><code class="language-text">                ┌──────────┐
                │  Config   │  --config / TOWL_CONFIG / .towl.toml + env vars
                └────┬─────┘
                     │
                ┌────▼─────┐
                │  Scanner  │  Walks directory tree, scans files concurrently
                └────┬─────┘
                     │
                ┌────▼─────┐
                │  Parser   │  Matches comment prefixes + TODO patterns
                └────┬─────┘
                     │
                ┌────▼─────┐
                │  LLM      │  --ai: validates TODOs with AI (optional)
                └────┬─────┘
                     │
              ┌──────┴──────┐
              │             │
        ┌─────▼────┐  ┌────▼─────┐
        │   TUI     │  │  Output   │  Non-interactive: formats + writes
        │ (default) │  │  (-N)     │
        └─────┬────┘  └──────────┘
              │
        ┌─────▼────┐
        │ Processor │  Replaces TODOs with GitHub issue links
        └──────────┘
</code></pre>
<ol>
<li><strong>Config</strong> loads settings from <code>.towl.toml</code> (or a custom path via <code>--config</code> / <code>TOWL_CONFIG</code>), merges environment variables for GitHub and LLM integration</li>
<li><strong>Scanner</strong> walks the directory tree using the <code>ignore</code> crate, scanning matching files concurrently with bounded parallelism</li>
<li><strong>Parser</strong> reads each file, matches comment prefixes and TODO patterns via compiled regex, extracts context lines and function names</li>
<li><strong>LLM</strong> (optional, <code>--ai</code>) validates each TODO with an AI model, classifying them as Valid, Invalid, or Uncertain</li>
<li><strong>TUI</strong> (default) presents an interactive interface for browsing, filtering, and selecting TODOs to create as GitHub issues</li>
<li><strong>Output</strong> (non-interactive) formats the collected <code>TodoComment</code> items into the requested format and writes to a file or stdout</li>
<li><strong>Processor</strong> replaces TODO comments in source files with GitHub issue links after issues are created</li>
</ol>
<h2 id="quick-example"><a class="header" href="#quick-example">Quick Example</a></h2>
<pre><code class="language-bash"># Scan current directory (opens interactive TUI)
towl scan

# Non-interactive: output as terminal table
towl scan -N

# Non-interactive: output to JSON file
towl scan -N -f json -o todos.json

# Filter to only FIXME comments
towl scan -N -t fixme

# AI analysis: validate TODOs and filter out invalid ones
towl scan -N --ai

# Create GitHub issues
towl scan -N -g

# Show current configuration
towl config
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="installation"><a class="header" href="#installation">Installation</a></h1>
<h2 id="from-cratesio"><a class="header" href="#from-cratesio">From crates.io</a></h2>
<pre><code class="language-bash">cargo install towl
</code></pre>
<p>Requires Rust 1.75 or later. Install Rust via <a href="https://rustup.rs/">rustup</a> if needed.</p>
<h2 id="from-source"><a class="header" href="#from-source">From Source</a></h2>
<pre><code class="language-bash">git clone https://github.com/glottologist/towl.git
cd towl
cargo build --release
</code></pre>
<p>The binary will be at <code>target/release/towl</code>.</p>
<h2 id="verify-installation"><a class="header" href="#verify-installation">Verify Installation</a></h2>
<pre><code class="language-bash">towl --version
towl --help
</code></pre>
<h2 id="requirements"><a class="header" href="#requirements">Requirements</a></h2>
<ul>
<li><strong>Rust</strong>: 1.75+</li>
<li><strong>git</strong>: Required on <code>PATH</code> for <code>towl init</code> (extracts GitHub owner/repo from the git remote)</li>
<li><strong>OS</strong>: Linux, macOS, Windows</li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="quick-start"><a class="header" href="#quick-start">Quick Start</a></h1>
<h2 id="1-initialise-configuration"><a class="header" href="#1-initialise-configuration">1. Initialise Configuration</a></h2>
<p>Inside a git repository with a GitHub remote:</p>
<pre><code class="language-bash">towl init
</code></pre>
<p>This creates <code>.towl.toml</code> with sensible defaults. GitHub owner/repo are auto-detected from <code>git remote get-url origin</code> at runtime (not stored in the config file).</p>
<p>If <code>.towl.toml</code> already exists, use <code>--force</code> to overwrite:</p>
<pre><code class="language-bash">towl init --force
</code></pre>
<h2 id="2-scan-for-todos-interactive"><a class="header" href="#2-scan-for-todos-interactive">2. Scan for TODOs (Interactive)</a></h2>
<pre><code class="language-bash"># Scan the current directory (opens TUI)
towl scan

# Scan a specific path
towl scan src/

# Use a config file from a custom location
towl scan -c .config/.towl.toml
</code></pre>
<p>The interactive TUI lets you browse, filter, sort, peek at source code, and create GitHub issues from selected TODOs.</p>
<h2 id="3-scan-for-todos-non-interactive"><a class="header" href="#3-scan-for-todos-non-interactive">3. Scan for TODOs (Non-Interactive)</a></h2>
<p>Use <code>--non-interactive</code> / <code>-N</code> for CI pipelines and scripting:</p>
<pre><code class="language-bash"># Terminal table output
towl scan -N

# Enable verbose output (file counts, timing)
towl scan -N -v
</code></pre>
<h2 id="4-choose-an-output-format"><a class="header" href="#4-choose-an-output-format">4. Choose an Output Format</a></h2>
<p>Non-interactive mode supports multiple output formats:</p>
<pre><code class="language-bash"># Terminal table (default)
towl scan -N

# JSON file
towl scan -N -f json -o todos.json

# CSV file
towl scan -N -f csv -o todos.csv

# Markdown file
towl scan -N -f markdown -o todos.md

# TOML file
towl scan -N -f toml -o todos.toml
</code></pre>
<blockquote>
<p><strong>Note:</strong> File-based formats (<code>json</code>, <code>csv</code>, <code>toml</code>, <code>markdown</code>) require the <code>-o</code> flag with a matching file extension. Terminal/table formats always output to stdout.</p>
</blockquote>
<h2 id="5-filter-by-type"><a class="header" href="#5-filter-by-type">5. Filter by Type</a></h2>
<pre><code class="language-bash"># Only TODO comments
towl scan -N -t todo

# Only FIXME comments
towl scan -N -t fixme

# Only BUG comments
towl scan -N -t bug
</code></pre>
<p>Available types: <code>todo</code>, <code>fixme</code>, <code>hack</code>, <code>note</code>, <code>bug</code></p>
<h2 id="6-create-github-issues"><a class="header" href="#6-create-github-issues">6. Create GitHub Issues</a></h2>
<p>Set your GitHub token:</p>
<pre><code class="language-bash">export TOWL_GITHUB_TOKEN=ghp_your_token_here
</code></pre>
<p>Then create issues from TODOs:</p>
<pre><code class="language-bash"># Create GitHub issues (non-interactive)
towl scan -N -g

# Preview issues without creating them
towl scan -N -g -n
</code></pre>
<p>In interactive mode, select TODOs with <code>Space</code> and press <code>Enter</code> to create issues.</p>
<h2 id="7-view-configuration"><a class="header" href="#7-view-configuration">7. View Configuration</a></h2>
<pre><code class="language-bash">towl config

# From a custom config path
towl config -c .config/.towl.toml
</code></pre>
<p>Displays a tree view of all active settings including file extensions, exclude patterns, comment prefixes, TODO patterns, and GitHub configuration.</p>
<p>You can also set the <code>TOWL_CONFIG</code> environment variable to avoid passing <code>--config</code> every time:</p>
<pre><code class="language-bash">export TOWL_CONFIG=.config/.towl.toml
towl scan
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="configuration"><a class="header" href="#configuration">Configuration</a></h1>
<p>towl uses a <code>.towl.toml</code> file in the project root for configuration. All fields have sensible defaults -- you only need to override what you want to change.</p>
<p>You can point to a config file in a different location using:</p>
<ul>
<li><code>--config</code> / <code>-c</code> flag on <code>scan</code> and <code>config</code> commands</li>
<li><code>TOWL_CONFIG</code> environment variable</li>
</ul>
<p>The <code>--config</code> flag takes precedence over <code>TOWL_CONFIG</code>, which takes precedence over the default <code>.towl.toml</code>.</p>
<h2 id="config-file"><a class="header" href="#config-file">Config File</a></h2>
<p>Create <code>.towl.toml</code> manually or run <code>towl init</code>:</p>
<pre><code class="language-toml">[parsing]
file_extensions = ["rs", "toml", "json", "yaml", "yml", "sh", "bash"]
exclude_patterns = ["target/*", ".git/*"]
include_context_lines = 10
</code></pre>
<h2 id="parsing-section"><a class="header" href="#parsing-section">Parsing Section</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Type</th><th>Default</th><th>Description</th></tr></thead><tbody>
<tr><td><code>file_extensions</code></td><td><code>string[]</code></td><td><code>["rs", "toml", "json", "yaml", "yml", "sh", "bash"]</code></td><td>File extensions to scan</td></tr>
<tr><td><code>exclude_patterns</code></td><td><code>string[]</code></td><td><code>["target/*", ".git/*"]</code></td><td>Glob patterns to exclude</td></tr>
<tr><td><code>include_context_lines</code></td><td><code>integer</code></td><td><code>10</code></td><td>Number of surrounding lines to capture (1-50)</td></tr>
<tr><td><code>comment_prefixes</code></td><td><code>string[]</code></td><td><code>["//", "^\\s*#", "/\\*", "^\\s*\\*"]</code></td><td>Regex patterns for comment line detection</td></tr>
<tr><td><code>todo_patterns</code></td><td><code>string[]</code></td><td>See below</td><td>Regex patterns for TODO extraction</td></tr>
<tr><td><code>function_patterns</code></td><td><code>string[]</code></td><td>See below</td><td>Regex patterns for function context detection</td></tr>
</tbody></table>
</div>
<h3 id="default-todo-patterns"><a class="header" href="#default-todo-patterns">Default TODO Patterns</a></h3>
<pre><code class="language-toml">todo_patterns = [
    "(?i)\\bTODO:\\s*(.*)",
    "(?i)\\bFIXME:\\s*(.*)",
    "(?i)\\bHACK:\\s*(.*)",
    "(?i)\\bNOTE:\\s*(.*)",
    "(?i)\\bBUG:\\s*(.*)",
]
</code></pre>
<p>All patterns are case-insensitive by default. Each pattern must contain a capture group <code>(.*)</code> for extracting the description text.</p>
<h3 id="default-function-patterns"><a class="header" href="#default-function-patterns">Default Function Patterns</a></h3>
<pre><code class="language-toml">function_patterns = [
    "^\\s*(pub\\s+)?fn\\s+(\\w+)",            # Rust
    "^\\s*def\\s+(\\w+)",                      # Python
    "^\\s*(async\\s+)?function\\s+(\\w+)",     # JavaScript
    "^\\s*(public|private|protected)?\\s*(static\\s+)?\\w+\\s+(\\w+)\\s*\\(",  # Java/C#
    "^\\s*func\\s+(\\w+)",                     # Go/Swift
]
</code></pre>
<h3 id="pattern-limits"><a class="header" href="#pattern-limits">Pattern Limits</a></h3>
<p>Each pattern field is limited to 100 entries. Individual regex patterns are limited to 256 characters. Config string values (e.g., owner, repo) are limited to 512 characters. These limits prevent denial-of-service via malicious configuration files.</p>
<h2 id="github-section"><a class="header" href="#github-section">GitHub Section</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Type</th><th>Default</th><th>Description</th></tr></thead><tbody>
<tr><td><code>rate_limit_delay_ms</code></td><td><code>integer</code></td><td><code>1000</code></td><td>Delay in ms between GitHub API calls</td></tr>
</tbody></table>
</div>
<p>Owner and repo are <strong>always</strong> auto-detected from <code>git remote get-url origin</code> at runtime -- they are not stored in the config file. Use <code>TOWL_GITHUB_OWNER</code> and <code>TOWL_GITHUB_REPO</code> environment variables to override if needed.</p>
<blockquote>
<p><strong>Note:</strong> The GitHub token is never stored in the config file. Use the <code>TOWL_GITHUB_TOKEN</code> environment variable.</p>
</blockquote>
<h2 id="llm-section"><a class="header" href="#llm-section">LLM Section</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Type</th><th>Default</th><th>Description</th></tr></thead><tbody>
<tr><td><code>provider</code></td><td><code>string</code></td><td><code>claude</code></td><td>LLM provider: <code>"claude"</code>, <code>"openai"</code>, <code>"claude-code"</code>, or <code>"codex"</code></td></tr>
<tr><td><code>model</code></td><td><code>string</code></td><td><code>claude-opus-4-6</code></td><td>Model identifier</td></tr>
<tr><td><code>base_url</code></td><td><code>string</code></td><td>Provider default</td><td>Custom endpoint URL (for Ollama, vLLM, etc.)</td></tr>
<tr><td><code>max_concurrent_analyses</code></td><td><code>integer</code></td><td><code>5</code></td><td>Max concurrent LLM requests (1-20)</td></tr>
<tr><td><code>max_analyse_count</code></td><td><code>integer</code></td><td><code>50</code></td><td>Max TODOs to analyse per scan (1-500)</td></tr>
<tr><td><code>max_tokens</code></td><td><code>integer</code></td><td><code>4096</code></td><td>LLM response token limit</td></tr>
<tr><td><code>command</code></td><td><code>string</code></td><td>Auto (provider-dependent)</td><td>Override CLI binary path</td></tr>
<tr><td><code>args</code></td><td><code>string[]</code></td><td>Auto (provider-dependent)</td><td>Override CLI arguments</td></tr>
</tbody></table>
</div>
<blockquote>
<p><strong>Note:</strong> The LLM API key is never stored in the config file. Use the <code>TOWL_LLM_API_KEY</code> environment variable. See <a href="getting-started/../guides/ai-analysis.html">AI Analysis</a> for usage details.</p>
</blockquote>
<h2 id="environment-variables"><a class="header" href="#environment-variables">Environment Variables</a></h2>
<p>Eight environment variables override defaults:</p>
<div class="table-wrapper"><table><thead><tr><th>Variable</th><th>Overrides</th><th>Description</th></tr></thead><tbody>
<tr><td><code>TOWL_CONFIG</code></td><td><code>DEFAULT_CONFIG_PATH</code></td><td>Path to a <code>.towl.toml</code> file (overridden by <code>--config</code> flag)</td></tr>
<tr><td><code>TOWL_GITHUB_TOKEN</code></td><td>--</td><td>GitHub personal access token (stored as <code>SecretString</code>, masked in logs)</td></tr>
<tr><td><code>TOWL_GITHUB_OWNER</code></td><td>git remote detection</td><td>GitHub repository owner</td></tr>
<tr><td><code>TOWL_GITHUB_REPO</code></td><td>git remote detection</td><td>GitHub repository name</td></tr>
<tr><td><code>TOWL_LLM_API_KEY</code></td><td><code>llm.api_key</code></td><td>LLM API key (stored as <code>SecretString</code>, env-only)</td></tr>
<tr><td><code>TOWL_LLM_PROVIDER</code></td><td><code>llm.provider</code></td><td>LLM provider (<code>"claude"</code> or <code>"openai"</code>)</td></tr>
<tr><td><code>TOWL_LLM_MODEL</code></td><td><code>llm.model</code></td><td>LLM model identifier</td></tr>
<tr><td><code>TOWL_LLM_BASE_URL</code></td><td><code>llm.base_url</code></td><td>Custom LLM endpoint URL</td></tr>
</tbody></table>
</div>
<h2 id="config-loading-order"><a class="header" href="#config-loading-order">Config Loading Order</a></h2>
<ol>
<li>Built-in defaults</li>
<li>Config file resolved as: <code>--config</code> flag &gt; <code>TOWL_CONFIG</code> env var &gt; <code>.towl.toml</code></li>
<li>Git remote auto-detection for owner/repo</li>
<li>Environment variable overrides (<code>TOWL_GITHUB_*</code>, <code>TOWL_LLM_*</code>)</li>
</ol>
<p>If no config file exists at the resolved path, defaults are used without error.</p>
<h2 id="viewing-active-configuration"><a class="header" href="#viewing-active-configuration">Viewing Active Configuration</a></h2>
<pre><code class="language-bash"># Show config from default .towl.toml
towl config

# Show config from a custom path
towl config -c .config/.towl.toml
</code></pre>
<p>Example output:</p>
<pre><code class="language-text">📋 Towl Configuration
┌─ Parsing
│  ├─ File Extensions: bash, json, rs, sh, toml, yaml, yml
│  ├─ Exclude Patterns: target/*, .git/*
│  ├─ Context Lines: 10
│  ├─ Comment Prefixes:
│  │  ├─ //
│  │  ├─ ^\s*#
│  │  ├─ /\*
│  │  └─ ^\s*\*
│  ├─ TODO Patterns:
│  │  ├─ (?i)\bTODO:\s*(.*)
│  │  ...
│  └─ Function Patterns:
│     ├─ ^\s*(pub\s+)?fn\s+(\w+)
│     ...
└─ GitHub
   ├─ Owner: glottologist
   ├─ Repo: towl
   └─ Token: not set
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="scanning-for-todos"><a class="header" href="#scanning-for-todos">Scanning for TODOs</a></h1>
<p>The <code>towl scan</code> command walks a directory tree, reads each matching file, and extracts TODO-style comments using compiled regex patterns.</p>
<h2 id="config-override"><a class="header" href="#config-override">Config Override</a></h2>
<p>By default, towl reads <code>.towl.toml</code> from the project root. Use <code>--config</code> / <code>-c</code> to load from a different path:</p>
<pre><code class="language-bash">towl scan -c .config/.towl.toml
towl scan -c .config/.towl.toml src/
</code></pre>
<p>You can also set the <code>TOWL_CONFIG</code> environment variable. The <code>--config</code> flag takes precedence over the env var.</p>
<h2 id="interactive-mode-default"><a class="header" href="#interactive-mode-default">Interactive Mode (Default)</a></h2>
<p>By default, <code>towl scan</code> opens an interactive TUI:</p>
<pre><code class="language-bash">towl scan
towl scan src/
</code></pre>
<p>See <a href="guides/./tui.html">Interactive TUI</a> for details on the TUI interface.</p>
<h2 id="non-interactive-mode"><a class="header" href="#non-interactive-mode">Non-Interactive Mode</a></h2>
<p>Use <code>--non-interactive</code> / <code>-N</code> to disable the TUI (for CI/scripting):</p>
<pre><code class="language-bash">towl scan -N
towl scan -N src/
towl scan -N -v
</code></pre>
<h2 id="how-scanning-works"><a class="header" href="#how-scanning-works">How Scanning Works</a></h2>
<ol>
<li><strong>Directory walk</strong> -- Uses the <code>ignore</code> crate to traverse the file tree, respecting <code>.gitignore</code> rules automatically</li>
<li><strong>Extension filter</strong> -- Only files matching <code>file_extensions</code> in config are read (default: <code>rs</code>, <code>toml</code>, <code>json</code>, <code>yaml</code>, <code>yml</code>, <code>sh</code>, <code>bash</code>)</li>
<li><strong>Exclude patterns</strong> -- Files matching <code>exclude_patterns</code> are skipped (default: <code>target/*</code>, <code>.git/*</code>)</li>
<li><strong>Concurrent scanning</strong> -- Matching files are scanned concurrently with bounded parallelism (up to 64 files at once)</li>
<li><strong>Content parsing</strong> -- Each file is read and scanned for lines matching <code>comment_prefixes</code>, then checked against <code>todo_patterns</code></li>
<li><strong>Context extraction</strong> -- Surrounding lines and enclosing function names are captured</li>
</ol>
<h2 id="verbose-mode"><a class="header" href="#verbose-mode">Verbose Mode</a></h2>
<p>The <code>-v</code> / <code>--verbose</code> flag prints scan metrics to stderr (non-interactive mode only):</p>
<pre><code class="language-bash">towl scan -N -v
</code></pre>
<pre><code class="language-text">Files scanned: 42
Files skipped: 3
Files errored: 0
Scan duration: 12ms
</code></pre>
<h2 id="filtering-by-type"><a class="header" href="#filtering-by-type">Filtering by Type</a></h2>
<p>Restrict results to a single TODO type:</p>
<pre><code class="language-bash">towl scan -N -t todo      # Only TODO comments
towl scan -N -t fixme     # Only FIXME comments
towl scan -N -t hack      # Only HACK comments
towl scan -N -t note      # Only NOTE comments
towl scan -N -t bug       # Only BUG comments
</code></pre>
<p>The filter value is case-insensitive on the command line but stored lowercase internally.</p>
<h2 id="github-issue-creation"><a class="header" href="#github-issue-creation">GitHub Issue Creation</a></h2>
<p>Create GitHub issues from found TODOs:</p>
<pre><code class="language-bash"># Create issues
towl scan -N -g

# Preview without creating
towl scan -N -g -n
</code></pre>
<p>When issues are created, towl automatically replaces the TODO comment in source files with a link to the created issue. Duplicate detection prevents creating issues for TODOs that already have a matching open issue.</p>
<p>In interactive mode, select TODOs with <code>Space</code> and press <code>Enter</code> to create issues.</p>
<h2 id="ai-analysis"><a class="header" href="#ai-analysis">AI Analysis</a></h2>
<p>Use the <code>--ai</code> flag to validate TODOs with an LLM:</p>
<pre><code class="language-bash"># Analyse and filter out invalid TODOs
towl scan -N --ai

# Interactive mode with AI analysis
towl scan --ai

# Create GitHub issues for valid TODOs only (enriched with AI reasoning)
towl scan -N --ai -g
</code></pre>
<p>In non-interactive mode, TODOs classified as Invalid are automatically excluded from the output. See <a href="guides/./ai-analysis.html">AI Analysis</a> for full details.</p>
<h2 id="combining-options"><a class="header" href="#combining-options">Combining Options</a></h2>
<p>Options compose freely:</p>
<pre><code class="language-bash"># Scan src/, output FIXME comments as JSON to a file, verbose
towl scan -N src/ -t fixme -f json -o fixmes.json -v

# Scan and create GitHub issues for TODO comments only
towl scan -N -t todo -g

# AI-validated FIXMEs as JSON
towl scan -N --ai -t fixme -f json -o fixmes.json

# Use a custom config for everything
towl scan -c .config/.towl.toml -N src/ -t fixme -f json -o fixmes.json
</code></pre>
<h2 id="resource-limits"><a class="header" href="#resource-limits">Resource Limits</a></h2>
<p>towl enforces hard limits to prevent runaway scans:</p>
<div class="table-wrapper"><table><thead><tr><th>Limit</th><th>Value</th><th>Purpose</th></tr></thead><tbody>
<tr><td>Max file size</td><td>10 MB</td><td>Skips binary/generated files</td></tr>
<tr><td>Max TODOs per file</td><td>10,000</td><td>Prevents single-file explosion</td></tr>
<tr><td>Max total TODOs</td><td>100,000</td><td>Caps overall result set</td></tr>
<tr><td>Max files scanned</td><td>100,000</td><td>Bounds directory walk</td></tr>
</tbody></table>
</div>
<p>When a limit is hit, scanning stops gracefully and returns the results collected so far.</p>
<h2 id="scan-result"><a class="header" href="#scan-result">Scan Result</a></h2>
<p>The scan produces a <code>ScanResult</code> containing:</p>
<ul>
<li><strong>todos</strong> -- The list of extracted <code>TodoComment</code> items</li>
<li><strong>files_scanned</strong> -- Number of files successfully read</li>
<li><strong>files_skipped</strong> -- Number of files skipped (wrong extension, excluded, too large)</li>
<li><strong>files_errored</strong> -- Number of files that failed to read (permissions, encoding)</li>
<li><strong>duration</strong> -- Wall-clock time for the scan</li>
</ul>
<p>Two convenience checks:</p>
<ul>
<li><code>all_files_failed()</code> -- Returns <code>true</code> when no files were scanned but errors occurred (likely a permissions or path issue)</li>
<li><code>is_clean()</code> -- Returns <code>true</code> when zero TODOs were found and zero files errored</li>
</ul>
<h2 id="path-safety"><a class="header" href="#path-safety">Path Safety</a></h2>
<ul>
<li><strong>Path traversal</strong> -- Paths containing <code>..</code> components are rejected</li>
<li><strong>Symlink resolution</strong> -- Symlinks are resolved before processing to prevent escape from the scan root</li>
<li><strong>.gitignore</strong> -- Respected automatically via the <code>ignore</code> crate</li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="interactive-tui"><a class="header" href="#interactive-tui">Interactive TUI</a></h1>
<p>By default, <code>towl scan</code> opens an interactive terminal interface powered by <a href="https://ratatui.rs">ratatui</a>. The TUI lets you browse, filter, sort, and peek at TODOs, then create GitHub issues from selected items.</p>
<h2 id="launching"><a class="header" href="#launching">Launching</a></h2>
<pre><code class="language-bash"># Opens TUI with TODOs from current directory
towl scan

# Opens TUI with TODOs from a specific path
towl scan src/
</code></pre>
<p>To bypass the TUI (for CI/scripting), use <code>--non-interactive</code> / <code>-N</code>.</p>
<h2 id="modes"><a class="header" href="#modes">Modes</a></h2>
<p>The TUI has six modes:</p>
<h3 id="browse"><a class="header" href="#browse">Browse</a></h3>
<p>The main view. Displays all TODOs in a scrollable list with type, description, file path, and line number.</p>
<p>When launched with <code>--ai</code>, each row shows a validity indicator (<code>V</code>/<code>I</code>/<code>?</code>) and is colour-coded: green for valid, red for invalid, yellow for uncertain.</p>
<div class="table-wrapper"><table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>
<tr><td><code>j</code> / <code>Down</code></td><td>Move cursor down</td></tr>
<tr><td><code>k</code> / <code>Up</code></td><td>Move cursor up</td></tr>
<tr><td><code>Space</code></td><td>Toggle selection on current item</td></tr>
<tr><td><code>a</code></td><td>Select all visible TODOs</td></tr>
<tr><td><code>n</code></td><td>Deselect all</td></tr>
<tr><td><code>f</code></td><td>Cycle type filter (All, TODO, FIXME, HACK, NOTE, BUG)</td></tr>
<tr><td><code>s</code></td><td>Cycle sort field (File, Line, Type, Priority)</td></tr>
<tr><td><code>r</code></td><td>Reverse sort order</td></tr>
<tr><td><code>p</code></td><td>Open peek view for current TODO</td></tr>
<tr><td><code>d</code></td><td>Delete selected invalid TODOs (requires <code>--ai</code>)</td></tr>
<tr><td><code>Enter</code></td><td>Confirm selection and proceed to create GitHub issues</td></tr>
<tr><td><code>q</code> / <code>Esc</code></td><td>Quit</td></tr>
<tr><td><code>Ctrl+C</code></td><td>Force quit (works in any mode)</td></tr>
</tbody></table>
</div>
<h3 id="peek"><a class="header" href="#peek">Peek</a></h3>
<p>Shows the source code surrounding the selected TODO with syntax context. The TODO line is highlighted. When <code>--ai</code> is active, the AI Analysis section is displayed below the source code with the validity, confidence score, and reasoning. The reasoning text word-wraps to fit the popup width.</p>
<div class="table-wrapper"><table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>
<tr><td><code>j</code> / <code>Down</code></td><td>Scroll down</td></tr>
<tr><td><code>k</code> / <code>Up</code></td><td>Scroll up</td></tr>
<tr><td><code>p</code> / <code>q</code> / <code>Esc</code></td><td>Close peek and return to browse</td></tr>
</tbody></table>
</div>
<h3 id="confirm"><a class="header" href="#confirm">Confirm</a></h3>
<p>Appears after pressing <code>Enter</code> in browse mode with selected TODOs. Shows a summary of the TODOs that will be created as GitHub issues.</p>
<div class="table-wrapper"><table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>
<tr><td><code>y</code> / <code>Enter</code></td><td>Confirm and start creating issues</td></tr>
<tr><td><code>n</code> / <code>q</code> / <code>Esc</code></td><td>Cancel and return to browse</td></tr>
</tbody></table>
</div>
<h3 id="creating"><a class="header" href="#creating">Creating</a></h3>
<p>Displays a progress view while GitHub issues are being created. Shows the current phase (initialising client, loading existing issues, creating issues, replacing TODOs in files) and a progress counter.</p>
<p>No keyboard input is accepted during creation (except <code>Ctrl+C</code> to force quit).</p>
<h3 id="done"><a class="header" href="#done">Done</a></h3>
<p>Shows the results after issue creation completes -- number of issues created, any errors encountered. Press <code>q</code>, <code>Esc</code>, or <code>Enter</code> to exit.</p>
<h3 id="delete-confirm-requires---ai"><a class="header" href="#delete-confirm-requires---ai">Delete Confirm (requires <code>--ai</code>)</a></h3>
<p>Appears after pressing <code>d</code> in Browse mode with selected invalid TODOs. Lists the TODOs that will be removed from source files.</p>
<div class="table-wrapper"><table><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody>
<tr><td><code>y</code> / <code>Enter</code></td><td>Confirm and delete the TODO comment lines</td></tr>
<tr><td><code>n</code> / <code>q</code> / <code>Esc</code></td><td>Cancel and return to browse</td></tr>
</tbody></table>
</div>
<p>Only TODOs marked as Invalid by the AI are eligible for deletion.</p>
<h2 id="workflow"><a class="header" href="#workflow">Workflow</a></h2>
<ol>
<li>Run <code>towl scan</code> to open the TUI (with <code>--ai</code>, a progress bar shows during analysis)</li>
<li>Browse the TODO list -- use <code>f</code> to filter by type, <code>s</code>/<code>r</code> to sort</li>
<li>Press <code>p</code> to peek at source code around a TODO</li>
<li>Select TODOs with <code>Space</code> (or <code>a</code> to select all visible)</li>
<li>Press <code>Enter</code> to review selected TODOs</li>
<li>Press <code>y</code> to create GitHub issues</li>
<li>towl creates the issues, skips duplicates, and replaces TODO comments with issue links in source files</li>
</ol>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ai-analysis-1"><a class="header" href="#ai-analysis-1">AI Analysis</a></h1>
<p>towl can use an LLM (Claude or any OpenAI-compatible model) to validate whether each TODO is still relevant. The <code>--ai</code> flag triggers analysis that determines if a TODO is <strong>Valid</strong>, <strong>Invalid</strong>, or <strong>Uncertain</strong>.</p>
<h2 id="setup"><a class="header" href="#setup">Setup</a></h2>
<p>Set your API key as an environment variable:</p>
<pre><code class="language-bash"># Claude (default)
export TOWL_LLM_API_KEY=sk-ant-your-key-here

# Or for OpenAI
export TOWL_LLM_API_KEY=sk-your-openai-key
export TOWL_LLM_PROVIDER=openai
</code></pre>
<p>The API key is stored as a <code>SecretString</code> and never written to config files or logs.</p>
<h2 id="basic-usage"><a class="header" href="#basic-usage">Basic Usage</a></h2>
<pre><code class="language-bash"># Non-interactive: analyse and filter out invalid TODOs
towl scan -N --ai

# Interactive: analyse and show results in TUI
towl scan --ai

# Combine with other flags
towl scan -N --ai -t fixme -f json -o fixmes.json
towl scan -N --ai -g  # create GitHub issues for valid TODOs only
</code></pre>
<h2 id="how-it-works-1"><a class="header" href="#how-it-works-1">How It Works</a></h2>
<p>For each TODO, the LLM receives:</p>
<ol>
<li><strong>TODO description</strong> -- the comment text</li>
<li><strong>Expanded context</strong> -- ~30 lines of surrounding source code</li>
<li><strong>Function body</strong> -- the complete enclosing function (if detected)</li>
</ol>
<p>The LLM determines:</p>
<ul>
<li><strong>Is it resolved?</strong> -- Does the code already do what the TODO asks?</li>
<li><strong>Is it relevant?</strong> -- Does the code/feature still exist?</li>
<li><strong>Is it actionable?</strong> -- Is the TODO clear and specific?</li>
</ul>
<p>Based on these checks, each TODO is classified as Valid, Invalid, or Uncertain with a confidence score (0-100%).</p>
<h2 id="non-interactive-mode-1"><a class="header" href="#non-interactive-mode-1">Non-Interactive Mode</a></h2>
<p>With <code>-N --ai</code>, invalid TODOs are automatically filtered out of the results:</p>
<pre><code class="language-bash">towl scan -N --ai
# Only valid and uncertain TODOs appear in output

towl scan -N --ai -g
# GitHub issues created only for valid TODOs, enriched with AI reasoning
</code></pre>
<h2 id="interactive-mode-tui"><a class="header" href="#interactive-mode-tui">Interactive Mode (TUI)</a></h2>
<p>With <code>--ai</code> (no <code>-N</code>), a progress bar is displayed while TODOs are being analysed:</p>
<pre><code class="language-text">  Analysing TODOs [████████████░░░░░░░░░░░░░░░░░░] 12/30
</code></pre>
<p>Once analysis completes, the TUI launches with results:</p>
<ul>
<li><strong>Validity column</strong> -- Each TODO shows <code>V</code> (Valid), <code>I</code> (Invalid), or <code>?</code> (Uncertain)</li>
<li><strong>Colour coding</strong> -- Green for valid, red for invalid, yellow for uncertain</li>
<li><strong>Peek view</strong> -- Press <code>p</code> to see the LLM's reasoning below the source code (text wraps to fit the popup width)</li>
<li><strong>Delete invalid TODOs</strong> -- Select invalid TODOs and press <code>d</code> to remove them from source files (with confirmation)</li>
</ul>
<h3 id="delete-workflow"><a class="header" href="#delete-workflow">Delete Workflow</a></h3>
<ol>
<li>Select invalid TODOs with <code>Space</code> (or <code>a</code> to select all visible)</li>
<li>Press <code>d</code> to open the delete confirmation dialog</li>
<li>Review the list of TODOs that will be removed</li>
<li>Press <code>y</code> to confirm deletion, or <code>n</code> to cancel</li>
<li>towl removes the comment lines from source files using atomic writes</li>
</ol>
<blockquote>
<p><strong>Note:</strong> Only TODOs marked as Invalid by the AI can be deleted via <code>d</code>. Valid and Uncertain TODOs are excluded from deletion.</p>
</blockquote>
<h2 id="github-issue-enrichment"><a class="header" href="#github-issue-enrichment">GitHub Issue Enrichment</a></h2>
<p>When creating GitHub issues (either with <code>-g</code> or via the TUI), valid TODOs include an <strong>AI Analysis</strong> section in the issue body:</p>
<pre><code class="language-markdown">## AI Analysis

**Validity:** Valid
**Confidence:** 92%

### Reasoning

The caching layer referenced in this TODO has not been implemented.
The function currently makes direct database calls on every request.

### Enhanced Description

This TODO identifies a performance bottleneck where database queries
are executed on every request without caching. Adding a caching layer
would reduce database load and improve response times.
</code></pre>
<h2 id="configuration-1"><a class="header" href="#configuration-1">Configuration</a></h2>
<p>Add a <code>[llm]</code> section to <code>.towl.toml</code>:</p>
<pre><code class="language-toml">[llm]
provider = "claude"                      # "claude" or "openai"
model = "claude-opus-4-6"             # model identifier
# base_url = "http://localhost:11434/v1"  # for Ollama/vLLM
max_concurrent_analyses = 5              # concurrent LLM requests
max_analyse_count = 50                   # max TODOs to analyse per scan
max_tokens = 4096                        # LLM response token limit
</code></pre>
<h3 id="environment-variables-1"><a class="header" href="#environment-variables-1">Environment Variables</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead><tbody>
<tr><td><code>TOWL_LLM_API_KEY</code></td><td>--</td><td>API key (required for <code>--ai</code>)</td></tr>
<tr><td><code>TOWL_LLM_PROVIDER</code></td><td><code>claude</code></td><td><code>"claude"</code>, <code>"openai"</code>, <code>"claude-code"</code>, or <code>"codex"</code></td></tr>
<tr><td><code>TOWL_LLM_MODEL</code></td><td><code>claude-opus-4-6</code></td><td>Model identifier</td></tr>
<tr><td><code>TOWL_LLM_BASE_URL</code></td><td>Provider default</td><td>Custom endpoint URL</td></tr>
</tbody></table>
</div>
<h3 id="using-claude-code-or-codex-cli"><a class="header" href="#using-claude-code-or-codex-cli">Using Claude Code or Codex CLI</a></h3>
<p>If you have <code>claude</code> (Claude Code) or <code>codex</code> (OpenAI Codex CLI) installed, you can use them directly without an API key:</p>
<pre><code class="language-bash"># Use Claude Code CLI
export TOWL_LLM_PROVIDER=claude-code
towl scan --ai

# Use Codex CLI
export TOWL_LLM_PROVIDER=codex
towl scan --ai
</code></pre>
<p>Or set in <code>.towl.toml</code>:</p>
<pre><code class="language-toml">[llm]
provider = "claude-code"   # or "codex"
# command = "/custom/path/to/claude"   # optional override
# args = ["-p", "--output-format", "json"]  # optional override
</code></pre>
<p>No <code>TOWL_LLM_API_KEY</code> is needed -- the CLI agents manage their own authentication.</p>
<p><strong>Auto-fallback:</strong> If the CLI binary is not found on PATH, towl automatically falls back to the corresponding API provider (<code>claude-code</code> -&gt; Claude API, <code>codex</code> -&gt; OpenAI API). The API fallback requires <code>TOWL_LLM_API_KEY</code> to be set.</p>
<h3 id="using-with-ollama-or-local-models"><a class="header" href="#using-with-ollama-or-local-models">Using with Ollama or Local Models</a></h3>
<pre><code class="language-bash">export TOWL_LLM_PROVIDER=openai
export TOWL_LLM_MODEL=llama3
export TOWL_LLM_BASE_URL=http://localhost:11434/v1
export TOWL_LLM_API_KEY=ollama  # Ollama doesn't need a real key

towl scan -N --ai
</code></pre>
<h2 id="rate-limiting"><a class="header" href="#rate-limiting">Rate Limiting</a></h2>
<p>Two configurable limits prevent excessive API usage:</p>
<div class="table-wrapper"><table><thead><tr><th>Limit</th><th>Default</th><th>Config field</th></tr></thead><tbody>
<tr><td>Concurrent requests</td><td>5</td><td><code>max_concurrent_analyses</code></td></tr>
<tr><td>Total TODOs analysed</td><td>50</td><td><code>max_analyse_count</code></td></tr>
</tbody></table>
</div>
<p>When the TODO count exceeds <code>max_analyse_count</code>, only the first N TODOs are analysed. A warning is logged for the remainder.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="output-formats"><a class="header" href="#output-formats">Output Formats</a></h1>
<p>In non-interactive mode (<code>-N</code>), towl supports five output formats. Terminal-based formats write to stdout; file-based formats require the <code>-o</code> flag with a matching file extension.</p>
<blockquote>
<p><strong>Note:</strong> Output format flags only apply in non-interactive mode. The interactive TUI has its own display. Use <code>towl scan -N -f &lt;format&gt;</code> to select a format.</p>
</blockquote>
<h2 id="terminal--table-default"><a class="header" href="#terminal--table-default">Terminal / Table (default)</a></h2>
<pre><code class="language-bash">towl scan -N
# or explicitly:
towl scan -N -f table
towl scan -N -f terminal
</code></pre>
<p>Renders an ASCII table to stdout:</p>
<pre><code class="language-text">┌──────┬─────────────────────────┬──────────────────┬──────┬──────────┐
│ Type │ Description             │ File             │ Line │ Function │
├──────┼─────────────────────────┼──────────────────┼──────┼──────────┤
│ TODO │ Implement caching       │ src/lib/cache.rs │   42 │ process  │
│ FIXME│ Handle timeout          │ src/lib/net.rs   │  108 │ connect  │
└──────┴─────────────────────────┴──────────────────┴──────┴──────────┘
</code></pre>
<blockquote>
<p><strong>Note:</strong> <code>table</code> and <code>terminal</code> are aliases -- both produce the same output.</p>
</blockquote>
<h2 id="json"><a class="header" href="#json">JSON</a></h2>
<pre><code class="language-bash">towl scan -N -f json -o todos.json
</code></pre>
<p>Produces structured JSON with a summary and TODOs grouped by type:</p>
<pre><code class="language-json">{
  "summary": {
    "total": 2,
    "by_type": {
      "TODO": 1,
      "FIXME": 1
    }
  },
  "todos": {
    "TODO": [
      {
        "id": "abc123",
        "file_path": "src/lib/cache.rs",
        "line_number": 42,
        "column_start": 5,
        "column_end": 30,
        "todo_type": "Todo",
        "description": "Implement caching",
        "original_text": "// TODO: Implement caching",
        "context_lines": ["fn process() {", "    // TODO: Implement caching", "    unimplemented!()"],
        "function_context": "process"
      }
    ]
  }
}
</code></pre>
<h2 id="csv"><a class="header" href="#csv">CSV</a></h2>
<pre><code class="language-bash">towl scan -N -f csv -o todos.csv
</code></pre>
<p>Produces a CSV file with a header row:</p>
<pre><code class="language-csv">Type,Description,File,Line,Column Start,Column End,Function,Original Text,Context Lines
TODO,Implement caching,src/lib/cache.rs,42,5,30,process,// TODO: Implement caching,"fn process() {|    // TODO: Implement caching|    unimplemented!()"
</code></pre>
<p>Context lines are joined with <code>|</code> separators within a single quoted field.</p>
<h2 id="markdown"><a class="header" href="#markdown">Markdown</a></h2>
<pre><code class="language-bash">towl scan -N -f markdown -o todos.md
</code></pre>
<p>Produces a Markdown document with sections grouped by TODO type:</p>
<pre><code class="language-markdown"># TODOs

## TODO (1)

### Implement caching
- **File:** src/lib/cache.rs
- **Line:** 42
- **Function:** process

**Context:**
&gt; fn process() {
&gt;     // TODO: Implement caching
&gt;     unimplemented!()
</code></pre>
<h2 id="toml"><a class="header" href="#toml">TOML</a></h2>
<pre><code class="language-bash">towl scan -N -f toml -o todos.toml
</code></pre>
<p>Produces a TOML file with a summary table and grouped items:</p>
<pre><code class="language-toml">[summary]
total = 2

[summary.by_type]
TODO = 1
FIXME = 1

[[todos.TODO]]
description = "Implement caching"
file_path = "src/lib/cache.rs"
line_number = 42
function_context = "process"
</code></pre>
<h2 id="extension-validation"><a class="header" href="#extension-validation">Extension Validation</a></h2>
<p>File-based formats require the output path to have a matching extension:</p>
<div class="table-wrapper"><table><thead><tr><th>Format</th><th>Required extension</th></tr></thead><tbody>
<tr><td><code>json</code></td><td><code>.json</code></td></tr>
<tr><td><code>csv</code></td><td><code>.csv</code></td></tr>
<tr><td><code>toml</code></td><td><code>.toml</code></td></tr>
<tr><td><code>markdown</code></td><td><code>.md</code></td></tr>
</tbody></table>
</div>
<p>Mismatched extensions produce an error:</p>
<pre><code class="language-text">Error: Invalid output path: expected .json extension for JSON format
</code></pre>
<h2 id="choosing-a-format"><a class="header" href="#choosing-a-format">Choosing a Format</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Use case</th><th>Format</th></tr></thead><tbody>
<tr><td>Interactive browsing</td><td>TUI (default, no <code>-N</code>)</td></tr>
<tr><td>Quick terminal check</td><td><code>table</code> (<code>-N</code>, default format)</td></tr>
<tr><td>CI/CD integration</td><td><code>json</code></td></tr>
<tr><td>Spreadsheet import</td><td><code>csv</code></td></tr>
<tr><td>Documentation / reports</td><td><code>markdown</code></td></tr>
<tr><td>Config-style tooling</td><td><code>toml</code></td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="filtering"><a class="header" href="#filtering">Filtering</a></h1>
<p>towl supports filtering scan results by TODO type using the <code>-t</code> / <code>--todo-type</code> flag.</p>
<h2 id="filter-by-type"><a class="header" href="#filter-by-type">Filter by Type</a></h2>
<pre><code class="language-bash">towl scan -t todo      # Only TODO comments
towl scan -t fixme     # Only FIXME comments
towl scan -t hack      # Only HACK comments
towl scan -t note      # Only NOTE comments
towl scan -t bug       # Only BUG comments
</code></pre>
<p>The filter value is case-insensitive -- <code>TODO</code>, <code>todo</code>, and <code>Todo</code> all work.</p>
<h2 id="available-types"><a class="header" href="#available-types">Available Types</a></h2>
<p>towl recognises five built-in TODO types:</p>
<div class="table-wrapper"><table><thead><tr><th>Type</th><th>Matches</th><th>Typical use</th></tr></thead><tbody>
<tr><td><code>todo</code></td><td><code>TODO:</code></td><td>Planned work</td></tr>
<tr><td><code>fixme</code></td><td><code>FIXME:</code></td><td>Known broken code</td></tr>
<tr><td><code>hack</code></td><td><code>HACK:</code></td><td>Temporary workarounds</td></tr>
<tr><td><code>note</code></td><td><code>NOTE:</code></td><td>Important context</td></tr>
<tr><td><code>bug</code></td><td><code>BUG:</code></td><td>Known defects</td></tr>
</tbody></table>
</div>
<p>Each type is matched via the corresponding regex pattern in the <code>todo_patterns</code> configuration. The default patterns are case-insensitive (<code>(?i)</code>).</p>
<h2 id="combining-with-output-formats"><a class="header" href="#combining-with-output-formats">Combining with Output Formats</a></h2>
<p>Filtering works with any output format:</p>
<pre><code class="language-bash"># FIXMEs as JSON
towl scan -t fixme -f json -o fixmes.json

# BUGs as Markdown
towl scan -t bug -f markdown -o bugs.md

# NOTEs in terminal table
towl scan -t note
</code></pre>
<h2 id="without-filtering"><a class="header" href="#without-filtering">Without Filtering</a></h2>
<p>When no <code>-t</code> flag is provided, all recognised types are included in the output. The results are grouped by type in all formats.</p>
<h2 id="custom-patterns"><a class="header" href="#custom-patterns">Custom Patterns</a></h2>
<p>You can add custom TODO patterns in <code>.towl.toml</code>. Each pattern must contain a capture group <code>(.*)</code> for extracting the description:</p>
<pre><code class="language-toml">[parsing]
todo_patterns = [
    "(?i)\\bTODO:\\s*(.*)",
    "(?i)\\bFIXME:\\s*(.*)",
    "(?i)\\bHACK:\\s*(.*)",
    "(?i)\\bNOTE:\\s*(.*)",
    "(?i)\\bBUG:\\s*(.*)",
    "(?i)\\bXXX:\\s*(.*)",
]
</code></pre>
<blockquote>
<p><strong>Note:</strong> Custom patterns extend the set of matched comments but do not add new filter types to <code>-t</code>. The built-in five types are always available for filtering.</p>
</blockquote>
<div style="break-before: page; page-break-before: always;"></div><h1 id="api-overview"><a class="header" href="#api-overview">API Overview</a></h1>
<p>towl is structured as a library (<code>towl</code> crate) with a thin binary wrapper. The library exposes modules for configuration, scanning, parsing, output, and error handling.</p>
<h2 id="module-map"><a class="header" href="#module-map">Module Map</a></h2>
<pre><code class="language-text">towl (lib)
├── cli          Command-line argument parsing (clap)
├── comment      TODO types and comment structures
│   ├── todo     TodoType enum, TodoComment struct
│   └── error    TowlCommentError
├── config       Configuration loading and validation
│   ├── types    TowlConfig, ParsingConfig, GitHubConfig, Owner, Repo
│   ├── git      GitRepoInfo (git remote discovery)
│   └── error    TowlConfigError
├── scanner      Directory walking and file filtering
│   ├── types    Scanner, ScanResult
│   └── error    TowlScannerError
├── parser       Regex-based TODO extraction
│   ├── types    Parser, Pattern
│   └── error    TowlParserError
├── output       Formatting and writing results
│   ├── formatter
│   │   ├── formatters   CsvFormatter, JsonFormatter, MarkdownFormatter,
│   │   │                TableFormatter, TomlFormatter
│   │   └── error        FormatterError
│   ├── writer
│   │   ├── writers      StdoutWriter, FileWriter
│   │   └── error        WriterError
│   └── error            TowlOutputError
├── github       GitHub issue creation
│   ├── client   GitHubClient
│   ├── types    CreatedIssue
│   └── error    TowlGitHubError
├── processor    TODO replacement with issue links
│   ├── types    Processor, ProcessorResult
│   └── error    TowlProcessorError
├── llm          LLM-powered TODO validation
│   ├── analyse  analyse_todos, gather_expanded_context
│   ├── claude   ClaudeProvider (Anthropic API)
│   ├── openai   OpenAiProvider (OpenAI-compatible API)
│   ├── cli      ClaudeCodeProvider, CodexProvider (CLI agents)
│   ├── prompt   System prompt and user content construction
│   ├── types    AnalysisResult, AnalysisSummary, Validity, LlmUsage
│   └── error    TowlLlmError
├── tui          Interactive terminal UI
│   ├── app      App, AppMode, SortField, PeekState
│   ├── input    Action, handle_input
│   ├── render   draw
│   └── error    TowlTuiError
└── error        Top-level TowlError (aggregates all error types)
</code></pre>
<h2 id="data-flow"><a class="header" href="#data-flow">Data Flow</a></h2>
<pre><code class="language-text">TowlConfig ──► Scanner ──► Parser ──► Output
   │              │            │          │
   │              │            │          ├─ FormatterImpl (enum dispatch)
   │              │            │          └─ WriterImpl (enum dispatch)
   │              │            │
   │              │            └─ Vec&lt;TodoComment&gt;
   │              │
   │              └─ ScanResult { todos, files_scanned, ... }
   │
   ├─ ParsingConfig + GitHubConfig + LlmConfig
   │
   └─ LlmConfig ──► LlmProvider ──► analyse_todos ──► AnalysisSummary
</code></pre>
<h2 id="key-types"><a class="header" href="#key-types">Key Types</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Type</th><th>Module</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>TowlConfig</code></td><td><code>config</code></td><td>Top-level configuration container</td></tr>
<tr><td><code>ParsingConfig</code></td><td><code>config</code></td><td>File extensions, patterns, context lines</td></tr>
<tr><td><code>GitHubConfig</code></td><td><code>config</code></td><td>Owner, repo, token</td></tr>
<tr><td><code>Scanner</code></td><td><code>scanner</code></td><td>Directory walk + file filtering</td></tr>
<tr><td><code>ScanResult</code></td><td><code>scanner</code></td><td>Structured scan output with metrics</td></tr>
<tr><td><code>Parser</code></td><td><code>parser</code></td><td>Regex-based TODO extraction</td></tr>
<tr><td><code>TodoComment</code></td><td><code>comment</code></td><td>A single extracted TODO item</td></tr>
<tr><td><code>TodoType</code></td><td><code>comment</code></td><td>Enum: Todo, Fixme, Hack, Note, Bug</td></tr>
<tr><td><code>Output</code></td><td><code>output</code></td><td>Formatter + writer combination</td></tr>
<tr><td><code>GitHubClient</code></td><td><code>github</code></td><td>Authenticated GitHub API client</td></tr>
<tr><td><code>CreatedIssue</code></td><td><code>github</code></td><td>Metadata for a created GitHub issue</td></tr>
<tr><td><code>Processor</code></td><td><code>processor</code></td><td>Replaces TODOs with issue links in source files</td></tr>
<tr><td><code>ProcessorResult</code></td><td><code>processor</code></td><td>Summary of a batch replacement operation</td></tr>
<tr><td><code>LlmProvider</code></td><td><code>llm</code></td><td>Enum-dispatched LLM provider (Claude, OpenAI, CLI agents)</td></tr>
<tr><td><code>AnalysisResult</code></td><td><code>llm</code></td><td>LLM validation result for a single TODO</td></tr>
<tr><td><code>AnalysisSummary</code></td><td><code>llm</code></td><td>Aggregate counts from a batch analysis run</td></tr>
<tr><td><code>Validity</code></td><td><code>llm</code></td><td>TODO validity classification (Valid, Invalid, Uncertain)</td></tr>
<tr><td><code>App</code></td><td><code>tui</code></td><td>TUI application state and mode management</td></tr>
<tr><td><code>AppMode</code></td><td><code>tui</code></td><td>Current UI mode (Browse, Peek, Confirm, etc.)</td></tr>
<tr><td><code>TowlError</code></td><td><code>error</code></td><td>Top-level error aggregating all sub-errors</td></tr>
</tbody></table>
</div>
<h2 id="error-hierarchy"><a class="header" href="#error-hierarchy">Error Hierarchy</a></h2>
<pre><code class="language-text">TowlError
├── TowlConfigError      Config loading, TOML parsing, git discovery
├── TowlScannerError     File walk, I/O, resource limits
│   └── TowlParserError  Regex compilation, pattern validation
├── TowlOutputError      Formatting, file writing
│   ├── FormatterError   Serialisation failures
│   └── WriterError      I/O, path traversal
├── TowlGitHubError      API errors, auth, rate limiting
├── TowlProcessorError   File replacement errors
├── TowlTuiError         Terminal I/O errors
└── TowlLlmError         LLM API, auth, parsing, I/O
</code></pre>
<p>All error types use <code>thiserror</code> for <code>Display</code> and <code>Error</code> trait implementations. Conversion between levels uses <code>#[from]</code> attributes for ergonomic <code>?</code> propagation.</p>
<h2 id="constants"><a class="header" href="#constants">Constants</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Name</th><th>Value</th><th>Module</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>MAX_FILE_SIZE</code></td><td>10 MB</td><td>scanner</td><td>Skip oversized files</td></tr>
<tr><td><code>MAX_TODO_COUNT</code></td><td>10,000</td><td>scanner</td><td>Per-file TODO cap</td></tr>
<tr><td><code>MAX_TOTAL_TODO_COUNT</code></td><td>100,000</td><td>scanner</td><td>Global TODO cap</td></tr>
<tr><td><code>MAX_FILES_SCANNED</code></td><td>100,000</td><td>scanner</td><td>Directory walk cap</td></tr>
<tr><td><code>MAX_PATTERN_LENGTH</code></td><td>256 chars</td><td>parser</td><td>Regex length limit</td></tr>
<tr><td><code>REGEX_SIZE_LIMIT</code></td><td>256 KB</td><td>parser</td><td>Compiled regex size limit</td></tr>
<tr><td><code>MAX_TOTAL_PATTERNS</code></td><td>50</td><td>parser</td><td>Total patterns across all categories</td></tr>
<tr><td><code>MAX_CONFIG_PATTERNS</code></td><td>100</td><td>config</td><td>Per-field pattern array cap</td></tr>
<tr><td><code>DEFAULT_CONFIG_PATH</code></td><td><code>.towl.toml</code></td><td>config</td><td>Default config file</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="scanner"><a class="header" href="#scanner">Scanner</a></h1>
<p>The scanner walks a directory tree, filters files by extension and exclude patterns, reads content, and delegates to the parser for TODO extraction.</p>
<h2 id="scanner-1"><a class="header" href="#scanner-1"><code>Scanner</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Scanner {
    parser: Parser,
    config: ParsingConfig,
}
<span class="boring">}</span></code></pre></pre>
<h3 id="constructor"><a class="header" href="#constructor">Constructor</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn new(config: ParsingConfig) -&gt; Result&lt;Self, TowlScannerError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Creates a new scanner. Compiles all regex patterns from the config during construction so pattern errors are caught early.</p>
<h3 id="scan"><a class="header" href="#scan"><code>scan</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn scan(&amp;self, path: PathBuf) -&gt; Result&lt;ScanResult, TowlScannerError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Recursively scans <code>path</code> for TODO comments. Returns a <code>ScanResult</code> on success.</p>
<p><strong>Behaviour:</strong></p>
<ol>
<li>Validates the path (rejects path traversal)</li>
<li>Walks the directory using the <code>ignore</code> crate (respects <code>.gitignore</code>)</li>
<li>Filters files by extension (<code>file_extensions</code> config)</li>
<li>Skips files matching <code>exclude_patterns</code></li>
<li>Skips files larger than <code>MAX_FILE_SIZE</code> (10 MB)</li>
<li>Reads and parses each file asynchronously via <code>tokio::fs</code></li>
<li>Collects results until a resource limit is reached or the walk completes</li>
</ol>
<p><strong>Errors:</strong></p>
<ul>
<li><code>InvalidPath</code> -- Path contains traversal components (<code>..</code>)</li>
<li><code>FileTooLarge</code> -- File exceeds 10 MB</li>
<li><code>TooManyTodos</code> -- Single file exceeds 10,000 TODOs</li>
<li><code>TooManyFiles</code> -- Walk exceeds 100,000 files</li>
<li><code>UnableToReadFileAtPath</code> -- I/O error reading a specific file</li>
<li><code>UnableToWalkFile</code> -- Directory walk error</li>
<li><code>ParsingError</code> -- Regex or parsing failure (propagated from parser)</li>
</ul>
<h2 id="scanresult"><a class="header" href="#scanresult"><code>ScanResult</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct ScanResult {
    pub todos: Vec&lt;TodoComment&gt;,
    pub files_scanned: usize,
    pub files_skipped: usize,
    pub files_errored: usize,
    pub duration: std::time::Duration,
}
<span class="boring">}</span></code></pre></pre>
<h3 id="methods"><a class="header" href="#methods">Methods</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub const fn all_files_failed(&amp;self) -&gt; bool
<span class="boring">}</span></code></pre></pre>
<p>Returns <code>true</code> when <code>files_scanned == 0</code> and <code>files_errored &gt; 0</code>. Indicates a likely permissions or path issue where no files could be read.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub const fn is_clean(&amp;self) -&gt; bool
<span class="boring">}</span></code></pre></pre>
<p>Returns <code>true</code> when <code>todos</code> is empty and <code>files_errored == 0</code>. A clean scan with no issues.</p>
<h2 id="resource-limits-1"><a class="header" href="#resource-limits-1">Resource Limits</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Constant</th><th>Value</th><th>Trigger</th></tr></thead><tbody>
<tr><td><code>MAX_FILE_SIZE</code></td><td>10,485,760 bytes (10 MB)</td><td>File skipped</td></tr>
<tr><td><code>MAX_TODO_COUNT</code></td><td>10,000</td><td>Error for that file</td></tr>
<tr><td><code>MAX_TOTAL_TODO_COUNT</code></td><td>100,000</td><td>Scan stops, returns partial</td></tr>
<tr><td><code>MAX_FILES_SCANNED</code></td><td>100,000</td><td>Scan stops, returns partial</td></tr>
</tbody></table>
</div>
<h2 id="example"><a class="header" href="#example">Example</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use towl::config::ParsingConfig;
use towl::scanner::Scanner;
use std::path::PathBuf;

let config = ParsingConfig::default();
let scanner = Scanner::new(config)?;
let result = scanner.scan(PathBuf::from(".")).await?;

println!("Found {} TODOs in {} files", result.todos.len(), result.files_scanned);

if result.all_files_failed() {
    eprintln!("Warning: no files could be read");
}
<span class="boring">}</span></code></pre></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="parser"><a class="header" href="#parser">Parser</a></h1>
<p>The parser reads file content, identifies comment lines using regex patterns, extracts TODO items, and captures surrounding context.</p>
<h2 id="parser-1"><a class="header" href="#parser-1"><code>Parser</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Parser {
    comment_patterns: Vec&lt;Regex&gt;,
    patterns: Vec&lt;Pattern&gt;,
    function_patterns: Vec&lt;Regex&gt;,
    context_lines: usize,
}
<span class="boring">}</span></code></pre></pre>
<p>The parser is <code>pub(crate)</code> -- it is used internally by <code>Scanner</code> and not exposed in the public API. The public interface is through the module-level functions.</p>
<h3 id="construction"><a class="header" href="#construction">Construction</a></h3>
<p>Created internally by <code>Scanner::new()</code> using <code>Parser::new(config)</code>. All regex patterns are compiled once during construction.</p>
<h2 id="public-functions"><a class="header" href="#public-functions">Public Functions</a></h2>
<h3 id="validate_patterns"><a class="header" href="#validate_patterns"><code>validate_patterns</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn validate_patterns(config: &amp;ParsingConfig) -&gt; Result&lt;(), TowlParserError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Validates all regex patterns in the config without creating a parser. Useful for checking configuration before starting a scan.</p>
<p><strong>Checks:</strong></p>
<ul>
<li>Each pattern is valid regex</li>
<li>Each pattern is within <code>MAX_PATTERN_LENGTH</code> (256 characters)</li>
<li>Compiled regex is within <code>REGEX_SIZE_LIMIT</code> (256 KB)</li>
</ul>
<h3 id="parse_content"><a class="header" href="#parse_content"><code>parse_content</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn parse_content(
    config: &amp;ParsingConfig,
    path: &amp;Path,
    content: &amp;str,
) -&gt; Result&lt;Vec&lt;TodoComment&gt;, TowlParserError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Parses file content for TODO comments. Creates a temporary parser, runs extraction, and returns the results.</p>
<h2 id="parsing-pipeline"><a class="header" href="#parsing-pipeline">Parsing Pipeline</a></h2>
<p>For each line in the file:</p>
<ol>
<li><strong>Comment detection</strong> -- Check if the line matches any <code>comment_prefixes</code> pattern</li>
<li><strong>TODO matching</strong> -- Check if the comment matches any <code>todo_patterns</code> pattern</li>
<li><strong>Type classification</strong> -- Determine the <code>TodoType</code> from the matched pattern</li>
<li><strong>Description extraction</strong> -- Extract the description via the first capture group <code>(.*)</code></li>
<li><strong>Context capture</strong> -- Grab <code>include_context_lines</code> lines above and below</li>
<li><strong>Function detection</strong> -- Search upward (within 3 lines) for a <code>function_patterns</code> match</li>
</ol>
<h2 id="pattern-types"><a class="header" href="#pattern-types">Pattern Types</a></h2>
<h3 id="comment-prefixes"><a class="header" href="#comment-prefixes">Comment Prefixes</a></h3>
<p>Regex patterns that identify comment lines:</p>
<div class="table-wrapper"><table><thead><tr><th>Default pattern</th><th>Matches</th></tr></thead><tbody>
<tr><td><code>//</code></td><td>C-style line comments</td></tr>
<tr><td><code>^\s*#</code></td><td>Shell/Python comments</td></tr>
<tr><td><code>/\*</code></td><td>C-style block comment start</td></tr>
<tr><td><code>^\s*\*</code></td><td>C-style block comment continuation</td></tr>
</tbody></table>
</div>
<h3 id="todo-patterns"><a class="header" href="#todo-patterns">TODO Patterns</a></h3>
<p>Regex patterns with a capture group for the description:</p>
<div class="table-wrapper"><table><thead><tr><th>Default pattern</th><th>Matches</th></tr></thead><tbody>
<tr><td><code>(?i)\bTODO:\s*(.*)</code></td><td>TODO comments</td></tr>
<tr><td><code>(?i)\bFIXME:\s*(.*)</code></td><td>FIXME comments</td></tr>
<tr><td><code>(?i)\bHACK:\s*(.*)</code></td><td>HACK comments</td></tr>
<tr><td><code>(?i)\bNOTE:\s*(.*)</code></td><td>NOTE comments</td></tr>
<tr><td><code>(?i)\bBUG:\s*(.*)</code></td><td>BUG comments</td></tr>
</tbody></table>
</div>
<p>All default patterns are case-insensitive (<code>(?i)</code>).</p>
<h3 id="function-patterns"><a class="header" href="#function-patterns">Function Patterns</a></h3>
<p>Regex patterns to detect enclosing function names:</p>
<div class="table-wrapper"><table><thead><tr><th>Default pattern</th><th>Language</th></tr></thead><tbody>
<tr><td><code>^\s*(pub\s+)?fn\s+(\w+)</code></td><td>Rust</td></tr>
<tr><td><code>^\s*def\s+(\w+)</code></td><td>Python</td></tr>
<tr><td><code>^\s*(async\s+)?function\s+(\w+)</code></td><td>JavaScript</td></tr>
<tr><td><code>^\s*(public|private|protected)?\s*(static\s+)?\w+\s+(\w+)\s*\(</code></td><td>Java/C#</td></tr>
<tr><td><code>^\s*func\s+(\w+)</code></td><td>Go/Swift</td></tr>
</tbody></table>
</div>
<h2 id="constants-1"><a class="header" href="#constants-1">Constants</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Constant</th><th>Value</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>MIN_CONTEXT_LINES</code></td><td>1</td><td>Minimum context window</td></tr>
<tr><td><code>MAX_CONTEXT_LINES</code></td><td>50</td><td>Maximum context window</td></tr>
<tr><td><code>FORWARD_SEARCH_LINES</code></td><td>3</td><td>Lines searched upward for function context</td></tr>
<tr><td><code>MAX_PATTERN_LENGTH</code></td><td>256</td><td>Maximum regex pattern string length</td></tr>
<tr><td><code>REGEX_SIZE_LIMIT</code></td><td>262,144</td><td>Maximum compiled regex size (256 KB)</td></tr>
<tr><td><code>MAX_TOTAL_PATTERNS</code></td><td>50</td><td>Maximum total patterns across all categories</td></tr>
</tbody></table>
</div>
<h2 id="errors"><a class="header" href="#errors">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlParserError {
    InvalidRegexPattern(String, regex::Error),
    UnknownConfigPattern(TowlCommentError),
    RegexGroupMissing,
    PatternTooLong(usize, usize),
    TooManyTotalPatterns { count: usize, max_allowed: usize },
}
<span class="boring">}</span></code></pre></pre>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>InvalidRegexPattern</code></td><td>Regex failed to compile</td></tr>
<tr><td><code>UnknownConfigPattern</code></td><td>Pattern matched but type could not be determined</td></tr>
<tr><td><code>RegexGroupMissing</code></td><td>Pattern lacks a capture group</td></tr>
<tr><td><code>PatternTooLong</code></td><td>Pattern exceeds 256 characters</td></tr>
<tr><td><code>TooManyTotalPatterns</code></td><td>Total patterns across all categories exceeds 50</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="config"><a class="header" href="#config">Config</a></h1>
<p>The config module loads settings from <code>.towl.toml</code>, merges environment variables, and provides the <code>init</code> command for generating default configuration.</p>
<h2 id="towlconfig"><a class="header" href="#towlconfig"><code>TowlConfig</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct TowlConfig {
    pub parsing: ParsingConfig,
    pub github: GitHubConfig,
    pub llm: LlmConfig,
}
<span class="boring">}</span></code></pre></pre>
<h3 id="towlconfigload"><a class="header" href="#towlconfigload"><code>TowlConfig::load</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>impl TowlConfig {
    pub fn load(path: Option&lt;&amp;PathBuf&gt;) -&gt; Result&lt;Self, TowlConfigError&gt;;
}
<span class="boring">}</span></code></pre></pre>
<p>Loads configuration with this precedence:</p>
<ol>
<li>Built-in defaults</li>
<li>Config file resolved as: explicit <code>path</code> argument &gt; <code>TOWL_CONFIG</code> env var &gt; <code>.towl.toml</code></li>
<li>Git remote auto-detection for owner/repo</li>
<li>Environment variable overrides (<code>TOWL_GITHUB_*</code>, <code>TOWL_LLM_*</code>)</li>
</ol>
<p>If no config file exists, defaults are used without error.</p>
<h3 id="init"><a class="header" href="#init"><code>init</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn init(path: &amp;Path, force: bool) -&gt; Result&lt;(), TowlConfigError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Creates a <code>.towl.toml</code> file at the given path. Validates that a GitHub git remote exists but does not write owner/repo to the file (they are always detected at runtime).</p>
<ul>
<li>Fails if the file already exists (unless <code>force</code> is <code>true</code>)</li>
<li>Validates the path for traversal attacks</li>
<li>Serializes <code>ParsingConfig</code> and <code>LlmConfig</code> defaults to TOML</li>
</ul>
<h2 id="parsingconfig"><a class="header" href="#parsingconfig"><code>ParsingConfig</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct ParsingConfig {
    pub file_extensions: HashSet&lt;String&gt;,
    pub exclude_patterns: Vec&lt;String&gt;,
    pub include_context_lines: usize,
    pub comment_prefixes: Vec&lt;String&gt;,
    pub todo_patterns: Vec&lt;String&gt;,
    pub function_patterns: Vec&lt;String&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>All fields have defaults via <code>#[serde(default)]</code>:</p>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Default</th></tr></thead><tbody>
<tr><td><code>file_extensions</code></td><td><code>rs</code>, <code>toml</code>, <code>json</code>, <code>yaml</code>, <code>yml</code>, <code>sh</code>, <code>bash</code></td></tr>
<tr><td><code>exclude_patterns</code></td><td><code>target/*</code>, <code>.git/*</code></td></tr>
<tr><td><code>include_context_lines</code></td><td><code>10</code></td></tr>
<tr><td><code>comment_prefixes</code></td><td><code>//</code>, <code>^\s*#</code>, <code>/\*</code>, <code>^\s*\*</code></td></tr>
<tr><td><code>todo_patterns</code></td><td><code>TODO:</code>, <code>FIXME:</code>, <code>HACK:</code>, <code>NOTE:</code>, <code>BUG:</code> (case-insensitive)</td></tr>
<tr><td><code>function_patterns</code></td><td>Rust, Python, JS, Java/C#, Go patterns</td></tr>
</tbody></table>
</div>
<p>Each pattern array is limited to <code>MAX_CONFIG_PATTERNS</code> (100) entries.</p>
<h2 id="githubconfig"><a class="header" href="#githubconfig"><code>GitHubConfig</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct GitHubConfig {
    pub token: SecretString,
    pub owner: Owner,
    pub repo: Repo,
    pub rate_limit_delay_ms: u64,
}
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>token</code> is stored as <code>secrecy::SecretString</code> and masked in debug/display output</li>
<li><code>owner</code> and <code>repo</code> are auto-detected from <code>git remote get-url origin</code> at runtime (not serialised to config)</li>
<li><code>rate_limit_delay_ms</code> adds a delay between GitHub API calls (default: 1000ms)</li>
</ul>
<h3 id="environment-variable-overrides"><a class="header" href="#environment-variable-overrides">Environment Variable Overrides</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Variable</th><th>Overrides</th></tr></thead><tbody>
<tr><td><code>TOWL_CONFIG</code></td><td><code>DEFAULT_CONFIG_PATH</code> (overridden by explicit <code>path</code> argument)</td></tr>
<tr><td><code>TOWL_GITHUB_TOKEN</code></td><td>-- (env-only)</td></tr>
<tr><td><code>TOWL_GITHUB_OWNER</code></td><td>git remote detection</td></tr>
<tr><td><code>TOWL_GITHUB_REPO</code></td><td>git remote detection</td></tr>
</tbody></table>
</div>
<h2 id="owner--repo"><a class="header" href="#owner--repo"><code>Owner</code> / <code>Repo</code></a></h2>
<p>Validated newtype wrappers providing type safety:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Owner(String);
pub struct Repo(String);
<span class="boring">}</span></code></pre></pre>
<h3 id="try_new"><a class="header" href="#try_new"><code>try_new</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn try_new(s: impl Into&lt;String&gt;) -&gt; Result&lt;Self, TowlConfigError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Constructs a new <code>Owner</code> or <code>Repo</code>, rejecting values exceeding <code>MAX_CONFIG_STRING_LENGTH</code> (512 characters).</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>ConfigValueTooLong</code> -- Value exceeds 512 characters</li>
</ul>
<p>Both also implement:</p>
<ul>
<li><code>Display</code>, <code>Default</code>, <code>Debug</code>, <code>Clone</code>, <code>PartialEq</code>, <code>Eq</code></li>
<li><code>Serialize</code>, <code>Deserialize</code></li>
</ul>
<h2 id="llmconfig"><a class="header" href="#llmconfig"><code>LlmConfig</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct LlmConfig {
    pub provider: String,
    pub model: String,
    pub base_url: Option&lt;String&gt;,
    pub api_key: SecretString,
    pub max_concurrent_analyses: usize,
    pub max_analyse_count: usize,
    pub max_tokens: u32,
    pub max_retries: usize,
    pub command: Option&lt;String&gt;,
    pub args: Option&lt;Vec&lt;String&gt;&gt;,
}
<span class="boring">}</span></code></pre></pre>
<ul>
<li><code>api_key</code> is stored as <code>secrecy::SecretString</code> and masked in debug output (env-only via <code>TOWL_LLM_API_KEY</code>)</li>
<li><code>provider</code> selects the LLM backend: <code>"claude"</code>, <code>"openai"</code>, <code>"claude-code"</code>, or <code>"codex"</code></li>
<li><code>command</code> and <code>args</code> allow overriding the CLI binary path and arguments for CLI providers</li>
</ul>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Default</th></tr></thead><tbody>
<tr><td><code>provider</code></td><td><code>"claude"</code></td></tr>
<tr><td><code>model</code></td><td><code>"claude-opus-4-6"</code></td></tr>
<tr><td><code>base_url</code></td><td><code>None</code> (provider default)</td></tr>
<tr><td><code>max_concurrent_analyses</code></td><td><code>5</code></td></tr>
<tr><td><code>max_analyse_count</code></td><td><code>50</code></td></tr>
<tr><td><code>max_tokens</code></td><td><code>4096</code></td></tr>
<tr><td><code>max_retries</code></td><td><code>3</code></td></tr>
</tbody></table>
</div>
<h3 id="environment-variable-overrides-1"><a class="header" href="#environment-variable-overrides-1">Environment Variable Overrides</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Variable</th><th>Overrides</th></tr></thead><tbody>
<tr><td><code>TOWL_LLM_API_KEY</code></td><td>-- (env-only)</td></tr>
<tr><td><code>TOWL_LLM_PROVIDER</code></td><td><code>llm.provider</code></td></tr>
<tr><td><code>TOWL_LLM_MODEL</code></td><td><code>llm.model</code></td></tr>
<tr><td><code>TOWL_LLM_BASE_URL</code></td><td><code>llm.base_url</code></td></tr>
</tbody></table>
</div>
<h2 id="gitrepoinfo-internal"><a class="header" href="#gitrepoinfo-internal"><code>GitRepoInfo</code> (internal)</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) struct GitRepoInfo {
    pub owner: Owner,
    pub repo: Repo,
}
<span class="boring">}</span></code></pre></pre>
<h3 id="from_path"><a class="header" href="#from_path"><code>from_path</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) async fn from_path&lt;P: AsRef&lt;Path&gt;&gt;(path: P) -&gt; Result&lt;Self, TowlConfigError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Internal function that discovers the git remote URL by running <code>git remote get-url origin</code> and parses the owner and repo name. Supports both HTTPS and SSH URL formats. Not part of the public API.</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>GitRepoNotFound</code> -- Not inside a git repository</li>
<li><code>GitRemoteNotFound</code> -- No <code>origin</code> remote configured</li>
<li><code>GitInvalidUrl</code> -- Could not parse owner/repo from the URL</li>
</ul>
<h2 id="errors-1"><a class="header" href="#errors-1">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlConfigError {
    PathTraversalAttempt(PathBuf),
    ConfigAlreadyExists(PathBuf),
    WriteToFileError(PathBuf, std::io::Error),
    UnableToParseToml(toml::ser::Error),
    CouldNotCreateConfig(ConfigError),
    GitRepoNotFound { message: String },
    GitRemoteNotFound { message: String },
    GitInvalidUrl { url: String, message: String },
    TooManyConfigPatterns { field: String, count: usize, max_allowed: usize },
    ConfigValueTooLong { field: String, length: usize, max_length: usize },
    ContextLinesOutOfRange { value: usize, min: usize, max: usize },
}
<span class="boring">}</span></code></pre></pre>
<h2 id="constants-2"><a class="header" href="#constants-2">Constants</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Constant</th><th>Value</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>DEFAULT_CONFIG_PATH</code></td><td><code>.towl.toml</code></td><td>Default config file name</td></tr>
<tr><td><code>MAX_CONFIG_PATTERNS</code></td><td>100</td><td>Maximum entries per pattern array</td></tr>
<tr><td><code>MAX_CONFIG_STRING_LENGTH</code></td><td>512</td><td>Maximum length for any single config string</td></tr>
<tr><td><code>MIN_CONTEXT_LINES</code></td><td>1</td><td>Minimum <code>include_context_lines</code> value</td></tr>
<tr><td><code>MAX_CONTEXT_LINES</code></td><td>50</td><td>Maximum <code>include_context_lines</code> value</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="output"><a class="header" href="#output">Output</a></h1>
<p>The output module combines a formatter and a writer to produce scan results in the requested format and destination.</p>
<h2 id="output-1"><a class="header" href="#output-1"><code>Output</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Output {
    writer: WriterImpl,
    formatter: FormatterImpl,
}
<span class="boring">}</span></code></pre></pre>
<h3 id="constructor-1"><a class="header" href="#constructor-1">Constructor</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn new(
    output_format: OutputFormat,
    output_path: Option&lt;PathBuf&gt;,
) -&gt; Result&lt;Self, TowlOutputError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Creates an output handler by selecting the appropriate formatter and writer.</p>
<p><strong>Format-to-writer mapping:</strong></p>
<div class="table-wrapper"><table><thead><tr><th>Format</th><th>Writer</th><th>Output path</th></tr></thead><tbody>
<tr><td><code>Table</code> / <code>Terminal</code></td><td><code>StdoutWriter</code></td><td>Must be <code>None</code></td></tr>
<tr><td><code>Json</code></td><td><code>FileWriter</code></td><td>Required, must end in <code>.json</code></td></tr>
<tr><td><code>Csv</code></td><td><code>FileWriter</code></td><td>Required, must end in <code>.csv</code></td></tr>
<tr><td><code>Toml</code></td><td><code>FileWriter</code></td><td>Required, must end in <code>.toml</code></td></tr>
<tr><td><code>Markdown</code></td><td><code>FileWriter</code></td><td>Required, must end in <code>.md</code></td></tr>
</tbody></table>
</div>
<h3 id="save"><a class="header" href="#save"><code>save</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn save(&amp;self, todos: &amp;[TodoComment]) -&gt; Result&lt;(), TowlOutputError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Formats the TODOs and writes them to the destination. TODOs are grouped by type before formatting.</p>
<h2 id="outputformat"><a class="header" href="#outputformat"><code>OutputFormat</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum OutputFormat {
    Table,
    Json,
    Csv,
    Toml,
    Markdown,
    Terminal,
}
<span class="boring">}</span></code></pre></pre>
<p>Used as a CLI argument via <code>clap::ValueEnum</code>. <code>Table</code> and <code>Terminal</code> are treated identically.</p>
<h2 id="formatter-dispatch"><a class="header" href="#formatter-dispatch">Formatter Dispatch</a></h2>
<p>Internally, <code>FormatterImpl</code> is an enum that dispatches to the correct formatter without dynamic dispatch:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) enum FormatterImpl {
    Csv(CsvFormatter),
    Json(JsonFormatter),
    Markdown(MarkdownFormatter),
    Table(TableFormatter),
    Toml(TomlFormatter),
}
<span class="boring">}</span></code></pre></pre>
<p>Each formatter implements the internal <code>Formatter</code> trait:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) trait Formatter {
    fn format(
        &amp;self,
        todos: &amp;HashMap&lt;&amp;TodoType, Vec&lt;&amp;TodoComment&gt;&gt;,
        total_count: usize,
    ) -&gt; Result&lt;Vec&lt;String&gt;, FormatterError&gt;;
}
<span class="boring">}</span></code></pre></pre>
<h2 id="writer-dispatch"><a class="header" href="#writer-dispatch">Writer Dispatch</a></h2>
<p><code>WriterImpl</code> dispatches between stdout and file output:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) enum WriterImpl {
    Stdout(StdoutWriter),
    File(FileWriter),
}
<span class="boring">}</span></code></pre></pre>
<p>Each writer implements the internal <code>Writer</code> trait:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub(crate) trait Writer {
    async fn write(&amp;self, content: Vec&lt;String&gt;) -&gt; Result&lt;(), WriterError&gt;;
}
<span class="boring">}</span></code></pre></pre>
<h3 id="filewriter"><a class="header" href="#filewriter"><code>FileWriter</code></a></h3>
<p>Validates the output path on construction:</p>
<ul>
<li>Rejects path traversal (<code>..</code> components)</li>
<li>Resolves symlinks before writing</li>
</ul>
<h3 id="stdoutwriter"><a class="header" href="#stdoutwriter"><code>StdoutWriter</code></a></h3>
<p>Writes each formatted line to stdout followed by a newline.</p>
<h2 id="errors-2"><a class="header" href="#errors-2">Errors</a></h2>
<h3 id="towloutputerror"><a class="header" href="#towloutputerror"><code>TowlOutputError</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlOutputError {
    InvalidOutputPath(String),
    UnableToFormatTodos(FormatterError),
    UnableToWriteTodos(WriterError),
}
<span class="boring">}</span></code></pre></pre>
<h3 id="formattererror"><a class="header" href="#formattererror"><code>FormatterError</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum FormatterError {
    SerializationError(String),
    IntegerOverflow(usize),
}
<span class="boring">}</span></code></pre></pre>
<h3 id="writererror"><a class="header" href="#writererror"><code>WriterError</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum WriterError {
    IoError(std::io::Error),
    PathTraversal(PathBuf),
}
<span class="boring">}</span></code></pre></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="github"><a class="header" href="#github">GitHub</a></h1>
<p>The GitHub module creates issues from TODO comments, detects duplicates, and handles rate limiting.</p>
<h2 id="githubclient"><a class="header" href="#githubclient"><code>GitHubClient</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct GitHubClient {
    // private fields
}
<span class="boring">}</span></code></pre></pre>
<p>Authenticated GitHub API client for creating issues from TODO comments. Maintains a cache of existing issue titles and TODO IDs for deduplication. Includes rate-limit handling with configurable delays and automatic retries.</p>
<h3 id="new"><a class="header" href="#new"><code>new</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn new(config: &amp;GitHubConfig) -&gt; Result&lt;Self, TowlGitHubError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Creates a new client from a <code>GitHubConfig</code>. Exposes the <code>SecretString</code> token once to build the Octocrab API client.</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>MissingToken</code> -- Token is empty</li>
<li><code>ApiError</code> -- Octocrab client failed to build</li>
</ul>
<h3 id="load_existing_issues"><a class="header" href="#load_existing_issues"><code>load_existing_issues</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn load_existing_issues(&amp;mut self) -&gt; Result&lt;(), TowlGitHubError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Paginates through all existing issues (open and closed) in the repository, caching their titles and embedded TODO IDs. Call this before <code>create_issue</code> to enable duplicate detection.</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>ApiError</code> -- GitHub API call failed</li>
<li><code>AuthError</code> -- Invalid or expired token</li>
<li><code>RepositoryNotFound</code> -- Owner/repo combination does not exist</li>
</ul>
<h3 id="issue_exists"><a class="header" href="#issue_exists"><code>issue_exists</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn issue_exists(&amp;self, todo: &amp;TodoComment) -&gt; bool
<span class="boring">}</span></code></pre></pre>
<p>Returns <code>true</code> if a matching issue already exists, checked by TODO ID (embedded in issue body) or by generated title.</p>
<h3 id="create_issue"><a class="header" href="#create_issue"><code>create_issue</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn create_issue(
    &amp;mut self,
    todo: &amp;TodoComment,
) -&gt; Result&lt;CreatedIssue, TowlGitHubError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Creates a GitHub issue for a TODO comment. Generates a title with type prefix, truncated description, and file location. The body includes file path, line number, column range, description, function context, original comment, and surrounding code.</p>
<p>Automatically retries on rate limiting (up to 3 attempts).</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>IssueAlreadyExists</code> -- Duplicate detected</li>
<li><code>RateLimitExceeded</code> -- Rate limit hit after max retries</li>
<li><code>ApiError</code> -- GitHub API failure</li>
<li><code>AuthError</code> -- Authentication failure</li>
</ul>
<h3 id="issue-title-format"><a class="header" href="#issue-title-format">Issue Title Format</a></h3>
<p>Follows a conventional commit-style pattern:</p>
<pre><code class="language-text">todo: implement caching layer for database queries
fixme: handle timeout in network connect
bug: null pointer when processing empty input
</code></pre>
<p>The type prefix is the lowercase TODO type (<code>todo</code>, <code>fixme</code>, <code>hack</code>, <code>note</code>, <code>bug</code>), followed by a colon and the full description. Titles exceeding 256 characters are truncated at word boundaries with <code>...</code>.</p>
<h3 id="issue-body-sections"><a class="header" href="#issue-body-sections">Issue Body Sections</a></h3>
<ol>
<li><strong>TODO Details</strong> -- Type, file, line, column range</li>
<li><strong>Description</strong> -- Extracted description text (Markdown-escaped)</li>
<li><strong>Function Context</strong> -- Enclosing function name (if detected)</li>
<li><strong>Original Comment</strong> -- Full comment line in a code block</li>
<li><strong>Context</strong> -- Surrounding source lines in a code block</li>
<li><strong>TODO ID</strong> -- Embedded identifier for deduplication</li>
</ol>
<h3 id="duplicate-detection"><a class="header" href="#duplicate-detection">Duplicate Detection</a></h3>
<p>Issues are deduplicated by two methods:</p>
<ol>
<li><strong>TODO ID</strong> -- Each issue body contains <code>*TODO ID: {file_path}_L{line_number}*</code>. If any existing issue body contains the same ID, the TODO is skipped.</li>
<li><strong>Title match</strong> -- If the generated title matches an existing issue title exactly, the TODO is skipped.</li>
</ol>
<h2 id="createdissue"><a class="header" href="#createdissue"><code>CreatedIssue</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct CreatedIssue {
    pub number: u64,
    pub title: String,
    pub html_url: String,
    pub todo_id: String,
}
<span class="boring">}</span></code></pre></pre>
<p>Metadata for a successfully created GitHub issue. Implements <code>Serialize</code> and <code>Deserialize</code> for JSON roundtripping.</p>
<h2 id="errors-3"><a class="header" href="#errors-3">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlGitHubError {
    ApiError { message: String, source: Option&lt;octocrab::Error&gt; },
    AuthError,
    RateLimitExceeded { retry_after_secs: u64 },
    IssueAlreadyExists { title: String },
    RepositoryNotFound { owner: String, repo: String },
    MissingToken,
}
<span class="boring">}</span></code></pre></pre>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>ApiError</code></td><td>General GitHub API failure</td></tr>
<tr><td><code>AuthError</code></td><td>401 response -- invalid or expired token</td></tr>
<tr><td><code>RateLimitExceeded</code></td><td>403 with "rate limit" in message</td></tr>
<tr><td><code>IssueAlreadyExists</code></td><td>Duplicate detected before creation</td></tr>
<tr><td><code>RepositoryNotFound</code></td><td>404 response -- owner/repo not found</td></tr>
<tr><td><code>MissingToken</code></td><td><code>TOWL_GITHUB_TOKEN</code> not set or empty</td></tr>
</tbody></table>
</div>
<h2 id="example-1"><a class="header" href="#example-1">Example</a></h2>
<pre><pre class="playground"><code class="language-rust no_run"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use towl::config::TowlConfig;
use towl::github::GitHubClient;

let config = TowlConfig::load(None)?;
let mut client = GitHubClient::new(&amp;config.github)?;

// Load existing issues for duplicate detection
client.load_existing_issues().await?;

// Create an issue (skips if duplicate)
if !client.issue_exists(&amp;todo) {
    let issue = client.create_issue(&amp;todo).await?;
    println!("Created #{}: {}", issue.number, issue.html_url);
}
<span class="boring">}</span></code></pre></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="llm"><a class="header" href="#llm">LLM</a></h1>
<p>The LLM module provides AI-powered TODO validation using Claude (Anthropic API) or any OpenAI-compatible endpoint.</p>
<h2 id="llmprovider"><a class="header" href="#llmprovider"><code>LlmProvider</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum LlmProvider {
    Claude(ClaudeProvider),
    OpenAi(OpenAiProvider),
    ClaudeCode(ClaudeCodeProvider),
    Codex(CodexProvider),
}
<span class="boring">}</span></code></pre></pre>
<p>Dispatches LLM calls to the configured provider. Follows towl's existing enum dispatch pattern (<code>FormatterImpl</code>, <code>WriterImpl</code>).</p>
<h3 id="call_raw"><a class="header" href="#call_raw"><code>call_raw</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn call_raw(
    &amp;self,
    user_content: &amp;str,
    system_prompt: &amp;str,
    api_key: &amp;SecretString,
) -&gt; Result&lt;(String, LlmUsage), TowlLlmError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Sends a prompt to the LLM and returns the response text and token usage.</p>
<h3 id="build_provider"><a class="header" href="#build_provider"><code>build_provider</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn build_provider(config: &amp;LlmConfig) -&gt; Result&lt;LlmProvider, TowlLlmError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Factory function that creates the appropriate provider from configuration.</p>
<h2 id="analyse_todos"><a class="header" href="#analyse_todos"><code>analyse_todos</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn analyse_todos(
    todos: &amp;mut [TodoComment],
    config: &amp;LlmConfig,
    on_progress: impl FnMut(usize, usize),
) -&gt; Result&lt;AnalysisSummary, TowlLlmError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Main entry point for TODO analysis. Calls <code>on_progress(completed, total)</code> after each TODO is analysed, allowing callers to render progress feedback (e.g. a progress bar).</p>
<p>For each TODO (up to <code>max_analyse_count</code>):</p>
<ol>
<li>Reads expanded context (~30 lines around the TODO + full function body)</li>
<li>Constructs a prompt with the TODO description, file path, and code context</li>
<li>Calls the LLM to determine validity</li>
<li>Parses the structured JSON response into an <code>AnalysisResult</code></li>
<li>Attaches the result to <code>TodoComment.analysis</code></li>
<li>Calls <code>on_progress</code> with the current count</li>
</ol>
<p><strong>Errors:</strong></p>
<ul>
<li><code>NotConfigured</code> -- <code>TOWL_LLM_API_KEY</code> not set</li>
<li><code>UnsupportedProvider</code> -- Provider is not "claude" or "openai"</li>
<li><code>ApiError</code>, <code>AuthError</code>, <code>RateLimited</code> -- From the LLM API</li>
</ul>
<h2 id="validity"><a class="header" href="#validity"><code>Validity</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum Validity {
    Valid,
    Invalid,
    Uncertain,
}
<span class="boring">}</span></code></pre></pre>
<p>Whether a TODO is still valid:</p>
<div class="table-wrapper"><table><thead><tr><th>Value</th><th>Meaning</th></tr></thead><tbody>
<tr><td><code>Valid</code></td><td>TODO describes work that still needs to be done</td></tr>
<tr><td><code>Invalid</code></td><td>TODO has been resolved, is irrelevant, or is nonsensical</td></tr>
<tr><td><code>Uncertain</code></td><td>Cannot determine validity from available context</td></tr>
</tbody></table>
</div>
<h2 id="analysisresult"><a class="header" href="#analysisresult"><code>AnalysisResult</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct AnalysisResult {
    pub validity: Validity,
    pub reasoning: String,
    pub is_resolved: bool,
    pub is_relevant: bool,
    pub is_actionable: bool,
    pub confidence: f64,
    pub enrichment: String,
}
<span class="boring">}</span></code></pre></pre>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody>
<tr><td><code>validity</code></td><td>Overall assessment</td></tr>
<tr><td><code>reasoning</code></td><td>Explanation of why the TODO is valid/invalid/uncertain</td></tr>
<tr><td><code>is_resolved</code></td><td>Whether the code already implements what the TODO asks</td></tr>
<tr><td><code>is_relevant</code></td><td>Whether the code/feature the TODO references still exists</td></tr>
<tr><td><code>is_actionable</code></td><td>Whether the TODO describes a clear, specific task</td></tr>
<tr><td><code>confidence</code></td><td>0.0-1.0 confidence in the assessment</td></tr>
<tr><td><code>enrichment</code></td><td>Enhanced description suitable for a GitHub issue body</td></tr>
</tbody></table>
</div>
<h2 id="analysissummary"><a class="header" href="#analysissummary"><code>AnalysisSummary</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct AnalysisSummary {
    pub valid_count: usize,
    pub invalid_count: usize,
    pub uncertain_count: usize,
    pub error_count: usize,
}
<span class="boring">}</span></code></pre></pre>
<p>Summary counts returned by <code>analyse_todos()</code>.</p>
<h2 id="providers"><a class="header" href="#providers">Providers</a></h2>
<h3 id="claudeprovider"><a class="header" href="#claudeprovider"><code>ClaudeProvider</code></a></h3>
<p>POST to <code>https://api.anthropic.com/v1/messages</code> with headers:</p>
<ul>
<li><code>x-api-key</code>: API key</li>
<li><code>anthropic-version</code>: <code>2023-06-01</code></li>
</ul>
<p>System prompt is a top-level <code>system</code> field (not in the messages array).</p>
<h3 id="openaiprovider"><a class="header" href="#openaiprovider"><code>OpenAiProvider</code></a></h3>
<p>POST to <code>{base_url}/chat/completions</code> with <code>Authorization: Bearer {key}</code>.
Default base URL: <code>https://api.openai.com/v1</code>. Configurable for Ollama, vLLM, etc.</p>
<p>System prompt is the first message in the <code>messages</code> array with <code>role: "system"</code>.</p>
<h3 id="claudecodeprovider"><a class="header" href="#claudecodeprovider"><code>ClaudeCodeProvider</code></a></h3>
<p>Invokes the <code>claude</code> CLI as a subprocess with <code>-p --output-format json</code>. The combined system prompt and user content are passed as the final argument. No API key required.</p>
<p>Default command: <code>claude</code>. Configurable via <code>llm.command</code> and <code>llm.args</code>.</p>
<p>Auto-falls back to <code>ClaudeProvider</code> (API) if the CLI binary is not found on PATH.</p>
<h3 id="codexprovider"><a class="header" href="#codexprovider"><code>CodexProvider</code></a></h3>
<p>Invokes the <code>codex</code> CLI as a subprocess with <code>-q</code>. The combined prompt is passed as the final argument. No API key required.</p>
<p>Default command: <code>codex</code>. Configurable via <code>llm.command</code> and <code>llm.args</code>.</p>
<p>Auto-falls back to <code>OpenAiProvider</code> (API) with <code>gpt-4o</code> if the CLI binary is not found on PATH.</p>
<h3 id="is_cli_provider"><a class="header" href="#is_cli_provider"><code>is_cli_provider</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub const fn is_cli_provider(&amp;self) -&gt; bool
<span class="boring">}</span></code></pre></pre>
<p>Returns <code>true</code> for <code>ClaudeCode</code> and <code>Codex</code> variants. Used to skip the API key requirement for CLI-based providers.</p>
<h2 id="configuration-2"><a class="header" href="#configuration-2">Configuration</a></h2>
<p>See <a href="api/../getting-started/configuration.html#llm-section">Configuration</a> for the <code>[llm]</code> config section.</p>
<h2 id="errors-4"><a class="header" href="#errors-4">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlLlmError {
    ApiError { message: String, status: Option&lt;u16&gt; },
    AuthError,
    RateLimited { retry_after_secs: u64 },
    ParseError { message: String },
    NotConfigured,
    UnsupportedProvider { provider: String },
    IoError { message: String },
}
<span class="boring">}</span></code></pre></pre>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>ApiError</code></td><td>LLM API returned a non-200 status</td></tr>
<tr><td><code>AuthError</code></td><td>401 -- invalid or missing API key</td></tr>
<tr><td><code>RateLimited</code></td><td>429 -- too many requests</td></tr>
<tr><td><code>ParseError</code></td><td>LLM response could not be parsed as valid JSON</td></tr>
<tr><td><code>NotConfigured</code></td><td><code>TOWL_LLM_API_KEY</code> environment variable not set</td></tr>
<tr><td><code>UnsupportedProvider</code></td><td>Provider is not "claude", "openai", "claude-code", or "codex"</td></tr>
<tr><td><code>IoError</code></td><td>File I/O error during context gathering</td></tr>
</tbody></table>
</div>
<h3 id="retryable-errors"><a class="header" href="#retryable-errors">Retryable Errors</a></h3>
<p><code>TowlLlmError</code> implements <code>is_retryable()</code> which returns <code>true</code> for:</p>
<ul>
<li><code>RateLimited</code> -- always retryable</li>
<li><code>ApiError</code> with status &gt;= 500 -- server errors</li>
<li><code>ApiError</code> with no status -- network failures</li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="processor"><a class="header" href="#processor">Processor</a></h1>
<p>The processor replaces TODO comments in source files with GitHub issue links after issues are created.</p>
<h2 id="processor-1"><a class="header" href="#processor-1"><code>Processor</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Processor;
<span class="boring">}</span></code></pre></pre>
<p>Stateless processor that operates on batches of <code>(TodoComment, CreatedIssue)</code> pairs. All methods are associated functions (no <code>self</code>).</p>
<h3 id="replace_todos"><a class="header" href="#replace_todos"><code>replace_todos</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub async fn replace_todos(
    repo_root: &amp;Path,
    replacements: &amp;[(TodoComment, CreatedIssue)],
) -&gt; ProcessorResult
<span class="boring">}</span></code></pre></pre>
<p>Replaces TODO comments in source files with <code>GH_ISSUE: &lt;issue_url&gt;</code> links.</p>
<p><strong>Behaviour:</strong></p>
<ol>
<li>Groups replacements by file path for efficient batch processing</li>
<li>For each file, validates the path stays within <code>repo_root</code></li>
<li>Reads file content, replaces each TODO line, writes back atomically</li>
<li>Returns a <code>ProcessorResult</code> with counts and per-file errors</li>
</ol>
<p><strong>Path safety:</strong></p>
<ul>
<li>Both the file path and repo root are canonicalised before comparison</li>
<li>Files outside the repo root are rejected with <code>PathOutsideRoot</code></li>
<li>Issue URLs must start with <code>https://github.com/</code> or are rejected</li>
</ul>
<p><strong>Replacement format:</strong></p>
<p>The comment prefix (e.g., <code>// </code>, <code># </code>, <code>/* </code>) is preserved. The TODO text after the prefix is replaced:</p>
<pre><code class="language-text">// TODO: Implement caching    --&gt;    // GH_ISSUE: https://github.com/owner/repo/issues/42
# FIXME: Handle timeout        --&gt;    # GH_ISSUE: https://github.com/owner/repo/issues/43
</code></pre>
<p><strong>Atomic writes:</strong></p>
<p>Files are written via a tempfile in the same directory, then atomically persisted. This prevents partial writes if the process is interrupted.</p>
<p><strong>Empty input:</strong></p>
<p>If <code>replacements</code> is empty, returns immediately with zero counts and no I/O.</p>
<h2 id="processorresult"><a class="header" href="#processorresult"><code>ProcessorResult</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct ProcessorResult {
    pub files_modified: usize,
    pub todos_replaced: usize,
    pub errors: Vec&lt;(PathBuf, TowlProcessorError)&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>Summary of a batch replacement operation. The <code>errors</code> field contains per-file errors that did not abort the overall operation -- other files continue processing.</p>
<h2 id="errors-5"><a class="header" href="#errors-5">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlProcessorError {
    FileReadError(PathBuf, std::io::Error),
    FileWriteError(PathBuf, std::io::Error),
    LineOutOfBounds { path: PathBuf, line: usize, total_lines: usize },
    CommentPrefixNotFound { path: PathBuf, line: usize },
    PathOutsideRoot { path: PathBuf, root: PathBuf },
    InvalidIssueUrl { url: String },
}
<span class="boring">}</span></code></pre></pre>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>FileReadError</code></td><td>Failed to read source file</td></tr>
<tr><td><code>FileWriteError</code></td><td>Failed to write modified file</td></tr>
<tr><td><code>LineOutOfBounds</code></td><td>TODO line number exceeds file length</td></tr>
<tr><td><code>CommentPrefixNotFound</code></td><td>Column offset points past end of line</td></tr>
<tr><td><code>PathOutsideRoot</code></td><td>File is outside the repository root</td></tr>
<tr><td><code>InvalidIssueUrl</code></td><td>URL does not start with <code>https://github.com/</code></td></tr>
</tbody></table>
</div>
<h2 id="example-2"><a class="header" href="#example-2">Example</a></h2>
<pre><pre class="playground"><code class="language-rust no_run"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>use towl::processor::Processor;

let replacements = vec![(todo, created_issue)];
let result = Processor::replace_todos(repo_root, &amp;replacements).await;

println!("Modified {} files, replaced {} TODOs", result.files_modified, result.todos_replaced);

for (path, err) in &amp;result.errors {
    eprintln!("Error in {}: {}", path.display(), err);
}
<span class="boring">}</span></code></pre></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="tui"><a class="header" href="#tui">TUI</a></h1>
<p>The TUI module provides an interactive terminal interface for browsing, filtering, and acting on TODO comments. Built on <a href="https://ratatui.rs">ratatui</a> and <a href="https://github.com/crossterm-rs/crossterm">crossterm</a>.</p>
<h2 id="run"><a class="header" href="#run"><code>run</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn run(
    todos: Vec&lt;TodoComment&gt;,
    github_config: &amp;GitHubConfig,
    repo_root: &amp;Path,
) -&gt; Result&lt;(), TowlTuiError&gt;
<span class="boring">}</span></code></pre></pre>
<p>Launches the interactive TUI. Takes ownership of the terminal (raw mode, alternate screen). Terminal state is always restored on exit, even on error.</p>
<p><strong>Errors:</strong></p>
<ul>
<li><code>TowlTuiError::Io</code> -- Terminal I/O failure</li>
</ul>
<h2 id="app"><a class="header" href="#app"><code>App</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct App {
    // private fields
}
<span class="boring">}</span></code></pre></pre>
<p>Core TUI application state. Manages the TODO list, selection set, cursor position, filtering, sorting, and mode transitions.</p>
<h3 id="constructor-2"><a class="header" href="#constructor-2">Constructor</a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn new(todos: Vec&lt;TodoComment&gt;) -&gt; Self
<span class="boring">}</span></code></pre></pre>
<p>Creates a new app with all TODOs visible, no selection, sorted by file path.</p>
<h3 id="state-accessors"><a class="header" href="#state-accessors">State Accessors</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Method</th><th>Returns</th><th>Description</th></tr></thead><tbody>
<tr><td><code>todos()</code></td><td><code>&amp;[TodoComment]</code></td><td>Full TODO list</td></tr>
<tr><td><code>filtered_indices()</code></td><td><code>&amp;[usize]</code></td><td>Indices into <code>todos()</code> after filtering/sorting</td></tr>
<tr><td><code>cursor()</code></td><td><code>usize</code></td><td>Current cursor position in filtered list</td></tr>
<tr><td><code>filter_type()</code></td><td><code>Option&lt;TodoType&gt;</code></td><td>Active type filter (<code>None</code> = show all)</td></tr>
<tr><td><code>sort_field()</code></td><td><code>SortField</code></td><td>Current sort field</td></tr>
<tr><td><code>sort_ascending()</code></td><td><code>bool</code></td><td>Sort direction</td></tr>
<tr><td><code>is_selected(idx)</code></td><td><code>bool</code></td><td>Whether a TODO index is selected</td></tr>
<tr><td><code>selected_count()</code></td><td><code>usize</code></td><td>Number of selected TODOs</td></tr>
<tr><td><code>selected_todos()</code></td><td><code>Vec&lt;TodoComment&gt;</code></td><td>Cloned copies of selected TODOs</td></tr>
<tr><td><code>mode()</code></td><td><code>&amp;AppMode</code></td><td>Current UI mode</td></tr>
</tbody></table>
</div>
<h3 id="navigation"><a class="header" href="#navigation">Navigation</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Method</th><th>Effect</th></tr></thead><tbody>
<tr><td><code>move_up()</code></td><td>Move cursor up (clamped to 0)</td></tr>
<tr><td><code>move_down()</code></td><td>Move cursor down (clamped to list end)</td></tr>
</tbody></table>
</div>
<h3 id="selection"><a class="header" href="#selection">Selection</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Method</th><th>Effect</th></tr></thead><tbody>
<tr><td><code>toggle_select()</code></td><td>Toggle selection on cursor item</td></tr>
<tr><td><code>select_all_visible()</code></td><td>Select all items in filtered view</td></tr>
<tr><td><code>deselect_all()</code></td><td>Clear all selections</td></tr>
</tbody></table>
</div>
<h3 id="filtering-and-sorting"><a class="header" href="#filtering-and-sorting">Filtering and Sorting</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Method</th><th>Effect</th></tr></thead><tbody>
<tr><td><code>cycle_filter()</code></td><td>Cycle: All -&gt; TODO -&gt; FIXME -&gt; HACK -&gt; NOTE -&gt; BUG -&gt; All</td></tr>
<tr><td><code>cycle_sort()</code></td><td>Cycle: File -&gt; Line -&gt; Priority -&gt; Type -&gt; File</td></tr>
<tr><td><code>reverse_sort()</code></td><td>Toggle ascending/descending</td></tr>
</tbody></table>
</div>
<h3 id="mode-transitions"><a class="header" href="#mode-transitions">Mode Transitions</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Method</th><th>Transition</th></tr></thead><tbody>
<tr><td><code>enter_confirm()</code></td><td>Browse -&gt; Confirm (requires selection)</td></tr>
<tr><td><code>cancel_confirm()</code></td><td>Confirm -&gt; Browse</td></tr>
<tr><td><code>start_creating()</code></td><td>Confirm -&gt; Creating</td></tr>
<tr><td><code>finish_creating()</code></td><td>Creating -&gt; Done</td></tr>
<tr><td><code>enter_peek()</code></td><td>Browse -&gt; Peek (loads source context)</td></tr>
<tr><td><code>exit_peek()</code></td><td>Peek -&gt; Browse</td></tr>
</tbody></table>
</div>
<h2 id="appmode"><a class="header" href="#appmode"><code>AppMode</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum AppMode {
    Browse,
    Peek(PeekState),
    Confirm,
    Creating(CreatingState),
    Done(DoneState),
    DeleteConfirm(Vec&lt;TodoComment&gt;),
}
<span class="boring">}</span></code></pre></pre>
<p>The current UI mode determines which view is rendered and which keys are active.</p>
<div class="table-wrapper"><table><thead><tr><th>Mode</th><th>View</th><th>Input</th></tr></thead><tbody>
<tr><td><code>Browse</code></td><td>Scrollable TODO list</td><td>Navigate, select, filter, sort, peek</td></tr>
<tr><td><code>Peek</code></td><td>Source code overlay around a TODO</td><td>Scroll, dismiss</td></tr>
<tr><td><code>Confirm</code></td><td>Summary of selected TODOs</td><td>Confirm or cancel</td></tr>
<tr><td><code>Creating</code></td><td>Progress indicator during issue creation</td><td>None (Ctrl+C to abort)</td></tr>
<tr><td><code>Done</code></td><td>Results summary (issues created, errors)</td><td>Dismiss to exit</td></tr>
<tr><td><code>DeleteConfirm</code></td><td>Confirmation dialog for deleting invalid TODOs</td><td>Confirm or cancel</td></tr>
</tbody></table>
</div>
<h2 id="sortfield"><a class="header" href="#sortfield"><code>SortField</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum SortField {
    File,
    Line,
    Priority,
    Type,
}
<span class="boring">}</span></code></pre></pre>
<p>Field used to sort the TODO list. Cycle with the <code>s</code> key in Browse mode.</p>
<ul>
<li><strong>File</strong> -- Sort by file path, then by line number within each file</li>
<li><strong>Line</strong> -- Sort by line number globally</li>
<li><strong>Priority</strong> -- Sort by TODO type priority (Bug=1, Fixme=2, Hack=3, Todo=4, Note=5)</li>
<li><strong>Type</strong> -- Sort alphabetically by type name</li>
</ul>
<h2 id="supporting-types"><a class="header" href="#supporting-types">Supporting Types</a></h2>
<h3 id="peekstate"><a class="header" href="#peekstate"><code>PeekState</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct PeekState {
    pub lines: Vec&lt;(usize, String)&gt;,
    pub file: String,
    pub todo_line: usize,
    pub scroll: usize,
    pub analysis: Option&lt;AnalysisResult&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>State for the source-code peek overlay. Contains numbered source lines around the TODO, with scroll position. When <code>--ai</code> is active, <code>analysis</code> holds the LLM result -- the reasoning text is word-wrapped to the popup width during rendering.</p>
<h3 id="creatingstate"><a class="header" href="#creatingstate"><code>CreatingState</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct CreatingState {
    pub phase: String,
    pub progress: usize,
    pub total: usize,
    pub errors: Vec&lt;String&gt;,
    pub created_issues: Vec&lt;CreatedIssue&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>State tracked during background GitHub issue creation. Updated via channel messages from the spawned task.</p>
<h3 id="donestate"><a class="header" href="#donestate"><code>DoneState</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct DoneState {
    pub created_issues: Vec&lt;CreatedIssue&gt;,
    pub errors: Vec&lt;String&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>Final state after issue creation completes, showing results and any errors.</p>
<h2 id="action"><a class="header" href="#action"><code>Action</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum Action {
    Continue,
    Quit,
}
<span class="boring">}</span></code></pre></pre>
<p>Result of processing a keyboard event. <code>Continue</code> keeps the event loop running; <code>Quit</code> exits the TUI.</p>
<h2 id="handle_input"><a class="header" href="#handle_input"><code>handle_input</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub fn handle_input(
    app: &amp;mut App,
    timeout: std::time::Duration,
) -&gt; std::io::Result&lt;Action&gt;
<span class="boring">}</span></code></pre></pre>
<p>Polls for keyboard input and dispatches to mode-specific handlers. Returns <code>Action::Quit</code> on <code>q</code>, <code>Esc</code> (in appropriate modes), or <code>Ctrl+C</code>.</p>
<h2 id="errors-6"><a class="header" href="#errors-6">Errors</a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlTuiError {
    Io(std::io::Error),
}
<span class="boring">}</span></code></pre></pre>
<p>Terminal I/O errors from crossterm or ratatui operations.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="types"><a class="header" href="#types">Types</a></h1>
<p>Core data types used across the towl library.</p>
<h2 id="todotype"><a class="header" href="#todotype"><code>TodoType</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TodoType {
    Todo,
    Fixme,
    Hack,
    Note,
    Bug,
}
<span class="boring">}</span></code></pre></pre>
<p>Represents the category of a TODO comment.</p>
<h3 id="display"><a class="header" href="#display">Display</a></h3>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Display</th></tr></thead><tbody>
<tr><td><code>Todo</code></td><td><code>TODO</code></td></tr>
<tr><td><code>Fixme</code></td><td><code>FIXME</code></td></tr>
<tr><td><code>Hack</code></td><td><code>HACK</code></td></tr>
<tr><td><code>Note</code></td><td><code>NOTE</code></td></tr>
<tr><td><code>Bug</code></td><td><code>BUG</code></td></tr>
</tbody></table>
</div>
<h3 id="as_filter_str"><a class="header" href="#as_filter_str"><code>as_filter_str</code></a></h3>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub const fn as_filter_str(&amp;self) -&gt; &amp;'static str
<span class="boring">}</span></code></pre></pre>
<p>Returns the lowercase filter string used for CLI filtering:</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Filter string</th></tr></thead><tbody>
<tr><td><code>Todo</code></td><td><code>"todo"</code></td></tr>
<tr><td><code>Fixme</code></td><td><code>"fixme"</code></td></tr>
<tr><td><code>Hack</code></td><td><code>"hack"</code></td></tr>
<tr><td><code>Note</code></td><td><code>"note"</code></td></tr>
<tr><td><code>Bug</code></td><td><code>"bug"</code></td></tr>
</tbody></table>
</div>
<h3 id="conversions"><a class="header" href="#conversions">Conversions</a></h3>
<ul>
<li><code>TryFrom&lt;&amp;str&gt;</code> -- Case-insensitive conversion from string</li>
<li><code>clap::ValueEnum</code> -- CLI argument parsing</li>
</ul>
<h3 id="trait-implementations"><a class="header" href="#trait-implementations">Trait Implementations</a></h3>
<p><code>Debug</code>, <code>Clone</code>, <code>Copy</code>, <code>PartialEq</code>, <code>Eq</code>, <code>Hash</code>, <code>Serialize</code>, <code>Deserialize</code>, <code>ValueEnum</code></p>
<h2 id="todocomment"><a class="header" href="#todocomment"><code>TodoComment</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct TodoComment {
    pub id: String,
    pub file_path: PathBuf,
    pub line_number: usize,
    pub column_start: usize,
    pub column_end: usize,
    pub todo_type: TodoType,
    pub original_text: String,
    pub description: String,
    pub context_lines: Vec&lt;String&gt;,
    pub function_context: Option&lt;String&gt;,
    pub analysis: Option&lt;AnalysisResult&gt;,
}
<span class="boring">}</span></code></pre></pre>
<p>A single TODO comment extracted from a source file.</p>
<div class="table-wrapper"><table><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody>
<tr><td><code>id</code></td><td>Unique identifier (generated per extraction)</td></tr>
<tr><td><code>file_path</code></td><td>Path to the source file</td></tr>
<tr><td><code>line_number</code></td><td>1-based line number</td></tr>
<tr><td><code>column_start</code></td><td>0-based start column of the TODO marker</td></tr>
<tr><td><code>column_end</code></td><td>0-based end column of the TODO marker</td></tr>
<tr><td><code>todo_type</code></td><td>Category (<code>Todo</code>, <code>Fixme</code>, etc.)</td></tr>
<tr><td><code>original_text</code></td><td>The full original comment line</td></tr>
<tr><td><code>description</code></td><td>Extracted description text after the marker</td></tr>
<tr><td><code>context_lines</code></td><td>Surrounding source lines (configurable window)</td></tr>
<tr><td><code>function_context</code></td><td>Enclosing function name, if detected</td></tr>
<tr><td><code>analysis</code></td><td>LLM validation result, populated when <code>--ai</code> is used (skipped during serialisation if <code>None</code>)</td></tr>
</tbody></table>
</div>
<h3 id="trait-implementations-1"><a class="header" href="#trait-implementations-1">Trait Implementations</a></h3>
<p><code>Debug</code>, <code>Clone</code>, <code>PartialEq</code>, <code>Serialize</code>, <code>Deserialize</code></p>
<h2 id="scanresult-1"><a class="header" href="#scanresult-1"><code>ScanResult</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct ScanResult {
    pub todos: Vec&lt;TodoComment&gt;,
    pub files_scanned: usize,
    pub files_skipped: usize,
    pub files_errored: usize,
    pub duration: std::time::Duration,
}
<span class="boring">}</span></code></pre></pre>
<p>Returned by <code>Scanner::scan()</code>. See <a href="api/./scanner.html">Scanner</a> for details.</p>
<h2 id="owner--repo-1"><a class="header" href="#owner--repo-1"><code>Owner</code> / <code>Repo</code></a></h2>
<p>Newtype wrappers for GitHub owner and repository names:</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub struct Owner(String);
pub struct Repo(String);
<span class="boring">}</span></code></pre></pre>
<p>Both provide <code>try_new(impl Into&lt;String&gt;) -&gt; Result&lt;Self, TowlConfigError&gt;</code> (validates length) and <code>Display</code>. See <a href="api/./config.html">Config</a> for details.</p>
<h2 id="outputformat-1"><a class="header" href="#outputformat-1"><code>OutputFormat</code></a></h2>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum OutputFormat {
    Table,
    Json,
    Csv,
    Toml,
    Markdown,
    Terminal,
}
<span class="boring">}</span></code></pre></pre>
<p>CLI-facing enum for selecting output format. <code>Table</code> and <code>Terminal</code> produce identical output. See <a href="api/./output.html">Output</a> for details.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="errors-7"><a class="header" href="#errors-7">Errors</a></h1>
<p>towl uses typed errors throughout, built with <code>thiserror</code>. Each module defines its own error enum, and the top-level <code>TowlError</code> aggregates them.</p>
<h2 id="error-hierarchy-1"><a class="header" href="#error-hierarchy-1">Error Hierarchy</a></h2>
<pre><code class="language-text">TowlError
├── TowlConfigError
├── TowlScannerError
│   └── TowlParserError
│       └── TowlCommentError
├── TowlOutputError
│   ├── FormatterError
│   └── WriterError
├── TowlGitHubError
├── TowlProcessorError
├── TowlTuiError
└── TowlLlmError
</code></pre>
<h2 id="towlerror"><a class="header" href="#towlerror"><code>TowlError</code></a></h2>
<p>Top-level error type used by the CLI binary. All sub-error types convert automatically via <code>#[from]</code>.</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>pub enum TowlError {
    Config(TowlConfigError),
    Scanner(TowlScannerError),
    Output(TowlOutputError),
    GitHub(TowlGitHubError),
    Processor(TowlProcessorError),
    Tui(TowlTuiError),
    Llm(TowlLlmError),
}
<span class="boring">}</span></code></pre></pre>
<h2 id="towlconfigerror"><a class="header" href="#towlconfigerror"><code>TowlConfigError</code></a></h2>
<p>Errors during configuration loading, initialisation, and validation.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>PathTraversalAttempt(PathBuf)</code></td><td>Config path contains <code>..</code></td></tr>
<tr><td><code>ConfigAlreadyExists(PathBuf)</code></td><td><code>towl init</code> without <code>--force</code> on existing file</td></tr>
<tr><td><code>WriteToFileError(PathBuf, io::Error)</code></td><td>Failed to write config file</td></tr>
<tr><td><code>UnableToParseToml(toml::ser::Error)</code></td><td>TOML serialisation failure</td></tr>
<tr><td><code>CouldNotCreateConfig(ConfigError)</code></td><td>Config crate loading error</td></tr>
<tr><td><code>GitRepoNotFound { message }</code></td><td>Not inside a git repository</td></tr>
<tr><td><code>GitRemoteNotFound { message }</code></td><td>No <code>origin</code> remote</td></tr>
<tr><td><code>GitInvalidUrl { url, message }</code></td><td>Cannot parse owner/repo from remote URL</td></tr>
<tr><td><code>TooManyConfigPatterns { field, count, max_allowed }</code></td><td>Pattern array exceeds 100 entries</td></tr>
<tr><td><code>ConfigValueTooLong { field, length, max_length }</code></td><td>Config string exceeds 512 characters</td></tr>
<tr><td><code>ContextLinesOutOfRange { value, min, max }</code></td><td>Context lines outside 1..=50</td></tr>
<tr><td><code>RateLimitDelayTooHigh { value, max }</code></td><td>Rate limit delay exceeds maximum</td></tr>
</tbody></table>
</div>
<h2 id="towlscannererror"><a class="header" href="#towlscannererror"><code>TowlScannerError</code></a></h2>
<p>Errors during directory walking and file reading.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>UnableToWalkFile(ignore::Error)</code></td><td>Directory traversal error</td></tr>
<tr><td><code>ParsingError(TowlParserError)</code></td><td>Parser failure (propagated)</td></tr>
<tr><td><code>UnableToReadFileAtPath(PathBuf, io::Error)</code></td><td>File I/O error</td></tr>
<tr><td><code>InvalidPath { path }</code></td><td>Path could not be canonicalised</td></tr>
<tr><td><code>FileTooLarge { path, size, max_allowed }</code></td><td>File exceeds 10 MB</td></tr>
<tr><td><code>TooManyTodos { path, count, max_allowed }</code></td><td>File exceeds 10,000 TODOs</td></tr>
</tbody></table>
</div>
<h2 id="towlparsererror"><a class="header" href="#towlparsererror"><code>TowlParserError</code></a></h2>
<p>Errors during regex compilation and TODO extraction.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>InvalidRegexPattern(String, regex::Error)</code></td><td>Regex failed to compile</td></tr>
<tr><td><code>UnknownConfigPattern(TowlCommentError)</code></td><td>Pattern matched but type unknown</td></tr>
<tr><td><code>RegexGroupMissing</code></td><td>Pattern lacks a capture group <code>(.*)</code></td></tr>
<tr><td><code>PatternTooLong(usize, usize)</code></td><td>Pattern exceeds 256 characters</td></tr>
<tr><td><code>TooManyTotalPatterns { count, max_allowed }</code></td><td>Total patterns across all categories exceeds 50</td></tr>
</tbody></table>
</div>
<h2 id="towlcommenterror"><a class="header" href="#towlcommenterror"><code>TowlCommentError</code></a></h2>
<p>Errors in comment type resolution.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>UnknownTodoType { comment }</code></td><td>String does not map to a known <code>TodoType</code></td></tr>
</tbody></table>
</div>
<h2 id="towloutputerror-1"><a class="header" href="#towloutputerror-1"><code>TowlOutputError</code></a></h2>
<p>Errors during formatting and writing.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>InvalidOutputPath(String)</code></td><td>Missing/wrong extension, terminal format with file path</td></tr>
<tr><td><code>UnableToFormatTodos(FormatterError)</code></td><td>Formatter failure</td></tr>
<tr><td><code>UnableToWriteTodos(WriterError)</code></td><td>Writer failure</td></tr>
</tbody></table>
</div>
<h2 id="formattererror-1"><a class="header" href="#formattererror-1"><code>FormatterError</code></a></h2>
<p>Errors in output formatting.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>SerializationError(String)</code></td><td>JSON/TOML/CSV serialisation failure</td></tr>
<tr><td><code>IntegerOverflow(usize)</code></td><td>Count exceeds safe integer bounds</td></tr>
</tbody></table>
</div>
<h2 id="writererror-1"><a class="header" href="#writererror-1"><code>WriterError</code></a></h2>
<p>Errors in output writing.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>IoError(io::Error)</code></td><td>File system I/O error</td></tr>
<tr><td><code>PathTraversal(PathBuf)</code></td><td>Output path contains <code>..</code></td></tr>
</tbody></table>
</div>
<h2 id="towlgithuberror"><a class="header" href="#towlgithuberror"><code>TowlGitHubError</code></a></h2>
<p>Errors from GitHub API interactions.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>ApiError { message, source }</code></td><td>General GitHub API failure</td></tr>
<tr><td><code>AuthError</code></td><td>401 response -- invalid or expired token</td></tr>
<tr><td><code>RateLimitExceeded { retry_after_secs }</code></td><td>403 with rate limit message</td></tr>
<tr><td><code>IssueAlreadyExists { title }</code></td><td>Duplicate detected before creation</td></tr>
<tr><td><code>RepositoryNotFound { owner, repo }</code></td><td>404 response -- owner/repo not found</td></tr>
<tr><td><code>MissingToken</code></td><td><code>TOWL_GITHUB_TOKEN</code> not set or empty</td></tr>
</tbody></table>
</div>
<h2 id="towlprocessorerror"><a class="header" href="#towlprocessorerror"><code>TowlProcessorError</code></a></h2>
<p>Errors from replacing TODO comments with issue links in source files.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>FileReadError(PathBuf, io::Error)</code></td><td>Failed to read source file</td></tr>
<tr><td><code>FileWriteError(PathBuf, io::Error)</code></td><td>Failed to write modified file</td></tr>
<tr><td><code>LineOutOfBounds { path, line, total_lines }</code></td><td>TODO line number exceeds file length</td></tr>
<tr><td><code>CommentPrefixNotFound { path, line }</code></td><td>Column offset points past end of line</td></tr>
<tr><td><code>PathOutsideRoot { path, root }</code></td><td>File is outside the repository root</td></tr>
<tr><td><code>InvalidIssueUrl { url }</code></td><td>URL does not start with <code>https://github.com/</code></td></tr>
</tbody></table>
</div>
<h2 id="towlllmerror"><a class="header" href="#towlllmerror"><code>TowlLlmError</code></a></h2>
<p>Errors from LLM API interactions and analysis.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>ApiError { message, status }</code></td><td>LLM API returned a non-200 status</td></tr>
<tr><td><code>AuthError</code></td><td>401 -- invalid or missing API key</td></tr>
<tr><td><code>RateLimited { retry_after_secs }</code></td><td>429 -- too many requests</td></tr>
<tr><td><code>ParseError { message }</code></td><td>LLM response could not be parsed as valid JSON</td></tr>
<tr><td><code>NotConfigured</code></td><td><code>TOWL_LLM_API_KEY</code> environment variable not set</td></tr>
<tr><td><code>UnsupportedProvider { provider }</code></td><td>Provider is not "claude", "openai", "claude-code", or "codex"</td></tr>
<tr><td><code>IoError { message }</code></td><td>File I/O error during context gathering</td></tr>
</tbody></table>
</div>
<p><code>is_retryable()</code> returns <code>true</code> for <code>RateLimited</code>, <code>ApiError</code> with status &gt;= 500, and <code>ApiError</code> with no status (network failures).</p>
<h2 id="towltuierror"><a class="header" href="#towltuierror"><code>TowlTuiError</code></a></h2>
<p>Errors from the interactive terminal UI.</p>
<div class="table-wrapper"><table><thead><tr><th>Variant</th><th>Cause</th></tr></thead><tbody>
<tr><td><code>Io(io::Error)</code></td><td>Terminal I/O error from crossterm or ratatui</td></tr>
</tbody></table>
</div>
<h2 id="error-propagation"><a class="header" href="#error-propagation">Error Propagation</a></h2>
<p>Errors propagate upward using <code>?</code> and <code>#[from]</code>:</p>
<pre><code class="language-text">TowlCommentError  --&gt;  TowlParserError    --&gt;  TowlScannerError  --&gt;  TowlError
FormatterError    --&gt;  TowlOutputError     --&gt;  TowlError
WriterError       --&gt;  TowlOutputError     --&gt;  TowlError
TowlGitHubError   ----------------------------&gt;  TowlError
TowlProcessorError ---------------------------&gt;  TowlError
TowlTuiError      ----------------------------&gt;  TowlError
TowlLlmError      ----------------------------&gt;  TowlError
</code></pre>
<p>All errors implement <code>std::fmt::Display</code> with human-readable messages and <code>std::error::Error</code> for standard error handling.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="architecture"><a class="header" href="#architecture">Architecture</a></h1>
<p>towl follows a pipeline architecture: Config -&gt; Scanner -&gt; Parser -&gt; TUI / Output. Each stage is a separate module with clear boundaries and typed errors.</p>
<h2 id="pipeline"><a class="header" href="#pipeline">Pipeline</a></h2>
<pre><code class="language-text">                ┌──────────┐
                │  Config   │  --config / TOWL_CONFIG / .towl.toml + env vars
                └────┬─────┘
                     │
                ┌────▼─────┐
                │  Scanner  │  Walks directory tree, scans files concurrently
                └────┬─────┘
                     │
                ┌────▼─────┐
                │  Parser   │  Matches comment prefixes + TODO patterns
                └────┬─────┘
                     │
              ┌──────┼──────┐
              │      │      │
        ┌─────▼────┐ │ ┌────▼─────┐
        │   TUI    │ │ │  Output   │  Non-interactive: formats + writes
        │ (default)│ │ │  (-N)     │
        └─────┬────┘ │ └──────────┘
              │      │
        ┌─────▼─────┐│
        │ Processor  ││  Replaces TODOs with GitHub issue links
        └───────────┘│
                ┌────▼─────┐
                │   LLM     │  --ai: validates TODOs with AI
                └──────────┘
</code></pre>
<h2 id="module-boundaries"><a class="header" href="#module-boundaries">Module Boundaries</a></h2>
<h3 id="config-srclibconfig"><a class="header" href="#config-srclibconfig">Config (<code>src/lib/config/</code>)</a></h3>
<ul>
<li>Resolves config file path: <code>--config</code> flag &gt; <code>TOWL_CONFIG</code> env var &gt; <code>.towl.toml</code></li>
<li>Loads config using the <code>config</code> crate</li>
<li>Merges environment variable overrides (<code>TOWL_GITHUB_*</code>, <code>TOWL_LLM_*</code>)</li>
<li>Discovers GitHub owner/repo from <code>git remote get-url origin</code></li>
<li>Validates pattern array sizes</li>
<li>Produces <code>TowlConfig</code> containing <code>ParsingConfig</code> + <code>GitHubConfig</code> + <code>LlmConfig</code></li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>types.rs</code> -- <code>TowlConfig</code>, <code>ParsingConfig</code>, <code>GitHubConfig</code></li>
<li><code>defaults.rs</code> -- Default values for config fields</li>
<li><code>display.rs</code> -- <code>Display</code> implementation for config tree view</li>
<li><code>newtypes.rs</code> -- <code>Owner</code> and <code>Repo</code> newtype wrappers</li>
<li><code>validation.rs</code> -- Config validation logic</li>
<li><code>git.rs</code> -- <code>GitRepoInfo</code> for parsing git remotes</li>
<li><code>error.rs</code> -- <code>TowlConfigError</code></li>
</ul>
<h3 id="scanner-srclibscanner"><a class="header" href="#scanner-srclibscanner">Scanner (<code>src/lib/scanner/</code>)</a></h3>
<ul>
<li>Accepts a <code>ParsingConfig</code> and a root path</li>
<li>Walks the directory tree using the <code>ignore</code> crate (respects <code>.gitignore</code>)</li>
<li>Filters files by extension and exclude patterns</li>
<li>Scans files concurrently with bounded parallelism (up to 64 files)</li>
<li>Reads files asynchronously via <code>tokio::fs</code></li>
<li>Enforces resource limits (file size, TODO counts, file counts)</li>
<li>Delegates content parsing to the <code>Parser</code></li>
<li>Returns <code>ScanResult</code> with TODOs and scan metrics</li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>types.rs</code> -- <code>Scanner</code> implementation</li>
<li><code>limits.rs</code> -- <code>ScanResult</code> and resource limit constants</li>
<li><code>walker.rs</code> -- Directory walker construction</li>
<li><code>error.rs</code> -- <code>TowlScannerError</code></li>
</ul>
<h3 id="parser-srclibparser"><a class="header" href="#parser-srclibparser">Parser (<code>src/lib/parser/</code>)</a></h3>
<ul>
<li>Compiles regex patterns once during construction</li>
<li>Identifies comment lines via <code>comment_prefixes</code></li>
<li>Extracts TODO items via <code>todo_patterns</code></li>
<li>Captures context lines (configurable window, 1-50)</li>
<li>Detects enclosing function names via <code>function_patterns</code></li>
<li>Produces <code>Vec&lt;TodoComment&gt;</code></li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>types.rs</code> -- <code>Parser</code> implementation</li>
<li><code>context.rs</code> -- Context line extraction logic</li>
<li><code>pattern.rs</code> -- Pattern compilation and matching</li>
<li><code>error.rs</code> -- <code>TowlParserError</code></li>
</ul>
<h3 id="tui-srclibtui"><a class="header" href="#tui-srclibtui">TUI (<code>src/lib/tui/</code>)</a></h3>
<ul>
<li>Full-screen terminal interface using ratatui and crossterm</li>
<li>Browse, filter, sort, and peek at TODOs</li>
<li>Select TODOs and create GitHub issues with progress tracking</li>
<li>Replaces TODO comments in source files with issue links via the Processor</li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>app.rs</code> -- <code>App</code> state machine and <code>AppMode</code> enum (Browse, Peek, Confirm, Creating, Done)</li>
<li><code>input.rs</code> -- Keyboard event handling and action dispatch</li>
<li><code>render.rs</code> -- UI rendering (list, peek popup, confirm dialog, progress view)</li>
<li><code>error.rs</code> -- <code>TowlTuiError</code></li>
</ul>
<h3 id="llm-srclibllm"><a class="header" href="#llm-srclibllm">LLM (<code>src/lib/llm/</code>)</a></h3>
<ul>
<li>AI-powered TODO validation using Claude, OpenAI, or local CLI agents</li>
<li>Enum-dispatched providers following the same pattern as <code>FormatterImpl</code>/<code>WriterImpl</code></li>
<li>Gathers expanded context (~30 lines) and full function bodies for each TODO</li>
<li>Constructs structured prompts and parses JSON responses</li>
<li>Retry logic with exponential backoff via <code>backon</code></li>
<li>CLI providers (<code>claude-code</code>, <code>codex</code>) auto-fall back to API providers if the binary is not on PATH</li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>analyse.rs</code> -- <code>analyse_todos()</code>, <code>gather_expanded_context()</code>, retry logic</li>
<li><code>claude.rs</code> -- <code>ClaudeProvider</code> (Anthropic Messages API)</li>
<li><code>openai.rs</code> -- <code>OpenAiProvider</code> (OpenAI Chat Completions API)</li>
<li><code>cli.rs</code> -- <code>ClaudeCodeProvider</code>, <code>CodexProvider</code> (subprocess-based)</li>
<li><code>prompt.rs</code> -- System prompt and user content construction</li>
<li><code>types.rs</code> -- <code>AnalysisResult</code>, <code>AnalysisSummary</code>, <code>Validity</code>, <code>LlmUsage</code>, JSON extraction</li>
<li><code>error.rs</code> -- <code>TowlLlmError</code></li>
</ul>
<h3 id="processor-srclibprocessor"><a class="header" href="#processor-srclibprocessor">Processor (<code>src/lib/processor/</code>)</a></h3>
<ul>
<li>Replaces TODO comments in source files with GitHub issue links</li>
<li>Groups replacements by file for efficient batch processing</li>
<li>Validates file paths stay within the repository root</li>
<li>Returns <code>ProcessorResult</code> with counts and error details</li>
</ul>
<p>Submodules:</p>
<ul>
<li><code>types.rs</code> -- <code>Processor</code> and <code>ProcessorResult</code></li>
<li><code>error.rs</code> -- <code>TowlProcessorError</code></li>
</ul>
<h3 id="github-srclibgithub"><a class="header" href="#github-srclibgithub">GitHub (<code>src/lib/github/</code>)</a></h3>
<ul>
<li>Creates GitHub issues from <code>TodoComment</code> items via the Octocrab API</li>
<li>Loads existing issues to detect and skip duplicates</li>
<li>Constructs issue titles and bodies with file/line metadata</li>
</ul>
<h3 id="output-srcliboutput"><a class="header" href="#output-srcliboutput">Output (<code>src/lib/output/</code>)</a></h3>
<ul>
<li>Combines a <code>FormatterImpl</code> and a <code>WriterImpl</code></li>
<li>Groups TODOs by type before formatting</li>
<li>Uses enum dispatch (not trait objects) for zero-cost abstraction</li>
</ul>
<pre><code class="language-text">Output
├── FormatterImpl (enum dispatch)
│   ├── CsvFormatter
│   ├── JsonFormatter
│   ├── MarkdownFormatter
│   ├── TableFormatter
│   └── TomlFormatter
└── WriterImpl (enum dispatch)
    ├── StdoutWriter
    └── FileWriter
</code></pre>
<h2 id="key-design-decisions"><a class="header" href="#key-design-decisions">Key Design Decisions</a></h2>
<h3 id="enum-dispatch-over-trait-objects"><a class="header" href="#enum-dispatch-over-trait-objects">Enum Dispatch Over Trait Objects</a></h3>
<p>Both <code>FormatterImpl</code> and <code>WriterImpl</code> use enum variants rather than <code>Box&lt;dyn Trait&gt;</code>. This provides:</p>
<ul>
<li>Static dispatch (no vtable overhead)</li>
<li>Exhaustive matching at compile time</li>
<li>Simpler lifetime management</li>
</ul>
<h3 id="regex-compilation-strategy"><a class="header" href="#regex-compilation-strategy">Regex Compilation Strategy</a></h3>
<p>All regex patterns are compiled once during <code>Scanner::new()</code> / <code>Parser::new()</code> and reused for every file. This avoids per-file compilation overhead.</p>
<h3 id="concurrent-file-scanning"><a class="header" href="#concurrent-file-scanning">Concurrent File Scanning</a></h3>
<p>The scanner discovers all scannable files first, then scans them concurrently using <code>futures::stream::buffer_unordered</code> with a concurrency limit of 64. This provides significant speedup on large codebases while bounding resource usage.</p>
<h3 id="async-io"><a class="header" href="#async-io">Async I/O</a></h3>
<p>File reading uses <code>tokio::fs</code> for non-blocking I/O. The scanner is async, allowing integration into async applications. The CLI uses <code>#[tokio::main]</code>.</p>
<h3 id="tui-event-loop"><a class="header" href="#tui-event-loop">TUI Event Loop</a></h3>
<p>The TUI uses a synchronous event loop with crossterm polling. GitHub issue creation runs in a background tokio task, communicating progress back to the UI via an <code>mpsc</code> channel. This keeps the UI responsive during network operations.</p>
<h3 id="error-type-hierarchy"><a class="header" href="#error-type-hierarchy">Error Type Hierarchy</a></h3>
<p>Each module owns its error type. Errors propagate upward via <code>#[from]</code> conversions:</p>
<pre><code class="language-text">TowlCommentError → TowlParserError → TowlScannerError → TowlError
FormatterError → TowlOutputError → TowlError
WriterError → TowlOutputError → TowlError
TowlProcessorError → TowlError
TowlTuiError → TowlError
TowlLlmError → TowlError
</code></pre>
<h3 id="newtype-pattern"><a class="header" href="#newtype-pattern">Newtype Pattern</a></h3>
<p><code>Owner</code> and <code>Repo</code> are newtype wrappers over <code>String</code>, preventing accidental misuse (e.g., passing an owner where a repo is expected).</p>
<h3 id="secret-handling"><a class="header" href="#secret-handling">Secret Handling</a></h3>
<p>The GitHub token is stored as <code>secrecy::SecretString</code>, which:</p>
<ul>
<li>Masks the value in <code>Debug</code> and <code>Display</code> output</li>
<li>Zeroes memory on drop</li>
<li>Prevents accidental logging</li>
</ul>
<h2 id="directory-layout"><a class="header" href="#directory-layout">Directory Layout</a></h2>
<pre><code class="language-text">src/
├── bin/
│   └── towl.rs              CLI binary
└── lib/
    ├── mod.rs                Library root
    ├── cli/
    │   └── mod.rs            Clap argument definitions
    ├── comment/
    │   ├── mod.rs
    │   ├── todo.rs           TodoType, TodoComment
    │   └── error.rs          TowlCommentError
    ├── config/
    │   ├── mod.rs
    │   ├── types.rs          TowlConfig, ParsingConfig, GitHubConfig
    │   ├── defaults.rs       Default config values
    │   ├── display.rs        Config Display implementation
    │   ├── newtypes.rs       Owner, Repo newtypes
    │   ├── validation.rs     Config validation
    │   ├── git.rs            GitRepoInfo
    │   └── error.rs          TowlConfigError
    ├── scanner/
    │   ├── mod.rs
    │   ├── types.rs          Scanner
    │   ├── limits.rs         ScanResult, resource limits
    │   ├── walker.rs         Directory walker construction
    │   └── error.rs          TowlScannerError
    ├── parser/
    │   ├── mod.rs
    │   ├── types.rs          Parser
    │   ├── context.rs        Context line extraction
    │   ├── pattern.rs        Pattern compilation
    │   └── error.rs          TowlParserError
    ├── github/
    │   ├── mod.rs
    │   ├── client.rs         GitHubClient
    │   ├── types.rs          CreatedIssue
    │   └── error.rs          TowlGitHubError
    ├── llm/
    │   ├── mod.rs             LlmProvider enum dispatch
    │   ├── analyse.rs         analyse_todos, gather_expanded_context
    │   ├── claude.rs          ClaudeProvider
    │   ├── openai.rs          OpenAiProvider
    │   ├── cli.rs             ClaudeCodeProvider, CodexProvider
    │   ├── prompt.rs          System prompt construction
    │   ├── types.rs           AnalysisResult, Validity, JSON extraction
    │   └── error.rs           TowlLlmError
    ├── processor/
    │   ├── mod.rs
    │   ├── types.rs          Processor, ProcessorResult
    │   └── error.rs          TowlProcessorError
    ├── tui/
    │   ├── mod.rs             TUI entry point and event loop
    │   ├── app.rs             App state machine, AppMode
    │   ├── input.rs           Keyboard input handling
    │   ├── render.rs          UI rendering
    │   └── error.rs           TowlTuiError
    ├── output/
    │   ├── mod.rs             Output
    │   ├── error.rs           TowlOutputError
    │   ├── formatter/
    │   │   ├── mod.rs         FormatterImpl
    │   │   ├── error.rs       FormatterError
    │   │   └── formatters/
    │   │       ├── mod.rs     Formatter dispatch
    │   │       ├── csv.rs
    │   │       ├── json.rs
    │   │       ├── markdown.rs
    │   │       ├── table.rs
    │   │       └── toml.rs
    │   └── writer/
    │       ├── mod.rs         WriterImpl
    │       ├── error.rs       WriterError
    │       └── writers/
    │           ├── file.rs    FileWriter
    │           └── stdout.rs  StdoutWriter
    └── error/
        └── mod.rs             TowlError

tests/
├── integration/               Integration tests
├── property/                  Property-based tests
└── fixtures/                  Test fixtures
</code></pre>
<h2 id="dependencies"><a class="header" href="#dependencies">Dependencies</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Crate</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>clap</code></td><td>CLI argument parsing</td></tr>
<tr><td><code>tokio</code></td><td>Async runtime and file I/O</td></tr>
<tr><td><code>serde</code> / <code>serde_json</code> / <code>toml</code></td><td>Serialisation</td></tr>
<tr><td><code>regex</code></td><td>TODO pattern matching</td></tr>
<tr><td><code>ignore</code></td><td>Directory walking (respects <code>.gitignore</code>)</td></tr>
<tr><td><code>thiserror</code></td><td>Error type derivation</td></tr>
<tr><td><code>secrecy</code></td><td>Secret string handling</td></tr>
<tr><td><code>config</code></td><td>Configuration file loading</td></tr>
<tr><td><code>octocrab</code></td><td>GitHub API client</td></tr>
<tr><td><code>ratatui</code></td><td>Terminal UI framework</td></tr>
<tr><td><code>crossterm</code></td><td>Terminal input/output</td></tr>
<tr><td><code>futures</code></td><td>Async stream utilities</td></tr>
<tr><td><code>reqwest</code></td><td>HTTP client (rustls TLS)</td></tr>
<tr><td><code>backon</code></td><td>Retry logic with exponential backoff</td></tr>
<tr><td><code>which</code></td><td>CLI binary PATH detection</td></tr>
<tr><td><code>proptest</code></td><td>Property-based testing</td></tr>
<tr><td><code>rstest</code></td><td>Parameterised testing</td></tr>
<tr><td><code>insta</code></td><td>Snapshot testing</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="security"><a class="header" href="#security">Security</a></h1>
<p>towl applies defence-in-depth across configuration, scanning, and output.</p>
<h2 id="path-traversal-protection"><a class="header" href="#path-traversal-protection">Path Traversal Protection</a></h2>
<p>All user-supplied paths are checked for <code>..</code> components before use:</p>
<ul>
<li><strong>Config paths</strong> -- <code>towl init --path</code> rejects traversal attempts</li>
<li><strong>Scan paths</strong> -- <code>towl scan &lt;path&gt;</code> validates before walking</li>
<li><strong>Output paths</strong> -- <code>-o &lt;path&gt;</code> is validated and symlinks are resolved</li>
</ul>
<p>The check uses <code>contains_path_traversal()</code> which inspects each path component for <code>..</code>.</p>
<h2 id="symlink-resolution"><a class="header" href="#symlink-resolution">Symlink Resolution</a></h2>
<p>Output file paths are resolved via <code>std::fs::canonicalize()</code> before writing. This prevents symlink-based escape from the intended output directory.</p>
<h2 id="resource-limits-2"><a class="header" href="#resource-limits-2">Resource Limits</a></h2>
<p>Hard limits prevent denial-of-service via large repositories or malicious inputs:</p>
<div class="table-wrapper"><table><thead><tr><th>Limit</th><th>Value</th><th>Purpose</th></tr></thead><tbody>
<tr><td>Max file size</td><td>10 MB</td><td>Prevents reading huge binary/generated files</td></tr>
<tr><td>Max TODOs per file</td><td>10,000</td><td>Bounds per-file memory usage</td></tr>
<tr><td>Max total TODOs</td><td>100,000</td><td>Bounds overall memory usage</td></tr>
<tr><td>Max files scanned</td><td>100,000</td><td>Bounds directory walk</td></tr>
<tr><td>Max pattern length</td><td>256 chars</td><td>Prevents regex DoS via long patterns</td></tr>
<tr><td>Max compiled regex</td><td>256 KB</td><td>Bounds regex engine memory</td></tr>
<tr><td>Max total patterns (combined)</td><td>50</td><td>Bounds total regex compilation across all categories</td></tr>
<tr><td>Max patterns per config field</td><td>100</td><td>Limits config file attack surface</td></tr>
</tbody></table>
</div>
<h2 id="secret-handling-1"><a class="header" href="#secret-handling-1">Secret Handling</a></h2>
<p>The GitHub token (<code>TOWL_GITHUB_TOKEN</code>) is:</p>
<ul>
<li><strong>Never stored in config files</strong> -- Only accepted via environment variable</li>
<li><strong>Stored as <code>SecretString</code></strong> -- Uses the <code>secrecy</code> crate</li>
<li><strong>Masked in debug output</strong> -- <code>Debug</code> and <code>Display</code> show <code>[REDACTED]</code></li>
<li><strong>Zeroed on drop</strong> -- Memory is cleared when the config is dropped</li>
</ul>
<h2 id="environment-variable-restriction"><a class="header" href="#environment-variable-restriction">Environment Variable Restriction</a></h2>
<p>Eight environment variables are read:</p>
<div class="table-wrapper"><table><thead><tr><th>Variable</th><th>Purpose</th></tr></thead><tbody>
<tr><td><code>TOWL_CONFIG</code></td><td>Config file path override</td></tr>
<tr><td><code>TOWL_GITHUB_TOKEN</code></td><td>GitHub authentication</td></tr>
<tr><td><code>TOWL_GITHUB_OWNER</code></td><td>Repository owner override</td></tr>
<tr><td><code>TOWL_GITHUB_REPO</code></td><td>Repository name override</td></tr>
<tr><td><code>TOWL_LLM_API_KEY</code></td><td>LLM API authentication</td></tr>
<tr><td><code>TOWL_LLM_PROVIDER</code></td><td>LLM provider override</td></tr>
<tr><td><code>TOWL_LLM_MODEL</code></td><td>LLM model override</td></tr>
<tr><td><code>TOWL_LLM_BASE_URL</code></td><td>Custom LLM endpoint URL</td></tr>
</tbody></table>
</div>
<p>Secrets (<code>TOWL_GITHUB_TOKEN</code>, <code>TOWL_LLM_API_KEY</code>) are stored as <code>SecretString</code> and never written to config files or logs. No other environment variables influence behaviour.</p>
<h2 id="config-file-safety"><a class="header" href="#config-file-safety">Config File Safety</a></h2>
<ul>
<li><strong><code>--force</code> required</strong> for overwriting existing config files</li>
<li><strong>Pattern array limits</strong> -- Each pattern field is capped at 100 entries</li>
<li><strong>Pattern length limits</strong> -- Individual regex patterns capped at 256 characters</li>
<li><strong>TOML parsing</strong> -- Uses <code>config</code> crate with <code>serde</code> for type-safe deserialization</li>
</ul>
<h2 id="git-integration"><a class="header" href="#git-integration">Git Integration</a></h2>
<ul>
<li>Git operations use <code>tokio::process::Command</code> to run <code>git</code> as a subprocess</li>
<li>Only read-only git commands are executed (<code>git remote get-url origin</code>)</li>
<li>No git credentials are accessed or stored</li>
</ul>
<h2 id="llm-cli-subprocess-safety"><a class="header" href="#llm-cli-subprocess-safety">LLM CLI Subprocess Safety</a></h2>
<p>When using <code>claude-code</code> or <code>codex</code> providers, towl spawns CLI subprocesses:</p>
<ul>
<li><strong>Command validation</strong> -- Relative paths containing <code>..</code> or non-absolute <code>/</code> are rejected</li>
<li><strong>Timeout</strong> -- CLI processes are killed after 120 seconds</li>
<li><strong>Input via stdin</strong> -- Prompts are piped through stdin, not shell arguments (prevents injection)</li>
<li><strong>Stderr capture</strong> -- CLI error output is captured and included in error messages</li>
</ul>
<h2 id="gitignore-respect"><a class="header" href="#gitignore-respect">.gitignore Respect</a></h2>
<p>The <code>ignore</code> crate automatically respects <code>.gitignore</code> rules during directory walking, preventing scanning of files the user has excluded from version control.</p>
<h2 id="error-messages"><a class="header" href="#error-messages">Error Messages</a></h2>
<p>Error messages include file paths and context for debugging but do not expose internal implementation details or sensitive data. The <code>SecretString</code> type ensures tokens cannot leak through error formatting.</p>
<h2 id="threat-model"><a class="header" href="#threat-model">Threat Model</a></h2>
<div class="table-wrapper"><table><thead><tr><th>Threat</th><th>Mitigation</th></tr></thead><tbody>
<tr><td>Path traversal via config/scan/output paths</td><td><code>..</code> component detection, symlink resolution</td></tr>
<tr><td>Regex DoS via malicious patterns</td><td>Pattern length limit (256), regex size limit (256 KB), total pattern cap (50)</td></tr>
<tr><td>Memory exhaustion via large repos</td><td>File size, TODO count, and file count limits</td></tr>
<tr><td>Token leakage</td><td><code>SecretString</code>, env-only token, masked debug</td></tr>
<tr><td>Config file overwrite</td><td><code>--force</code> flag required</td></tr>
<tr><td>Arbitrary file write via symlinks</td><td><code>canonicalize()</code> on output paths</td></tr>
<tr><td>Scanning outside intended directory</td><td><code>.gitignore</code> respect, extension filtering</td></tr>
<tr><td>CLI command injection via LLM providers</td><td>Relative path rejection, stdin piping (not shell args), 120s timeout</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="cicd"><a class="header" href="#cicd">CI/CD</a></h1>
<p>towl uses GitHub Actions for continuous integration and documentation deployment.</p>
<h2 id="documentation-deployment"><a class="header" href="#documentation-deployment">Documentation Deployment</a></h2>
<p>The <code>docs.yml</code> workflow builds and deploys the mdBook documentation to GitHub Pages.</p>
<p><strong>Trigger:</strong> Pushes to <code>main</code> that modify files in <code>docs/</code> or the workflow file itself. Can also be triggered manually via <code>workflow_dispatch</code>.</p>
<p><strong>Pipeline:</strong></p>
<ol>
<li><strong>Build</strong> -- Installs mdBook, runs <code>mdbook build docs</code>, uploads the <code>docs/book/</code> directory as a Pages artifact</li>
<li><strong>Deploy</strong> -- Deploys the artifact to GitHub Pages</li>
</ol>
<p><strong>Permissions required:</strong></p>
<ul>
<li><code>contents: read</code> -- Read repository files</li>
<li><code>pages: write</code> -- Deploy to GitHub Pages</li>
<li><code>id-token: write</code> -- Authenticate with Pages</li>
</ul>
<p>The workflow uses concurrency control (<code>group: pages</code>, <code>cancel-in-progress: true</code>) to prevent overlapping deployments.</p>
<h2 id="running-locally"><a class="header" href="#running-locally">Running Locally</a></h2>
<p>Build and preview the documentation locally:</p>
<pre><code class="language-bash"># Install mdBook
cargo install mdbook

# Build docs
mdbook build docs

# Serve with live reload
mdbook serve docs
</code></pre>
<p>The built documentation is output to <code>docs/book/</code>.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="contributing"><a class="header" href="#contributing">Contributing</a></h1>
<h2 id="getting-started"><a class="header" href="#getting-started">Getting Started</a></h2>
<pre><code class="language-bash">git clone https://github.com/glottologist/towl.git
cd towl
cargo build
</code></pre>
<h3 id="requirements-1"><a class="header" href="#requirements-1">Requirements</a></h3>
<ul>
<li><strong>Rust</strong> 1.75+ (see <code>rust-toolchain.toml</code>)</li>
<li><strong>git</strong> on <code>PATH</code></li>
</ul>
<h2 id="development-commands"><a class="header" href="#development-commands">Development Commands</a></h2>
<pre><code class="language-bash"># Build
cargo build

# Run all tests
cargo nextest run   # preferred
cargo test          # fallback

# Clippy (strict)
cargo clippy --all-targets --all-features

# Format
cargo fmt

# Run the binary
cargo run -- scan
cargo run -- scan -f json -o todos.json
cargo run -- config
cargo run -- init
</code></pre>
<h2 id="project-structure"><a class="header" href="#project-structure">Project Structure</a></h2>
<p>See <a href="reference/./architecture.html">Architecture</a> for a full layout. Key entry points:</p>
<ul>
<li><code>src/bin/towl.rs</code> -- CLI binary</li>
<li><code>src/lib/mod.rs</code> -- Library root</li>
<li><code>tests/</code> -- Integration and property-based tests</li>
</ul>
<h2 id="testing"><a class="header" href="#testing">Testing</a></h2>
<h3 id="test-hierarchy"><a class="header" href="#test-hierarchy">Test Hierarchy</a></h3>
<p>Tests follow a strict hierarchy:</p>
<ol>
<li><strong>proptest</strong> (property-based) -- First choice for pure functions, parsers, validators, serialisation roundtrips</li>
<li><strong>rstest</strong> (parameterized) -- For specific known cases (&lt; 10 inputs with exact expected outputs)</li>
<li><strong>Standalone</strong> -- Last resort, for complex integration scenarios</li>
</ol>
<h3 id="running-tests"><a class="header" href="#running-tests">Running Tests</a></h3>
<pre><code class="language-bash"># All tests
cargo nextest run

# Specific module
cargo nextest run scanner

# Property-based tests only
cargo nextest run proptest

# Integration tests only
cargo nextest run --test '*'
</code></pre>
<h2 id="code-style"><a class="header" href="#code-style">Code Style</a></h2>
<ul>
<li>Follow Rust naming conventions (<code>snake_case</code> for functions, <code>CamelCase</code> for types)</li>
<li>All public items need doc comments (<code>///</code>)</li>
<li>No <code>#[allow(...)]</code> attributes -- fix the underlying issue</li>
<li>No <code>.unwrap()</code> / <code>.expect()</code> in production code -- use <code>?</code> with typed errors</li>
<li>No <code>as</code> numeric casts -- use <code>try_from</code> / <code>into</code> / <code>From</code></li>
<li>Minimise <code>.clone()</code> -- prefer borrowing, see Clone Reduction Policy</li>
</ul>
<h2 id="error-handling"><a class="header" href="#error-handling">Error Handling</a></h2>
<ul>
<li>Use <code>thiserror</code> for error type derivation</li>
<li>Each module defines its own error enum</li>
<li>Errors propagate upward via <code>?</code> and <code>#[from]</code></li>
<li>Never silently discard <code>Result</code> values</li>
</ul>
<h2 id="adding-a-new-output-format"><a class="header" href="#adding-a-new-output-format">Adding a New Output Format</a></h2>
<ol>
<li>Create <code>src/lib/output/formatter/formatters/yourformat.rs</code></li>
<li>Implement the <code>Formatter</code> trait</li>
<li>Add a variant to <code>FormatterImpl</code> in <code>formatters/mod.rs</code></li>
<li>Add dispatch in <code>FormatterImpl::format()</code></li>
<li>Add a variant to <code>OutputFormat</code> in <code>src/lib/cli/mod.rs</code></li>
<li>Update the format-to-writer mapping in <code>Output::new()</code></li>
<li>Add tests (proptest for roundtrips, rstest for edge cases)</li>
</ol>
<h2 id="adding-a-new-todo-type"><a class="header" href="#adding-a-new-todo-type">Adding a New TODO Type</a></h2>
<ol>
<li>Add a variant to <code>TodoType</code> in <code>src/lib/comment/todo.rs</code></li>
<li>Update <code>Display</code>, <code>TryFrom&lt;&amp;str&gt;</code>, <code>as_filter_str()</code></li>
<li>Add a default pattern to <code>default_todo_patterns()</code> in <code>src/lib/config/types.rs</code></li>
<li>Add a pattern mapping in the parser</li>
<li>Update tests</li>
</ol>
<h2 id="pull-requests"><a class="header" href="#pull-requests">Pull Requests</a></h2>
<ul>
<li>Keep PRs focused on a single change</li>
<li>Include tests for new functionality</li>
<li>Ensure <code>cargo clippy</code> passes with zero warnings</li>
<li>Ensure <code>cargo fmt</code> produces no changes</li>
<li>Ensure all tests pass</li>
</ul>

                    </main>

                    <nav class="nav-wrapper" aria-label="Page navigation">
                        <!-- Mobile navigation buttons -->


                        <div style="clear: both"></div>
                    </nav>
                </div>
            </div>

            <nav class="nav-wide-wrapper" aria-label="Page navigation">

            </nav>

        </div>




        <script>
            window.playground_copyable = true;
        </script>


        <script src="elasticlunr.min.js"></script>
        <script src="mark.min.js"></script>
        <script src="searcher.js"></script>

        <script src="clipboard.min.js"></script>
        <script src="highlight.js"></script>
        <script src="book.js"></script>

        <!-- Custom JS scripts -->

        <script>
        window.addEventListener('load', function() {
            window.setTimeout(window.print, 100);
        });
        </script>


    </div>
    </body>
</html>