1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
3687
3688
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
5073
5074
5075
5076
5077
5078
5079
5080
5081
5082
5083
5084
5085
5086
5087
5088
5089
5090
5091
5092
5093
5094
5095
5096
5097
5098
5099
5100
5101
5102
5103
5104
5105
5106
5107
5108
5109
5110
5111
5112
5113
5114
5115
5116
5117
5118
5119
5120
5121
5122
5123
5124
5125
5126
5127
5128
5129
5130
5131
5132
5133
5134
5135
5136
5137
5138
5139
5140
5141
5142
5143
5144
5145
5146
5147
5148
5149
5150
5151
5152
5153
5154
5155
5156
5157
5158
5159
5160
5161
5162
5163
5164
5165
5166
5167
5168
5169
5170
5171
5172
5173
5174
5175
5176
5177
5178
5179
5180
5181
5182
5183
5184
5185
5186
5187
5188
5189
5190
5191
5192
5193
5194
5195
5196
5197
5198
5199
5200
5201
5202
5203
5204
5205
5206
5207
5208
5209
5210
5211
5212
5213
5214
5215
5216
5217
5218
5219
5220
5221
5222
5223
5224
5225
5226
5227
5228
5229
5230
5231
5232
5233
5234
5235
5236
5237
5238
5239
5240
5241
5242
5243
5244
5245
5246
5247
5248
5249
5250
5251
5252
5253
5254
5255
5256
5257
5258
5259
5260
5261
5262
5263
5264
5265
5266
5267
5268
5269
5270
5271
5272
5273
5274
5275
5276
5277
5278
5279
5280
5281
5282
5283
5284
5285
5286
5287
5288
5289
5290
5291
5292
5293
5294
5295
5296
5297
5298
5299
5300
5301
5302
5303
5304
5305
5306
5307
5308
5309
5310
5311
5312
5313
5314
5315
5316
5317
5318
5319
5320
5321
5322
5323
5324
5325
5326
5327
5328
5329
5330
5331
5332
5333
5334
5335
5336
5337
5338
5339
5340
5341
5342
5343
5344
5345
5346
5347
5348
5349
5350
5351
5352
5353
5354
5355
5356
5357
5358
5359
5360
5361
5362
5363
5364
5365
5366
5367
5368
5369
5370
5371
5372
5373
5374
5375
5376
5377
5378
5379
5380
5381
5382
5383
5384
5385
5386
5387
5388
5389
5390
5391
5392
5393
5394
5395
5396
5397
5398
5399
5400
5401
5402
5403
5404
5405
5406
5407
5408
5409
5410
5411
5412
5413
5414
5415
5416
5417
5418
5419
5420
5421
5422
5423
5424
5425
5426
5427
5428
5429
5430
5431
5432
5433
5434
5435
5436
5437
5438
5439
5440
5441
5442
5443
5444
5445
5446
5447
5448
5449
5450
5451
5452
5453
5454
5455
5456
5457
5458
5459
5460
5461
5462
5463
5464
5465
5466
5467
5468
5469
5470
5471
5472
5473
5474
5475
5476
5477
5478
5479
5480
5481
5482
5483
5484
5485
5486
5487
5488
5489
5490
5491
5492
5493
5494
5495
5496
5497
5498
5499
5500
5501
5502
5503
5504
5505
5506
5507
5508
5509
5510
5511
5512
5513
5514
5515
5516
5517
5518
5519
5520
5521
5522
5523
5524
5525
5526
5527
5528
5529
5530
5531
5532
5533
5534
5535
5536
5537
5538
5539
5540
5541
5542
5543
5544
5545
5546
5547
5548
5549
5550
5551
5552
5553
5554
5555
5556
5557
5558
5559
5560
5561
5562
5563
5564
5565
5566
5567
5568
5569
5570
5571
5572
5573
5574
5575
5576
5577
5578
5579
5580
5581
5582
5583
5584
5585
5586
5587
5588
5589
5590
5591
5592
5593
5594
5595
5596
5597
5598
5599
5600
5601
5602
5603
5604
5605
5606
5607
5608
5609
5610
5611
5612
5613
5614
5615
5616
5617
5618
5619
5620
5621
5622
5623
5624
5625
5626
5627
5628
5629
5630
5631
5632
5633
5634
5635
5636
5637
5638
5639
5640
5641
5642
5643
5644
5645
5646
5647
5648
5649
5650
5651
5652
5653
5654
5655
5656
5657
5658
5659
5660
5661
5662
5663
5664
5665
5666
5667
5668
5669
5670
5671
5672
5673
5674
5675
5676
5677
5678
5679
5680
5681
5682
5683
5684
5685
5686
5687
5688
5689
5690
5691
5692
5693
5694
5695
5696
5697
5698
5699
5700
5701
5702
5703
5704
5705
5706
5707
5708
5709
5710
5711
5712
5713
5714
5715
5716
5717
5718
5719
5720
5721
5722
5723
5724
5725
5726
5727
5728
5729
5730
5731
5732
5733
5734
5735
5736
5737
5738
5739
5740
5741
5742
5743
5744
5745
5746
5747
5748
5749
5750
5751
5752
5753
5754
5755
5756
5757
5758
5759
5760
5761
5762
5763
5764
5765
5766
5767
5768
5769
5770
5771
5772
5773
5774
5775
5776
5777
5778
5779
5780
5781
5782
5783
5784
5785
5786
5787
5788
5789
|
#+TITLE: ArchSetup Tasks
#+AUTHOR: Craig Jennings
#+DATE: 2026-02-14
#+PRIORITIES: A D D
* Archsetup Priority Scheme
Four levels, matching the Emacs config (=org-highest-priority ?A=, =org-lowest-priority ?D=, =org-default-priority ?D= in =modules/org-config.el=). Priority answers "how much does this matter"; a date answers "when". They are independent — assign both deliberately. Org priority alone never schedules anything, which is why undated [#A]/[#B] tasks feel ungrounded.
- [#A] Must happen. Broken install, data loss, security, or a blocker for other work. An [#A] REQUIRES a SCHEDULED or DEADLINE date — if it can't be dated, it isn't really an A; drop it to B. (The main agenda always shows open A's.)
- [#B] Should happen, this cycle. Real improvement or fix with no hard date. Surfaces in the agenda's priority-B block only while undated; add a SCHEDULED date when you commit to a week and it moves into the schedule.
- [#C] Nice to have / someday. Kept for the record, low urgency. Date it only when it graduates to B.
- [#D] Default / unsorted. A bare TODO with no cookie is D. Stays out of the agenda — the inbox of priorities. Triage D's up to A/B/C or let them sit.
Rule of thumb: A = dated-and-must; B = the active backlog; C = parking lot; D = untriaged. Fixing the undated A/B tasks means either dating them or demoting to C.
** Tags
The vocabulary is open — topic tags are coined as needed — so these are conventions, not a closed set. A task carries at most one type tag, optionally the effort/autonomy tags, and any number of topic tags. Because the set is open, the task audit leaves topic tags alone (it doesn't strip "unknown" tags).
- *Type* (one per task where the kind is clear): =:feature:= new capability, =:bug:= fix for broken behavior, =:test:= test coverage or test infra, =:refactor:= restructure with no behavior change, =:chore:= tooling / meta / housekeeping.
- *Effort / autonomy*: =:quick:= a spare-moment fix (minutes, not a sitting); =:solo:= Claude can carry it end to end — there's a build path, a test path, and no upfront decision needed (a leftover manual spot-check doesn't disqualify it).
- *Topic / area* (open): the subsystem a task touches — e.g. =:hyprland:= =:waybar:= =:mpd:= =:music:= =:network:= =:tooling:= =:llm:= =:eask:= =:pocketbook:= =:cmail:=. Coin a new one when it aids filtering.
* Next Session Focus
On 2026-07-19, Craig selected the following autonomous work. Status after the
2026-07-19 evening session: 5 shipped (dotfiles, pushed), 3 held for a design
decision. Each shipped item has its own DONE task below.
Shipped: notification loudness -40% (808ca23); clock panel right-click dismiss
(fc9a2b7, live-verified); date-format scrolling as a date-only ring (9dfe082);
show the active wired interface (22867f9); connect the best saved WiFi profile
on enable (9105361).
Held for a design decision (not solo — each needs Craig's call, kept as TODO
below):
- Order network connections by availability — the task wants one tiered list
(available saved -> available unsaved -> unavailable saved), but the panel
spec says "three labelled groups, never one merged list" with Saved MRU-first.
Reorder within Saved only, or merge into one list (overriding the spec)?
- Indicate hotspot/metered WiFi in amber — "hotspot" is ambiguous (connected-to
a phone hotspot vs the machine running an AP), and metered detection needs new
nmcli reads on the status fast path (contract is "one nmcli call").
- Audio doctor mic/input health — points at docs/specs/2026-07-10-audio-doctor-
input-side-spec.org (DRAFT, four decisions open).
* Archsetup Open Work
** TODO [#B] calendar-sync fails headless after every boot until authinfo is unlocked :bug:dotfiles:gpg:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-04
:END:
Found in the 2026-10-04 velox health check. The calendar-sync timer (every 10 minutes)
failed on every run from the 2026-09-29 boot onward (711 journal errors, last success
2026-09-27) with "Decryption failed, Bad session key". The feed URLs live in
=~/.authinfo.gpg= (dotfiles =common/=), which is symmetric AES256. The batch Emacs
the timer runs can't answer a passphrase prompt, so the sync fails until something
interactive caches that passphrase in gpg-agent. The agent's 400-day cache TTL means
it holds once entered, but every reboot starts cold.
Re-encrypting to the GPG key alone doesn't fix it. It only swaps which passphrase
has to be typed per boot, and on 2026-10-04 ratio's agent had no GPG key cached at
all (its sync works only on the cached symmetric passphrase), so the switch would
break ratio until the key is unlocked there.
The fix that actually makes it headless: install =pam-gnupg= so login presets the
key passphrase into gpg-agent, then re-encrypt authinfo to the =c@cjennings.net= key
(B832F070). That needs the key passphrase to match the login password, or a decision
to make it match. pam-gnupg is three pieces, and all three are required: add
=allow-preset-passphrase= to =gpg-agent.conf= (the current config lacks it, and
without it the preset is refused), list the encryption subkey's keygrip in
=~/.pam-gnupg=, and add the =pam_gnupg.so= auth and session lines to the login PAM
stack. Do it on both daily drivers. Re-encrypt by piping decrypt into
encrypt, never via plaintext on disk, and verify by comparing plaintext hashes.
** TODO [#C] Closed tasks pinned to the agenda by a live planning line :chore:quick:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-25
:END:
Four closed tasks in the Resolved section kept the SCHEDULED or DEADLINE they
carried while open, so org still renders them on the agenda as weeks overdue. Find
them with the grep below rather than by line number — an insertion anywhere above
shifts every number, and the first version of this task carried four that were
already stale when I wrote them:
#+begin_src sh :results output
grep -nE '^(CLOSED|SCHEDULED|DEADLINE):.*(CLOSED|SCHEDULED|DEADLINE):' todo.org
#+end_src
My org config sets =org-agenda-skip-scheduled-if-done= to nil, so a terminal
keyword doesn't suppress them — only removing the planning line does.
This is the top-level counterpart to the rule that strips the planning line from
a dated sub-task entry. An interactive close stamps =CLOSED:= and leaves any
pre-existing =SCHEDULED:= in place, which is how all four survived.
Fix: delete the SCHEDULED/DEADLINE token from each line the grep returns. At =**=
keep the =CLOSED:= cookie; at =***= and deeper delete the whole planning line,
CLOSED included, since a dated log heading carries its date in the heading. All
four current instances are =**=, but the task is built to be run later, which is
when a deeper one could appear. Verify by re-running the grep and getting no
output.
The pattern is order-independent on purpose. Matching =CLOSED:.*SCHEDULED:= would
miss a planning line written the other way round and then report clean over an
instance it never looked at, and the =^= anchor is what stops the grep matching
its own source line.
Found 2026-09-25 while fixing a fifth instance that a review caught in the
then-uncommitted diff. That one is fixed; these four predate it and were left
out so the commit didn't grow a second concern.
** TODO [#B] velox's USB hub tears down and rebuilds under the Jabra :bug:velox:hardware:audio:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-25
:END:
The hub at usb 3-2 and whatever hangs off 3-2.1 disconnect and re-enumerate
together, near-daily, and it takes the call audio with it. When the Jabra
Speak2 75 comes back as a new device number, Zoom doesn't follow it, so output
goes nowhere while the device name and every setting still look correct.
Grading: Major severity (audio dies mid-call and needs a manual re-pick; no
data loss, and a workaround exists) x most-users-frequently (near-daily across
the whole retained journal, on the primary call path) = P2 = [#B].
Evidence, 2026-09-25 from velox's persistent journal:
- Today: 11:06:01 the Jabra (0b0e:24ef) enumerates on 3-2.1 as device 5;
11:06:02 both it and parent hub 3-2 (device 4) disconnect; 11:06:03 the hub
returns as device 6 and the Jabra as 7. One second after plug-in.
- The same 3-2 / 3-2.1 pair drops on 09-18, 09-20, 09-21 (five times between
08:49 and 08:53), 09-21 18:08, 09-22 (four), 09-23, 09-24 (two), 09-25.
- Companion lines in today's window: "5:0: failed to get current value for
ch 0 (-22)", "cannot get min/max values for control 2 (id 5)", and
"ucsi_acpi USBC000:00: unknown error 256".
The parent hub is 0a12:4010 — a dock or dongle, not the headset — so the hub is
the likelier fault and the Jabra the casualty. Some of the listed drops are
plausibly me unplugging the dock at the end of a day; the 09-21 cluster of five
inside five minutes and today's one-second-after-plug-in re-enumeration are not.
Not :solo: — splitting hub from headset needs hardware I have to move: the Jabra
on a direct port with no dock, a different dock or cable, and the dock with
something else on it. The log work is done.
*** 2026-09-25 Fri @ 12:30:00 -0400 Work's read: bypass the dock, which runs the experiment for free
Work landed the same conclusion about the hub being the fault rather than the
Jabra, and drew the better practical consequence from it. Both call paths now
have a measured failure mode — flaky hub on wired, flaky HFP on bluetooth — so
choosing between them is the wrong frame. Plugging the Jabra straight into a
laptop port avoids both.
That also collapses the hardware isolation this task is waiting on. If the direct
port holds for a few days of real calls, the hub is implicated and the headset is
cleared, with no deliberate test to run — just using it is the experiment. If it
drops anyway, the fault is downstream of the hub and this task's scope changes.
Try the direct port first and read the result off the journal.
Cross-boot queries need =journalctl _TRANSPORT=kernel= with no =-b=. Plain
=journalctl -k= implies =-b= and silently scopes to the current boot, which is
what made this look like a single event with no baseline.
** TODO [#B] xhci on velox refuses D3hot thousands of times per boot :bug:velox:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-25
:END:
=xhci_hcd 0000:c3:00.0: Refused to change power state from D0 to D3hot=, at a
roughly fixed rate all session, every boot. The controller never reaches D3hot,
so it holds D0 for the life of the boot — a power-management failure and a
plausible battery cost on a laptop.
Grading: Minor severity (nothing the user does fails; log noise plus a suspected
but unmeasured power cost) x every-boot-every-time = P2 = [#B]. Regrade to Major
if the drain turns out to be measurable — grading the being-in-it, a controller
pinned in D0 costs continuously rather than in a bounded trickle.
Counts across the five retained boots (oldest to newest): 15772, 1859, 16644,
9987, 5747. Today's 5747 is the lowest of the set, not an anomaly.
Lead, not a conclusion: the installer puts TLP on every battery machine and TLP
owns USB power policy. Check its USB autosuspend handling against both these
refusals and the hub instability in the task above — they may share a cause.
Not :solo: — the diagnosis half is mine to run, but deciding whether to change
velox's power policy is a preference call about battery versus device stability,
so it needs your answer before anything is written.
** TODO [#B] The installer never makes the journal persistent :bug:solo:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-25
:END:
=configure_encrypted_autologin= writes =/etc/systemd/journald.conf.d/
retention.conf= with =SystemMaxUse=500M= and stops there (archsetup:3624).
=Storage== is left at its default of =auto=, which is persistent only when
=/var/log/journal= already exists — and on a fresh Arch install it does not. So
a machine this installer builds keeps no journal across a reboot, and any
post-incident question that spans a boot is unanswerable on it.
Grading: Minor severity (the machine works; what's lost is the ability to
diagnose across a reboot) x every-fresh-install = P2 = [#B]. Not [#A] because no
live machine is impaired — see below.
Both daily drivers already carry a hand-written =persistent.conf= with
=Storage=persistent=, so this bites only future installs. velox's is dated
2026-08-13 17:48, an hour before the installer's own journald block ran at
18:51 on rebuild day, which is how I know the installer didn't write it. Ratio
has the same pair of files.
Fix: add =Storage=persistent= to the block that already writes retention.conf,
with a test pinning it beside the SystemMaxUse assertion. One file, and it
belongs in the config the installer already owns rather than a second drop-in.
Found while verifying a work handoff that had concluded velox kept no baseline —
it did, because of the hand-written file. A fresh machine wouldn't have.
** TODO [#C] Airplane panel key follow-ups from the f977418 commit :refactor:dotfiles:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-23
:END:
Four items left standing when the airplane keybind moved into the net panel
(dotfiles f977418, 2026-09-23). None blocked the commit. archsetup drives the
dotfiles work end to end per the standing rule in notes.org.
- The AIRPLANE console key is shown on desktops (net/src/net/gui.py:576). On
ratio the flow is: confirm the prompt, then "airplane mode isn't available
on this machine". PanelModel already carries has_wifi and has_speedtest
capability flags; a battery or laptop flag could desensitize the key before
the question is asked. This one is a design call, which is why the task
isn't :solo:.
- "LEAVE AIRPLANE" is hardcoded in net/src/net/classify.py:52 and diag.py:84
instead of shared from viewmodel.AIRPLANE_LEAVE_KEY. Tests pin the coupling,
so a rename would surface, but one source is cleaner.
- tests/net/panel_smoke.py:57 checks the DOCTOR and SPEED TEST console keys
only. Add AIRPLANE, and consider a smoke step that opens the confirm dialog
and cancels. Needs a compositor to run.
- Two pre-existing comments on untouched lines still say "keybind":
hyprland/.local/bin/airplane-mode:38 and
tests/airplane-mode/test_airplane_mode.py:353.
** TODO [#C] Post-install check that every mimeapps.list handler exists :feature:quick:solo:
:PROPERTIES:
:CREATED: [2026-09-22 Tue]
:LAST_REVIEWED: 2026-09-22
:END:
Work's optional ask from the 2026-09-18 libreoffice handoff, kept because the
failure it catches is silent.
The dotfiles =common/.config/mimeapps.list= "[Default Applications]" block
names a .desktop file per type. When the package behind one is missing,
xdg-mime does not error — it falls through to the next application claiming
that type. On velox that meant every .pptx opened PowerPoint inside the
Windows VM for a month, and the only symptom was that it felt slow.
Add a post-install check that reads every .desktop named in that block and
reports the ones absent from the machine. Declaring the packages (done
2026-09-22 for libreoffice-fresh, imv and git-lfs) fixes today's instance; this
catches the next one, including a handler the dotfiles add later.
Natural home is =scripts/post-rebuild-check=, which already runs this shape of
verification.
** TODO [#B] Own the meeting transcription service install :feature:velox:ratio:tooling:
:PROPERTIES:
:CREATED: [2026-09-22 Tue]
:LAST_REVIEWED: 2026-09-22
:END:
Work built a self-hosted meeting transcription service (whisper.cpp plus
pyannote diarization) that has run on ratio and velox since 2026-09-17, and
handed the service side here on 2026-09-19 because it is machine setup rather
than application work. Installed by hand on both machines today; nothing
reinstalls it.
The bundle is in [[file:working/meeting-transcription-service/][working/meeting-transcription-service/]], with work's handoff note
beside it. Scanned for credentials on arrival: clean.
What the install has to provide per machine:
- =~/.local/share/pyannote-diarize/.venv= — Python 3.12, CPU torch,
pyannote.audio 4.0.7, about 1.3 GB, built with uv.
- =~/.local/share/whisper-models/ggml-large-v3-turbo-q5_0.bin=, plus
whisper-cpp itself.
- The three =src/= scripts where the units expect them, and both user units
enabled with linger on so the path unit fires without a login session.
Open decisions before this is buildable, which is why it isn't =:solo:=:
- Where the code lives in this repo — a new top-level dir, or under =scripts/=.
- How the Hugging Face step is handled. Accepting the pyannote model terms and
caching the model is one-time, online, and interactive. The token is a
credential and this repo is anonymously cloneable, so it cannot be committed
here; the installer can only prompt for it or read it from the private
secrets path.
Known rough edge work flagged: when two runs overlap the second finds the lock
held, the worker returns silently, and the client reports "finished without
producing a transcript". Rerunning works. The message should name the lock.
** TODO [#C] Orchestrator sequence pin misses an added step :test:quick:solo:
:PROPERTIES:
:CREATED: [2026-09-17 Thu]
:LAST_REVIEWED: 2026-09-17
:END:
Noticed during the 2026-08-08 pre-vacation sweep and never filed; confirmed
still open 2026-09-17.
=tests/installer-steps/test_orchestrators.py= defines recorder stubs only for
the sub-steps it expects. A step added to an orchestrator without updating the
pin calls an undefined function: bash prints "command not found" to stderr,
nothing reaches stdout, and the recorded sequence still matches. The
=returncode= assertion sees only the last call's status, so the test passes
unless the new step happens to be last. The module docstring claims it catches
"a dropped, added, or reordered" call; it catches drops and reorders.
Fix: define =command_not_found_handle() { echo "UNSTUBBED:$1"; }= in the
generated script so an unstubbed call lands in stdout and fails the equality,
plus a test proving it (the file already has one of those for guarded helpers,
=test_the_check_would_notice_a_missing_call=).
Grading: Minor severity (the VM harness still runs the real steps; only the
fast pin is blind) x some developers sometimes (fires only when a step is added
without updating the pin) = P3 = [#C].
** TODO [#B] Swap velox's MT7925 for an Intel AX210 :chore:velox:hardware:
:PROPERTIES:
:CREATED: [2026-09-16 Wed]
:LAST_REVIEWED: 2026-09-16
:END:
Ordered from the Framework Marketplace on 2026-09-16; waiting on delivery.
An Intel AX210 will replace velox's MediaTek MT7925
(RZ717, Filogic 360) in the M.2 2230 slot. The ordered part is AX210.NGWG.NV
(the .NV suffix means no vPro). A vPro card won't work in the Framework, so
check the part number on the card when it arrives.
Why: HFP call audio on the MT7925 fails in firmware. It sends zero-filled SCO
frames on sentinel handle 0x0E00, and the kernel logs "SCO packet for unknown
connection handle 3584". It's pending upstream with no fix. Three headsets fail
on velox. The MT7925 is also step 4 of the hibernate-freeze mitigations. Losing
WiFi 7 is fine; the AX210 does WiFi 6E and BT 5.3. linux-firmware-intel is
already installed on velox, and the installer's firmware trim only runs on
Intel-CPU Framework 13s, so archsetup needs no change. See the Bluetooth wedge
bug's 2026-09-16 entries.
Before the swap: note the saved WiFi profiles (NetworkManager keeps them, they
aren't tied to the card), and power off fully (not hibernate). Keep the MT7925
in case the AX210 is a dud.
After the swap, verify:
- lspci -k shows the AX210 on iwlwifi; the Bluetooth controller enumerates as
Intel (btintel) and hci0 is up.
- WiFi joins a saved network, and net status/probe/diagnose read it correctly.
The net code has an nl80211 path written for the mt7925, so check that the
signal line still shows.
- Re-pair the Sonys and any other Bluetooth devices, since the bonds belong to
the old controller's address. Use the agent-backed pairing recipe in the KB,
then count key sections to confirm a real bond.
- A real call load of several minutes in HFP, counting "Failure in Bluetooth
audio transport" and kernel "unknown connection handle" errors. Target zero.
Note the Intel risk: AX201/AX211 had an eSCO handle-reuse bug with WirePlumber
0.5.17 after repeated profile switching.
- Suspend/resume and one hibernate cycle with the new card.
Then close the Bluetooth wedge bug, or regrade it, and update the MT7925 step
in the hibernate mitigations.
** DOING [#C] Net doctor takes the DNS ladder on a DoT-blocking captive portal :bug:dotfiles:network:
:PROPERTIES:
:CREATED: [2026-09-14 Mon]
:LAST_REVIEWED: 2026-09-14
:END:
An open captive-portal network that filters TCP 853 before login, 2026-09-14.
The network's resolver
172.20.0.1 answers plain UDP 53, but TCP 853 is filtered until you log in. With
resolved pinned to DNSOverTLS=yes, every lookup on the WiFi link times out.
The probe read "no-internet" instead of "captive", and the doctor ran
repair:dns-test (cleanup-unverified), then repair:dns-override (fail,
reverted), twice (08:32, 08:35). It never offered portal-login. Craig had to
tether to his phone. A dns-override to 1.1.1.1 can't pass a walled garden
anyway. The same flow worked on a different portal on 09-13.
Once DNS resolves (through the tether), the pinned probe sees the portal and
diagnose recommends net portal.
Also unexplained: the probe log reads "online" on wlp192s0 from 08:43 to
09:50, then flaps captive/online until 09:52, while a pinned curl shows the
portal still intercepting. Check whether the probe follows the default route
(the tether) while labeling the WiFi interface.
Remaining before close: a live doctor run on that network with the tether
unplugged (the fix is committed and live on velox; ratio gets it on its next
dotfiles pull).
Follow-ups the review found in existing code (not in this fix):
- repair.py _extract_portal_url (~603-619) rejects any URL containing a
detection-host name anywhere, query string included. This portal echoes the
requested URL in its OS= parameter, so the portal URL the probe found gets
thrown away, and repair_portal_login (~687) falls back to opening a trigger
page (neverssl.com). The login should still work through interception, but
the found URL is lost. Fix: match detection hosts against the hostname only,
or pass the probe's portal_url through.
- probe.py CLOUDFLARE_HOSTS: a controller that hosts its login page on 1.1.1.1
itself (older Cisco wireless, https://1.1.1.1/login.html) reads as
reachable, in both the followed-redirect and body branches.
Grading: Major severity (no path online on such a network without knowing to
run net portal by hand; the doctor's own repairs can't succeed) × some users,
sometimes (networks that filter 853 before login; another portal worked) =
P3 = [#C].
*** 2026-09-14 Mon @ 12:12:48 -0400 Found the root cause in the probe's IP fallback, fixed it test-first
My first guess (extend the tunnel-dot shape test to WiFi) was wrong. With DNS
dead, run_probe falls back to http://1.1.1.1/ over the WiFi. The portal answers that
literal with HTTP 200 and a meta refresh, so curl's effective URL stays on
1.1.1.1. classify_ip only checked the effective host, and called the page
"reachable". So the probe said no-internet, diagnose emitted no portal row, and
the classifier fell through to dns-test. The hostname path already reads
body-level redirects; the IP path didn't.
Fix in dotfiles net/src/net/probe.py: classify_ip reads the body with
extract_portal_url and calls it captive when the target host isn't Cloudflare.
Tests in tests/net/test_net.py, all using a placeholder-scrubbed portal
fixture: classify_ip normal, boundary, and error cases (Cloudflare body
redirect, v6 literal, refresh with no URL, javascript: target, followed
redirect wins); probe_and_cache end to end; diagnose emitting the portal row;
doctor --fix running portal-login instead of dns-test/dns-override. Red with
four failures, green at 991. The live IP probe over that network now
classifies it captive and finds the portal URL.
*** 2026-09-14 Mon @ 12:22:23 -0400 Committed and pushed the fix as dotfiles b2688e4
An isolated review approved it. I added its two Minor test gaps (JS redirect,
malformed target) and made the comments vendor-generic. Full make test: forked
and shared runs each 4401 OK, faces 171 pass. I sent an FYI to the dotfiles
inbox. The net CLI runs from the repo, so velox has the fix now.
** TODO [#B] Bluetooth audio link drops wedge the PipeWire graph on velox :bug:velox:audio:
:PROPERTIES:
:CREATED: [2026-09-14 Mon]
:LAST_REVIEWED: 2026-09-14
:END:
First seen 2026-09-14 with the Jabra Speak2 55 MS on velox.
There are two faults, and the second makes the first much worse.
1. The link drops. "Failure in Bluetooth audio transport" hit six times between
10:09 and 11:10: on A2DP (sep1/fd0) and HFP (fd60), at 10:09, three times
around 10:10, 10:54, and 11:10. Each one matches a kernel "Bluetooth: hci0:
ACL (or SCO) packet for unknown connection handle" line. The controller is
the MediaTek MT7925 (0e8d:7925, btusb; WiFi firmware build 20260813).
Suspects I haven't separated yet: MT7925 btusb firmware or driver, range or
2.4 GHz coexistence with the WiFi on the same chip, or A2DP/HFP profile
switching when the mic opens.
2. The graph wedges. After the drops, the Jabra's nodes sat in error and every
client round trip hung: pactl info, wpctl status/inspect, pw-dump, and the
mic-mute key (four stuck wpctl set-mute processes). pw-cli info 0 still
answered, and no thread was spinning. Only a restart of pipewire,
pipewire-pulse, and wireplumber cleared it. Versions: pipewire 1.6.8,
wireplumber 0.5.17, bluez 5.87, kernel 6.18 LTS.
Grading: Major severity (all audio control is dead until a manual service
restart; the restart is the only workaround) × most users, frequently (six
drops in about an hour of use on the one Bluetooth speaker in play) = P2 =
[#B]. One session of data; re-read the frequency row if it doesn't recur.
Next: on a recurrence, capture btmon and the kernel log across a drop, and
check whether the wedge follows only HFP use. Try the same speaker on ratio to
split controller from device. Look upstream for MT7925 "unknown connection
handle" reports and for wireplumber bluez nodes stuck in error.
*** 2026-09-15 Tue @ 14:12:11 -0400 Reproduced the link drop with a second speaker, a Speak2 75
The replacement Jabra Speak2 75 dropped the same way while
paired straight to velox's MT7925: "Failure in Bluetooth audio transport" at
12:47:20 (A2DP, sep3/fd0) and 12:47:49 (HFP, fd62), with a kernel "hci0: ACL
packet for unknown connection handle" line at 12:47:21. The graph didn't wedge
this time; pactl, wpctl, and pw-dump all kept answering. A second device with
the same signature points further toward the controller side than the speaker.
I removed velox's pairings for both Jabras, and the 75 now runs over its USB
cable (0b0e:24ef), which works for playback and mic. Its Link 390 dongle
(0b0e:2e56) enumerates but was never linked to the speaker, and pairing one
needs Jabra Direct (Windows/Mac only).
*** 2026-09-16 Wed @ 13:29:34 -0400 A pairing that never bonded looks like this bug, so rule it out first
A one-shot bluetoothctl pair, with the adapter at Pairable: no (velox's normal
state), can leave a bond file with no key section. It shows up as transport and
AVDTP failures and the device dropping the link. The 09-15 log records the
Speak2 75 as Bonded: yes, so this doesn't explain the 75's drops. The 55 MS's
bond state was never checked, and its pairing is gone now. On a recurrence,
count the device's key sections first (the one-liner in the node).
[[id:f23b7085-c35e-43c1-ae02-9e9c67e3848b][bluetoothctl one-shot pair can complete without bonding]]
*** 2026-09-16 Wed @ 13:43:40 -0400 Third device, same kernel handle errors; pairing and settling ruled out
The Sony WF-1000XM6 (bonded, one LinkKey) failed the same way today, and the
evidence now points at the MT7925's SCO path.
Work's session: transport failures from 10:44 to 10:50, about 90 clean seconds,
then six more in two minutes (11:01 to 11:04) during a Meet call, with the buds
beeping on each disconnect. The kernel logged 23 "SCO packet for unknown
connection handle" and 38 "ACL packet for unknown connection handle" errors in
exactly those two windows. mpv played over A2DP for twenty minutes with no
failures. Every failure came after the card switched to a headset profile. SCO
carries HFP voice. In work's run, a 35 s full-duplex hold of each HFP codec
(CVSD, mSBC, LC3-SWB) passed with zero failures while real calls failed. The
earlier "link settling after a fresh pair" reading was wrong: the quiet stretch
was just quiet.
Mine, from 13:23: the card churned between profiles for six minutes, then
pipewire-pulse refused about 200 connections as "too many client application
connections" (13:30:07 to 13:30:10). pactl, wpctl, and pw-cli info 0 all hung,
the same wedge as 09-14, and restarting the three user units cleared it. After
the restart, a 20 s LC3-SWB hold failed: the mic was pure digital zero, both
bluez nodes went to error, and the kernel logged SCO and ACL handle errors.
10 s holds of CVSD and mSBC passed. Then at 13:40:00, :17, and :22, three
headset-profile switches failed the same way while mpv played, each time an
app opened the mic. WirePlumber then had LC3-SWB saved as the headset profile.
What this rules out: a bad bond (the Sonys are bonded), settling (the failures
came back under load). The codec isn't ruled out. The only failure with a
known codec was LC3-SWB (which passed work's hold), CVSD and mSBC have only
passes on record, and the codec in play during the three 13:40 failures wasn't
captured. Three devices share the signature, and the handle errors come from
the controller.
Still untested: WiFi/Bluetooth coexistence on the shared MT7925. velox was on a
busy shared WiFi network through every failure. Next test: a real call load
with WiFi off and the network over a USB tether, counting transport failures
and kernel handle errors. Then a btmon capture across a failure.
Workaround until then: take calls on the Speak2 75 over USB. With autoswitch on,
any app that opens a mic pulls Bluetooth music into HFP and hits the fault.
[[id:eaa85ba0-ad4f-4a38-a27f-c39b879c341f][Measure Bluetooth audio dropouts across the load that fails, not a quiet window]]
*** 2026-09-16 Wed @ 14:42:50 -0400 Codec-filter experiment and a saved-profile trap
Work tried pinning the call codec at about 14:30 with Craig's go-ahead: it left
lc3_swb out of monitor.bluez.properties bluez5.codecs and restarted wireplumber.
The filter does reach HFP. LC3-SWB disappeared, and headset-head-unit became
MSBC. But every A2DP profile, AAC included, vanished too, even though all the
A2DP codec names were listed. Work reverted within a minute and all six profiles
came back. It's unclear whether the config dropped A2DP or the three-second wait
was just too short for re-enumeration. If codec pinning is still wanted after
the AX210 swap, retest on a day without calls and wait longer before judging.
The wireplumber.conf.d directory is empty now, so the revert is on disk.
The saved-profile trap: WirePlumber saves profiles by name, and the name
headset-head-unit maps to whichever codec is best at load time (LC3-SWB with
the default config, MSBC under the filter). At 14:42 both the card's
default-profile and saved-headset-profile read headset-head-unit, so calls
autoswitch to LC3-SWB. And because default-profile is a headset profile, the
card may come back in HFP (call-quality music) after a reconnect or a
wireplumber restart, until something switches it to a2dp-sink. Nothing changed:
work asked for a hold on velox audio for the rest of 09-16.
*** 2026-09-16 Wed @ 15:08:18 -0400 Matched to an upstream MT7925 firmware report; fix is a new card
The kernel's "SCO packet for unknown connection handle 3584" is handle 0x0E00,
the sentinel handle in an August 2026 linux-bluetooth report: "MT7925
(0e8d:0717): HFP microphone unusable — firmware delivers zero-filled (e)SCO
frames on sentinel handle 0x0E00" (bluez/bluetooth-next PR #744). It's the same
chip under a different USB ID; velox enumerates as 0e8d:7925. That report
used PipeWire 1.6.8 and MT7925 BT firmware from 20260622 and 20260810, and its
frames were all zeros, matching the zero-filled LC3-SWB mic hold here. The
reporter traced it to the firmware's transparent-mode receive path. A
handle-rewrite patch only proved the payload is zeros. There's no maintainer
reply, nothing merged, and no fix. The reporter also found it depends on the
headset. velox at the time: kernel 6.18.51-lts, linux-firmware-mediatek
20260910, BT firmware built 20260813.
A related report on Intel AX201/AX211 shows handle reuse after repeated
A2DP/HFP switching with WirePlumber 0.5.17 (velox's version), and the bluetooth
maintainer called that a firmware bug too. Autoswitch churn makes these faults
more likely.
So the leading explanation is MediaTek firmware, not the headsets and not our
config. Coexistence stays untested, but the upstream match means the WiFi-off
test no longer decides the fix. The fix is replacing the card: an Intel AX210
is ordered (see the swap task at the top of Open Work). The dotfiles net code
also calls ratio's card an mt7925, so testing on ratio wouldn't separate
controller from headset. That's unconfirmed, because ratio didn't answer ssh.
Once the AX210 is in, re-run the call-load test there. Close this bug if it's
clean, or regrade it if Intel shows the same signature.
*** 2026-09-22 Tue @ 01:47:56 -0400 A fourth failure on 09-17, and restarting pipewire-pulse is not a full recovery
From the work session's 09-19 handoff, read back from velox's journal.
The failure recurred Thursday 2026-09-17 at 10:59:32 EDT: kernel "ACL packet
for unknown connection handle 3837" on boot a440a2a0, with wireplumber logging
"Failure in Bluetooth audio transport" for 58:18:62:AA:62:9D at 10:59:11 and
10:59:32. That boot carried five unknown-handle lines. Daily transport-failure
counts now run 13 on 09-14, 3 on 09-15, 44 on 09-16, and 11 on 09-17 (eight of
them between 08:56 and 08:58). The handle differs each time, which is what the
upstream sentinel-handle report predicts, so this is the same firmware fault
rather than a new one.
Recovery gap worth knowing before the AX210 lands: restarting pipewire-pulse
strands every Chromium and Electron audio helper process. Each of those apps
stays silent until it is itself restarted, so the service restart alone leaves
the browser and any Electron app on a call dead. Restart the apps too.
** TODO [#B] Visual separator between adjacent waybar modules :feature:waybar:dotfiles:quick:
:PROPERTIES:
:CREATED: [2026-09-13 Sun]
:LAST_REVIEWED: 2026-09-13
:END:
Captured by Craig 2026-07-20 and routed here from the roam inbox, then lost
in the processed pile: the wind (weather) value runs straight into the date
with no visual stop, so the wind figure reads as the start of the date. Add
a light separator or spacing between adjacent modules so each one's edge is
unmistakable. Check the current bar first (the mic/PTT merge and the weather
chip grouping landed after the capture), then decide the form: a thin rule,
a dot glyph, or just margin. That choice is mine, so not solo; the CSS itself
is a quick change in the dotfiles waybar stylesheet.
** TODO [#C] Saving and restoring a window configuration :feature:hyprland:research:
:PROPERTIES:
:CREATED: [2026-09-13 Sun]
:LAST_REVIEWED: 2026-09-13
:END:
Research idea captured by Craig 2026-07-24 (the one item of that batch that
never got filed): when I want a specific window orientation, I indicate it
and the window-plus-app configuration reappears. What would we need to know
or store to make that happen? Is there another desktop or OS that does it,
what information do they keep, and what are their rules? Explore how far
Hyprland can get, document thoroughly, and review the findings with me before
building anything. The deliverable is a research note under docs/design, so
this is not solo.
** TODO [#C] maint backup_freshness probe blind to backup_run remedy runs :bug:maint:dotfiles:solo:
:PROPERTIES:
:CREATED: [2026-09-12 Sat]
:LAST_REVIEWED: 2026-09-12
:END:
From home's 2026-09-12 velox health check. The backup_freshness probe
(dotfiles =maint/src/maint/probes/services.py=) parses the tail of
=/var/log/rsyncshot.log=, which only the root crontab lines append to. The
=backup_run= remedy (=remedies.py=) runs rsyncshot without that redirect, so
right after a successful =maint fix backup_run= (DAILY.0 rotated on truenas
at 21:29) the probe still said "daily 272h ago" and stayed CRITICAL until
the next cron daily landed.
Fix: make the remedy log the way the cron lines do (append the run to
=/var/log/rsyncshot.log=), so the probe and the remedy read and write the
same record. Reading the snapshot directories instead is the other route,
but they live on truenas, so the probe can't see them without a network
call on the status path.
Grading: Minor (a false CRITICAL that hides nothing, but persists for days)
x some users sometimes (only after a by-hand remedy) = P3 = [#C]. Solo: the
remedy-logs fix is mechanical and the maint suite covers remedies. Dotfiles
work, which this project carries end to end.
** TODO [#B] Loopback-only binds in maint's unexpected-listener set :bug:maint:dotfiles:
:PROPERTIES:
:CREATED: [2026-09-12 Sat]
:LAST_REVIEWED: 2026-09-12
:END:
From home's 2026-09-12 velox health check. The slack-mcp-deepsat container's
=docker-proxy= on =127.0.0.1:13080= is a persistent unexpected-listener warn
on velox (ratio runs the same container). A loopback bind is not a LAN
exposure, and the firewall digest already distinguishes wildcard-bound
listeners from the rest.
Two routes: treat loopback-only listeners as informational (drop them from
the unexpected set, keep them in the rows view), or curate =docker-proxy= as
expected per machine from the panel's MARK EXPECTED. The first is the durable
fix; the second is the workaround that works today.
Not solo: whether a loopback port belongs in the signal at all is my call
(a local port is still reachable from a browser), so the design question
comes first. Grading: Minor (a permanent warn that trains me to skim past
the listeners row) x every user every time (both daily drivers run the
container, and the warn never clears) = P2 = [#B].
** TODO [#B] Speedtest button cancels an in-flight run :feature:dotfiles:network:
:PROPERTIES:
:CREATED: [2026-09-01 Tue]
:LAST_REVIEWED: 2026-09-01
:END:
From the roam inbox (routed 2026-09-01), Craig's words: "pressing the
speedtest button on network admin panel when speedtest is already running
should cancel the speedtest. However, we should leave any numbers on the
display as if the speedtest completed successfully."
Net panel work lives in ~/.dotfiles; archsetup owns it end-to-end per the
standing rule in notes.org. Distinct from the [#C] speedtest-history task
(that one persists results over time; this one is in-flight cancel
semantics). Behavior is fully specified: second press kills the running
test, display keeps whatever numbers are already shown as a completed
result. [#B]: real improvement to the active panel family, no hard date.
** TODO [#B] gcalcli in the installer, token carried from the other daily driver :feature:velox:tooling:solo:
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
:LAST_REVIEWED: 2026-08-25
:END:
From home's 2026-08-25 handoff: velox's 08-13 reinstall left it without
gcalcli, and on 08-21 both calendar write paths on velox were down at once
(the google-calendar MCP with expired tokens, and no gcalcli), so a booked lab
appointment sat uncalendared for three days. The daily-drivers one-time-setup
drift, exactly.
Part 1 is done (2026-08-25, this session): =pipx install gcalcli==4.5.1= on
velox to match ratio, then ratio's =~/.local/share/gcalcli/{oauth,cache}=
copied over tailscale (=oauth= is a 1 KB pickled google-auth credential,
=chmod 600=). =gcalcli list= on velox returned all six calendars with no
re-consent, so the token is portable between the daily drivers and the OAuth
click-through is not needed when the other machine is reachable.
Part 2, this task: make the installer do it.
- =pip_install gcalcli= in the tool set beside =pip_install yt-dlp= (archsetup
~line 3109; =pip_install= wraps =pipx install= as =$username=). Pin or not:
ratio and velox are both 4.5.1; unpinned matches how yt-dlp is installed.
- The credential can't be installed: add a named post-install manual step
"copy =~/.local/share/gcalcli/oauth= from the other daily driver
(=scp <other>:.local/share/gcalcli/oauth ~/.local/share/gcalcli/=,
=chmod 600=), or run =gcalcli init= per
=assets/2026-02-01-gcalcli-setup.org= when neither machine has it."
- A =post-rebuild-check= item: gcalcli on PATH and the oauth file present, so
the drift is caught by the checker rather than by a missed appointment.
- Tests: an installer-steps pytest asserting the tool set carries
=pip_install gcalcli=; a post-rebuild-check test for the new item, both
states.
- When it lands, confirm back to home (=inbox-send home=) so it can retire
its "gcalcli is not installed on velox" notes.
Grading: feature, no hard date, real improvement to the install = [#B].
:solo: — build path (installer + checker + tests) and verify path (pytest;
the live proof already exists on velox) with no open decision.
** TODO [#A] Topgrade guarded-upgrade build :feature:maint:dotfiles:
SCHEDULED: <2026-09-23 Wed>
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
:LAST_REVIEWED: 2026-09-17
:SPEC_ID: 81cdfd72-db96-43d3-aa03-779878c99f3e
:END:
The waybar maint module's "topgrade freshness" warning never clears: the stamp
is written only when topgrade exits 0, and the =hypr-live-update-guard=
PreTransaction hook (mesa, wayland, hyprland, vulkan-*, nvidia-utils,
xorg-xwayland under a live Hyprland) plus any failing ecosystem step makes that
exit almost unreachable. Diagnosed 2026-08-24/25; the fix is specced, not
hacked, because it spans two repos and the design is contested.
Spec: [[file:docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org][2026-08-25-topgrade-guarded-upgrade-spec.org]] (DOING).
Four open decisions, all mine to make before the spec can move:
1. Freshness means *state* (a guarded upgrade still un-applied stays stale),
not recency (any run stamps).
2. Primary mechanism is alternative D: an armed boot-time oneshot ordered
before =getty@tty1= (no display manager to order against).
3. The boot run is arch-only (=topgrade --only system=), not the full sweep.
4. The arm flag lives on a persistent path and is one-shot.
Then: flip the decisions DONE, run spec-review (DRAFT → READY), run
spec-response to decompose the four phases into build tasks here, file the
vNext =[#D]= kernel-reboot item, and commit the spec.
*** 2026-09-12 Sat @ 23:26:50 -0500 Containers step and initramfs-read findings from the 2026-09-12 velox run
home's velox health check hit another route to the unreachable exit 0 this
spec is about: topgrade exited 1 only because its containers step tried to
=docker pull= locally built images (=cj/telega-server=, =telega-server-glycin=;
ratio has the same shape with its own local images) and got "pull access
denied", so the stamp was applied by hand with =maint stamp topgrade=. Two
fixes: add =containers= to the =--disable= list in the guarded-upgrade's
topgrade invocation, or list the local images under =[containers]
ignored_containers= in topgrade.toml. I lean to disabling the step: pulling
newer images underneath running containers isn't an upgrade path I use, and
an enumerated ignore list rots as images come and go.
Also for the =kernel-modules-check= gate: reading the initramfs needs root.
The images are 0600, so an unprivileged =lsinitcpio= exits 1 with "Unable
to read file" on stderr and nothing on stdout; piped into =grep -c=, that
empty stdout reads as 0 and looks like a missing module. The gate has to
run as root and check the exit status, not just the count.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-dated to 09-23; the "four open decisions" list above is stale
The spec is still DRAFT with =Decisions [7/7]=: every decision is made,
including the four listed above. What's left is the isolated spec-review,
DRAFT → READY, and spec-response into build tasks. velox has
=linux-lts 6.18.51 → 6.18.52= pending today, which is the kernel-hold case this
spec exists for, so no plain topgrade on velox until the hold is built or the
kernel update runs as its own session.
*** 2026-10-05 Mon @ 06:08:00 -0500 Spec-review ran: Not ready, 13 blocking findings
Before the review I named the script =upgrade-guarded= and added =containers= to
its topgrade =--disable= list. The review recorded 48 findings in the spec's
=Review findings= section (13 blocking, 27 should-fix, 8 optional); the spec stays
DRAFT until spec-response dispositions them. The blockers cluster in four places:
the existing stamp writers (wrapper and doctor.py) would mark a deferred run
fresh; the split run's =--ignore= needs a dependency closure; =kernel-modules-check=
as written fails forever on ratio's headerless =linux-lts-strix=; and the boot run
has no defined form (package source without network, timeout that can kill pacman
mid-commit, =informant read= run as the user). Also: the installed topgrade
rejects a comma-joined =--disable= list, so each step is its own argument.
The 2026-10-04 velox health check ran a plain topgrade that moved the kernel
6.18.51 → 6.18.55, against the note above. I checked the gate by hand before the
reboot (=dkms status= installed for 6.18.55, =zfs.ko= in both initramfs images
read with sudo) and the reboot was clean, but the hold this spec builds would
have kept the kernel out of that run.
Next: spec-response on the findings, then DRAFT → READY and decomposition into
build tasks here.
*** 2026-10-05 Mon @ 12:30:00 -0500 Spec READY; build tasks below
The spec went DRAFT → READY at 12:10 after three review rounds. Its
Implementation phases section is the contract every task below points at,
and it moves to DOING with these tasks (the first task confirms that). Nine
non-blocking residual findings stayed open, and the first task closes them
before any code. The four-decisions list in the body above is history.
The order follows the spec's commit groups: archsetup Phase 1, rollout step
(a) on both daily drivers, dotfiles Phase 2 and step (b), archsetup Phase 3
and step (c) (velox only after the VM gate), the Phase 4 README, then the
flip. While Phase 2 sits unpushed, I push nothing from that dotfiles
checkout, because any push carries it.
On velox nothing runs plain topgrade, =yay -Syu= or =pacman -Syu=: not a
shell, not the =sysupgrade= alias, and not the panel's UPDATE or TOPGRADE,
which run =yay -Syu= and topgrade until step (b) lands Phase 2 there. Before
step (a), the kernel and zfs-dkms move only in a session I start on purpose,
with the gate checked by hand as on 2026-10-04. From step (a) until step
(b), I update velox only with =upgrade-guarded= from a terminal.
*** TODO Close the nine residual spec findings :chore:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable: the spec, with all nine "round 3 residual" findings under
Review findings applied and completed in place, the way the other findings
were: =DONE=, a =CLOSED:= line, and a =Disposition:= line (accepted, or
modified with the reason). Repo: archsetup, the spec file only.
First the status heading. spec-response flips it READY → DOING when it files
these tasks. If it still reads READY when I start, I flip it before anything
else, in three lines: the keyword DOING, a newest-first history line ("READY
→ DOING. Decomposed into build tasks under the archsetup todo.org parent,
SPEC_ID 81cdfd72-db96-43d3-aa03-779878c99f3e."), and the Metadata Status
cell set to doing.
The line numbers inside the findings predate the round-3 consolidation, so I
locate each edit by its quoted OLD text, never by line number:
- Decision 5 Consequences: the snapshot-hold clause ("the hold moves only
when a later session holds a newer one").
- AC13's everyday claims: P1-steps' step-7 bullet gains the release of the
previous held_snapshot, and Testing maps AC13 to P1-steps too.
- Finish step 1: when several steps fail, the first one's failed_step and
detail win.
- Recovery's power-cut bullet: register the version after =->=, and nothing
when the line is a removed line or there is none.
- Exit codes and interrupts: narrow the bare-=wait= qualifier.
- ZFSBootMenu: roll back with MOD+R (never ENTER, MOD+X or MOD+C) in
Recovery, D-zbm and its variant.
- Design and Decision 5: REBOOT hides from the panel's next probe, and the
fire-time refusal covers the window before it.
- The D-zbm variant: =ssh -t=, the headers' upgrading line as the reset cue,
and a repeat on a fresh VM when the reset missed extraction.
- P1-steps' fake-sudo bullet and Readiness: =yay -Sua= is what never runs,
since =yay -Pwq= already ran.
The snapshot-hold and REBOOT findings both rewrite Decision 5's
Consequences, so I apply those two together. I also reword the three places
that still say ZFSBootMenu boots the held snapshot (Design's ZFS fallback
paragraph, the note after AC13, and Risks' last-resort bullet) to a MOD+R
rollback, so no section points a reader at ENTER.
This task goes first because several findings change test bullets (P1-steps)
and the manual procedures (Recovery, D-zbm) that later tasks build against.
Verified by re-reading. The findings quote their own OLD text, so a
whole-file search always matches; I check outside Review findings only. For
each replaced OLD string this prints 0, and for each inserted or replacing
NEW sentence (the first-failure rule in Finish is an insertion, so its anchor
stays) it prints 1:
: sed '/^\* Review findings/,/^\* Implementation phases/d' SPEC | grep -cF 'TEXT'
Each residual reads DONE with a CLOSED line and a disposition. The Review
findings heading cookie reads 138 of 138, updated by hand or with =emacs
--batch= and =org-update-statistics-cookies=, because an edit outside Emacs
doesn't recompute it. The status heading reads DOING. It's a doc edit, so
there's no suite to run.
Contract: Review findings, the nine "round 3 residual" entries.
*** TODO Thresholds mirror, check 9 and hold-aware prune :feature:installer:zfs:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): Phase 1's three standalone changes, one commit
each, each green on =make test-unit=:
- =configs/maintenance-thresholds.toml='s =[updates] guard_patterns=
rewritten to exactly the hook heredoc's 15 Target patterns, with the
comment reworded; the TOML pin test in =tests/installer-steps/=.
- =scripts/post-rebuild-check= check 9 (the =10-= hook name wherever the
guard is installed) behind =PRC_GUARD_BIN= and =PRC_PACMAN_HOOKS_DIR=, with
the header, usage and counters moved to 9 and every test env setting
=PRC_GUARD_BIN= empty.
- =scripts/zfs-pre-snapshot='s prune skips rows with userrefs above 0, and
=tests/zfs-pre-snapshot/fake-zfs= gains hold, release and userrefs, which
the =--complete= tests reuse.
Tests: P1-installer's TOML pin, post-rebuild-check and zfs-pre-snapshot
cases.
None of these is live on a machine until step (a) installs or re-copies it,
so they land first. Check 9 flags a legacy hook name if post-rebuild-check
runs before step (a); that's the check working.
Verified by =make test-unit= green before and after, plus a mutation spot
check: hand-edit one TOML pattern and the pin test goes red.
Contract: Phase 1, Other Phase 1 changes; Phase 1 tests, P1-installer.
*** TODO kernel-modules-check gate :feature:zfs:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): =scripts/kernel-modules-check=, the stateless
reader that says whether each named kernel would boot with its modules:
usage and exit codes, the per-pkgbase items, the =--since= initramfs and
snapshot items, the =sudo -n lsinitcpio= read on a ZFS root, and the seams
=KMC_MODULES_DIR=, =KMC_BOOT_DIR= and =KMC_DKMS=. It doesn't depend on
=upgrade-guarded=, so it lands first.
Tests: P1-gate in =tests/kernel-modules-check/=, with fakes for dkms,
findmnt, =zfs list= and an lsinitcpio that fails unless run through the
fake sudo.
Verified by =make test-unit= green, plus a read-only real run from the repo
on ratio against =linux-lts=, expecting exit 0 (btrfs root, so no sudo
read). I don't point it at the running =linux-lts-strix=: its zfs is only
'added', and the gate never checks it. The first real =sudo -n lsinitcpio=
read is step (a)'s structural run on velox.
Contract: Phase 1, The gate; Phase 1 tests, P1-gate.
*** TODO upgrade-guarded foundation: modes, preconditions, sets, record :feature:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): =scripts/upgrade-guarded= up to, but not
including, the closure and the transaction steps:
- mode and option parsing (exit 2, no record);
- the preconditions in order: usage, EUID 0, the flock held on fd 9,
=db.lck=, and the =10-= hook's Target lines;
- the sets: pending (with =pacman -Qu='s exit-1-empty rule and its
=[ignored]= rows dropped), GPU patterns, kernel set, DKMS set and live;
- the record (maint's cache envelope, tmp file then rename) and Finish's
result, failed_step and detail rules, with the reason as the last stderr
line;
- the exit codes, and the INT/TERM/HUP trap that waits on the child's PID;
- =--dry-run= on its private =CHECKUPDATES_DB= with =checkupdates --nocolor=;
- the harness: every seam, the fakes on PATH, and subprocess-driven
unittest suites.
Nothing calls the script until Phase 2, so a partial script is harmless.
Tests: P1-sets' cases that need neither the closure nor a transaction (the
three-kernel fixture with the foreign headerless kernel, firmware and
api-headers, and the unowned vmlinuz, minus its gate.pkgbases clause; the
empty DKMS set; the =-Qu= exit handling; the fail-closed hook), plus a
supporting case that the pending set drops =[ignored]= rows; and P1-steps'
usage, EUID 0, lock, =db.lck= and =--dry-run= cases (the probe =--dbpath=
clause lands with the closure). P1-sets' post-transaction deferred-set case
waits for the everyday-run task, and the gate.pkgbases clause for the
=--complete= task.
Verified by =make test-unit= green, plus a real =--dry-run= from the repo on
ratio. It refuses with exit 3 naming the missing =10-= hook, because ratio
keeps the legacy name until step (a). With =UPGRADE_GUARDED_HOOK= pointed at
the legacy file, it prints ratio's real sets, and =linux-lts-strix= isn't
pending.
Contract: Phase 1, Identity and privilege; Modes and options;
Preconditions; Sets; The record; Finish; Exit codes and interrupts; Seams
and fakes.
*** TODO upgrade-guarded dependency closure :feature:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): the closure loop, meaning the read-only =-Sup=
print-mode probe, the everyday stage-A seed, the live-only stage-B seed,
the =--complete= GPU seed, the two line forms, every refusal rule with its
detail, the iteration bound, the kinds each stage assigns, and the held set
built from them. =--dry-run= then prints the GPU closure, the predicted
deferred set and the would-be =-Su= argv. I keep this apart from the
transaction steps because it's the riskiest logic in Phase 1, and it gets
its own verification pass.
Tests: P1-closure, except its single =-Sy= case and the "=-Su= runs" clause
of its new-dependency case, which need the everyday transaction (next
task); P1-sets' held-set cases (the full =--ignore= list with each entry its
own element, the everyday half of the live and not-live case, the
=zfs-dkms=/=zfs-utils= pair held together, a pkgrel-only bump not held),
asserted on =--dry-run='s printed =-Su= argv until the next task re-points
them at the fake pacman's real call; P1-steps' =--dry-run= probe =--dbpath=
clause. The orphan case's "no =-Su= runs" clause is vacuous until the next
task adds the transaction, and bites from then on.
Verified by =make test-unit= green, plus the real =--dry-run= on ratio
(hook seam as before), expecting a closure without a refusal. A refusal
naming a real orphan is itself a finding I clear by hand before step (a).
Contract: Phase 1, Dependency closure; Sets (Kinds, Held set, GPU closure).
*** TODO upgrade-guarded everyday run, arm procedure and stamp :feature:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): the everyday sequence, steps 1 to 10, except step
7's kernel-side detection, which lands with the snapshot-hold task:
- =yay -Pwq= news capture and the record's merge, dedup and cap;
- the informant clear under =timeout 60 sudo -n=;
- one =sudo -n pacman -Sy=, then =-Su= with the =--ignore= list and no =-y=;
- =yay -Sua= on every run, and the sweep argv by absolute path;
- step 9's flag cases, with the arm procedure (resolve check, =-Sw=, the
=install= and =mv=) and the unit-enabled seam;
- Finish's stamp step through =UPGRADE_GUARDED_MAINT=, the everyday stamp
predicate, and the first-failure rule the first task adds to Finish;
- the canonical fixture
=tests/upgrade-guarded/fixtures/upgrade_deferred.json= from a fixed fake
run.
Tests: P1-steps (the =sudo -n= refusal, news, informant, the exit-124
abort, the sweep argv and its real-binary =--version= check, the failing
yay, the INT cases); P1-closure's single =-Sy= case and the "=-Su= runs"
clause of its new-dependency case; P1-sets' post-transaction deferred-set
case (a fake post-transaction =-Qu= intersected with the held set, excluding
an =[ignored]= row), with its held-set cases re-pointed at the real =-Su=
call; P1-record-stamp's everyday cases and the fixture equality;
P1-arm-boot's two everyday flag cases; and one case for the first-failure
rule: a failing fake yay and a failing fake sweep leave failed_step aur and
yay's detail.
Verified by =make test-unit= green on ratio, where =/usr/bin/topgrade=
exists, so the real-binary check runs instead of skipping. The first run
against real pacman is M-split-live at step (a).
Contract: Phase 1, Everyday sequence; The arm flag; Stamp predicate;
Finish; The record (Fixtures).
*** TODO upgrade-guarded --complete and its gate entry :feature:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): the =--complete= sequence, steps 1 to 7, built and
tested on fixtures that aren't a ZFS root (the fake findmnt reports another
filesystem, so no snapshot or hold runs):
- refresh then flag removal, 'nothing to complete', the pre-stage-1 record
and gate entry, stage 1 (with the reinstall targets when a gate is
already open), the gate in its structural or =--since= form, the GPU step
(stage 2, arm, or the unit-not-enabled line), and the tty-only reboot
prompt after Finish;
- the =--complete= stamp predicate and the Invariants.
Nothing calls or installs the script until Phase 1 is done, so a
=--complete= without its hold is harmless; the next task adds the hold.
Tests: P1-complete except its three snapshot-hold bullets; the
gate.pkgbases clause of P1-sets' three-kernel case; P1-record-stamp's
=--complete= cases; P1-arm-boot's =--complete= arm, unit-not-enabled and
prompt cases; the =--complete= half of P1-sets' live and not-live case; the
=--complete= side of P1-steps' news and informant cases.
Verified by =make test-unit= green.
Contract: Phase 1, =--complete= sequence; Invariants; Stamp predicate;
Exit codes and interrupts (the prompt).
*** TODO Snapshot hold and the everyday AUR kernel-side check :feature:maint:zfs:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup), after the =--complete= task:
- =--complete= step 3's own snapshot and hold on a ZFS root, the post-gate
=--since= target rule, and the release of the previous hold;
- everyday step 7's kernel-side detection after yay: the gate entry, the
pre-yay snapshot hold, the flag removal and the immediate record write;
- the trap's matching step-7 path.
Tests: P1-complete's three snapshot-hold bullets; P1-steps' AUR kernel-side
case, with its INT cases and the release-of-the-previous-hold clause the
first task adds. Its "a following =--complete= runs the =--since= form"
clause needs the previous task, which is why this one comes after it.
Verified by =make test-unit= green. The first real snapshot and hold is
M-boot-armed's =--complete= in the VM, which opens the entry on a ZFS root.
Contract: Phase 1, The snapshot hold; Everyday sequence (step 7); Exit
codes and interrupts; Invariants.
*** TODO upgrade-guarded --apply-armed boot form :feature:maint:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): =--apply-armed=, steps 1 to 8. That means reading
the list the unit moved off the flag path (refusing a missing, empty or
unparseable list, or a live Hyprland), the interrupted pre-write, the tty1
banner, the journal mirror through a SIGINT-ignoring process substitution,
the informant clear, the =vercmp= drop of entries already at or above
their version, the offline =-Sp= check that refuses kernel-side members and
stale versions, the =-S --needed= transaction from the cache, and Finish
with its stamp predicate.
Tests: P1-arm-boot's =--apply-armed= cases, including the =setsid=
process-group SIGINT case (no SIGPIPE death, the fake lock released, the
record interrupted); P1-steps' clause that =--apply-armed= never calls
=yay -Pwq=, and its =--apply-armed= informant and exit-124 cases;
P1-record-stamp's =--apply-armed= success case.
Verified by =make test-unit= green. The real boot path is the VM gate.
Contract: Phase 1, =--apply-armed= sequence; Exit codes and interrupts;
Stamp predicate.
*** TODO Script install step and health-check workflow edit :feature:installer:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup), two commits that close Phase 1's group:
1. The =hyprland()= step that installs =hypr-live-update-guard= also installs
=upgrade-guarded= and =kernel-modules-check= to =/usr/local/bin=, with
P1-installer's source-inspection test.
2. A doc commit after the code: =docs/workflows/system-health-check.org=
Phase 3, per the contract's seven bullets. Step 6 runs =upgrade-guarded=
where step (a) is done. Kernel and DKMS days go through =--complete=.
Until step (c), the GPU set finishes from a console. Where step (a)
isn't done and a kernel or DKMS package is pending, plain topgrade
doesn't run. The strix addendum is keyed to each invocation. The script
stamps, so there's no hand stamp. The 2026-09-12 containers entry gets a
resolution note.
After this, Phase 1 is complete and step (a) can start.
Verified by =make test-unit= green, and the workflow edit re-read against
the contract's bullets.
Contract: Phase 1, Files and install; The system-health-check edit; Phase
1 tests, P1-installer.
*** TODO Rollout step (a) on ratio :chore:ratio:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
After Phase 1's last commit, by hand on ratio, in the contract's order:
- pull archsetup, then =sudo install -m 755= both scripts to
=/usr/local/bin/=;
- write =/etc/pacman.d/hooks/10-hypr-live-update-guard.hook= from the
installer heredoc, confirm its Targets, remove the legacy unprefixed hook
and check =ls /etc/pacman.d/hooks/=;
- diff, then re-copy =configs/maintenance-thresholds.toml= to
=~/.config/archsetup/=;
- confirm =pacman -Qmq= lists no DKMS-set package;
- confirm =upgrade-guarded --dry-run= exits 0;
- run M-split-live on ratio (Manual testing and validation).
Ratio's root is btrfs, so the =zfs-pre-snapshot= install and the structural
gate run belong to velox's step (a).
Verified by M-split-live on ratio passing, and post-rebuild-check's check 9
clean here (the =10-= hook present, the legacy name gone). If I build Phase
2 on ratio, its dotfiles checkout is the stowed, live one, so each Phase 2
commit is live on ratio as it's written; that's why Phase 2 starts only
after step (a) on the build machine.
Contract: Phase 4, Rollout (a) and M-split-live.
*** TODO Rollout step (a) on velox :chore:velox:zfs:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
After Phase 1's last commit, by hand on velox: the same list as ratio's
step (a), plus the ZFS-root items:
- =sudo install -m 755 scripts/zfs-pre-snapshot /usr/local/bin/= for the
hold-aware prune;
- =kernel-modules-check "$(cat /usr/lib/modules/$(uname -r)/pkgbase)"= exits
0, which proves the =sudo -n lsinitcpio= read against the real image;
- velox's guard hook was hand-placed under the legacy name, so the =10-=
rewrite and the legacy removal matter here too;
- run M-split-live on velox.
On velox nothing runs plain topgrade, =yay -Syu= or =pacman -Syu=: not a
shell, not the =sysupgrade= alias, and not the panel's UPDATE or TOPGRADE,
which run =yay -Syu= and topgrade until step (b) lands Phase 2 there. Before
this task, the kernel and zfs-dkms move only in a session I start on
purpose, with the gate checked by hand: =dkms status= installed for the new
kernel, and =zfs.ko= in each initramfs, read with sudo. From here until
step (b), I update velox only with =upgrade-guarded= from a terminal, and
the health-check workflow's step 6 runs it.
Phase 2 can be built on ratio while this waits, but I don't push any Phase 2
commit until this is done, because any dotfiles pull on velox would bring it
in.
Verified by M-split-live on velox, the structural gate run exiting 0, and
post-rebuild-check's check 9 clean here (the =10-= hook present, the legacy
name gone).
Contract: Phase 4, Rollout (a) and M-split-live.
*** TODO maint deferred row, APPLY and gate-aware REBOOT :feature:maint:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (dotfiles), one commit, because the row and APPLY land together:
- the =upgrade_deferred= probe and the helper it shares with
=topgrade_freshness= (N, never raises, None on a missing or malformed
record), the severity order, the armed text from the flag, and the
evidence (rows, detail, held_snapshot, and news lines not in
=upgrade_news_dismissed=, which stays empty until DISMISS lands);
- =apply_deferred= (tier confirm, kind terminal), its =LEVER_KEYS= builder,
=topgrade_freshness='s =deferred= evidence, =doctor.review='s routing,
=panel.fire_press= launching =foot --hold -e upgrade-guarded --complete=
detached, and =maint fix apply_deferred= on inherited stdio;
- Freshness and REBOOT: the wall note, REBOOT hidden while a gate is open
and shown while armed, =iter_fix='s fire-time refusal, and =review=
omitting =reboot= while a gate is open;
- the byte-identical fixture copy at
=tests/maint/fixtures/upgrade_deferred.json=, with its test module's
docstring naming the archsetup source.
I land this before the lever repoint, so gate-aware REBOOT already exists
the first time a lever runs =upgrade-guarded=, whose AUR step can open a
gate. Until the repoint the row reflects only shell runs, and with no
record it reads OK '0 deferred'.
Repo: =~/.dotfiles=, a separate project. Confirm the crossing before
starting, or run it from a dotfiles session. This starts after step (a) on
the machine I build Phase 2 on (=uname -n=): its dotfiles checkout is stowed
and live, so each commit reaches that machine as it's written. Baseline:
=make test= in =~/.dotfiles=; the red screen-lock suite on ratio is the
known failure, tracked in "screen-lock test suite red on ratio".
Tests: P2-row, P2-apply, and P2-text's wall-note case. No test imports
gui.py.
Verified by the =tests/maint= suites and =make test= green apart from that
known red; =cmp= on the two fixture copies; and =make test-panel-maint=, the
AT-SPI smoke over the fixture boards. =make test= never runs it, and it's
the only check that drives gui.py, which this commit edits. Ratio has no
weston or sway, so the smoke uses the live compositor and opens a panel
window; I run it on a spare workspace, and a SKIP is not a pass. The real
APPLY press is M-boot-armed on ratio and velox.
Contract: Phase 2, The deferred row; APPLY; Freshness and REBOOT; Phase 1,
The record (Fixtures).
*** TODO maint levers repointed to upgrade-guarded :feature:maint:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (dotfiles), the one commit the spec ties together:
- UPDATE and TOPGRADE argv and the missing-binary message;
- doctor.py's =topgrade_run= stamp deleted;
- the guard UX removed: the =live_update= tag, =iter_fix='s refusal and
guard event, =--force= and its sentinel wrap, =_update_force=,
=_rearm_after_guard=, the guard branches in =_on_fired=, cli.py and
panel.py, and the review suffix; =panel.guarded= becomes a set test, and
the tripped arm line becomes the annotation;
- the =sysupgrade= conditional in both common alias files, with minimal's
files left as symlinks;
- the stale text: the =topgrade_freshness= docstring and absent-stamp
error, the doctor.py and guard.py docstrings, the README =maint fix=
usage line, and the wrapper's header and its test docstring;
- =tests/maint/panel_smoke.py='s "arm line shows the exact update argv"
check, which pins =yay -Syu --noconfirm=, now matching
=upgrade-guarded --no-topgrade=. The spec doesn't list the file, but the
repoint breaks it.
The runner's long-remedy timeout (new session, SIGINT to the group, 120 s,
then SIGKILL, 'timed out — interrupted') can land as its own commit just
before this one. UPDATE and TOPGRADE are already user-kind and marked long,
so it applies to today's =yay -Syu= and topgrade argv the moment it lands.
That's safe: today a timeout SIGKILLs only the direct child
(=subprocess.run='s timeout), and a SIGINT to the whole group stops pacman
at a package boundary instead.
Repo: =~/.dotfiles=, a separate project. Confirm the crossing before
starting, or run it from a dotfiles session.
Tests: P2-levers, P2-guard-ux (including the rewritten
test_panel_levers.py and test_panel_phase10.py cases), and P2-text's
alias, absent-stamp and usage-line cases.
Verified by the =tests/maint= suites and =make test= green apart from the
known screen-lock red, and =make test-panel-maint= passing on a spare
workspace (a SKIP is not a pass). After it, the first UPDATE press on the
build machine runs the same command M-split-live already ran there.
Contract: Phase 2, Levers; Guard UX; Alias and README (the alias).
*** TODO maint DISMISS, held-package CVE/QUEUE/strip, README :feature:maint:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (dotfiles), three commits after the repoint:
1. DISMISS: the =dismiss= key kind, dispatched explicitly in =_digest_key=,
arm-then-fire, and the GTK-free panel.py helper that overwrites
=upgrade_news_dismissed= with the record's whole news list. Tests:
P2-dismiss.
2. CVE, QUEUE and the strip: deferred names out of =cve_queued=, the CVE
caption, UPDATE's binding and REVIEW & FIX; =[HELD]= tags in QUEUE; and
the strip's 'P pending · N held'. Tests: P2-cve-queue.
3. The 'The live-update guard' section of =maint/README.md= rewritten for
the new levers, the sweep argv, the deferred row, APPLY, DISMISS and the
armed state, with no press-again or =--force=. The guard banner and the
wrapper paragraph stay.
Then I push Phase 2, and only once step (a) is done on velox. Until then I
push nothing from this dotfiles checkout, because any push carries it.
Repo: =~/.dotfiles=, a separate project. Confirm the crossing before
starting, or run it from a dotfiles session.
Verified by the =tests/maint= suites and =make test= green apart from the
known red, =make test-panel-maint= passing on a spare workspace (the strip's
pending caption is in its path; a SKIP is not a pass), and the README
section re-read against the contract.
Contract: Phase 2, DISMISS; CVE, QUEUE and the strip; Alias and README.
*** TODO Rollout step (b) on the other daily driver :chore:dotfiles:velox:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Pull the Phase 2 commits into the other daily driver's dotfiles, only after
step (a) is done there. The build machine already has them through its live
checkout, so this step is for the other one: velox when I build Phase 2 on
ratio. If I build it on velox, this is ratio's step (b) and the topic tag
moves to =:ratio:=.
Verified by that machine's dotfiles HEAD matching the remote, the
=tests/maint= suites green there, and =maint status= there listing the
=upgrade_deferred= row.
Contract: Phase 4, Rollout (b); the Phase 2 intro.
*** TODO archsetup-boot-upgrade.service installer step :feature:installer:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (archsetup): an installer step that writes the unit from a
heredoc with =ARCHSETUP_USERNAME= substituted by sed, runs
=install -d -m 0755 /var/lib/archsetup=, and enables the unit without
=--now=, documented in its own comments. The spec names no function. It
can sit beside the guard install in =hyprland()=, or in a top-level
function called from there, and the source-inspection idiom reads either.
Tests: P3-unit in =tests/installer-steps/=: every directive the contract
lists, plus the absence of Requires=, BindsTo=, RequiredBy=, network-online
and Restart=.
Verified by =make test-unit= green. The boot behavior itself is the VM gate.
Contract: Phase 3, Installer step; The unit; Disarm and failure; Phase 3
tests, P3-unit.
*** TODO Rollout step (c) on ratio :chore:ratio:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
After Phase 3's commit, by hand on ratio: write the unit from the installer
heredoc with the username substituted, then run
=sudo install -d -m 0755 /var/lib/archsetup=, =sudo systemctl daemon-reload=
and =sudo systemctl enable archsetup-boot-upgrade.service=. The VM gate
holds back only velox, so ratio can go first.
Then, on the first day =upgrade-guarded --dry-run= lists a GPU-kind
package, run M-boot-armed on ratio and M-boot-ratio on the same armed boot.
Run the strix check on the first =--complete= that lands linux or
linux-lts. All three are under Manual testing and validation.
Verified by those three manual tests.
Contract: Phase 4, Rollout (c) and the ratio-path confirmation.
*** TODO VM gate for the boot unit :test:installer:zfs:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Run the five VM-gate tests in the =make test-keep FS_PROFILE=zfs= VM,
following Phase 3's VM procedure, in this order: M-split-console,
M-boot-armed, then M-boot-midtx on the same VM, then M-boot-timeout and
M-boot-fail. M-boot-midtx needs the VM M-boot-armed leaves, because it
times its cut from that boot's journal, so nothing reinstalls between them.
Each test is a child of Manual testing and validation.
=make test-keep= bundles archsetup from HEAD, so Phase 1 and Phase 3 must be
committed first. The VM clones the published dotfiles, so this runs after
the Phase 2 push; M-boot-timeout and M-boot-fail then read maint's
failed-units and deferred rows inside the VM, which AC6 asks for. I never
use =debug-vm.sh= here: it restores the clean-install snapshot and erases
the install under test.
M-boot-timeout and M-boot-fail use small inline stand-in fakes in place of
the Phase 1 fake sudo and pacman: they pass every query to the real pacman
and sleep or fail only on the transaction, so the VM tests don't depend on
how the unit-test fakes are laid out.
The unit reaches velox only after all five pass.
Contract: Phase 3, VM procedure and VM gate; Testing / Verification /
Rollout.
*** TODO Rollout step (c) on velox :chore:velox:zfs:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Only after the VM gate passes: the same four steps as ratio's step (c).
Then, on the first day =upgrade-guarded --dry-run= lists a GPU-kind
package, run M-boot-armed on velox and M-boot-news.
Verified by those two manual tests.
Contract: Phase 4, Rollout (c).
*** TODO maint README flow and recovery steps :chore:maint:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
Deliverable (dotfiles), one commit to the 'The live-update guard' section
of =maint/README.md=, after Phase 3. It adds the flow line, the
=kernel-modules-check <pkgbase>= diagnosis note for a CRIT row (only
=--complete= clears the gate), and the recovery steps as their only copy.
The steps come from the spec's Recovery as the first task amends it: the
MOD+R rollback and the version-after-the-arrow rule.
Repo: =~/.dotfiles=, a separate project. Confirm the crossing before
starting, or run it from a dotfiles session.
The non-gating D-zbm drill and its power-cut variant exercise these steps
(Manual testing and validation), and nothing waits on them.
Verified by =make test= green apart from the known screen-lock red, and the
section re-read against the contract.
Contract: Phase 4, README flow and recovery; Recovery; Recovery drill.
*** TODO Flip the spec to IMPLEMENTED (+ dated history line + Metadata mirror) :chore:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
This waits until every task above is done and the gating manual tests have
passed: M-split-live on both machines, the five VM-gate tests,
M-boot-armed on ratio and velox, M-boot-news, M-boot-ratio and the strix
check. Then tick each acceptance criterion against the evidence that
Testing / Verification / Rollout maps to it. AC1 stays unticked until a live
run with a GPU-kind entry pending has shown the hook silent. Last, the spec
edits: the status keyword DOING → IMPLEMENTED, a dated history line that
says why, and the Metadata Status mirror set to implemented. D-zbm is
non-gating and doesn't hold this.
Contract: the spec's status heading and Metadata table.
** TODO [#D] Stale-kernel age in maint :feature:maint:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-10-05
:END:
The guarded-upgrade build holds the kernel and DKMS sets on every everyday
run, so kernel security fixes wait for an =upgrade-guarded --complete= I
start on purpose. The deferred row is the only reminder, and it shows a
kernel most days, so it stops reading as a nag. This is the spec's vNext: a
maint age for how long the kernel set has been held, graded so an overdue
dedicated session shows up.
The design is still open. The record has no held-since field, so the age
needs either a new field (both fixture copies change in one rollout) or a
derivation from pacman.log. maint's CVE probe doesn't cover it: as of
2026-10-05 the kernel advisories in its cache carry no fixed version, so it
never flags a held kernel.
I kept this out of v1 on purpose. It replaces the "vNext kernel-reboot
item" in the build parent's original body.
Spec: [[file:docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org][guarded-upgrade spec]], Scope tiers and Risks (Standing kernel
deferral).
** TODO [#C] post-rebuild-check: probe that Emacs frames come up Wayland-native :feature:emacs:velox:solo:quick:
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
:LAST_REVIEWED: 2026-08-25
:END:
On 2026-08-24, the first Emacs 31.1 start on velox opened its first frame on
XWayland (=:0=) with the pgtk "unsupported under X" dialog, while every later
frame went to =wayland-1=. The cause was in the Emacs config (a startup buffer
sweep killed =*Warnings*= while 31.1's warnings.el held it for a deferred
display, so the first =make-frame= failed and emacsclient fell back to
=$DISPLAY=); fixed in =.emacs.d= the same night. The trap generalizes: any
first-frame error on a PGTK daemon silently lands the session on X, and nothing
in the post-rebuild pass would notice.
Design, under the script's fail-closed contract (a probe that cannot run
reports a finding, never a pass):
- Bound every =emacsclient= call with =timeout=, as the =systemctl= calls are.
A daemon stuck in a prompt is a finding, not a hang.
- No daemon (=emacsclient= cannot connect): a visible finding, "not checked:
no Emacs daemon, start Emacs and re-run". Emacs is started on demand here,
so this is the common state right after a rebuild, and the line is the point.
- Daemon up but no GUI frame yet: never skip, and never call
=pgtk-backend-display-class= with no frame (it errors with "Frames are not
in use"). Request one invisible frame through a waiting client in the
background, =timeout 20 emacsclient -c -F '((visibility . nil) (name .
"prc-probe"))'=, so the probe walks the same first-frame path that failed on
2026-08-24. Then wait, bounded (poll for a frame named =prc-probe= for up to
the same 20 s), before inspecting: the =-e= must not run before the =-c=
has connected. Zero pgtk frames after the request is a finding in its own
right, because a frame request that produced nothing is the first-frame
failure this check hunts. Delete the probe frame after reading.
- Inspect every pgtk frame, not the selected display, and only pgtk frames:
a tty client frame (=emacsclient -t= in tmux) carries =$DISPLAY= as its
display parameter and its terminal is not a display, so it would both trip
the =:0= rule and make =pgtk-backend-display-class= error.
#+begin_src sh
emacsclient -e '(mapcar (lambda (f) (list (frame-parameter f (quote display)) (pgtk-backend-display-class (frame-terminal f)))) (seq-filter (lambda (f) (eq (framep f) (quote pgtk))) (frame-list)))'
#+end_src
Expected: at least one entry, every display equal to =$WAYLAND_DISPLAY=,
every class =GdkWaylandDisplay=. An empty list, a =:0= entry, or a
=GdkX11Display= is a finding. A build without =pgtk-backend-display-class=
is a finding too: the installer installs =emacs-wayland=.
- Limit, stated in the check's output: it sees live frames only. A first X
frame that was already closed is invisible, so this reports the machine's
current state, not its history. The invisible probe frame is created and
never mapped, so it exercises =make-frame= (where 2026-08-24 failed), not
the window-show path.
Tests alongside the other checks, one per state: no daemon, no frame (probe
frame requested and waited for), probe frame never appears, Wayland-only, a
=:0= frame present, a tty client frame present alongside Wayland frames, hung
daemon, an =*ERROR*= reply from =emacsclient -e= (exit 1, a finding), non-pgtk
build.
** TODO [#B] Timeline spine test picks the wrong "next" event off Denver :bug:dotfiles:test:
:PROPERTIES:
:CREATED: [2026-08-24 Mon]
:LAST_REVIEWED: 2026-08-24
:END:
=make test= in dotfiles is red before any of this session's work. Two failures,
both in =settings/faces/timeline-face-spine.test.mjs=: "event bars never leave
the plot" and "exactly one event is marked as next, and it is the soonest ahead".
NOT the bug =c96a216= fixed. Every =spineRows= call in that file is pinned to
=JUL=, and =scene()= and =EVENTS()= both default to it, so the fixture side is
already clean and the file's own guard test passes.
TWO THINGS TO SETTLE, and they may be one bug or two:
1. =timeline-face-spine.js:466= — =const next = timedOnly(events).find((e) => e.s
>= refMs)= takes the first array element starting at or after now, which is
the *soonest* only if =events= is sorted by start time. The test's failure
message is exactly that it is not: a bar ahead of the spine starts at x=1651.2
while the one marked =event-next= sits at x=2132.8. Either sort before the
find, or use a min-by rather than a find.
2. Why it is red *here* and presumably green on ratio. The most recent commit to
=timeline-face-spine.js= is =ffe43ab feat(settings): draw home where the
machine is, not where its zone is=. This machine is =America/Denver= (Craig
travelling); the tests pass =home("New Orleans")= explicitly. If a
machine-resolved home overrides the explicit argument, the geometry drifts
and the test is machine-dependent — which makes it useless as a gate, since it
would only ever fail on the machine nobody runs it on. Confirm by running the
faces suite with =TZ=America/Chicago= and again with =TZ=America/Denver=.
If item 2 confirms, the design question is whether machine-resolved home belongs
in the pure geometry layer at all, or whether the host should resolve it and pass
it in — which is what the test already assumes.
Not blocking the Lua port: =make test-faces= is disjoint from the hypr config and
the three suites that work touches.
** TODO [#B] Qt apps render oversized on velox :bug:velox:solo:
:PROPERTIES:
:CREATED: [2026-08-19 Wed]
:LAST_REVIEWED: 2026-08-19
:END:
From the roam inbox, Craig's words: "qt apps look huge on velox. how do we make
it look better on this particular machine, and not change ratio. it seems they
should have different QT configs."
The shape is per-machine Qt scaling. velox is a high-DPI Framework panel and
ratio drives ordinary-DPI monitors, so one global Qt scale factor cannot suit
both. The fix has to be host-scoped rather than a value written into the shared
config, which is the same tier split the dotfiles already use.
Grading: Minor severity (apps work, they are just the wrong size) x every user
every time (every Qt app launch on velox) = P2 = [#B].
*** 2026-08-19 Wed @ 15:05:00 -0700 Root cause found and fixed; needs a logout to take effect
velox's =conf.d/local.conf= scaled the panel twice. The monitor line sets
=1.566667= and the same file exported =QT_SCALE_FACTOR,1.5= and =GDK_SCALE,1.5=,
and Qt 6 on Wayland already takes its scale from the compositor, so the two
multiplied. Measured rather than reasoned: with the override Qt reports a
960x640 logical screen, without it 1440x960, and 2256/1.566667 is exactly 1440.
That is 1.5x too large, which matches "huge" precisely.
Those env lines were not careless. The comment above them explains they existed
to compensate for =xwayland:force_zero_scaling = true= in the shared
hyprland.conf, which makes XWayland clients render unscaled and tiny. The
approach was what failed: an env var reaches every app, so fixing XWayland broke
every native Wayland client. Removing the vars alone would have traded "Qt huge"
for "Zoom tiny", so velox now turns =force_zero_scaling= off for itself instead.
XWayland scales through the compositor there, coming out correctly sized and
slightly soft. ratio is untouched and needs nothing, its monitor being scale 1.
=force_zero_scaling= took effect on =hyprctl reload=. The env removal will not:
Hyprland applies =env== lines with setenv at parse time and never unsets them,
so the running compositor still hands 1.5 to everything it spawns. Craig has to
log out and back in.
*** VERIFY Is CALIBRE_OVERRIDE_DPI still needed after the scaling fix?
The same file pins =CALIBRE_OVERRIDE_DPI,96= with the comment "calibre renders
oversized at the 1.57 compositor scale". Calibre is a Qt app, so that was almost
certainly this same double-scaling seen through one application and worked
around per-app rather than at the root. With the multiplier gone, the pin is
probably redundant and may now render calibre too small.
Left in place rather than removed on a guess, since it was validated at 96 on
2026-06-27 and calibre has its own DPI handling. Worth opening calibre after the
next login and deciding by eye.
The cursor entry in the same file records this identical failure a third time:
"Pre-scaling it (the old 36 = 24 x 1.5) double-applied on top of the
compositor's scale." Three instances of one mistake in one file, two previously
fixed in isolation without anyone naming the pattern.
** TODO [#C] Waybar panels launch expanded instead of collapsed :bug:dotfiles:waybar:
:PROPERTIES:
:CREATED: [2026-08-19 Wed]
:LAST_REVIEWED: 2026-08-19
:END:
From the roam inbox, Craig's words: "waybar panels should start up collapsed.
currently both the left and the right waybar panels launch expanded."
Panel source is =~/.dotfiles=. Its heading in the roam inbox read "archsetup."
with a period rather than a colon, so the routing prefix did not match cleanly;
claimed on the plain reading of the text.
Grading: Cosmetic severity (presentation only, nothing is lost) x every user
every time (every session start) = P3 = [#C].
** VERIFY [#C] The visible analog clock avoids being dragged :velox:
:PROPERTIES:
:CREATED: [2026-08-19 Wed]
:LAST_REVIEWED: 2026-08-19
:END:
From the roam inbox, captured verbatim: "the visible analog clock avoids being
dragged. ask me about this."
Filed as a VERIFY because the capture asks for a conversation rather than
describing a defect. What is the clock avoiding being dragged by, and is the
avoidance the bug or the intended behaviour?
** TODO [#C] A failed hostname lookup takes seven seconds :bug:
:PROPERTIES:
:CREATED: [2026-08-19 Wed]
:LAST_REVIEWED: 2026-08-19
:END:
=getent hosts fake-vm= takes about 7.2 seconds to return not-found on velox.
Measured repeatedly with the cache flushed between runs. Anything that looks up
a name that does not exist pays it: an ssh typo, shell completion, a script
probing for a host.
Not caused by the DNSSEC change. A/B measured today, cache flushed each time:
7691ms and 7232ms on =allow-downgrade= against 6804ms and 7482ms on =yes=, so
the setting makes no difference and this predates it. The likely shape is the
tailnet search domain (=search tailf3bb8c.ts.net=) being tried first, then the
two DoT upstreams, each with its own timeout, before NXDOMAIN comes back.
Found because it blew a 20-second timeout in
=tests.net-scenarios.test_run_net_scenarios=, which shells out to ssh a
deliberately-bogus =root@fake-vm=. That suite passes on its own and the failure
did not recur, so the timeout needed this latency plus the DNS disruption from
the clock testing running alongside it. Worth knowing that the suite sits close
enough to the edge for a slow resolver to tip it.
Grading: Minor severity (nothing behaves wrong, it just waits) x some users
sometimes (every failed lookup, which is occasional rather than constant) = P3 =
[#C].
** TODO [#B] Signal tray icon invisible under waybar (Electron 43 well-known-name SNI) :bug:waybar:velox:
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
:LAST_REVIEWED: 2026-08-25
:END:
Since signal-desktop 8.24.0 (Electron 43.4.0) Signal's tray item registers
under a well-known bus name (=org.freedesktop.StatusNotifierItem-<pid>-1=)
and answers Properties.Get/GetAll only when addressed by that name. waybar's
GDBus proxy addresses the owning unique name instead, reads back no Id or
Category, and logs "Invalid Status Notifier Item", so the icon never shows.
With =--start-in-tray= that leaves Signal running with no window and no icon;
launching it again from fuzzel raises the existing window. Slack (older
Electron, unique-name registration) is unaffected. Measured 2026-08-25 with a
bus monitor: the same connection returns the value for the well-known name
and "error occurred in Get" for its own unique name.
Grading: Major severity (the app is unreachable from the desktop while
"running") × every user every time on velox = P1 by the matrix, held at
[#B] because the workaround (relaunch to raise the window) is cheap and the
fix is upstream.
Upstream: [[https://github.com/Alexays/Waybar/issues/5240][Waybar #5240]] (open, proposes a raw-call fallback in item.cpp
proxyReady) and [[https://github.com/signalapp/Signal-Desktop/issues/7992][Signal-Desktop #7992]] (open, "Upstream Change Needed").
Downgrading to 8.23.0 is closed off: 8.24.x migrated the SQLCipher schema to
1770 and 8.23.0 quits with DBVersionFromFutureError (tried and reverted
2026-08-25).
Re-test after a waybar or signal-desktop upgrade, from the repo root:
#+begin_src sh :results output
grep -c 'Invalid Status Notifier Item' "$(\ls -t ~/.local/var/log/waybar-*.log | head -1)"
n=$(busctl --user list --no-legend | awk '$1 ~ /StatusNotifierItem-/ && $3=="signal-desktop"{print $1}')
busctl --user call "$n" /StatusNotifierItem org.freedesktop.DBus.Properties Get ss org.kde.StatusNotifierItem Id
busctl --user call "$(busctl --user call org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus GetNameOwner s "$n" | cut -d'"' -f2)" /StatusNotifierItem org.freedesktop.DBus.Properties Get ss org.kde.StatusNotifierItem Id
#+end_src
Expected when fixed: 0 "Invalid" lines in a fresh waybar log, or both Get
calls returning the Id (either side fixing it clears the icon).
Ratio is on 8.21.0 and unaffected until its next upgrade brings 8.24.x.
Alternatives if it drags on: change Signal's tray setting so it keeps a
window (=~/.config/Signal/ephemeral.json= =system-tray-setting=), or run a
waybar carrying the #5240 fallback.
** TODO [#B] Truenas session-host VM for long-running agent sessions :feature:tooling:
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
:LAST_REVIEWED: 2026-08-13
:END:
A small VM on truenas (TrueNAS SCALE KVM) as the home for long-running /
away-mode agent sessions. The case, per Craig 2026-08-13: truenas is the
only machine on ethernet, so network recovery after an outage is automatic
(wifi hosts may never reassociate unattended); it's UPS-backed through
blip-to-hours outages; it has the Comet KVM for out-of-band recovery; and
appliance uptime discipline means it doesn't reboot for workstation
reasons. Tonight's live demonstration of anchor-staleness risk (39 min
unlogged during a bare-metal recovery) is the motivating incident — the
session's durability equals the anchor's lag at interruption, so a host
that doesn't get interrupted is worth real money.
Costs to engineer around: a third environment to keep synced (repos,
rulesets, tailnet identity); credential provisioning — GATED on the
secrets-repo work (the [#A] secrets task above): the VM should be the
secrets bundle's second consumer after the personal ISO, not another
hand-copied key sprawl; a firm RAM carve-out so builds don't fight the
ZFS ARC; headless only — desktop-coupled sessions stay on ratio/velox.
Build deliberately AFTER the vacation, not before Sunday.
Companion idea (cheaper, complementary): put ratio on the UPS.
** TODO [#B] post-rebuild-check: route every probe through one guarded helper :refactor:solo:
:PROPERTIES:
:CREATED: [2026-08-17 Mon]
:LAST_REVIEWED: 2026-08-17
:END:
The script works and is well tested, but its shape keeps producing the same
bug. Across three review rounds the reviewer found FOUR separate instances of
"the probe failed and the check reported ok", each in a different place:
=systemctl= in check 1, the enablement read in check 2, =find= in check 3, and
=grep= in check 4. A fifth was latent in an unguarded staged write. Every one
was individually fixed, and I only stopped finding more because someone kept
looking.
That is a design problem rather than four bugs. The script has five
hand-written probes, and each one has to remember to branch on its own exit
status. Nothing enforces it, nothing fails a review that forgets it, and the
failure is invisible because the wrong behaviour is a clean "ok".
Shape: one helper every probe must go through, which cannot return a value
without an explicit success, so that "I could not read this" is
unrepresentable as "nothing to report". Roughly:
: probe "<what>" <command...> # sets a value on success, records a finding otherwise
Then each check consumes the helper's result rather than a raw command
substitution, and a new check written later inherits the discipline instead of
having to re-derive it. Worth pairing with a test that asserts no check can
report ok when its probe exits non-zero, generically, so the fifth instance is
caught by the suite rather than by a reviewer.
Not urgent: the current version is correct as far as anyone has found, ships
with 58 tests, and proved itself on a genuinely wedged machine. This is
prevention.
Grading: Minor severity (no known live defect, the risk is future) x
most-users-frequently (every future edit to this script) = P3 = [#C]... except
the failure mode is silent and the script's whole job is catching silent
failures, so a regression here is uniquely undetectable. P2 = [#B].
:solo: — the surface is one script and its suite, the refactor is
behaviour-preserving, and the existing 58 tests plus a mutation battery are
the objective check that it stayed so.
** TODO [#B] velox's systemd --user spins at 96% and cannot resolve unit files :bug:velox:
:PROPERTIES:
:CREATED: [2026-08-17 Mon]
:LAST_REVIEWED: 2026-08-17
:END:
Live on velox 2026-08-17 from about 10:29. =systemd --user= (pid 2235) sits
in state R at 96% CPU, measured over a 3-second sample rather than taken from
the lifetime average. It stopped logging at 10:29, so its timers appear to
have stopped firing too.
The split is the diagnostic: =systemctl --user list-units= still returns
instantly, while =is-enabled=, =cat=, =show=, and =list-unit-files= all hang
indefinitely. So the manager answers from its in-memory unit list and wedges
on anything that has to resolve unit files. It is spinning in userspace, not
blocked on I/O (=/proc/2235/wchan= is 0, no syscall pending).
Remedies tried, neither worked: =systemctl --user daemon-reexec= hangs like
every other unit-file call, and the signal form (=kill -59=, SIGRTMIN+25)
was accepted but changed nothing. The next step is a logout/login or reboot,
which is Craig's call because it closes his running session. I deliberately
did not kill the manager: that would tear down the graphical session and
everything under it.
Suspected cause is the powerprofilesctl crash loop filed above, whose
repeated activation attempts against a masked unit are the only new load on
this machine. I cannot prove it, and I have to name the other candidate
honestly: my own =post-rebuild-check= runs called =systemctl --user
is-enabled= roughly thirty times per run over several runs, and the wedge
appeared during that window. The crash loop predates those runs by an hour
and a half, which is why it is the leading suspect rather than the certain
one.
What it costs: unit-file operations are unavailable, user timers appear
stopped, and a core is pinned on a laptop running on battery.
Grading: Major severity (a pinned core and stopped user timers, invisible
unless you look) x rare edge case (one machine, specific conditions) = P2 =
[#B]... except that this is a live, ongoing drain on a travelling machine
rather than a latent defect, so it takes [#A] until the machine is back to
normal. Re-grade to [#B] once resolved and the question is only prevention.
*** 2026-08-17 Mon @ 19:57:42 -0700 The reboot cleared it; re-graded [#A] to [#B] as the task instructed
velox rebooted at 16:04. The wedge is gone: =systemctl --user is-enabled
roam-sync.timer= now answers =enabled= in well under a second, where every
unit-file call hung indefinitely before, and =list-timers= shows
calendar-sync, roam-sync and agenda-render-cache all firing on schedule
again. So the remedy the task named — a logout or reboot — was taken and
worked.
Nothing here was diagnosed further, which means the cause is still unproven
and both candidates in the body stand. What is left is prevention, and the
task's own grading says that is [#B]: the live-drain argument was the only
thing holding it at [#A], and the drain has stopped. Re-graded per that
instruction rather than by a fresh judgment.
Reproducing it deliberately is the open question, and it is not obviously
worth doing — it costs a wedged session to learn something the crash-loop fix
may make moot.
** TODO [#B] post-rebuild-check needs a reference-host mode :feature:velox:solo:
:PROPERTIES:
:CREATED: [2026-08-17 Mon]
:LAST_REVIEWED: 2026-08-17
:END:
=scripts/post-rebuild-check= ships and works, but its first live run on velox
2026-08-17 showed the output is mostly steady state rather than drift. Of the
8 findings that survived three rounds of false-positive removal, comparing
against ratio says exactly ONE is real: =obsbot-wb-guard= is enabled on ratio
and merely linked on velox, which is the deliberate deferral recorded
2026-08-16. The other three unit findings (=emacs=, =geoclue-agent=,
=obs-record-watchdog.timer=) are linked on ratio too, and the three =.claude=
absences are absent on ratio too.
So the signal-to-noise is about 1:7, and the thing that separates them is a
comparison against the other daily driver — the same discipline that kept the
2026-08-16 session honest when check 4 read as nine projects missing
=CLAUDE.md= and ratio turned out to be missing the identical files.
Shape: =--reference-host <host>= runs the same five checks on the far machine
over tailscale (ssh, read-only) and reports only the *differences*. Findings
present on both machines are steady state and get summarized as a count rather
than listed. Falls back to the current standalone behavior when the reference
host is unreachable, and says so.
Grading: Minor severity (the tool works and its findings are accurate; they
are just buried) x every use = P3 = [#C]... except that a check nobody reads
is a check that isn't run, which is the failure mode the whole task existed to
close. Most-users-frequently x Major = P2 = [#B].
:solo: — the checks exist, the ssh path is proven (the 2026-08-17 session ran
exactly this comparison by hand), and correctness is verifiable locally by
diffing the two reports.
*** 2026-08-21 Fri @ 07:10:00 -0700 The premise moved: velox now reports 1 finding, not 8
Re-scope before building. The 1:7 ratio this task argues from is gone, and two
of the three things it cites as noise are fixed at the source rather than
filtered.
=87ff0b7= gave check 2 a machine-local expected-disabled list, so the four unit
findings are declared intent rather than noise, and an entry whose unit turns
out to be enabled is itself reported so the list cannot rot. =3fbf3e0= dropped
=.claude= from check 4's expected set, since the gitignore sweep writes that
line into every project whether or not one exists. velox went 8 findings to 1.
So the open question is no longer "how do we cut the noise" but whether a live
reference-host diff still earns its place against a static declaration of
intent. They are different tools: the list is offline, explicit, and states
what a machine means; the diff is automatic and catches drift nobody declared.
The reference-host comparison is still what *found* all of this, twice, by
hand. That is an argument for it and not against.
Worth knowing this task already contained the whole 8-to-1 analysis when it was
filed 2026-08-17, and a session on 2026-08-20 re-derived it from scratch without
reading it. Not :solo: any more — the design call above is Craig's.
** TODO [#C] screen-lock test suite red on ratio :bug:test:dotfiles:
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
:LAST_REVIEWED: 2026-08-13
:END:
tests.screen-lock.test_screen_lock fails 21 of 23 (+1 error) on ratio,
verified pre-existing with unrelated changes stashed (2026-08-13). Breaks
the make test green bar for every dotfiles commit until triaged. Suspect
environmental (the suite exercises hyprlock/relaunch behavior that may
need session state this shell lacks) or a regression from a recent
screen-lock commit — diagnose, then fix or mark/skip with a reason.
Grading: Major severity (blinds the pre-commit gate for the whole repo) ×
every-commit frequency on this machine, but test-infra only, no user
impact = [#C] judgment call rather than matrix-dictated.
** TODO [#C] Keyboard backlight binding + boot default :feature:dotfiles:velox:
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
:LAST_REVIEWED: 2026-08-13
:END:
Velox's kbd backlight (chromeos::kbd_backlight since the AMD board) boots
at 0 and the dotfiles carry no keyboard-brightness keybinding at all (swept
2026-08-13 — never existed). Add: Hyprland binds (XF86KbdBrightness* or a
chord) driving brightnessctl -d "chromeos::kbd_backlight", a sane boot
default, and a udev rule granting the video/input group write access so it
works without sudo (a bare ssh session got EPERM). Check whether Fn+Space
(EC-handled on Frameworks) already cycles it — if so the bind is a
complement, not the only path. Ratio: n/a (desktop).
** TODO [#B] Post-rebuild verification pass :feature:velox:solo:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-09-17
:END:
A rebuilt machine looks finished and isn't. Five gaps surfaced on velox
within two days of the 2026-08-13 reinstall, and three of them LOOKED
fine: a stowed unit file, an enabled timer, a present git clone. From the
.emacs.d handoffs 2026-08-14 (inbox, both PROCESSED) plus what this
session found independently.
The generalizable fix is one pass the installer runs at the end, or a
=post-rebuild-check= script the checklist points at. Each item is cheap
and turns a silent no-op into a visible line:
1. =systemctl --user list-units --state=failed= — calendar-sync had been
failing every 15 minutes for two days with nobody watching.
2. Every stowed/linked user unit that is NOT enabled. roam-sync and
signal-receive came back linked and inert; two others were never
linked at all. A unit file being present is not the same as running.
3. Every tracked =*.local.el.example= (or =*.local.*=) with no sibling
real file. Three exist in .emacs.d; all three were gone on velox.
4. Every gitignore-mode project missing its =.ai/=, =.claude/=,
=CLAUDE.md=, =todo.org=, =inbox/=. A reinstall drops the entire
working state of every such project — 374 files and 4.5 MB in
.emacs.d's case — and nothing carries it: not git, not stow, not the
bootstrap.
5. =signal-cli listAccounts= non-empty. velox lost its registration, and
because agent-text relays to a hardcoded velox, that breaks the phone
channel for the WHOLE FLEET, not just this machine.
6. =mbsync --list= parses. The Proton Bridge TLS cert
(=~/.config/protonbridge.pem=, referenced by =~/.mbsyncrc=) is generated
per *installation*, so it cannot be restored or copied between machines.
Its absence aborts the config parse, which kills *every* account — gmail
and dmail need no bridge and died anyway. The error names only the missing
pem, so "no mail at all" and "this one file is missing" look unrelated.
Re-derive it off the running bridge's own handshake, no GUI, no secrets:
=openssl s_client -connect 127.0.0.1:1143 -starttls imap -showcerts </dev/null | sed -n '/BEGIN CERTIFICATE/,/END CERTIFICATE/p' > ~/.config/protonbridge.pem=
7. The bridge password (=~/.config/.cmailpass=) is per-install too. It is a
real file rather than a stow symlink, so it survived the rebuild holding
the *previous* install's value — worse than absent, because it looks
right. Diagnostic trap: the bridge answers a wrong password with =no such
user=, which reads as "no account signed in" and sends you hunting a login
problem that doesn't exist. Never treat =no such user= as evidence about
account state.
*The distinction that organizes all seven* (from the .emacs.d handoff
2026-08-14, inbox): every artifact that broke was generated on the machine by
an application rather than carried by git, stow, or dotfiles. But they split
two ways, and conflating them is what produces a file that exists, looks
right, and authenticates against nothing:
- *Restore* — the old value is still correct: gitignored tooling (1),
roam clone state (3), =*.local.el= configs (5).
- *Re-derive* — the old value is worthless because the application minted a
new one: signal-cli registration (4), bridge cert (6), bridge password (7).
So the checklist wants two columns, not one.
Originally graded [#A] because item 5 was live and silently disabling paging, with
a flight that Sunday forcing the date. Both inputs have expired: the flight was
2026-08-17 and item 5 no longer gates anything time-boxed, so the 2026-09-17
review dropped this to [#B] and unscheduled it. Kept here because the reason the
grade was ever [#A] is worth knowing; it is not the current read.
*** 2026-09-13 Sun @ 07:14:54 -0500 Moved the three gap reports into the reinstall working dir
The 2026-08-14 reports that define the five gaps moved out of inbox/ into
[[file:docs/design/2026-08-14-velox-reinstall-gaps-1.org][gaps 1-4]],
[[file:docs/design/2026-08-14-velox-reinstall-gaps-2.org][gap 5]] and
[[file:docs/design/2026-08-14-velox-reinstall-gaps-3.org][the two email-side gaps]] (the Bridge cert and the Bridge password).
They file with the rest of the reinstall artifacts when that task closes.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-graded [#A] → [#B]; gaps 6 and 7 are the remainder
=scripts/post-rebuild-check= ships and covers gaps 1-5, with two [#B]
follow-ups filed (the guarded probe helper and the reference-host mode). Both
reasons for [#A] are gone: gap 5 is a check now, and the flight has passed.
What the script still lacks is the two email-side gaps: =mbsync --list=
parsing (the per-install Bridge cert) and the Bridge password (a real IMAP
login against =127.0.0.1:1143=, never reading =no such user= as account
state). Add those two, then close.
** TODO [#B] Restoring a git repo from backup can resurrect a dangerous diff :bug:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-08-14
:END:
My 2026-08-14 restore of =~/org= from the salvage brought back roam's
=.git= deliberately ("simpler, preserves everything exactly"). It also
brought back a clone ten commits stale AND an uncommitted =inbox.org=
emptied to zero bytes. roam-sync is the repo's only committer and commits
whatever it finds, so enabling that timer would have committed the
emptying and pushed it — deleting the live inbox items ON RATIO. A
.emacs.d session caught it, verified ratio's copy was a strict superset,
discarded the local diff, fast-forwarded, and only then enabled the timer.
Lesson to encode somewhere durable: restoring a git repo from a backup is
not the safe option it looks like. For any repo with a live remote,
re-clone and carry only proven-needed work; where a backup copy is
restored anyway, reconcile it against the remote BEFORE any
auto-committing timer is enabled. The failure here would have been silent
and landed on a different machine.
** TODO [#B] Nothing installs the .emacs.d systemd user units :bug:velox:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-08-14
:END:
=~/.emacs.d/systemd/= ships four user units (agenda-render-cache
service+timer, calendar-sync service+timer). On ratio they are symlinked
into =~/.config/systemd/user/= by hand. Nothing does that on a fresh
machine: they are not stowed (they live in .emacs.d, not dotfiles) and
archsetup does not link them.
Consequence found on velox 2026-08-14: the world wallpaper face drew
nothing, because it reads =~/.cache/settings/agenda.json= and the timer
that exports it was never installed. Calendar sync was silently dead for
the same reason — which is the second time that particular timer has gone
missing (see the 2026-08-01 session, where its auto-start was the bug).
Linked and enabled by hand on velox; export verified (316 bytes, 1 event).
Fix belongs in whichever owns the seam: either .emacs.d gains an install
step for its own units, or archsetup links them alongside the dotfiles
stow. Prefer the former — the repo that ships a unit should install it.
Grading: Major severity (two background services silently absent, and the
failure looks like a data problem rather than a missing timer) x every
fresh install = P2 = [#B].
** TODO [#C] Panel can leave a channel selected with nothing to show :bug:dotfiles:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-08-14
:END:
velox's store carries channel "pair" with pair_sel unset, so
channels.selected_pair() returns None and wallpaper.apply() fails every
time. Found 2026-08-14 when the new session-start restore reported
"unavailable" and fell through to the waypaper fallback — the machine
still showed dark-lion, which looks exactly like the bug that was just
fixed. ratio is fine (channel world, pair_sel 0).
Two candidate fixes, needs a call: either the panel refuses to switch to a
channel whose selection is empty, or apply() falls back to the first
minted pair/set when the index is unset. The second is friendlier and
matches "the store is the source of truth" — a channel with exactly one
plausible reading should not be a dead end.
Grading: Minor severity (one fallback still puts a wallpaper up) x some
users sometimes = P3 = [#C].
** TODO [#B] archsetup doesn't clone rulesets :bug:velox:
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-09-17
:END:
A fresh install has claude but no =ai=, no skills, no rules, no hooks,
because =~/code/rulesets= is never cloned. Found on velox 2026-08-14 when
=ai= wasn't on PATH. archsetup clones dotemacs, dotfiles, the suckless
tools and itself, so rulesets is the one workstation repo it misses, and
without it the whole agent tooling layer is absent on a rebuilt machine.
Fix: clone it alongside the others (=RULESETS_REPO=, defaulting to
git@cjennings.net:rulesets.git) and run =make install= afterwards, which
is what links the 54 symlinks into ~/.claude and ~/.local/bin. Graded
Major severity (a rebuilt machine silently loses every agent workflow)
x most-users-frequently = P2 = [#B]. Worked around by hand on velox
already; this is the durable half.
** TODO [#B] Hibernate in the settings dial power actions :feature:dotfiles:
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
:LAST_REVIEWED: 2026-08-13
:END:
Add hibernate alongside suspend/lock in the settings module's dial power
actions. The wlogout exit menu already carries it (keybind h) and needs no
work; the dial is the remaining surface. Sequencing (Craig confirmed the
dial placement 2026-08-13):
1. DONE 2026-08-14 00:14 — hibernate proven end to end on velox, driven
from the exit menu so the wiring was exercised too. Evidence: the boot
id was unchanged across the cycle (f14152f9…) and uptime kept counting
3h15m → 3h18m, so it genuinely resumed rather than rebooting; the
journal carries "PM: hibernation: hibernation exit" and the
HibernateLocation EFI variable being cleared. Took 9.3s wall. The whole
chain works: suspend-to-disk into the LUKS-encrypted swap, resume via
the keyfile embedded in the initramfs, one passphrase at ZBM.
2. Then consider suspend-then-hibernate as the default lid behavior
(systemd sleep.conf HibernateDelaySec) — hibernate's savings with no
button at all; possibly a "deep sleep" toggle in the module.
3. Then the dial action itself.
Note: ratio has no swap partition, so hibernate stays velox-only until
ratio gets one; the dial entry should degrade gracefully where there's no
resume target.
** TODO [#A] Move secrets out of public dotfiles → private repo + combined personal ISO :feature:security:dotfiles:
:PROPERTIES:
:CREATED: [2026-08-11 Tue]
:LAST_REVIEWED: 2026-08-11
:END:
Structural fix for the root cause behind both 2026-08-09/10 leaks: secrets live
in the *public* dotfiles repo and rely on in-place encryption, which two commits
defeated. Design agreed with Craig and tabled before build 2026-08-11. Full
detail + the pickup point are kept LOCAL (they map the setup, so not in this
public repo):
=.ai/private-design/2026-08-11-secrets-repo-and-combined-iso.org=.
Shape in one line: move secrets to a private, off-public-scan-path repo, keep
them encrypted even there (defense in depth), and deliver them to a fresh
install as an encrypted bundle baked into a *personal* ISO — riding the combined
archangel+archsetup ISO that's already ~80% built. Two ISO modes: generic
(shareable) vs personal (encrypted secrets, private).
[#A] because it closes a live-leak class, but *gated on the rotation VERIFY* —
don't start the migration until the credentials are rotated. Not started.
Not :solo: — repo standup and history rewrite are Craig's calls; promote to a
real spec (spec-create) when work resumes.
*Also bake the push-capable repo URLs into the personal ISO* (decided
2026-08-19). =archsetup:240= and =:245= default =archsetup_repo= and
=dotfiles_repo= to =https://git.cjennings.net/...=, the anonymous read-only
endpoint. That default is right for a stranger installing archsetup — no key on
the server — and wrong for my machines, which have to push: velox came back
from its rebuild unable to push either repo, and I only found out at a 403 four
days later. I decided against detecting an ssh key in the installer, because
archsetup never restores =~/.ssh= (I do that by hand), so key-presence at clone
time depends on ordering the installer doesn't control, and a naive "any key
means ssh" would break a stranger who happens to have one. The override already
exists and is documented — =ARCHSETUP_REPO= / =DOTFILES_REPO= in
=archsetup.conf.example= — so the personal ISO just needs to carry the ssh
form of both, alongside the secrets bundle. The generic ISO keeps the https
default untouched.
The gap that leaves is a curl|bash or stock-ISO install, which takes the https
default straight back. =post-rebuild-check= check 8 covers that path — it flags
a working repo whose origin is the read-only endpoint — so the ISO value is the
fix and the check is the net under it.
** TODO [#B] Settings toggles reset silently at session start :bug:dotfiles:
:PROPERTIES:
:CREATED: [2026-07-28 Tue]
:LAST_REVIEWED: 2026-09-17
:END:
Craig, from the roam inbox 2026-07-28: "launching into wayland doesn't honor previous caffeine settings ...or I expect any other settings in the desktop settings module." Captured right after the 08:59 reboot.
Confirmed, and it generalizes past caffeine. The settings module splits cleanly into two halves, and only one of them persists.
Persisted, in =~/.config/desktop-settings/state.json= (=store.py= =DEFAULTS=): program slots, idle-tripper stages, wallpaper. These come back correctly.
Not persisted — every one is derived live from a process or a compositor runtime option, so a session restart resets it to whatever =hyprland.conf= establishes:
- Caffeine — =caffeine_state()= is =pgrep -x hypridle= inverted, and =hyprland.conf:73= runs =exec-once = pkill -x hypridle; hypridle=. So every launch unconditionally starts hypridle, which means caffeine is *always* OFF after login. There is no code path that could restore it ON.
- Auto-dim — =dim_state()= reads =hyprctl getoption decoration:dim_inactive=, a compositor runtime value that resets to the config default on restart.
- Night light — =state()= is =pgrep -x gammastep=; the process dies with the session.
- DND — =dunstctl=; dunst restarts fresh from =exec-once=.
- Power profile / brightness — owned by powerprofilesctl and systemd-backlight, outside this module's scope.
Verified live 10 minutes after the reboot: hypridle running (caffeine OFF), dim =false=, gammastep not running, dnd =false=, power =balanced=. Every toggle sat at its factory position.
The failure is silent, which is what makes it bite: nothing tells you the value you set was discarded. That is the mechanism behind the 2026-07-27 lockout, where Craig believed caffeine was on and the screen locked anyway.
Grading: Major severity (the panel's core promise is holding these values, and the reset is silent and total across all four toggles) x most users frequently (every session start resets them, though it only harms when a deliberate non-default was set) = P2 = [#B].
Not :solo: — the fix needs Craig's call on *which* toggles should persist and whether persistence is per-toggle opt-in. Restoring night light at 3pm or caffeine on a laptop are both plausibly wrong, so this is a preference question, not a derivable one. The mechanism itself (extend =store.py= with a =toggles= block, restore on session start) is mechanical once that's settled.
Related: =[#B] Caffeine state is unreadable on both surfaces= covers display accuracy — whether the surfaces report the truth. This covers whether the value survives at all. Distinct bugs, same subsystem.
*** Side finding — gammastep loses a startup race and nothing relaunches it
=hyprland.conf:75= runs =exec-once = gammastep=, but no gammastep process is alive. Today's three launch logs tell the story: =gammastep-2026-07-28-090034.log= carries "Wayland connection experienced a fatal error: -1 / Temperature adjustment failed", and the other two are empty.
Launched by hand afterward it runs fine and survives, so gammastep is not broken — it loses a race against compositor readiness at session start. Nothing relaunches it, so night light is simply off for the whole session, silently. (An earlier read of this said night light "has likely never worked from the config". That was wrong: the failure is a startup race, not a permanent break.)
Worth its own task — the fix is a readiness wait or a retry around that exec-once, not a persistence change. Filed here for now because it surfaced during this investigation.
** TODO [#C] Re-apply the active program at session start :refactor:dotfiles:hyprland:
:PROPERTIES:
:CREATED: [2026-07-30 Thu]
:LAST_REVIEWED: 2026-07-30
:END:
Follow-up to the direction-aware persistence fix (dotfiles, 2026-07-30). I took the narrow repair first and deferred this deliberately.
=settings restore= replays individual toggles from =store.DEFAULTS["toggles"]= — caffeine, DND, and now dim. But =active_program= is stored and read by =programs.py= and never re-applied as a unit. So the panel's claim that a program is active is only true to the extent that the individual toggle replays happen to reconstruct it.
Right now the gap is small: dim, DND and caffeine are the fields that don't survive on their own, and all three now persist individually. Night light is deliberately excluded (time-of-day dependent) and the power profile is persisted by powerprofilesctl itself. So the observable behavior is close to correct today.
What's wrong is the structure, not the current output. Two sources of truth describe the same intent — a program definition and a list of remembered toggles — and nothing keeps them in step. Add a field to a program and it silently won't be replayed; change what a program means and the replay still asserts the old fields. The drift is invisible until someone notices the desktop disagreeing with the panel, which is exactly how the dim bug surfaced.
The change: have restore resolve the active program and apply it through =panel.apply_toggle=, with the remembered-toggle list becoming a fallback for a session with no active program. The =ASSERT_OFF= direction rule still applies per toggle and must survive the move — caffeine's ON-only assertion is a safety property, not an optimization (velox, 2026-07-22).
Grading: Minor severity (no wrong behavior today; it's a latent drift that produces one on the next change to a program's fields) x some users sometimes (only bites when a program definition changes) = P3 = [#C].
:solo: — the design was settled when the fix was chosen, the surface is enumerable (=session.restore=, =programs.py=, =panel.apply_toggle=), and it is verifiable locally: the existing restore suite plus a test that a stored active program is reconstructed field by field after a simulated session start.
** TODO [#B] "SCREEN OFF" reads as a state, not a stage :bug:dotfiles:design:
:PROPERTIES:
:CREATED: [2026-07-29 Wed]
:LAST_REVIEWED: 2026-07-29
:END:
Craig, 2026-07-29: "The module said SCREEN OFF, which I took to mean the screensaver was off."
=panel.py:43= labels the DPMS stage =SCREEN OFF=. It names the *action that stage performs* (power the display off), but it reads as a *status report* (the screen feature is off). Craig read the second meaning, concluded the screensaver was disabled, and reported the lock bug on that basis. It cost a wrong turn in a live diagnosis.
Two compounding factors. Nothing in the panel or the bar uses the word "screensaver" at all, so the thing he calls the screensaver is labelled =WATCH= and is not findable by its own name. And the rail's other labels are unambiguous verbs or nouns (=DIM=, =LOCK=, =SUSPEND=), so =SCREEN OFF= is the only one that parses two ways.
Fix direction: rename so the label names the action consistently with its siblings (=BLANK=, or =DISPLAY OFF=, or =SLEEP SCREEN=), and give =WATCH= a name that connects to what it is (=SCREENSAVER=, or =WATCH FACE=). Worth a look at the whole rail's vocabulary at once rather than patching one label.
Grading: Major severity (the label doesn't just look wrong, it produces a confident false belief about whether a security-adjacent control is active, and that belief drove a wrong turn in a live investigation) x most users frequently (every reading of the rail) = P2 = [#B].
An earlier draft graded this Minor, arrived at [#C], and then wrote [#B] beside it anyway with a justification. That is overriding the letter, which the format rule specifically forbids because it turns the matrix into a formality. The input that was wrong is the severity band: a label that reliably misinforms about state is more than cosmetic. Corrected the band, and the letter now follows from it.
Not :solo: — naming is Craig's taste call, and the rail's vocabulary should be decided as a set.
** TODO [#B] Night watch and the lock watchdog fight each other :bug:hyprland:dotfiles:
:PROPERTIES:
:CREATED: [2026-07-29 Wed]
:LAST_REVIEWED: 2026-09-17
:END:
ROOT CAUSE of the lockdead screens, found 2026-07-29 00:50 within minutes of the relaunch logging going live. hyprlock is not crashing. It is being killed on purpose, by us.
=settings/src/settings/watch.py:116= runs =pkill -x hyprlock=. That is deliberate and documented in the module's own header: the night watch *is* the lock screen, a normal window cannot paint over hyprlock's ext-session-lock surface, so =start()= maps the kiosk behind the lock and then kills hyprlock to reveal it already drawn. =stop()= re-locks first, then kills the kiosk, so the desktop never flashes.
=screen-lock='s watchdog cannot tell that apart from a crash. Any non-zero exit means "died while still locked, relaunch it", and SIGTERM reports 143.
The collision, on hypridle's own timings:
- t=450s: =loginctl lock-session= → screen-lock → hyprlock up, watchdog waiting on it.
- t=480s: =settings-watch start= maps the kiosk and kills hyprlock. Exit 143.
*Two relaunches after the mitigation, unexplained (found 2026-08-04).* The watch stage was parked in =db5ac60= at 2026-07-29 05:59, and nine of the eleven entries in =screen-lock.log= fall before that (07-28 19:00 through 07-29 05:08, all rc=143, the collision as diagnosed). But two more landed on *2026-07-30 10:58:11 and 10:58:13* — rc=143 then rc=137, SIGTERM then SIGKILL, two seconds apart. Craig unlocked the keyring 30 seconds later.
So parking the stage did not stop every relaunch. Either something else kills hyprlock, or that pair was Craig at the keyboard. The log cannot distinguish those, and the journal for that window shows only an unrelated settings tick.
What would settle it: the watchdog does not record *who* sent the signal, only the exit code. Logging the killer (or at least distinguishing a session-initiated unlock from an external kill) would turn this from a guess into a reading. Worth doing before designing the handshake, since the handshake assumes the night watch is the only thing killing hyprlock and that assumption is now unproven.
- t=480s + =LOCK_RELAUNCH_DELAY= (0.5s): the watchdog relaunches hyprlock, which comes back *on top of* the night watch.
- In that half-second gap the session is locked with no client, so Hyprland draws lockdead. The replacement hyprlock then blurs it.
That is exactly what Craig saw: lockdead text blurred behind a working lock screen, clearing on authentication.
Two consequences, and the second is worse than the cosmetic one:
1. The lockdead artifact.
2. *The night watch is silently broken.* It reveals itself and is covered by a relaunched hyprlock half a second later, every time. The feature has not worked since the watchdog shipped.
Evidence: =~/.local/var/log/screen-lock.log= carries nine entries, every one =rc=143=: three on 2026-07-28 (19:00:00, 22:53:56, 23:17:58) and six on 2026-07-29 (02:59:54 through 05:08:01). Zero entries from any other cause.
The AMD theory is dead. =amdgpu.runpm=0= was live for all three, there is no DPMS idle rule, and no coredump exists because nothing crashed. The wf-recorder hypothesis is also unsupported: a deliberate lock at 00:50 with a recorder running produced no relaunch, because I unlocked at 12 seconds and never reached 480s.
Grading: Major severity (the lock client is killed mid-lock on every cycle, the night-watch feature never actually shows, and a sustained collision can exhaust the watchdog's 30-attempt cap and leave the session locked with no client, recoverable only from another console) x most users frequently (every idle period reaching the watch stage; nine occurrences logged in eleven hours) = P2 = [#B].
An earlier draft graded this [#A] under the security carve-out. That was wrong and the correction is worth keeping. The carve-out covers privacy or security leaks, compliance violations, and safety issues. Nothing leaks here: the session stays locked throughout, the lockdead frame clears on authentication, and the harm is availability plus a silently broken feature. Invoking the carve-out inflated a P2 two bands into the always-visible [#A] gate, which is the exact abuse the matrix exists to prevent.
Not :solo: — the fix is a design decision between two subsystems, both of which Craig owns:
1. Teach the watchdog that SIGTERM is not a crash (skip relaunch on 143). Simple, but it weakens the "a kill re-locks rather than unlocks" property the watchdog was written for.
2. Have =settings-watch= tell the watchdog to stand down before it kills, via the flag file it already maintains at =$XDG_RUNTIME_DIR/settings-watch-relock=. Keeps the kill-re-locks property intact and is the more honest handshake.
3. Stop killing hyprlock at all and find another way to reveal the kiosk.
Option 2 is the one I would argue for, but it is Craig's call.
Previous title and framing of this task, kept for the record: "hyprlock still exits mid-lock; the watchdog relaunch is silent". The instrumentation that closed that gap is dotfiles =5bbe2c3=, and it paid for itself in about six hours.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-graded [#A] → [#B] and dropped the 07-31 deadline
The grading paragraph above already concluded [#B]; the heading never followed
it. The watch stage was parked in =db5ac60= on 2026-07-29, so the collision is
mitigated and the choice between the three fixes has no date pressure.
** TODO [#B] The wireguard gpg convention is inert; the installer can't read it :bug:security:network:
:PROPERTIES:
:CREATED: [2026-07-28 Tue]
:LAST_REVIEWED: 2026-09-17
:END:
Found 2026-07-28 by an independent review, while deciding whether to commit a newly-encrypted =wolf.conf.gpg=.
=assets/wireguard-config/.gitignore:3= states: "Ship configs gpg-encrypted (*.conf.gpg) only; the installer decrypts at import." That is false. =scripts/import-wireguard-configs.sh:43= globs =$dir/*.conf= and nothing else, there is no =gpg= call anywhere in it, and line 67 hard-errors "no .conf files in $dir". A =.conf.gpg= in that directory is invisible to the importer.
So the post-leak convention has a hole in the middle. The =.gitignore= permits tracking =*.conf.gpg= and tells you the installer will handle it, which invites committing encrypted secrets into a repo that cgit still serves anonymously, in exchange for a capability that does not exist. The encryption is real; the payoff is not.
=README.org:12= disagrees with the =.gitignore= too: "Nothing here is tracked except this note and the .gitignore." Written 13 minutes after the =.gitignore= on 2026-07-20, so the stricter line is the later one. Three sources, two positions, no resolution.
Grading: Major severity (the stated policy actively invites putting live key material into a world-cloneable repo on a false premise, and a leak here is unrecallable once cloned) x some users sometimes (only fires when someone adds a config) = P3 = [#C]... except this is the security carve-out, graded on severity alone because one occurrence with the right consequences is a showstopper. = [#B].
Not :solo: — the fix is a decision, not a build. Three options, and they interact with the open cgit move:
1. Teach the importer to decrypt =*.conf.gpg= (needs a passphrase or agent at install time, which on a fresh-install path may not exist — that is the likely reason it was never written).
2. Drop the =!*.conf.gpg= exception and keep the directory genuinely empty of secrets, tracked or not, until archsetup is off the public host.
3. Do the cgit move first, then revisit.
Until it is resolved, do not commit any =*.conf.gpg=. The encrypted =wolf.conf.gpg= now lives at =~/.config/wireguard/wolf.conf.gpg= (mode 600), moved out of this repo entirely on 2026-07-29. Untracked-in-tree was the wrong resting place for it: a single =git add -A= puts a secret into a world-cloneable repo, and the directory's own policy is unusable until the importer can actually read encrypted configs. The live NetworkManager profile is unaffected.
Related: =[#B] Move archsetup off cgit= and =[#B] Audit cgit-published repos for secrets and privacy=. Both are still open, and both argue for keeping new secrets out of this repo until they land.
** TODO [#C] Timer presets should start in one click :feature:dotfiles:timer:
:PROPERTIES:
:CREATED: [2026-07-28 Tue]
:LAST_REVIEWED: 2026-09-17
:END:
Craig, captured 2026-07-28, routed here by home's inbox-zero pass from the shared roam inbox.
Verbatim: "timer/alarm: timer preset buttons should be one click. we will have to adjust so the user knows to name the timer first (have the label on top of the timer buttons), have the text field on the same row, and add below. if the user adds the time value in the text field and presses enter, that should also start the timer."
Two changes in one. A preset button should start its timer on a single click rather than needing the name step first, and the input area gets rearranged so the naming flow is obvious: label above the preset buttons, text field on the same row, add below. Enter in the text field starts the timer too.
Panel source is =~/.dotfiles/timer/src/timer/= (=gui.py= for the view, =panel.py= for the presenter, =viewmodel.py= for state).
Not :solo: — the rearrangement is described but not settled, and the result is a visual judgment Craig has to see. The one-click behaviour is buildable on its own; the layout wants a pass in front of him. Per the UI-prototyping rule, sketch the arrangement before touching production code.
Related: =[#C] Add a time selector to the timer panel= covers a duration picker for the same input area. Design them together when either is picked up.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-graded [#B] → [#C]
Untouched for seven weeks, and the layout wants a prototype pass in front of me
before any production code.
** TODO [#C] Post-upgrade hooks: compositor restart reminder + font cache rebuild :feature:infra:ratio:solo:
:PROPERTIES:
:CREATED: [2026-07-25 Sat]
:LAST_REVIEWED: 2026-09-17
:END:
Handoff from home (2026-07-25), originally combining the 2026-06-07 stale-compositor incident and 2026-06-08 fontconfig crash diagnosis. Add two reproducible pacman =PostTransaction= hooks through archsetup; do not make one-off =/etc= edits:
1. On =Upgrade= of =hyprland=, =aquamarine=, or =mesa=, print a non-blocking reminder to log out and back in. Never restart the running compositor from the hook. This prevents a session from quietly continuing on a deleted Hyprland binary against newly installed libraries (ratio did so for 2.5 days before SIGABRT on 2026-06-04).
2. On =Upgrade= of =fontconfig=, =freetype2=, or =harfbuzz=, run =/usr/bin/fc-cache -f= after the transaction. The fontconfig 2.17→2.18 cache-format change left stale cache-9 files that crashed Qt6 apps in =FcCharSetHasChar= until the system font cache was rebuilt.
Acceptance: hook files are source-controlled and installed by archsetup; package/operation/action fields are asserted from the generated hook text; the reminder is print-only and exits successfully; the font hook runs only after successful matching upgrades and invokes the absolute =fc-cache= path. Validate with the fast installer tests plus a disposable pacman-hook parser/install check when practical.
*** 2026-09-17 Thu @ 08:59:59 -0400 Lead, not diagnosed: ratio's Hyprland aborted on 08-28
ratio logged a Hyprland SIGABRT coredump at 2026-08-28 15:44, three days after
the 714-package upgrade of 08-25. Check whether it's the stale-compositor shape
from 06-04 before citing it as a second occurrence.
** VERIFY Should coredump entries group as one journal-digest row per binary? :maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-07-24
:END:
Noticed in the 2026-07-24 sentry bug-hunt, round 8, while verifying the =journal_errors= probe against the live journal. Not filed as a bug — it needs your call on what "the same error" means here.
The probe's counting is correct, and I checked it rather than assumed: it reports 1674 real + 16 noise = 1690, and =journalctl -p 3 -b -o json | wc -l= returns exactly 1690 entries this boot. (A =wc -l= on =-o cat= says 23037, but that splits multi-line messages like coredump stack traces across lines — the probe counts entries, which is right.)
What's off is the *grouping* for multi-line messages. Thirteen =systemd-coredump= entries on ratio right now — nine usbredirect, three telega-server, one python3 — land as four separate digest rows (counts 4, 3, 3, 3) instead of one row per binary. =_signature()= blanks hex addresses and long integer runs, which handles the pid, but two dumps of the same binary still differ in frame count and thread layout, so their signatures diverge.
Consequence is modest: the digest's top-N rows get eaten by near-duplicates, so genuinely distinct errors fall off the evidence list sooner. It doesn't affect the metric's value or severity.
The question is what you'd want: group coredumps by the binary named in the first line (a special case for =systemd-coredump=), signature only the *first line* of any multi-line message (a general rule, and arguably the right one — the first line is the error, the rest is context), or leave it alone. The middle option is the smallest general change and I'd lean that way, but it changes grouping for every multi-line error, so it's yours to call.
** TODO [#C] World face: hour-format picker in the settings panel :feature:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
Put a 12/24-hour toggle for the world face in the desktop-settings panel. The knob already exists — =settings/faces/world.html= reads =hour12= from =SETTINGS= with a =?hour12== URI override, defaulting to 12 — but the only way to change it is to hand-edit a launch URI, which is not a way to change anything.
Build: (1) a =world_hour12= field in the wallpaper state, default true; (2) =project.py build_uri("world")= appends =&hour12== read from state; (3) a 12/24 toggle in the world channel's config section (=gui.py _conf_projected=, currently just a preview); (4) TDD the URI-building and the state round-trip. Solo — buildable, agent-verifiable through the URI and state tests plus a headless render, and nothing left to decide.
Re-scoped 2026-08-02, down from [#B]. As written on 2026-07-23 this also wanted an orientation picker, because the face then supported vertical and horizontal through =?layout==. The 2026-07-31 rebuild replaced that with the single vertical spine and its horizontal day axis, and no =layout= flag survives in =world.html= — so the orientation half has nothing left to pick between and is dropped rather than deferred. One toggle is parking-lot work, not this cycle's, hence [#C].
Not =:quick:= despite being small: four pieces with tests is a sitting rather than a spare moment. I said "probably quick" when recommending the re-scope and that was optimistic — the piece count didn't drop, only the field count within each.
** TODO [#C] Wallpaper channel: timed transitions as an alternative to sunrise/sunset :feature:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
From the roam inbox (Craig, claimed 2026-07-23): the wallpaper channel switches on sunrise/sunset today (the sun-pair mode, =settings/src/settings/wallpaper.py=, location read live via whereami with a state.json cache). Add a timed-schedule mode as an alternative: fixed clock times drive the transitions rather than the solar calc.
Not :solo: — the capture itself flags the missing inputs ("we'll need to know the transition times, and how many of them there are"). The count and the times are a design decision Craig owes: is it a two-image day/night flip at fixed hours, an N-way ring across the day, per-image dwell vs shared interval? The =set= channel already does fixed-interval cycling through a set, so the new part is specifically clock-anchored transition points, not just "a timer". Ask for the schedule shape at pickup, then build against the existing wallpaper.apply presenter vocabulary.
** TODO [#C] Auto-dim status forgotten on layout change :bug:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
From the roam inbox (Craig, 2026-07-25). If auto-dim is toggled off and the layout then changes, auto-dim silently comes back on. A layout switch should not touch the auto-dim state. Likely related to the 2026-07-25 =layout-cycle= rebuild (floating ring) or a hook it fires — check whether the layout-change path resets the dim toggle, and where auto-dim state lives. Grade: minor severity (dim re-enables unexpectedly, no data loss) x every layout change made while dim is off = P3 = [#C].
** TODO [#C] Maint queue button status wall needs a copy button :feature:dotfiles:maint:quick:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
From the roam inbox (Craig, 2026-07-25). The maintenance queue button shows the status wall but has no copy button. Add one, following the global COPY key pattern already on the maint doctor wall (dotfiles =8bc79ba=). Grade: cosmetic/feature = [#C].
** TODO [#B] Panel family: unify the look across net/bt/maint/audio and desktop-settings :feature:design:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
From the roam inbox (Craig, claimed 2026-07-24): the network, bt, maint, and audio waybar panels look alike, but the desktop-settings panel looks quite different. He wants them to read as one family. Deliverable: enumerate every difference (chrome, header layout, typography, spacing, control styling, color roles, close-button placement, section dividers) between the two groups and a plan to converge them on one look. Not :solo: — it needs a design pass and Craig's taste calls on which direction each group moves. When picked up, catalogue the deltas from live captures of all five, propose the shared design language (likely the Dupre instrument-console the settings panel uses, since that's the newest and most deliberate), then bring Craig the change list before touching code.
** TODO [#C] Panel text cut off — needs a few px more space :bug:dotfiles:quick:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
From the roam inbox (Craig, claimed 2026-07-24): panel labels look cut off; a few more pixels of space fixes it. He named the audio and bt panels, but his "before" capture is the networking panel (=~/pictures/screenshots/2026-07-23_202419.png=; "after" resizing =~/pictures/screenshots/2026-07-23_202458.png=), so the whole panel family likely shares the tight spacing. Confirm which panels clip at pickup, then add the padding/width. Grade: cosmetic × every glance at the affected panels = P3 = [#C]. Solo — buildable (CSS/size tweak) and screenshot-verifiable, no design call once the clipping panels are identified.
** TODO [#C] Night-watch live telemetry :feature:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
Craig, 2026-07-21 ("mind. blown."): drive the Dupre Night Watch screensaver (docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html, the idle-pipeline eye-candy stage in the desktop-settings spec) with real system data instead of synthetic signals — a passive status wall while the machine idles. Candidate mappings: scope = CPU load trace, drift chart = memory pressure history, spectrum = per-core utilization, VU pair = net throughput up/down, blinkenlights = disk I/O, tape counter = uptime, systems lamps / annunciator = maint status verdicts, engine-order telegraph = current power profile, VFD wire = maint status one-liner (temps, battery, pending updates). Browser prototype can poll a small local JSON endpoint; the production shape belongs to the idle-stage build. Depends on the spec's idle-pipeline implementation landing first.
** TODO [#C] Wlogout screen review :bug:hyprland:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
Craig, 2026-07-21: the wlogout window (Super+Shift+Q — lock/reboot/shutdown/logout/suspend/hibernate) "isn't great and has bugs." Review it end to end: catalogue the specific bugs, then assess the design against the Dupre instrument-console family (it predates the panel aesthetic). Config lives in dotfiles; the bind is hyprland.conf:428 (=pgrep -x wlogout || wlogout-menu=). Context: the desktop-settings panel spec withdrew lock/suspend in favor of this screen (2026-07-21 amendment), so it's now the sole owner of session-exit actions — worth being good. Grade each bug found via the severity×frequency matrix; this parent stays a [#C] review until specifics emerge.
** TODO [#A] Audit cgit-published repos for secrets and privacy :bug:security:
SCHEDULED: <2026-09-24 Thu>
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
Grading: security carve-out — cgit at git.cjennings.net serves every repo under scan-path=/var/git over unauthenticated https (any repo is anonymously cloneable). Raised [#B] → [#A] on 2026-08-09: the scan found a real live-credential leak (below), so this is now a confirmed exposure with an open rotation blocking, not a hypothetical. Drops back to [#B] once rotation is done and the visibility rulings are made.
Not :solo: — needs Craig's decisions and the credential rotation. Steps: list repos under /var/git; for each, decide intended public vs private; scan each for secrets (done, below); for any meant-to-be-private repo, actually restrict access (cgit repo.hide only hides from the index — a known repo name is still cloneable; use http auth or move it off the public scan-path); for public repos, confirm no secrets and add a pre-receive/CI secret scan. archsetup's own move is decided and tracked separately below.
*** VERIFY [#A] Rotate the credentials exposed by the 2026-08-09 dotfiles leak
SCHEDULED: <2026-09-24 Thu>
A plaintext credential file was briefly public in the dotfiles repo and was
confirmed pulled by an external crawler before the purge, so every credential
in it must be rotated. Full list, forensic detail, and the remediation record
are kept LOCAL, not in this public repo:
=.ai/private-design/2026-08-09-cgit-secrets-audit.md=. A second, low-severity
unencrypted token was also exposed and needs a re-auth + purge (same doc).
Craig's action; the purge only stopped further copies. Desktop alarm set
(=at= job 61, 15:00) as the backup nudge.
*** 2026-08-09 Sun @ 12:05:00 -0500 Found + purged a public plaintext-credential leak
A file that looked encrypted by its name was plaintext in one commit; a
second, content-verifying triage caught it. Purged from history in both repos
(the shareable dotfiles and this one) and reconciled the local clones; a fresh
anonymous clone no longer serves it. Everything else the audit flagged was
benign. The forensics, exact mechanism, and remediation steps are in the local
doc above (not published, since they map the setup). Follow-ons: the rotation
VERIFY above, velox reconcile on return, the secrets-repo split (top of Open
Work), the wireguard =.gitignore= bug (line ~191), the cgit move (below), and a
pre-receive secret-scan hook so this can't recur.
*** 2026-08-17 Mon @ 19:57:42 -0700 Moot — the 08-13 wipe re-cloned velox from the rewritten remotes
This asked velox to reconcile clones that no longer exist. The machine was
wiped and reinstalled on 2026-08-13, so every repo on it was cloned fresh
*after* the purge and never held the pre-rewrite history at all. The runbook
anticipated this ("fresh clones automatically carry the post-purge rewritten
git history"); nobody closed the task once the reinstall took that route.
Verified rather than assumed: both repos are level with =origin/main= today —
archsetup at =6faa31c=, dotfiles at =65940f2=, both trees clean.
One thing the reinstall did leave, and it is filed separately: the installer
cloned both repos =--depth 1=, so the history was present-but-truncated until
today's =git fetch --unshallow= (see the shallow-clone =[#A]=). A reconcile
against the rewritten remote was still unnecessary — a shallow clone of the
right history is not a diverged clone of the wrong one.
** TODO [#B] Move archsetup off cgit to cjennings@cjennings.net :chore:security:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-17
:END:
Decided (Craig, 2026-07-20): move the archsetup repo off the public cgit host (git@cjennings.net, scan-path /var/git) to Craig's private account remote cjennings@cjennings.net, so it is no longer world-cloneable. This is the archsetup-specific fix for the cgit-exposure finding above.
Plan: create a bare repo under cjennings's control off the cgit scan-path (e.g. =~cjennings/git/archsetup.git=); push current main + tags there; migrate the post-receive hook that publishes the installer to =/var/www/cjennings/archsetup= so curl-install keeps working (the single published file stays public by design; only the repo goes private); update the origin remote on ratio and velox to =cjennings@cjennings.net:git/archsetup.git=; remove =/var/git/archsetup.git= so cgit no longer serves it. Verify: anonymous =git clone https://git.cjennings.net/archsetup.git= fails, the new private clone works from both machines, and the curl-install URL still returns the installer. Keep the two daily drivers' remotes in sync (daily-drivers rule).
*** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: unstarted, and the exposure is confirmed live
Ran the task's own verification step as it stands today, which is the honest
way to check an unstarted task rather than reading its body back. Anonymous
=git ls-remote https://git.cjennings.net/archsetup.git= succeeded with no
credentials and returned =6faa31c= — this afternoon's HEAD. So the repo is
still world-cloneable and current to the commit, not a stale published
snapshot.
=origin= on this machine is still =git@cjennings.net:archsetup.git=, the cgit
account, so nothing has moved. Everything in the plan stands unchanged.
*** 2026-08-21 Fri @ 14:12:46 -0700 Recorded the publication mechanism: placement is the only control
The work project verified its own repo reads "not served" against a control
repo that reads PUBLIC, and reported the mechanism back: the host publishes via
=GIT_HTTP_EXPORT_ALL= over =GIT_PROJECT_ROOT=/var/git=, so *publication is
directory placement and nothing else* — there is no per-repo marker, no
=git-daemon-export-ok= file, no opt-in flag to check. A repo is public because
of where it sits.
That is the durable hazard for this task's plan, and it cuts both ways. It
confirms the approach — a bare repo created outside the scan-path is private by
construction, which is exactly what the plan already specifies. It also means
nothing in a repo itself records whether it is exposed, so any future move
*into* =/var/git= publishes silently, with no local artifact to notice. Their
own repo is private for this reason alone: it lives under =/var/cjennings/git/=,
outside the served root.
Caveat they raised and I agree with: any enumeration of the served set is a
snapshot, not a standing fact. The set moved twice while three projects were
measuring it. Verify placement at the time of the move rather than trusting a
recorded list.
** TODO [#B] Velox boot-failure retrospective — upgrade guard gaps :bug:zfs:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
Post-mortem for the 2026-07-15 velox no-kernel boot failure, from the archsetup/maint code review:
- maint's UPDATE remedy runs a plain =yay -Syu --noconfirm= (remedies.py:297). The live-update guard (guard.py) only matches mesa/hyprland (the 2026-06-07 live-swap class) — it never checks /boot, kernel, initramfs, or mkinitcpio exit. No post-upgrade /boot assertion exists. An interrupted kernel transaction slips straight through.
- Add a post-upgrade /boot assertion: after a transaction touching linux/linux-*, confirm vmlinuz-* + initramfs-*.img present and mkinitcpio exit 0; refuse to end the run (or page Craig) otherwise. Would have caught this.
- Sanoid-vs-actual dataset drift: configure_zfs_snapshots configures zroot/var/log + zroot/var/lib/pacman as separate datasets; velox's actual layout has neither separate (/var/log sits inside zroot/var). Reconcile.
- CONFIRMED (2026-07-21): the pre-pacman snapshot hook fired on velox — the 2026-07-15 no-kernel boot was recovered via the pre-pacman ZFS snapshot rollback, and velox is back on the tailnet running linux-lts 6.18.38 with initramfs present (2026-07-19 session). Root-cause hook-ordering fix shipped separately. Still open: the post-upgrade /boot assertion in guard.py and the sanoid-vs-actual dataset drift reconcile (the two bullets above).
*** 2026-08-26 Wed @ 22:35:01 -0600 The /boot assertion now lives in the topgrade spec; the dataset drift is what remains here
The post-upgrade /boot assertion is covered by the kernel-modules-check gate in
[[file:docs/specs/2026-08-25-topgrade-guarded-upgrade-spec.org][the topgrade guarded-upgrade spec]]
(dkms built for the new kernel, initramfs newer than vmlinuz, pre-pacman
snapshot on a ZFS root), which ships with that spec's Phase 1 rather than here.
What this task still owns is the sanoid-vs-actual dataset drift: whether to
split zroot/var/log and zroot/var/lib/pacman out as configure_zfs_snapshots
assumes, or change the config to match the layout velox actually has. That is
a call I have not made, so the task stays [#B] and not solo.
*** 2026-09-13 Sun @ 07:14:54 -0500 Filed the original boot-failure handoff under docs/design
The 2026-07-15 diagnosis and recovery plan that opened this task sat in
inbox/ as a processed file; it now lives at
[[file:docs/design/2026-07-15-velox-boot-failure-handoff.org][docs/design/2026-07-15-velox-boot-failure-handoff.org]]
so the timeline survives the inbox sweep.
** TODO [#C] Assess a Hyprland left-drag window gesture :feature:hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
Evaluate whether a global left-click drag can move ordinary windows without
breaking application selection, text interaction, or Wayland security
expectations. Document the safe modifier/gesture alternatives before changing
any binding.
** TODO [#B] Reconcile panel keybindings around Super+N :feature:hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-21
:END:
Put every panel on one consistent chord family — net, bluetooth, audio, timer,
and the maintenance console — as a shared modifier set plus a mnemonic letter
per panel (N/B/A/T/M). Today they open by waybar click only, so a uniform
family is what makes them keyboard-reachable and predictable. The immediate
move is swapping the notification and networking bindings so the primary panels
sit one Super-plus-letter chord away.
Maintenance (M) is the chord I want first — it is the panel I keep reaching for
without one.
Constraints:
- Super+Shift+A is already the PTT toggle, and the hold-to-talk grave bind is
load-bearing. Audit every current hyprland bind for conflicts before
proposing a family, and treat these two as fixed.
- Both machines have to work the same way. Velox can't QMK-remap, so the chords
have to be typable on a plain laptop keyboard.
Steps: settle the modifier family, audit the existing binds for collisions,
wire it through the dotfiles hyprland config, and document it in the keybind
reference.
The family itself is the one call I haven't made — the audit and the wiring
follow from it, so that decision comes first rather than last.
*** 2026-08-21 Fri @ 14:15:22 -0700 Merged the duplicate keybinding-family task into this one
Two tasks were carrying one job: this one and =[#B] Consistent keybinding family
for the panel console=, filed separately and both stalled. This one had the
tighter framing and the more recent review; that one had the better body — the
specific collisions, the velox plain-keyboard constraint, and maintenance-M as
the priority chord. Folded its detail in here and cancelled it, since two
half-specified tasks for one decision is plausibly why neither moved.
Not =:solo:= and not =:quick:=: the modifier family is a preference call I have
to make, judging what's load-bearing among the existing binds needs me too, and
the audit plus wiring plus docs runs past thirty minutes on its own.
** TODO [#B] Add storage-capacity signals to the maintenance module :feature:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
Investigate capacity and growth diagnostics for full disks, identify the
appropriate remedies, and incorporate a clear storage signal into the
maintenance console.
** TODO [#C] Add per-channel controls to the audio panel :feature:audio:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
Expose channel-level input and output volume controls without losing the
existing device-level workflow.
** DOING [#B] Widget gallery upgrades :feature:design:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-23
:END:
Usability + documentation pass over the [[file:docs/prototypes/panel-widget-gallery.html][panel widget gallery]], orthogonal to the component-generation spec work. Items 1-4 run as a no-approvals speedrun (Craig authorized 2026-07-12); item 5 is a joint brainstorm.
The =gallery-upgrades= branch this originally described is gone — no local or remote ref, and every gallery commit since has landed straight on main. Whether it was squash-merged or abandoned, the branch stopped describing how this work runs, so the line came out at the 2026-08-23 review rather than being left to mislead. Work on main.
*** TODO Extraction-readiness bar for every gallery component :refactor:design:
Craig's standing directive (2026-07-18, set while finishing the split-flap): every =DUPRE.*= builder should meet the bar the split-flap now sets, since these become regular components. The bar: a contract comment documenting every opt and the full handle surface; no page globals touched (page owns cadence via handles/callbacks, e.g. =onSettle=); all component CSS in one named =DUPRE_CSS= block; refactored until no opportunity worth doing remains (small named helpers, no duplication); construction axes declared via =STYLES= where the component has them. Sweep the existing builders against that list, fix the gaps, and make the bar a stated convention in the widgets.js header or README so new builders inherit it. Overlaps the component-generation spec's extraction phase — reconcile there rather than doing the work twice.
*Audited 2026-08-23 — the sweep covered the bulk and stopped short.* Ten commits
on 2026-07-18 (=43725ff= … =1dd929d=) carried ~104 of the 112 builders over. Two
criteria are fully met: 105 builders carry a real contract comment naming opts
and the handle surface, and component CSS is wholly consolidated — the gallery's
own =<style>= block holds nothing but page chrome (masthead, grid, toc, card
frames, validation lamps, palette). Three gaps remain, and they are the whole of
what's left:
1. *Seven builders were never swept*: =telegraphIndicator=, =radarSweep=,
=dotMatrix=, =flipDisc=, =dekatron=, =gearIndicator=, =blinkenlights=. Each
carries a one-line comment describing what the widget does, with no opts, no
handle surface, no CSS statement. They sit at the tail of =widgets.js= after
=responseGraph=, and the last batch was "well-through-response" — the sweep
stopped one builder short of the end and never came back. Not a clean cut:
=dayDateCal= is in that tail and does have a contract.
2. *The bar was never written down.* The README documents the API shape (Builder
contract) and =DUPRE_CSS= (Styling) — two of the five criteria. The checklist
itself appears nowhere, so a new builder inherits nothing and the sweep has to
be re-derived from this task every time.
3. *One page-global reach survived*: =patchBay= does
=window.addEventListener('resize', draw)= and never removes it. Fails the
no-page-globals criterion and leaks besides — a torn-down instance keeps
redrawing on every resize. The other two =document= reaches are benign
(=indexPlate= guards a shared SVG def; the other is the CSS injector).
*** 2026-07-18 Sat @ 04:32:20 -0500 Made the N20 split-flap an honest Solari mechanism
=GW.splitFlap= rebuilt from the drop-fade fake: charset-as-drum stepping (=opts.chars= is the flap order, one flip at a time through intermediates, staggered arrival), re-aim-not-queue retargeting, and the real two-half-panel fold (WAAPI, backfaces hidden), with =animate:false= collapsing to instant jump for reduced motion. Handle grew =setText=/=chars=/=reading()=; default width 3 → 4 cells. Nine probe checks written red-first (arrival, intermediates, one-flap stepping, re-aim discriminator, instant path); technique studied from HotFX and re-derived — no license on their repo, nothing copied (reference filed in =working/retro-stereo-widgets/references/=). Review: sound; its two test-strength notes addressed in the same change.
*** 2026-07-12 Sun @ 12:59:48 -0500 Added the card size toggle (1x/2x/3x, default 3x)
Masthead size chips apply CSS zoom per grid; 2x/3x drop the 1320px wrap cap so wide monitors get the room. CDP-verified: 84 cards, no exceptions, fader drag and toggle click both track at 3x (drag helpers are rect-ratio based, so zoom is transparent to them).
*** 2026-07-12 Sun @ 13:42:29 -0500 Retuned the scale split per Craig
Craig: cards a fifth smaller, fonts and palette back to normal but a little larger. The toggle (now S/M/L, "widget size") zooms only the stage — L is 2.4x, M 1.7x — while card text sits at its own raised base size (wname .95rem, note .85, spec-sheet .82) and the palette left the zoom rules entirely (tiles bumped 148→176px, fonts up a notch). Grid columns widen per size step instead of zooming. Probes re-run green.
*** 2026-07-12 Sun @ 13:52:11 -0500 Raised all reading text to 12-13pt per Craig
One move: html font-size 130%, scaling every rem-based size (notes/spec sheets/palette/masthead/readouts). Verified computed: notes 13.3pt, spec sheets 12.8pt, palette 12.5pt — was ~10pt. Widget-internal px sizes untouched (they are drawing, scaled by the stage zoom).
*** 2026-07-12 Sun @ 14:07:00 -0500 Added R32 mechanical timer dial from Craig's reference
Wind-up interval timer (Controls, after R29; gallery at 85 cards): knurled coin-edge dial rotating under a fixed red index, OFF/20/40/60 at 4.5°/min, hub screw, winding arrow, PUSH TO STOP / TURN TO START engraving, red fluted stop knob. Drag winds (turn to start), the dial runs itself back down at a demo minute-per-second (reduced-motion gated), red knob click zeroes it (push to stop). Spec sheet included. Reference filed (2026-07-12-mechanical-timer-dial.png). CDP-verified: wind → T-35 MIN, tick 35→34, stop → OFF, zero exceptions.
*** 2026-07-12 Sun @ 14:21:27 -0500 Added R33 four-way rocker from Craig's reference
Navigation pad (Controls, after R32; gallery at 86 cards): recessed plate, rubber pad, four outward silver arrows, center pivot nub. Quadrant-sized click zones step a tracked cursor (readout shows direction + x/y), pressed arrow flashes gold. Click-only — noted in the spec sheet as the most Emacs-portable control in the kit (it is literally arrow keys). Reference filed (2026-07-12-four-way-rocker.png). CDP-verified: all four quadrants step and accumulate correctly, zero exceptions.
*** 2026-07-12 Sun @ 14:26:20 -0500 Timing-marker dial judged covered (Craig concurred: probable dupe)
Scope timing-marker reference (VARIABLE TIMING / MARKER SEC): a knob rotates a printed scale disc under a fixed top index — that mechanism is R02 (vernier disc under hairline), and the value-in-a-window read is R20 (drum roller). BANKED enrichment, new to the kit: the backlit active-value window — the full scale stays visible but only the selected value glows through an amber window (R02 shows all + highlights none; R20 highlights one + hides the rest). Also noted: the 1-2-5 stepped decade scale (us/ms/s) as detent content for any stepped dial. Reference filed (2026-07-12-timing-marker-dial.png); no new card.
*** 2026-07-12 Sun @ 14:29:22 -0500 Added R34 four-way toggle selector from Craig's reference
NOT a dupe of R33: the rocker is momentary (press = step), this is stateful (the lever lives in A/B/C/D). Test-panel styling: printed X/Y axes + circle + diagonal legend square, A-D label rings, corner lamps wired by printed lines, chrome ball lever that throws to the clicked quadrant while its lamp takes the light (exactly one lit, jewel-a). Click-only. Gallery at 87 cards. Reference filed (2026-07-12-four-way-toggle.png). CDP-verified: default POS C per the photo, all quadrants select, single-lamp invariant holds, lever transform tracks, zero exceptions.
*** 2026-07-12 Sun @ 14:37:46 -0500 Added R35 day-date disc calendar from Craig's reference
Watch day-date complication (Indicators, after R31; gallery at 88 cards). Judged NOT covered: N04 is two nested input knobs, R02 one disc under a hairline — a compound READOUT from independent coaxial discs is a new indicator form. Cream disc, 31 radial date numerals (upside down at bottom like the movement), weekday names twice around inside, fixed yellow hand as the read index, movement-plate screws. Live: initializes to today; click rolls midnight one day (both discs step). Spec sheet notes the honest limitation — the 31-slot disc ignores short months, exactly like the hardware. Reference filed (2026-07-12-daydate-discs.png). CDP-verified: today correct (SUN 12), click → MON 13, both disc transforms track, zero exceptions.
*** 2026-07-12 Sun @ 14:40:47 -0500 Added R36 LED dot matrix from Craig's reference (answered: what display is this)
Answered Craig's question: an 8x8 red LED dot-matrix module (64 discrete LEDs behind a tinted window, multiplexed; Kingbright/Lite-On family part) on Kilpatrick Audio's K4816 Pattern Generator, drawing its K logo. Judged new form: seven-seg/starburst are fixed segments, VFD marquee is character cells, R10 is a text LCD — a free bitmap display is none of those. Built as R36 (Indicators; gallery at 89 cards): 8x8 paintable matrix starting on the reference K (17 dots), click toggles any dot, readout counts lit. Reference filed (2026-07-12-led-dot-matrix.png). CDP-verified: K = 17/64, paint on → 18, paint off → 17, zero exceptions.
*** 2026-07-12 Sun @ 18:11:02 -0500 Built the takuzu-survey Tier 1+2: R37-R47 + N23 alarm lifecycle (gallery at 100 cards)
Craig picked option 2 from my assessment of takuzu's historical-panel-components research (60+ candidates; ~a fifth of its "new" verdicts were stale against the current kit, and the magic-eye entry re-litigated a removal takuzu itself requested 2026-07-11). Built + CDP-verified in 4 commits: 9b625d6 R37 pin routing matrix (VCS3 many-to-many, pins seat/pull, route count) + R38 dead-man button (first held-state control: pointer-capture, dwell clock, release-to-safe) + R39 rotary telephone dial (wheel winds to the stop and returns, digit pacing authentic); 109b3dd R40 breaker panel (first system-thrown control: TRIP pops a breaker to the amber mid-state, two-step reset) + R41 DSKY (verb/noun grammar, V35 lamp test, OPR ERR on bad grammar — one bug caught by probe: stale verb register re-executed on bare ENTR, fixed) + R42 cam-timer drum (procedure-as-position, self-advances after a click starts it); eb926ea R43 attitude indicator (first two-axis instrument, 2D drag) + R44 heading bug + servo needle (commanded-vs-actual, shortest-path chase); cbcad5b R45 flip-disc array (bistable pixel, scaleX flip) + R46 dekatron (pulse-stepped glow, carry blink on wrap) + R47 landing gear (spatial three-greens + transit pulse) + N23 upgraded with the alarm lifecycle (flash → ACK steadies → new alarm re-arms → RESET / TEST). All spec sheets written. BANKED as idioms, not cards: mimic diagram, oscilloscope cluster, throttle quadrant, voice-loop strip, blinkenlights, lockout-tag disabled-with-reason, knife-switch skin, decade-switch bank, normalled-jack overlay, neon-vs-jewel, VHF detent, valve handwheel, circular chart, two-hand anti-tie-down. Judged covered/stale in the doc: warning flag (R11), split-flap (N20), strip-chart (N16), odometer (N25), interval timer (R32), telegraph (R30), foot switch (R24), light gun/trackball (the mouse), guarded-toggle two-step (R29 already does it). Full regression green at cbcad5b.
*** 2026-07-12 Sun @ 18:29:52 -0500 Control-grammars reference note + the six promoted banked cards (gallery at 106)
Craig picked option 3 (note first, then build) and asked for the reading list in home's inbox. Shipped: docs/design/2026-07-12-control-grammars-reference.org (b0cebcf — the seven named grammars each with a kit exemplar, the literature from Moran CLG through MIL-STD-1472/NASA HIDH, and seven proposed taxonomy axes; the gap-finding move is crossing task x cardinality for empty cells). Reading list delivered to home/inbox (2026-07-12-1822). Then promoted six banked items to cards, CDP-verified in two commits: 5ef616f R48 knife switch (side-view blade on a real hinge; the air gap is the proof) + R49 decade box (four skirted decade knobs compose 3,500 Ω style values) + R50 two-hand safety (arm window 0.5s, same-hand and timeout both fault); 8b4978b R51 voice-loop keyset (independent monitors, exclusive talk, per-loop activity flicker) + R52 blinkenlights (live ADDR/DATA lamps folding in a clickable switch register, octal readout) + R53 circular chart recorder (day-per-revolution pen trace, fresh paper on click). Full regression green at 8b4978b, 106 cards. Still banked (genuinely idioms): mimic diagram, scope cluster, throttle quadrant, lockout tag, knife-skin, normalled jacks, neon-vs-jewel, VHF detent, valve handwheel.
*** 2026-07-12 Sun @ 18:52:41 -0500 Added R54 vertical tape + R55 twin-needle gauge from the Ki-57 panel (gallery at 108)
Craig fed a Mitsubishi Ki-57 right-panel photo (instruments identified card by card, labels read from the Japanese: 速度計 airspeed, 遠方回転計 remote tach, 昇降計 VSI, 人工水平儀 artificial horizon, 高度計 altimeter, 気筒温度計 CHT, etc.). Two genuine gaps built: R54 vertical tape instrument (scrolling scale behind a fixed amber index — the Ki-57 remote tach's form, universal in glass cockpits; drag spins the tape) and R55 twin-needle gauge (mirrored FUEL/OIL half-scales, two needles one hub, per-half drag surfaces — the banked "twin mirrored gauge" from the French jet panel, distinct from N13 whose needles are read at their crossing). Cleanup pass earlier (c4fcee6): rotary-dial digits were painted under the finger wheel — now on top; legible demo defaults for pin matrix / voice loop / DSKY PROG; caption color unified; cam labels bumped. Reference filed (2026-07-12-ki57-right-panel.jpg). CDP-verified: tape drags (2400→1329), needles independent (fuel up leaves oil, oil down leaves fuel), zero exceptions.
*** 2026-07-12 Sun @ 18:59:08 -0500 Added R56 comfort-zone crossed needles (gallery at 109)
Craig fed a brass weather-station comfort meter and correctly picked it as different from both dual meters: N13 derives a NUMBER from the crossing (iso-curves), R55 reads two needles separately; this one lands the crossing in a printed categorical VERDICT (TOO WARM / TOO DRY / JUST RIGHT — active zone goes red, a digital advantage the paper face lacks). Brass knurled bezel, cream face, temp needle from bottom-left, humidity from bottom-right, per-half drag surfaces, needles clipped to the face (probe caught tails poking past the bezel at low values — fixed). Reference filed (2026-07-12-comfort-meter.png). CDP-verified: default 72F/45%/JUST RIGHT, warm and dry verdicts flip with the active label, zero exceptions.
*** 2026-07-12 Sun @ 19:13:41 -0500 Squash-merged to main (bc93388) + cleanup round 2
Squash merge of the 25-commit branch landed on main; branch deleted local+remote. Then a programmatic defect sweep (per-card overflow/empty/dead-readout/sparse bounding-box audit) caught R51's monitor bars streaking across the card — Chrome's CSS zoom miscomputes absolute left/right insets, fr tracks, AND stretched widths inside zoomed stages, so the keyset now uses fixed 52px grid tracks and fixed 40px flow bars (fixed px scale correctly under zoom; this is the third zoom-layout trap after the two in the same widget — noted for future stage-internal CSS: prefer fixed px inside .stagew). Seven intrinsically tiny widgets (toggle, chip, arm-to-fire, mini signal, ladder, thermometer, status lamp) got a .boost stage zoom (1.5x, compounds with the size toggle) so they stop floating in empty stages. Sweep false positives understood: clipped content (R43 horizon, R54 tape, N22 marquee, N25 counter) reports as overflow because getBoundingClientRect ignores clip; N13/N17/N28/R20/10 verified clean visually. Full regression green at 109 cards.
*** 2026-07-12 Sun @ 19:25:04 -0500 Page iteration: palette rebuilt as named instrument colors, moved to bottom; default size M
Per Craig: the palette left the top (now below Indicators) and stopped being a token dump — it is now a curated card of 36 NAMED colors grouped by role (Materials / Faces & inks / Lamps, LEDs & jewels / Screens & phosphors / Needles & controls), each with a name (Brass, Chart paper, Neon orange, Graticule green...) and where it appears on the instruments. No hex codes visible anywhere on the page (verified by innerText scan); page chrome (background, body text) deliberately excluded. Default widget size dropped 3x → M (1.7x). Data lives in a PALETTE array in gallery JS — the curation IS the data, since most instrument colors are local literals, not tokens.
*** 2026-07-12 Sun @ 13:01:27 -0500 Added the palette section
New "Palette — design tokens" section above Controls: 33 tiles read live from the generated =:root= rule via document.styleSheets (zero drift possible — what renders is what the widgets use). Hex tokens and glow rgb triples get swatches; mono/pulse-rate render as text tiles. Scales with the size toggle.
*** 2026-07-12 Sun @ 13:08:42 -0500 Added screen-color families + chips (first round)
Six period families defined (green = P1 phosphor built on =phos=, amber = P3 on the gold family, red = the nixie neon pulled out, blue = P4 white-blue, vfd = the marquee's =--vfd=, white = mono LCD), applied via scoped CSS vars with the shipped color as fallback — defaults are pixel-identical until a chip is clicked. Chips on the five clearly-screen widgets: R10 data matrix (amber), R17 CRT scope (green face), R19 waveform LCD (white), R31 radar (amber — the "in amber" one; green chip gives the classic PPI), N11 oscilloscope (green). Answers to the green question: the kit deliberately has four greens — phos #7fe0a0 (CRT trace), vfd #63e6c8 (marquee, blue-leaning), sevgrn #57d357 (LED), jewel-g #6fce33 (jewel lens); the phosphor-screen green is phos, not the others. Nixie left chipless: neon is only ever orange. Families live in gallery JS, not tokens.json — tokenize the winners once picked, the way amber earned its tokens. CDP-verified: recolors land on all five, interactions intact, zero exceptions.
*** 2026-07-12 Sun @ 13:15:19 -0500 Added spec sheets to all 84 cards
Every card now carries a collapsible "spec sheet" (a details element under the note) with up to 8 fields: input (always present, names the model — click ports everywhere including Emacs click-regions, drag needs a click/key idiom there, display widgets take no real input), solves, use (common vs specialty + where it shines), limits, origin, difficulty, prefer-when, and period (present on 41 of 84 — only where the component is clearly not timeless, e.g. nixie 1955-75 sits with keypads/telegraphs and clashes with VFD and LED seven-seg). Content lives in one INFO object keyed by card number; card() renders it. CDP-verified: 84/84 sheets present (missing-key check), fields render, zero exceptions.
*** 2026-07-14 Tue @ 02:00:49 -0500 Judged the two 2026-07-13 cross-needle references covered
The weather-station comfort meter is R56's form exactly (crossing lands in a printed categorical verdict; five zones vs R56's three is content, not mechanism). The SWR cross-needle meter is N13's defining mechanism (the crossing derives a number off printed iso-curves — forward/reflected watts → SWR). No new cards; both references stay filed in working/retro-stereo-widgets/references/.
*** DOING Widget validation pass
Craig walks all 110 cards; the lamps are his (click cycles off → amber → green, per-card localStorage key =gv-<no>=). Gate change with the option-1 approval (2026-07-12): the lamps no longer gate the widgets.js extraction (lossless, done) — they gate per-widget Emacs ports and the final catalogue blessing.
Runs as a *joint loop* (Craig, 2026-07-16): he walks a batch of 10-15 and reports card numbers + what's wrong; Claude fixes them in one pass, gates on the three probes, he reloads and re-walks. The lamps track progress only — they never recorded *what* was wrong with a card, so the defects live in this task body as they surface.
Statuses bake into the gallery source (=VSTATUS=, top of the lamp section) *periodically*, not once at completion as originally planned — localStorage is per-profile and dies with a cache clear, so at 8 cards baking is free and at 60 it's the difference between a record and a bad afternoon. Craig clicks "copy for source" under the index tally and pastes the block into the chat; the agent pastes it into =VSTATUS=. Precedence is localStorage → VSTATUS → off, so the live walk wins on the machine doing it and the baked record fills in on a fresh profile, after a clear, or on velox. Covered by =tests/gallery-probes/probe-vstatus.mjs=.
*The page can't copy for you; you press Ctrl+C* (settled 2026-07-16). Clicking "copy for source" drops the export into a selected textarea at the bottom-left of the page. Ctrl+C it and paste it into the chat. From a =file://= origin Chrome refuses both programmatic copy paths — =navigator.clipboard.writeText= rejects NotAllowedError, and =execCommand('copy')= returns *true while writing nothing*, which is worse because the page then claims a success it never had. A hand-typed Ctrl+C works fine, so nothing about the clipboard is broken: a Wayland-set clipboard crosses to X11 and Emacs reads it correctly (sentinel-verified against =gui-get-selection=).
*And the agent doesn't fetch it.* Never read the export with =wl-paste --primary=: PRIMARY holds whatever was last selected anywhere, so a blind read returns unrelated content (it surfaced a private SMS during this session). Craig pastes; the agent doesn't reach for it.
Progress: 8 green + 2 amber (R07, R52) of 109, baked 2026-07-16.
*** 2026-07-16 Thu @ 08:11:18 -0500 Ran the classification brainstorm — two empty cells and a missing axis
Brainstormed with Craig and wrote the result into [[file:docs/design/2026-07-12-control-grammars-reference.org][the control-grammars reference]] ("The brainstorm, run"). Crossed Foley's six elemental tasks against cardinality over the 109 cards.
Added *axis 8, set stability* (fixed at manufacture vs discovered at run time). The doc's original seven axes cannot find the kit's largest gap: axis 4 lumps all one-of-N together, so a chicken-head selector and a WiFi list share a cell that then reads as densely covered. The axis is invisible from inside hardware — every one-of-N in the kit has its positions engraved at manufacture — so a catalogue derived from period hardware inherits a blind spot exactly at dynamic sets, which is what its software consumers are made of.
Two empty cells, both wanted by live panels today: *select x one-of-N dynamic* (the bt device list, the net network list) and *text x alphanumeric* (the WiFi password; the kit enters digits three ways and text zero ways). Foley names six irreducible tasks; the kit serves five and a half.
Period models proposed rather than invented, per Craig's aesthetic gate: the answer to a changing set was never a control but a *re-labelable slot* — jukebox title-strip rack, Rolodex, scribble strip, switchboard strips. The slot is manufactured, the label is not.
Also found: the gallery cannot build its own chrome from its own kit (swatch chips, validation lamps, size toggle are all bespoke HTML; the size toggle duplicates card 06). That is a free completeness test worth re-running as the chrome grows.
Still open, recorded in the doc: the full seven-axis classification, the card-by-card audit behind the first-pass cell assignments, and the display side.
*** DOING Build the four widgets the taxonomy found :feature:design:
From the [[file:docs/design/2026-07-12-control-grammars-reference.org][taxonomy brainstorm]] (2026-07-16). Craig's gate: stick to the aesthetic — model a period control, don't invent a software-native one. Reference photo first per the usual pipeline, then judge, then card.
1. *Dynamic list* — model the jukebox wallbox title-strip rack (page-flip browse, rich slotted strips, select one). Fills the most damning cell; the bt and net panels both want it. Alternative model: Rolodex card spinner, better for long sets and weaker at showing several rows at once.
2. *Alphanumeric entry* — model the *industrial ABC-order keypad*. Craig's four references (=working/retro-stereo-widgets/references/2026-07-16-abc-keypad-*.png=, =-letter-drum-bank.png=) plus a survey superseded the teletype: the references are all ABC-order, not QWERTY, on stainless or membrane, with colour-coded function keys (yellow CANCEL, red CLEAR/NO, green ENTER/YES) that land on the kit's palette without translation. A faceplate, not furniture.
3. *Index typewriter* — stylus over a printed index plate, print lever commits (Hall 1881, AEG Mignon 1924). The survey's strongest find: select and commit on two separate controls is a grammar the kit has no exemplar of, and it is small and beautiful. Second card after the keypad.
Craig's brief (2026-07-16, after reading the AEG Mignon Model 4 reference): *reproduce the device, keep its extended character set, treat the layout as ours.* What he admires is that the plate considered high-ASCII at all — accents, section mark, fractions, the full punctuation ring — and it carries both cases with no shift key, which is how a keyless machine reaches a whole character set. What he doesn't admire is the Mignon's key order: the real plate runs =P U G Q / V I N A B / L D E T M= (a frequency layout), which you cannot read your way around. "We'll revisit the keys and the layout. The best ideas will be preserved."
So the layout ships as *data, not drawing* — a table the builder renders — because a revisable layout that requires touching the mechanism won't get revised. First pass: alphabetical, capitals block beside lowercase block at the same column offset (find the letter, then pick the case), the Mignon's two-block structure with a legible order. Expect it to change.
References: =working/retro-stereo-widgets/references/2026-07-16-mignon-aeg-detail.jpg= (Model 4, casing off, plate legible; CC BY-SA, Uwe Aranas, attribution required if reproduced) and =-mignon-4-index-typewriter.jpg= (museum context; public domain). Credits in =2026-07-16-mignon-CREDITS.txt=. Reference only, drawn from scratch.
**** 2026-07-16 Thu @ 16:39:05 -0500 Built R58 index typewriter (gallery at 111)
Item 3 done. =GW.indexPlate=: click a cell to move the stylus, pull the lever to print, and only the lever prints. The first card where selecting and committing are separate acts. Keyboard follows the same rule — typing SELECTS, Enter is the lever — because a keypress that printed would make this R57 with a nicer plate.
No case folding, unlike R57: the plate holds both cases as distinct cells, so Shift does the work a shift key would on a machine that has none. =KEYS= and =ACTIONS= are both *derived* from =LAYOUT= at load, so rewriting the table relays the plate, the keymap and the allowlist together — a binding aimed at a character the plate no longer carries isn't expressible.
Craig's brief honoured: kept the extended set (Ä Ö Ü ä ö ü ß § ½ ¼ and the punctuation row) and the no-shift both-cases plate; dropped the Mignon's frequency order for alphabetical, capitals beside lowercase at the same column offset.
Review (subagent) cleared the grammar — no path where selecting prints, none where the lever prints anything but the resting selection — and found five defects, all fixed:
- *Clicks bypassed press().* The contract says click and key must both route through it; R57 obeys, R58 had three direct handler bindings. No divergence yet, only because press was a pure dispatcher — add a guard to it and the mouse, the primary input on this card, would silently skip it with every probe green. Exactly the drift the rule exists to prevent, in the second card written against the rule.
- *Check 14c could not fail.* =/ss$/= on the readout also matches 'sss', so it passed even if selecting printed — blind to the one bug the card is about. Now exact, against the buffer.
- *cells{} collided on duplicate layout characters* and resolved =select('constructor')= through Object.prototype. Now =Object.create(null)=, plus a check that the table has no duplicates (14e couldn't see it: a duplicate inflates the drawn and declared counts equally).
- *Geometry broke on SHRINK.* The lever and legend anchor to the plate's top, CLR anchored to the viewBox, so a five-row table rode CLR up over the PRINT legend. Growth was always safe; shrink was the trap. VH now takes a floor, and I verified it by building the plate at 4, 5 and 9 rows rather than trusting the arithmetic: 190/192/280, no collisions.
- *No allowlist coverage on press().* R57 had it, R58 didn't.
Known gap, Craig's to weigh when he revisits the keys: no space cell, so Space isn't in KEYS and still scrolls the page (correct per the contract). You can type Mignon-4 but not "Mignon 4". The real machine has a separate space key.
4. [@4] *Chorded keyset* — six keys, no key per letter; the chord IS the character (Microwriter 1978, Perkins Brailler 1951). The most compact honest password field. The kit names the chorded grammar already (R50 is its safety form) but holds no chorded text control. Third, if a compact field is wanted.
5. [@5] *Swatch picker* — model the signal-lamp lens turret or theatrical gel wheel; a rotary whose detents are coloured lenses. Undecided whether it earns a card or stays page furniture.
6. [@6] *Legend switch* — the lit pushbutton whose cap legend and colour ARE the state, press to step (MIL-STD-1472 catalogs it; the doc already cites the standard). The validation lamps are one.
Items 1 and 2 fill the two empty cells and are wanted by live panels today; 3 and 4 are the survey's genuinely-new grammars; 5 and 6 come from the gallery-can't-build-itself finding and are lower stakes.
**** 2026-07-16 Thu @ 13:53:34 -0500 Built R57 ABC entry keypad — the text x alphanumeric cell is filled (gallery at 110)
Item 2 done. Modelled on the membrane reference (=working/retro-stereo-widgets/references/2026-07-16-abc-keypad-membrane-color.png=): a 0-9 block beside A-Z in alphabetical order, CLR / ENT / CANCEL, typed text in an amber window, 16-char cap with the window showing the tail. The kit's first free-text control, and R16's alphanumeric sibling.
Two deliberate departures from the photo, both recorded in the builder comment: its letters are *blue*, and the kit has no blue control colour (blue is only ever a screen phosphor or a jewel lens), so letters take the standard pale keycap and digits a darker one — which keeps the photo's two-tone digit/letter grouping without importing a foreign hue. CANCEL is amber, borrowed from the sibling reference whose CANCEL is yellow; CLEAR/NO and ENTER/YES keep the reference's red and green, which are already =--fail= and =--pass=.
TDD: four probe checks written first and confirmed red (A-Z in alphabetical order — a QWERTY drift would silently lose the idiom; a full 0-9 block; typed characters accumulate in order; ENT commits while CLR empties). All green, plus the card count bumped 109 → 110. Verified visually in both the empty and typed states rather than trusting the green run.
Craig then caught a layout flaw the membrane reference itself carries: with the digit block on the left, A-L sits in columns 3-5 while M-X starts at column 0, so the alphabet stops column-aligning with itself halfway down and the eye has to jump. Swapped to letters-left / digits-right, which is the *stainless* reference's arrangement — so the card is now a synthesis of the two photos (membrane colour-coding, stainless layout) and reads A-Z down one unbroken block. Card note and spec-sheet origin updated to credit both.
That swap also exposed a hole in my own checks: the A-Z check reads DOM order, which the builder controls by push order, so it would pass with every key rendered in the wrong place. Added a geometric check (letters left of digits, function block on the digit side, and A/D/G/J/M/S/Y sharing one x) that pins the layout Craig asked for.
Craig then caught the missing backspace: with only CLR, one mistyped character costs the whole entry. Added DEL, and on his call it sits in the digit block wearing amber while CLR is exiled to the far corner in red. Both swaps pull the same way — DEL is the key you reach for constantly and costs one character, CLR is the one you reach for almost never and costs the entry, so the safe key gets the good spot and the colour grades the cost before you read the legend. The three function keys now read as a ladder: amber takes one back, red throws it all away, green commits.
And he caught that CLR and CANCEL did the same thing — both just emptied the buffer, differing only in the readout string. Dropped CANCEL. The reference plate carries it because on that device the plate IS the whole terminal and has a transaction to abandon; here the keypad is one control inside a panel that owns its own dismiss, so CANCEL meant nothing the panel didn't already mean. SPACE widened to fill, DEL took the rightmost slot, and the now-unused amber gradient and tone came out with it. The only colour left is CLR and ENT, the two irreversible keys.
Adding the DEL check also caught order-dependence in my own suite: it emptied the buffer that the ENT/CLR check had been inheriting from the check above it — the same defect the reviewer found in check 9 this morning. That check now types its own buffer.
Review (subagent, since I wrote it all) cleared the widget itself — buffer logic, layout arithmetic, and the gradient id space all traced clean — and found three real defects, all fixed before the commit:
- *A typed space was invisible past 13 characters.* SVG collapses trailing whitespace, and past the display's truncation boundary there are no pad dots left for a space to displace, so SPACE became a keypress with no feedback: press, see nothing, press again, and carry two spaces you can't see in a passphrase you can't read back. The window now draws spaces as =␣= while the buffer keeps the real character. Verified by eye that the glyph isn't a tofu box.
- *The ENT check could not fail.* It typed NET5, pressed ENT, and asserted =/NET5/= — which typing the 5 had already made true. Deleting the whole ENT branch still passed, because the key then fell through to =buf += 'ENT'= and NET5 still matched. It now asserts the commit signal itself.
- *The DEL check asserted half its name*, computing the past-empty state and never looking at it.
Third vacuous check in one day, and the shape is consistent: checks written in the builder's own vocabulary inherit its assumptions and confirm what the code does rather than what it should do. The one that mattered was found by someone reading the render arithmetic cold.
**** 2026-07-16 Thu @ 14:42 -0500 Keyboard contract recorded, R57 built against it
Craig asked whether keyboard input is the widget's job or its container's, and picked "record the contract first, then build against it" — it's a decision about all 110 cards, not one.
The kit already held the answer in two halves. =GW.slideRule= takes arrow keys scoped to its *own focusable element* (the browser arbitrates focus, which is why it never fights the gallery's global Escape handler at line 1157) — that's the web-correct pattern. But Emacs can't do that at all: the SVG region is an image and never sees a keypress, so the mode's keymap must own delivery and call =press=. Same split as the tick contract, which the README already states: the page owns the ambient resource (the clock there, focus here), the builder exposes a handle.
Contract written into [[file:docs/prototypes/README.org][the widget-library README]] beside the tick contract: *the target owns focus and delivery, the builder declares what it accepts* via a =KEYS= table. Deliberately a table and not a function over a DOM event — the Emacs port installs it into a keymap and never sees a keydown, so a function would force it to re-derive the widget's intent and the two bindings would drift. Five rules, each one a bug otherwise shipped: never listen on =document=; spend =preventDefault= only where there's a default worth killing (Space scrolls, Backspace navigates back); let Tab and Escape bubble (Tab is how the page stays navigable, Escape belongs to the audit stepper); =press= filters rather than trusts (it appends whatever it's handed, so a stray F1 would land as text); click and key both route through =press= so they can't drift. A widget with no =KEYS= table takes no keys, which is most of them — the kit is click-first and that's what makes it port.
R57 built against it: =tabindex=, focus on click, element-scoped keydown, gold focus ring (an unlit focus state means typing vanishes into a card you thought was live). Five checks written first and confirmed red, including the contract's own rules — unfocused keys ignored (the no-document-listener rule, tested behaviourally), Space and Backspace claimed, Tab/Escape/F1 let through, unmapped keys dropped. Audited the kit: no document-level key listener anywhere in widgets.js, so the contract holds retroactively.
Spec sheet reworded. "Click-only, so it ports to Emacs unchanged" stopped being true, and the honest version is the opposite: keys are the *more* native idiom in Emacs, so the card ports better, not worse.
Review (subagent) found two High defects in the keyboard work, both fixed:
- *The contract's own rule was unmet by the commit that recorded it.* The README says "press filters, it does not trust", and the filter went into the keydown handler instead — leaving press wide open for the one caller the table exists to serve, since the Emacs port installs KEYS into a keymap and calls press directly with no handler in the stack. Now gated on =GW.abcKeypad.ACTIONS= (the plate's whole vocabulary), which is a superset of the KEYS values because CLR is a real key no keystroke reaches.
- *The focus ring never appeared on the path anyone uses.* =:focus-visible= doesn't match a mouse-driven focus on a non-text element in Chrome, and clicking the plate IS how it takes focus, so the ring showed only when tabbed to. Now =:focus=.
Also: two more weak checks. One claimed click and key can't drift while only reading a string (now enters the same sequence both ways and compares buffer + readout); one asserted Tab/Escape/F1 aren't swallowed, which couldn't fail because those keys return before the preventDefault (now also checks 'A' and Enter, which are mapped and reach the same code path).
*** 2026-07-16 Thu @ 15:20 -0500 Screen families on the keypad window, and a probe that was lying
Craig: offer the display in every colour, including the vfd marquee cyan. The window now takes the =--scr-*= vars with the shipped colours as fallbacks, so the default is pixel-identical until a chip is clicked (=--gold-hi= IS the amber family's =--scr-hi=, which is what makes that exact rather than close). Both the glass and the ink recolour — a screen that changes its text and keeps its backlight isn't a screen. R57 offers all six families where its siblings each carry five: a passphrase window has no reason to prefer one phosphor.
Found a real defect while adding the checks, and a nasty one: *probe-fams.mjs had no try/finally*, so a failing check orphaned its headless browser. The next run then connected to the survivor on the same port and reported results from a STALE page — old widgets.js, chips already clicked. It cost a confusing debug loop: R10's default ink read as green and R57's chips read as absent, neither of which was true of the actual page. A probe that answers from the previous run is worse than one that crashes. Wrapped the checks in try/finally like its two siblings already had, and swept nine orphaned chromes (the pattern is bracketed on the debug port, so it cannot touch Craig's daily browser, which has none).
Craig called the ABC entry keypad done at 110 cards.
Banked from the survey, not cards yet: CDU/MCDU scratchpad + line-select keys (a staged-commit *flow*, not a new selector), joystick scroll-and-fire alphabet, multi-tap/T9 letter cycling (the most practical small-panel password entry), trackball gesture (Atari Quantum), keypunch program drum (IBM 029), Enigma lampboard (a *feedback* idiom — steal the 26-lamp grid as a readout, not as entry), Teletype Model 15 tape perforator.
Two flags carried from the survey: no true A-Z *thumbwheel switch* could be verified (Digitran/Grayhill mil-spec wheels are 0-9 or hex only), so the letter-drum reference is a *combination lock* and a card must say so rather than imply a switch that may not exist; and the trackball-gesture details are single-sourced.
*** TODO Taxonomy audit — verify the first-pass cell assignments :design:
The cross's cell assignments were read off card names and spec sheets, not audited card by card, so "confidence of completeness" isn't earned yet. Walk all 109 against elemental task x cardinality x set stability, cell the display side (left uncelled deliberately), and argue the four untouched axes (grammar, state authority, persistence, era). Would either confirm the two empty cells or find more.
** DOING [#B] Retro widget catalogue :feature:design:
:PROPERTIES:
:SPEC_ID: 3ac0d42c-db1a-4d21-bce4-e63785fef0ba
:LAST_REVIEWED: 2026-08-23
:END:
The panel widget gallery ([[file:docs/prototypes/panel-widget-gallery.html][docs/prototypes/panel-widget-gallery.html]]) grows into a retro-instrument component catalogue: reference photos of period hardware → gallery cards (the visual + behavioral spec) → reusable components for three targets (emacs svg.el, web/React, waybar). Tokens single-sourced in [[file:docs/prototypes/tokens.json][tokens.json]] (gen_tokens.py emits web/waybar/elisp); svg.el proof widget shipped (gallery-widget.el, needle gauge). Reference photos live in [[file:working/retro-stereo-widgets/][working/retro-stereo-widgets/]]. Collection converged at R56, then reopened at R57 as the taxonomy found empty cells (110 cards, all behaviorally verified; probes in [[file:tests/gallery-probes/][tests/gallery-probes/]]).
Build runs per the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DOING; reviewed + decomposed 2026-07-12): web extraction first (ungated, lossless), then demand-gated Emacs/waybar ports. Banked variant/composition ledger lives in the 2026-07-11/12 session archive.
*The eight open subtasks are really one decision plus three builds* (noted at the
2026-08-23 review, because "8 open" reads as more contested than it is). Phase 1
shipped. Phases 3, 4 and 5 and the spec flip are each gated, directly or
transitively, on *Phase 2 — the demand inventory*, which is my matrix to write
and nobody else's. That single artifact has been the whole chain's blocker since
2026-07-12. The three genuinely independent items are the magic-eye rebuild, the
wind-direction rose, and weather kit integration.
Weather kit integration may already be unblocked: its note says live panel
verification "awaits a stowed desktop with a private weather location
configured", and both daily drivers are stowed now. Check whether
=$WEATHER_LAT=/=$WEATHER_LON= or =~/.config/weather/config.json= is set before
treating it as still waiting.
*** TODO [#B] Rebuild the magic-eye tube component :feature:design:
Reinstate the magic-eye tuning/level indicator (EM34/EM84/6E5 family), but
replace the earlier weak UI rather than reviving it unchanged. The component
must read as a real glass tube: P1-green phosphor bloom, a smooth shadow-angle
response, and a clearly different tuning-minimum / record-level interaction.
Build it to the current extraction-readiness bar with probes and a visual
comparison before proposing ports. Reference: [[https://en.wikipedia.org/wiki/File:Em11-ani.gif][EM11 animation]].
*** TODO [#B] Add a wind-direction rose component :feature:weather:design:
Bank the weather companion from the Home handoff: a 16-point meteorological
wind rose that reads source direction and optional speed, distinct from the
aviation heading selector. Revisit when the weather surface needs more than
the compact vane/speed slot; no implementation before that demand exists.
*** 2026-07-19 Sun @ 15:40:01 -0500 Filed component and hardware references
Inbox zero filed the clock/chronograph concepts in
[[file:working/clock-display-references/][working/clock-display-references/]]
and the knob, split-flap, and display references in the existing
[[file:working/retro-stereo-widgets/references/][widget references]]. The Home
component handoff was reconciled: Craig reinstated the magic eye subject to a
better tube UI; the wind rose is banked for a future richer weather surface.
*** DOING [#B] Weather kit integration :feature:weather:dotfiles:
[[file:working/weather-kit-integration/][Working package]] received from Home on 2026-07-18. Land the shared, dependency-free =weather= CLI in dotfiles first, with cache/location/rendering tests; then wire a compact Dupre Waybar surface and port the supplied chip renderer to svg.el when its placement and interaction contract are settled. The chip itself remains current-conditions-only. Its hover exposes the next six hours; clicking it toggles a separate five-day forecast panel with each day's high/low and sunny/rainy condition. No coordinates ship in the repository: machines configure =$WEATHER_LAT=/$WEATHER_LON= or =~/.config/weather/config.json= (with =--geo= for travellers).
**** 2026-07-19 Sun @ 05:21:15 -0500 Waybar forecast shipped
Dotfiles =f49764f= (pushed) extends the shared cached =weather= command with a normalized six-hour hourly outlook and five-day daily outlook. The Waybar chip now exposes the hourly forecast in its hover and toggles a Dupre GTK forecast panel on click. The full dotfiles unit suite passed. Live panel verification awaits a stowed desktop with a private weather location configured; the svg.el port remains later work.
**** 2026-07-19 Sun @ 05:26:36 -0500 Travel refresh linked to timezone update
Dotfiles =513c9d5= (pushed) makes the existing right-click =timezone-set= action reuse =whereami --json= — the same WiFi geolocation path and encrypted Google key that Emacs wttrin uses. A successful WiFi lookup atomically writes the machine-private weather config, clears the previous weather cache, and signals Waybar; the less-accurate IP timezone fallback intentionally does not change the weather location. Full dotfiles tests passed.
**** 2026-07-19 Sun @ 10:49:52 -0500 Waybar glyphs added
Dotfiles =50cd1d1= (pushed) adds compact Nerd Font weather-condition glyphs and either an eight-way wind-direction vane or gust glyph to the Waybar chip. The live module was signalled to redraw; full dotfiles tests passed.
**** 2026-07-19 Sun @ 10:52:17 -0500 Waybar glyph slots enlarged
Dotfiles =6f08634= (pushed) wraps the condition and wind glyph slots in Pango =x-large= spans while preserving compact numerical text. The active Waybar config was regenerated and the module signalled to redraw; full dotfiles tests passed.
**** 2026-07-19 Sun @ 10:58:50 -0500 Glyph treatment polished
Dotfiles =ad7cae7= (pushed) raises the chip's glyph and number baselines together, enlarges only the glyph slots to =xx-large=, and colors them with the reference renderer's bright gold. The live module was signalled to redraw; full dotfiles tests passed.
**** 2026-07-19 Sun @ 11:18:18 -0500 Condition glyph optically centered
Dotfiles =db77bc4= (pushed) corrects the Nerd Font condition glyph's internal-metrics offset independently from the already-centered vane and numeric readout. A live Waybar crop verified the glyph and temperature share a visual center; full dotfiles tests passed.
**** 2026-07-19 Sun @ 11:24:36 -0500 Weather chip grouped
Dotfiles =103cccb= (pushed) tightens the temperature/wind spacing into one reading and adds a subtle date-facing divider. A live crop verified the grouping; full dotfiles tests passed. Proposed but unapproved: retain gold condition/wind glyphs and use temperature text alone for a cool/neutral/hot color band.
**** 2026-07-19 Sun @ 11:27:19 -0500 Weather glyph bottoms aligned
Dotfiles =d0f48ec= (pushed) follows an identical-crop screenshot comparison to adjust the condition glyph's vertical offset. The live crop confirms it now shares the visual bottom edge with the vane, temperature, and neighbouring Waybar text; full dotfiles tests passed.
**** 2026-07-19 Sun @ 11:50:00 -0500 Weather numerals baseline-verified
Craig identified that the speed digit still sat lower than the date text. An exact live-crop measurement found the speed =8= bottom at y=45 and the =S= in =Sun= at y=41: a four-pixel mismatch. Dotfiles =f339ce9= (pushed) raises only the weather numeric Pango span by those four pixels. A fresh live capture measures both bottoms at y=35 (zero-pixel difference); focused and full dotfiles tests passed.
**** 2026-07-19 Sun @ 12:05:43 -0500 Comfort color and humidity signals added
Dotfiles =c3ef604= (pushed) colors the compact temperature by apparent temperature, preserving high contrast against the dark panel: cool blue below 55°F, near-white through 78°F, orange through 87°F, coral through 94°F, and vivid red at 95°F or hotter. The weather glyphs remain gold. Open-Meteo’s current relative humidity now appears in the hover and five-day panel without adding chip clutter. A live 92°F / feels-like 100°F refresh visibly rendered the red band; focused and full dotfiles tests passed.
*** 2026-07-12 Sun @ 10:14:10 -0500 Wrote the component-generation spec (DRAFT)
[[file:docs/specs/2026-07-12-component-generation-spec.org][2026-07-12-component-generation-spec.org]] via spec-create: full spine (summary, problem, goals, two-altitude design, alternatives, 8 decisions with 1 open, 5 phases, acceptance criteria, readiness dimensions, risks). Gallery cited as the prototype evidence per the ui-prototyping rule (filed references + R01-R31 iteration history + final at 52a43ec). vNext items logged as the "Widget catalogue vNext" task.
*** 2026-07-12 Sun @ 20:24:37 -0500 Web library packaging approved — classic-script widgets.js + GW namespace
Craig approved with the componentization go-ahead (option 1): =widgets.js= as a classic script exposing one =GW= namespace, relative =<script src>= so =file://= keeps working, shared helpers inside, framework wrappers vNext. Decision flipped DONE in the spec (cookie 8/8). Gate per the same approval: extraction proceeds ungated (lossless transform); the validation lamps gate only per-widget Emacs ports and the final blessing.
*** 2026-07-12 Sun @ 20:57:50 -0500 Spec reviewed and decomposed (DRAFT → READY → DOING)
spec-review passed (Ready): 3 findings, all accepted and folded same pass — option-1 supersession of the demand-gated extraction order (Phases 1-2 swapped: extraction first, ungated), the card-record refactor named in Phase 1, stale counts refreshed (109 cards / R56). Probe baseline repaired first (753380e — two stale assertions from the evening sprint). Phase tasks below; =:SPEC_ID:= stamped on this parent.
*** 2026-07-12 Sun @ 22:56:40 -0500 Phase 1 complete — all 109 widgets extracted into widgets.js
All 109 card builders lifted into [[file:docs/prototypes/widgets.js][docs/prototypes/widgets.js]] (classic script, =GW= namespace, shared engine: svgEl/polar/vuDb/drag helpers/seg7/SCREEN_FAMS/gradient defs); every card is now a declarative record rendered by one =card()= path, all wiring blocks deleted. Widget CSS (incl. pulse/flipdrop/reelspin keyframes) moved from the gallery =<style>= block into =GW_CSS=, injected by widgets.js — verified pixel-identical under forced reduced motion (masked live-date cards N26/R35). Tick contract settled: the page owns the clock + demo signal; live meters expose value-driven handles; widget-owned animation lives in builders behind reduced-motion gates. Finale: slide toggle (card 01) is the first fully-realized component — its four option groups are =GW.slideToggle= constructor opts backed by =STYLES=, the gallery chips a demo rig on =handle.setStyle=. README consumers section documents the GW API + tick contract. Per-batch gate stayed green throughout (probe.mjs, probe-fams.mjs, a 239-check behavioral suite, reduced-motion smoke). Commits acee657 → 7b3bc47 (18 batches), all pushed.
*** TODO Phase 2 — demand inventory (Emacs/waybar) :design:
Widget-to-target matrix for the scripted-port targets: walk the live waybar panels (net/bt/audio/maint) and the Emacs surfaces Craig names; record which cards each actually wants. Lands in the spec's appendix; Craig approves. Tree untouched. Not =:solo:= — the matrix is his call.
*** TODO Phase 3 — Emacs ports of demanded widgets :feature:
Extend the =gallery-widget.el= pattern per demanded widget: ERT (tokens, geometry normal/boundary/error, SVG structure, state-tracks-value) + rsvg-convert side-by-side against the card; keymap/click-region interaction per widget; wired into =make test-elisp=. Gated on the Phase 2 matrix and Craig's green lamp per widget.
*** TODO Phase 4 — waybar pilot: audio panel :feature:dotfiles:
Restyle the audio panel's GTK CSS onto =tokens-waybar.css= + the banked composition idioms. Lives in =~/.dotfiles=; archsetup drives edit/test/commit/push end to end + inbox note. Visual result gets a manual-testing checklist entry (daily-driver panel).
*** TODO Phase 5 — Level-2 generator go/no-go :design:
After ~5 hand ports, weigh widget-level codegen with evidence (mechanical duplication vs judgment per port). Recorded as a dated decision in the spec; go spawns its own spec.
*** TODO Flip the spec to IMPLEMENTED
When the phases above close: status heading keyword → =IMPLEMENTED=, dated history line with the reason, Metadata =Status= mirror. Three lines, one file.
*** 2026-09-13 Sun @ 07:21:18 -0500 Filed the two Maeda applets into the clock display references
The Line (C2, 1997) and Cosmos (C1, 1995) standalone applets sent from the
website project on 2026-07-30 sat in inbox/ as processed files; they and
their notes now live beside the other references as
=2026-07-30-maeda-{line,cosmos}-standalone.html= and =-notes.org=.
Reference only, regenerate rather than edit.
** TODO [#B] Net doctor expansion v1 — VM live verification :feature:dotfiles:network:solo:
:PROPERTIES:
:SPEC_ID: ce29b103-ed9d-4f56-bf8c-9ed8fe680ff3
:LAST_REVIEWED: 2026-08-25
:END:
Build the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion]] (IMPLEMENTED). Adds the control-plane cluster (rival-manager / nm-masked / keyfile-perms) and a sharper auth verdict to the shipped net doctor (=~/.dotfiles/net/=). Archsetup owns the dotfiles work end to end — edit, test, commit, and push in =~/.dotfiles=, then drop an inbox note. All build phases shipped and fake-verified; the one open piece is the VM live verification below.
*** 2026-07-11 Sat @ 02:47:47 -0500 Built the read-only control-plane probe
New module =net/src/net/control_plane.py= plus =diag.py= wiring, on dotfiles main (=21ca3ff=, pushed). =net diagnose= now carries a =control_plane= key in its envelope with three read-only signals: NetworkManager state (masked/failed/stopped/active, finer than the plain =is-active= the ladder used), any rival manager active alongside NM (dhcpcd, systemd-networkd, iwd via =systemctl is-active=), and the active profile's keyfile permissions (=stat= under an env-overridable =NET_NM_CONNECTIONS= root, default =/etc/NetworkManager/system-connections=). Detection only: no classifier change, no new step, no bearing on =overall=. Surfaced via =net diag --json=. The masked/failed read also runs in the nmcli-down early-return path, or a masked NM would short-circuit before the probe. Every read bounded through =cmd.run='s timeout; degrades to unknown / no-rival / None on an unreadable tool or unstattable file, so a probe that can't see never invents a fault. As a normal user the real 0700 root:root dir is unstattable, so the keyfile signal reads unknown (readable only under root, e.g. the doctor's privileged path). 21 new tests, full =make test= green; =/review-code= approved (two Minor items, both Phase 1 concerns already tracked in the spec: link-scoping the rivals and guarding the keyfile path in the chmod fix). Inbox note sent to dotfiles.
*** DOING [#B] Phase 1 — control-plane verdicts + privilege model
Unblocked now that panelkit ships. =classify= gains =rival-manager=/=nm-masked=/=keyfile-perms= (all =fixable=), ordered ahead of the generic not-running rule, with the masked check reachable in the NM-down early-return path. Fixes register as =VERBS= + =ACTIONS= (=disable-rival=/=unmask-nm=/=chmod-keyfile=), each narrowly scoped. Live verification needs a real rival/masked/bad-keyfile state (a VM or a deliberately-broken host), so not solo.
**** 2026-07-11 Sat @ 04:32:06 -0500 nm-masked verdict shipped + first panelkit integration
On dotfiles main (=d73eba6=, pushed). The clean, unambiguous verdict of the three, done first to prove the panelkit-in-net rails: =classify= reads =control_plane.nm_state=; on =masked= it returns a fixable =unmask-nm= verdict (=remedy_class="privileged"=) ahead of the generic nm-service rule (=nm-restart= can't start a masked unit). New priv verb =unmask-nm= (=systemctl unmask NetworkManager=), =repair_unmask_nm= (unmask → start → verify), narration entry. The doctor resolves a privileged verdict through =panelkit.resolve()= before running: can't-elevate (GUI, no passwordless, no tty) degrades to the manual guide instead of attempting; existing tiers (no remedy_class) run unchanged. The =net= shim + test add =panelkit/src= to sys.path. Design call shipped (flagged for Craig): CLI =net doctor --fix= is the deliberate act satisfying the Confirm floor, so no CLI prompt added; panelkit contributes the run/prompt/guide degradation on the CLI; the GUI arm-press stays its confirm (panel wiring pending). 765 net tests + 65 suites green, review-code clean. Inbox note sent.
**** 2026-07-11 Sat @ 05:16:24 -0500 rival-manager + keyfile-perms verdicts shipped
On dotfiles main (=c9f8604=, pushed). Craig approved proceeding with the default precedence. =classify= adds =disable-rival= (rival dhcpcd/networkd/iwd active + a failing link) and =chmod-keyfile= (bad-perms keyfile + a failing link), both =remedy_class="privileged"= through the same panelkit gate. PRECEDENCE settled: both gate on a local-link symptom (never nag next to a working link), sit behind rfkill/service (more fundamental) and ahead of the generic reset (naming the cause beats a blind reconnect); tested (rfkill beats rival, rival beats reset). Priv verbs =disable-rival= (three-unit allowlist), =chmod-keyfile=/=chown-keyfile= (paths validated under system-connections, no traversal). Repairs re-derive their target (=active_rivals= / the active connection). =fake-systemctl= gained a state dir so a disable flips the re-check. 782 net tests + 65 suites green, review-code clean. Two coverage edges deferred to the VM: the keyfile fix's root-owned pass (only reproduces under root), and whether disable-rival needs a follow-on reset to restore the link. All three Phase 1 verdicts now fake-complete; VM live-verification is the remaining piece (see the sub-task).
**** TODO Net Phase 1 live verification — run the scenario harness against a VM
The harness is BUILT (archsetup =a364c1e=): =scripts/testing/net-scenarios/{10-nm-masked,20-rival-manager,30-keyfile-perms}.sh= (break/fix/assert + the expected diagnose verdict) and =run-net-scenarios.sh= (rsyncs net+panelkit into a target over ssh, drives the scenarios). Scenario logic reviewed; the ssh transport plumbing is UNEXERCISED (marked first-draft) and wants a shakeout on the first real run. What remains needs Craig: a booted VM (a real kernel + network stack — NM does NOT run reliably in nspawn, so a container only shows the mask-symlink/keyfile-mode half, not "NM active") with NetworkManager + dhcpcd installed, reachable over ssh as root. Then =run-net-scenarios.sh --target root@HOST --profile <saved-profile>=. The two live questions: disable-rival was already chained to a follow-on reset (=be15a81=), so the run confirms whether the reset fires or NM auto-recovers; and the keyfile root-owned pass (only reproduces under root). The by-hand equivalent is the "Manual testing and validation" → "Net doctor privileged fixes" checklist.
***** 2026-07-21 Tue @ 08:20:00 -0500 Decided (Craig): agent runs it via the archangel test VM, deferred to a later session
No longer "needs Craig to provide a VM." The VM is the archangel test harness (=scripts/testing/create-base-vm.sh= + =vm-utils.sh=; ISO in =~/archangel-isos/=). Plan: in a later session the agent boots a fresh archangel VM (NetworkManager + dhcpcd), then runs =run-net-scenarios.sh --target root@HOST --profile <saved-profile>= against it, exercising the unexercised ssh transport on the first real run and answering the two live questions above. A 40-60 min VM operation; agent-driven, checkpointing and surfacing if it needs Craig's hands. This makes the sub-task agent-workable rather than blocked.
*** 2026-07-11 Sat @ 06:31:29 -0500 Built the sharpened auth verdict
On dotfiles main (=12e3e76=, pushed). =gather_context= derives an auth cause on an auth failure from the saved profile's key-mgmt + the scanned SECURITY flags for the SSID (not GENERAL.REASON, which only marks *that* auth failed): sae / hidden / enterprise / generic. =classify= turns sae and hidden into =fixable= Privileged profile-modifies (=key-mgmt sae= + PMF, or the hidden flag) routed through =panelkit.resolve()= like the Phase 1 control-plane verdicts; enterprise and generic stay =needs-user-action= with a cause-named message. Two new priv verbs (=conn-sae=, =conn-hidden=, uuid-validated single =nmcli connection modify= commands) + =repair_auth_sae=/=repair_auth_hidden=, each chained into a reset (FIX_CHAINS) so NM reconnects with the corrected profile. Hidden fires only on a non-empty scan missing the SSID, so a failed scan can't fabricate it. Tightened the =fake-nmcli= key-mgmt hook to require the =-g= prefix so the conn-sae modify isn't read back as a get_value. Pairwise (reason × profile-state) covered in =TestAuthCauseDerivation=. 813 net tests + 65 suites green, review-code Approve, voice. Inbox note sent. Live half (real WPA3/hidden profile-modify) is the VM/manual checklist.
*** 2026-07-12 Sun @ 09:14:00 -0500 Flipped the net spec to IMPLEMENTED and logged the vNext items
Spec status heading now IMPLEMENTED (dated history line + Status mirror); all four phase headings DONE. vNext items (flaky/drops cluster, DoT/DNSSEC verdict, profile hygiene) logged as the "Net doctor vNext" task. The privileged-fix live halves remain with the VM live-verification sub-task and the manual-testing checklist — findings there come back as bugs.
** DOING [#B] Run-time privilege model, standard across every panel doctor :feature:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
The audio input/output doctor is gaining a run-time privilege model (see [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]], decision "The doctor may use sudo, resolved by context at run time"). Craig's call, 2026-07-10: make it a standard, "revise the other panels to be consistent with these changes."
The model: a doctor resolves its privilege at run time from three signals — passwordless sudo available (=sudo -n true=, which never hangs), a tty to prompt at, and whether it is the GUI panel. Four remedy classes: Auto (user-scope, reversible), Privileged (needs sudo — runs where passwordless, prompts on a CLI tty, degrades to Guide in a GUI with neither), Reboot-tail (run the applicable part, then instruct the reboot), and Guide (physical/BIOS/wait-for-upstream, nothing to run). Safety floor: every Privileged and Reboot-tail remedy defaults to Confirm or Arm tier, never silent Auto, because passwordless sudo is not consequence-free.
The shared helper is built (see the dated entries below), maint is reconciled onto it, and net is wired: =classify.py= carries the =remedy_class= on its privileged verdicts and =doctor.py= resolves each through =panelkit.privmodel.resolve()= (net Phase 1, shipped 07-11). Adoption is the gate only — every panel's repair actions already exist; what adoption changes is whether and how an existing privileged action is allowed to run (RUN where passwordless, PROMPT on a CLI tty, GUIDE in a GUI), under the Confirm/Arm floor. What remains, checked against the tree 2026-08-25: bluetooth is part-wired (=bt/doctor.py= makes one =resolve(PRIVILEGED, ...)= call, no per-remedy classes yet — audit its individual fixes against the floor), and audio has nothing on the doctor side (pending the input-side spec). Each needs a real privileged host to verify =--fix= end to end, so not agent-solo.
Craig's decision, 2026-07-12: maint's harmless-reclaim privileged remedies (the silent CLEAN UP set — paccache keep3, journal vacuum, coredump clean) STAY silent-auto. The reconciliation gives that class a sanctioned, documented exception to the confirm floor rather than forcing Confirm/Arm; the value of the floor holds for everything else. Where sudo is not passwordless, maint should degrade per the model (prompt on a tty, guide in a GUI) instead of hard-failing.
*** 2026-07-11 Sat @ 03:48:57 -0500 Built the shared privilege model (panelkit)
On dotfiles main (=b987820=, pushed). Craig chose to share a library, not per-panel copies. New =panelkit= package (=panelkit/src/panelkit/=): pure library code in the dotfiles tree, no CLI, no stow package — a consumer reaches it by adding =panelkit/src= to =sys.path= alongside its own =<panel>/src= and =from panelkit import privmodel=. Self-contained (its own =cmd.py=) so a panel plus panelkit stays a coherent unit (keeps the pluggable-panel option open). =privmodel=: four classes (=auto=/=privileged=/=reboot-tail=/=guide=), =resolve(remedy_class, ctx)= → a =Resolution= (=RUN=/=PROMPT=/=GUIDE_ONLY=, =confirm=, =reboot_after=, =reason=) over a =PrivContext= (passwordless via =sudo -n true=, tty, is-GUI). Safety floor enforced and property-tested across all 8 context combos: every privileged/reboot-tail remedy that runs requires a deliberate confirmation, never silent auto. =PANELKIT_SUDO= test seam (="true"=/="false"=), =detect_context()= + =plan()= convenience. 19 tests, full =make test= green (65 suites), review-code clean. Inbox note sent to dotfiles. NOT wired to a consumer — that's the per-panel adoption above.
*** 2026-07-12 Sun @ 09:01:25 -0500 Reconciled maint onto the shared privilege model (dotfiles 7937a9e)
maint's doctor now resolves every privileged step through =panelkit.privmodel= instead of the old always-=sudo -n= hard-fail: RUN where passwordless (unchanged), PROMPT on a CLI tty (plain sudo, preceded by a wall note), GUIDE events in a GUI with neither — the typable command on the wall, nothing executed. User-scope steps never resolve; a macro's user steps still run around guided privileged ones. Craig's silent-auto decision (above) is implemented as the documented exception: recorded in =maint/remedies.py= (tier semantics) and =panelkit/privmodel.py= (the floor's own docstring), waiving only the confirm gesture, only for the auto-tier reclaim set. TDD (15 new tests red→green incl. both wall renderers), full suite 65/65, live-verified RUN + GUIDE end-to-end on this host via the real launcher shim. Bonus root-cause fix caught by the live run: =probes/logs.py= crashed on journalctl MESSAGE=null entries (key present, so the =.get= default never applied), which broke every real fix's re-probe — normalized at =_journal_lines= with a regression test. Remaining under this task: net Phase 1 / bt Phase 2 / audio adoption (queue items 2-3), and the PROMPT-path + GUI-wall manual checks (see Manual testing and validation).
** TODO [#C] Scrolling/Carousel layout: frame fit + wrap-around :hyprland:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-08
:END:
Demoted [#B] → [#C] at the 2026-08-08 review: no motion in two months —
parking lot until the frame-fit design gets real interest.
Disabled 2026-06-12 (bind and cycle entry points removed; Super+Shift+S reassigned to whole-desktop screenshot). The layout needs real work before it earns its chord back:
- What fits in each frame: column/frame sizing so windows land at usable widths instead of arbitrary slices.
- Wrap-around: navigating past the last frame should wrap to the first (and vice versa).
- Whatever else surfaces in daily use once the above land.
The support machinery was deliberately kept for this task: =layout-navigate= and =layout-resize= retain their scrolling branches, =waybar-layout= still renders the scrolling state, and the unbound legacy =cycle-layout= script still lists it. Re-enabling is two lines: add =scrolling= back to =LAYOUTS= in =layout-cycle= and restore a direct-jump bind (the old chord is taken now — pick a new one). The =tests/layout-cycle= suite pins the disabled state and will go red on re-enable, which is the reminder to update it.
** TODO [#B] Audit dotfiles/common directory :chore:dotfiles:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
Refiled from the archsetup task audit (2026-06-28), landed via ~/.dotfiles/inbox; the dotfiles content split into its own repo 2026-06-16 but the task tracking stays here per Craig (2026-07-02). Three parts:
- Review all 50+ scripts in =~/.local/bin= and remove unused ones.
- Check dotfiles for uninstalled packages and remove orphaned configs.
- Verify all stowed files are actually used.
*** 2026-07-21 Tue @ 08:30:00 -0500 The evidence report's ref counts are UNRELIABLE — redo the scan before any kill pass
Spot-checking the "zero references" list against the live tree during the 2026-07-21 task audit found the report is badly wrong: 12 of a 14-script sample flagged refs=0 are actively invoked. The scan missed the primary invocation paths — =hyprland.conf= =bind=/=exec-once= lines, waybar config =exec=/=on-click= handlers, pypr scratchpad configs, and =profile.d= autostart. Confirmed live examples: =layout-resize= (bound mod+H/L window resize), =net-fix= (waybar net right-click), =start-hyprland= (session launcher, profile.d + startx alias), =wait-for-tray= (exec-once gating signal-desktop tray), =waybar-layout=/=waybar-worldclock= (live waybar modules, worldclock has a test), =stash-window=/=stash-others=/=stash-restore= (bound mod+O family), =hypr-refocus-scratchpad= (exec-once), =monitor-dashboard= (pypr scratchpad on ratio+velox). Only =toggle-scratchpad= and =waybar-disk= came back genuinely unreferenced in that sample.
ACTION before the kill pass: redo the reference scan to grep all invocation sources — =hyprland.conf= (bind/binde/bindm/exec-once/exec), =waybar/config= (exec/on-click*/on-scroll*), =pypr/config.toml=, =profile.d/*=, systemd units, and other scripts — then re-bucket. Nothing was deleted; the current report can't be trusted for deletions. The three orphan config dirs (audacious, wofi, ranger) are independent of the script scan and still stand as candidates.
*** 2026-07-14 Tue @ 01:40:48 -0500 Built the audit evidence report
Shipped as =docs/2026-07-14-bin-audit-evidence.org= in the dotfiles repo (260752a). 150 scripts bucketed: 69 keep (referenced or cron-driven), 74 kill candidates (zero references in the tree), 7 flagged (all dwm-tier, expected on a hyprland host). Config sweep: audacious and wofi configs are orphan candidates, ranger needs an install-vs-delete call (declared in archsetup but not installed on ratio). Shell history was too shallow (~700 lines) to prove by-hand disuse either way — the kill pass stays Craig's call in the parent task.
** TODO [#C] net vpn CLI subcommand :feature:network:dotfiles:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-21
:END:
=cli.py= in the dotfiles =net/= package has no vpn/tunnel parser, so everything
the panel can already do with tunnels has no command-line equivalent. Fold the
panel's existing tunnel operations into a =net vpn ...= surface mirroring what
the Tunnels sub-view does — bring an overlay up, take it down, report status.
The operations themselves already exist and are tested: dotfiles =2d9d060=
probes tailscale / NM-wireguard / Proton, =21db05a= brings overlays up and down
from the panel, =31ba056= taught diagnose and doctor to understand tunnel
routes, and archsetup =2e40781= imports wireguard configs. This is a CLI surface
over shipped behavior, not new capability.
Graded [#C] rather than [#B]: the panel already does the job, so this is
convenience rather than a gap. It earns a bump if I find myself wanting tunnel
control from a bare TTY — which is the same recovery-path argument that made the
rest of =net= worth having as a CLI.
=:solo:= — the subcommand shape is obvious (it mirrors the panel), the =net=
package's fake-based harness covers the build and verify path, and archsetup owns
the dotfiles work end to end. Not =:quick:=: every prior net phase landed with
twenty-odd new tests and a review pass, so this runs past a spare moment.
Carved out of the =custom/net= umbrella when that closed on 2026-08-21.
** TODO [#B] Local offline LLM runtime + per-host model cache :tooling:llm:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-08
:END:
Add a local-LLM provisioning track so machines can run an offline coding agent when there's no network. Install =llama.cpp= (CPU + Vulkan where practical) and prefetch per-host model files while network is available; expose OpenAI-compat local endpoints (=127.0.0.1:8081= coding, =:8082= general; =:11434= reserved for =ollama= if used). Per the rulesets generic-agent-runtime design pass — rulesets becomes runtime-neutral and owns the runtime manifests + project instructions; archsetup owns machine provisioning + the per-machine model inventory. Source: handoff from rulesets 2026-05-28 ([[file:assets/outbox/2026-05-28-from-rulesets-local-llm-install.org][outbox copy]]).
Per-host model targets (from the handoff):
- *ratio* (Strix Halo, 128 GiB) — Qwen3-Coder-30B Q6_K (default) + Q4_K_M (compat) + Qwen3-Next-80B Q4_K_M (long-context fallback).
- *velox* (i7-1370P, 64 GiB iGPU) — Qwen3-Coder-30B Q4_K_M + an 8B fallback for low-latency triage.
Install behavior: prefetch idempotent (skip if file exists, match size/hash); download failure must NOT fail the install — surface a clear "local LLM support incomplete" follow-up instead. Ship a smoke-test command (boot endpoint + short prompt).
Decisions to resolve before code:
*** TODO Decide model cache location: per-user vs system-wide
Handoff lists both =~/.local/share/llm/models= (per-user) and =/srv/models/llm= (system-wide). Per-user matches the existing archsetup user-config style and avoids root ownership of large model files. System-wide matches the "machine-local model inventory" phrasing and shares cache across users on multi-user boxes (not the case here — single user per machine). Pick one as the default; the other stays available via =LLM_MODEL_CACHE=.
*** TODO Decide whether =ollama= ships by default or is opt-in
Handoff calls =ollama= "optional". Likely shape: =llama.cpp= is the only mandatory runtime; =ollama= behind =INSTALL_OLLAMA= (default no) for users who prefer its model-manager API. Confirm.
*** TODO Define config keys for the LLM block in =archsetup.conf.example=
Likely: =INSTALL_LOCAL_LLM= (default yes), =LLM_RUNTIME= (=llama.cpp= / =ollama=), =LLM_MODEL_CACHE= (path), =LLM_MODELS= (space-separated, or empty → per-host autodetect). Lock names + defaults before writing install code.
*** TODO Decide per-host model selection: auto-detect by =uname -n= vs explicit =LLM_MODELS=
Auto-detect against a known-host table (ratio → Q6_K + 80B, velox → Q4_K_M + 8B) is simple for current machines but brittle for any new host (silently picks no models). Explicit =LLM_MODELS= per machine in =archsetup.conf= is more verbose but never surprises. Pick the default; the other stays available.
*** TODO Decide network-down behavior for model prefetch
Three shapes: (a) emit =error_warn= and write =/var/lib/archsetup/state/llm-models-pending= for inspection; (b) install a one-shot systemd unit that retries on next boot with network; (c) just log and forget — user re-runs the prefetch helper manually when network returns.
Implementation work (gated on the decisions above):
*** TODO Install =llama.cpp= with CPU + Vulkan backend where supported
Add to the appropriate install section in =archsetup= (=llama.cpp= / =llama.cpp-vulkan= in AUR). Decide CPU-only vs Vulkan per host from the hardware detection already used for GPU drivers.
*** TODO Install =ollama= behind config flag (if Decision 2 = opt-in)
Add =ollama= package install gated on =INSTALL_OLLAMA=yes=.
*** TODO Configure shared model cache + OpenAI-compat local endpoints
Create =$LLM_MODEL_CACHE= with the right ownership; configure llama.cpp (and ollama if installed) to serve =127.0.0.1:8081= (coding) and =:8082= (general). Likely systemd user units; decide launcher pattern when implementing.
*** TODO Prefetch per-host models (idempotent, non-fatal on network failure)
Download the per-host model set (from Decision 4) into the cache; skip files that exist with matching size/hash. On failure, fall back per Decision 5. Models from HuggingFace GGUF mirrors (URLs locked at implementation time).
*** TODO Ship a local-LLM smoke-test command
Boot the configured endpoint and send a short prompt; surface success/failure + timing. Useful as both a post-install check and a triage tool when something later breaks. Likely =scripts/llm-smoke-test.sh=; runs at end of install if =INSTALL_LOCAL_LLM=yes=.
Acceptance: fresh VM install of the ratio profile reaches an endpoint on =:8081= that answers a smoke prompt; velox profile gets Q4_K_M + 8B and answers a prompt within reasonable laptop latency; network-down install completes successfully with the pending-models warning surfaced.
** TODO [#B] Test + CI infrastructure :test:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
Umbrella for the test-harness and CI-automation buildout. Consolidated from the 2026-06-28 task audit: these were scattered top-level tasks circling one effort, re-homed as children so the work reads as a unit. Each child ships independently and keeps the priority it carried before. No CI runner exists yet, so the CI/CD-pipeline child gates several of the others.
*** TODO [#B] Build CI/CD pipeline that runs archsetup on every commit
:PROPERTIES:
:LAST_REVIEWED: 2026-06-13
:END:
Core automation infrastructure - enables continuous validation
*** TODO [#B] Generate recovery scripts from test failures
:PROPERTIES:
:LAST_REVIEWED: 2026-06-13
:END:
Auto-create post-install fix scripts for failed packages - makes failures actionable
*** TODO [#B] Establish monthly review workflow
:PROPERTIES:
:LAST_REVIEWED: 2026-06-13
:END:
The diff engine now exists (=scripts/package-inventory= / =make package-diff=), so what remains here is the cadence, not the tooling: a scheduled prompt to run the diff and act on it. Subtasks 1-2 are the recurring human judgment the engine feeds; subtask 3 is the automation to schedule it.
**** TODO [#B] For packages in archsetup but not on system: determine if still needed
**** TODO [#B] For packages on system but not in archsetup: decide add or remove
**** TODO [#B] Schedule monthly package diff review
*** TODO [#B] Set up automated test schedule
:PROPERTIES:
:LAST_REVIEWED: 2026-06-28
:END:
Weekly full run to catch deprecated packages even without commits
*** TODO [#B] Implement manual test trigger capability
:PROPERTIES:
:LAST_REVIEWED: 2026-06-28
:END:
Allow on-demand test runs when automation is toggled off
*** TODO [#B] Create test results dashboard/reporting
:PROPERTIES:
:LAST_REVIEWED: 2026-06-28
:END:
Make test outcomes visible and actionable
*** TODO [#B] Block merges to main if tests fail
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Enforce quality gate - broken changes don't enter main branch
*** TODO [#B] Add network failure testing to test suite
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Simulate network disconnect mid-install to verify resilience
*** TODO [#B] Keep VM base images up to date
:PROPERTIES:
:LAST_REVIEWED: 2026-06-28
:END:
Regular updates to the Arch base VM image (qemu, built by =create-base-vm.sh=) with a review process and schedule. The harness is VM/qemu-based, not containers.
*** TODO [#B] Persist test logs for historical analysis
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Archive logs with review process and schedule to identify failure patterns and trends
*** TODO [#B] Implement automated deprecation detection
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Parse package warnings and repo metadata to catch upcoming deprecations proactively
*** TODO [#B] Monitor and optimize test execution time
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Keep test runs performant as installs and post-install tests grow (target < 2 hours)
*** TODO [#B] Set up alerts for deprecated packages
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Proactive monitoring integrated with testing
*** TODO [#C] Fix VM cloning machine-ID conflicts for parallel testing :no-sync:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Currently using snapshot-based testing which works but limits to sequential test runs
Cloned VMs fail to get DHCP/network even with machine-ID manipulation (truncate/remove)
Root cause: Truncating /etc/machine-id breaks systemd/NetworkManager startup
Need to investigate proper machine-ID regeneration that doesn't break networking
Would enable parallel test execution in CI/CD
Priority C because snapshot-based testing meets current needs
** TODO [#C] Review undeclared ratio packages for installer inclusion :chore:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-21
:END:
Triggered by the 2026-06-14 =make package-diff= run on ratio: 62 packages are installed but not declared in archsetup. Stripped of the structural buckets — pacstrap base/boot/kernel (base, linux*, grub, efibootmgr, sudo, btrfs-progs, fwupd, logrotate, ex-vi-compat, linux-lts-strix, zram-generator), the =make deps= VM set (qemu-full, virt-manager, virt-viewer, libguestfs, bridge-utils, dnsmasq, archiso), and the yay bootstrap — these 40 remain. Check the ones to add to the installer, then rerun =make package-diff= to confirm they clear.
Evidence report (2026-07-14, count now 64): [[file:docs/design/2026-07-14-undeclared-packages-evidence.org][docs/design/2026-07-14-undeclared-packages-evidence.org]] (archsetup 78081e4) — description, requirer, and install date per package, bucketed candidates (30) / dependency-pulled (6) / orphan libraries (7) / structural (21). The include/ignore pass reads the candidates bucket; the checklist below stays until Craig walks it.
Some entries are libraries likely pulled in as dependencies (blas-openblas, openblas, eigen, tk, lib32-openal, pkcs11-helper, gtk4-layer-shell, webkit2gtk, sane, freerdp, rust-bindgen) — check those only if you want them declared explicitly rather than left to dependency resolution.
git-lfs, imv and libreoffice-fresh are ticked: the installer declares all three
as of 2026-09-25, so a later walk of this list should skip them rather than
re-deriving the case for each. The rest of the list is untouched.
- [ ] aws-cli-v2
- [ ] bats
- [ ] blas-openblas
- [ ] drawio-desktop
- [ ] eigen
- [ ] emacs
- [ ] flatpak
- [ ] freerdp
- [ ] geeqie
- [X] git-lfs
- [ ] github-cli
- [ ] gtk4-layer-shell
- [ ] hugo
- [X] imv
- [ ] lc0
- [ ] lc0-network-sm
- [ ] ledger
- [ ] lib32-openal
- [X] libreoffice-fresh
- [ ] minidlna
- [ ] openai-codex
- [ ] openblas
- [ ] pacoloco
- [ ] pkcs11-helper
- [ ] proton-vpn-cli
- [ ] proton-vpn-daemon
- [ ] protontricks
- [ ] python-lyricsgenius
- [ ] python-pip
- [ ] python-pipx
- [ ] python-sphinx
- [ ] rust-bindgen
- [ ] sane
- [ ] shortwave
- [ ] spotify-launcher
- [ ] tidal-dl-ng
- [ ] tk
- [ ] typescript-language-server
- [ ] webkit2gtk
- [ ] whisper.cpp
*** 2026-08-21 Fri @ 14:28:18 -0700 Dropped to [#C], and the sharper measurement is on velox now
Re-graded [#B] → [#C]. Not a change of mind about the value — nothing has been
ticked since I filed it on 2026-06-14, across two full cycles, and by my own
scheme [#B] means "this cycle" while [#C] is the parking lot. The grade should
say where it actually sits.
The premise also moved. This list measures ratio, which carries years of
accumulated manual installs tangled up with whatever archsetup put there, so a
package being undeclared says little about whether it matters. Velox is the
better instrument now: rebuilt from archsetup on 2026-08-13 and working, so a
=make package-diff= there compares what the installer declares against what a
machine actually needs, with only days of drift on top. Re-run it on velox
before walking these forty by hand.
Worth noting the empirical result already came in. The gaps that actually hurt
after that rebuild — rulesets never cloned, the .emacs.d systemd units never
linked, the missing =*.local.*= configs — surfaced on their own, and not one of
them is on this list. That is evidence about what this kind of list catches.
Related but distinct: =[#B] Installed-package drift audit= below builds the tool
for the opposite direction (declared but missing, and provider substitutions).
If that lands first it plausibly subsumes the detection half of this one, leaving
only the include/ignore judgment.
** TODO [#B] Installed-package drift audit :chore:packages:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-02
:END:
Compare the packages explicitly declared by =pacman_install= / =aur_install=
against the installed system, separately from the existing inventory's
unexpected-package review. Report missing declared packages and substitutions
where a provider is installed instead of the named package (the ratio
=emacs= versus =emacs-wayland= case). It must not make package changes.
*On demand only* (Craig, 2026-08-02) — a command I run when I want it, not a
timer and not a startup line. It is the only mode that cannot nag me, and the
other two stay cheap to add later once the report has proven itself. This was
the open decision that kept the task off =:solo:=; with it answered the rest is
mechanical, so the tag goes on.
From the rulesets Emacs/package-audit handoffs, 2026-07-16. The existing
=scripts/audit-packages.sh= validates repository availability, not installed
machine state.
** TODO [#B] Security hardening + audit :security:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
Umbrella for the security-hardening and audit effort. Consolidated from the 2026-06-28 task audit, re-homing the scattered security tasks as children so the work reads as a unit. Each child ships independently and keeps its prior priority.
*** TODO [#B] Test security + functionality together
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
**** TODO [#B] Verify no unexpected open ports or services
*** TODO [#B] Security audit tooling
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
**** TODO [#B] Implement port scanning check
**** TODO [#B] Create security posture verification script
**** TODO [#B] Set up intrusion detection monitoring
*** TODO [#B] Document threat model and mitigations
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Identify attack vectors, what's mitigated, what remains
*** TODO [#B] Security education
:PROPERTIES:
:LAST_REVIEWED: 2026-06-24
:END:
Read recommended resources to make informed security decisions (see metrics for Claude suggestions)
*** TODO [#B] Create security checklist for cafe/public wifi scenarios
:PROPERTIES:
:LAST_REVIEWED: 2026-05-21
:END:
Practical guidelines for working in public spaces
** TODO [#A] Ensure sleep/suspend works on laptops
SCHEDULED: <2026-09-25 Fri>
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
Raised [#B] → [#A] and scheduled at the 2026-08-08 review: Craig leaves on
vacation ~2026-08-15 and velox is the travel machine — suspend and battery
drain must be verified working before departure. Verification plan: apply the
params, suspend velox, measure overnight drain; the hands-on resume check is
Craig's (a manual-testing entry rides the fix).
While on velox for this (out for repair until ~2026-08-11/12, so these ride
along when it returns):
- Append =DEVICES_TO_ENABLE_ON_STARTUP="bluetooth wifi"= to its
=/etc/tlp.d/01-custom.conf= (the installer now writes it; velox predates
that) and confirm radios unblocked after reboot.
- Pull dotfiles so the touchpad auto-detection lands; spot-check
=touchpad-auto --detect= prints the pixa name on real hardware.
- Set up the wolf WireGuard profile (Craig's 2026-08-08 decision: velox
should reach home over wolf on the road; profile exists on ratio only).
- Enable the rootless podman socket and install the
=72-usb-passthrough-cameras.rules= file (both live on ratio as of
2026-08-09; the installer now ships both, velox predates it).
Critical functionality for laptop use - current battery drain unacceptable
*NOTE:* This applies to Framework Laptop (velox), not Framework Desktop (ratio)
Add kernel parameter: ~rtc_cmos.use_acpi_alarm=1~ (will become systemd default)
Consider: ~acpi_mask_gpe=0x1A~ for battery drain, suspend-then-hibernate config
See Framework community notes on logind.conf and sleep.conf settings
*** 2026-08-17 Mon @ 19:57:42 -0700 Four of the five riders are done; WireGuard is the one left
The riders were written for "when velox returns from repair". It came back as
a full reinstall instead, and the installer carried most of them, so I checked
each on the live machine rather than reading the list back:
- tlp radio-enable — done. =/etc/tlp.d/01-custom.conf:10= carries
=DEVICES_TO_ENABLE_ON_STARTUP="bluetooth wifi"=, written by the installer.
- touchpad auto-detection — the dotfiles half is done: =touchpad-auto
--detect= prints =pixa3854:00-093a:0274-touchpad=. Read that carefully
though — it names the device the config expects, not a device delivering
events. The touchpad is still dead on the ribbon fault, so this rider is
satisfied and the hardware still is not.
- podman socket — done, =podman.socket= is enabled.
- camera udev — done, =72-usb-passthrough-cameras.rules= is installed.
- *wolf WireGuard — not done, and it is the one that was time-critical.* No
=~/.config/wireguard/wolf.conf.gpg= and no WireGuard profile in
NetworkManager. The 08-08 decision set this up specifically so velox could
reach home from the road, on the argument that it is cheap at home and
expensive from a hotel. velox is now in the hotel.
The suspend work itself is untouched — no kernel parameter, no drain
measurement. Only the riders moved.
*** 2026-08-25 Tue @ 11:57:48 -0600 Logged the two Aug 23 hibernate-leg failures
suspend-then-hibernate failed its hibernate leg twice on 2026-08-23 (21:06 and
23:29): "Failed to put system to sleep. System resumed again: Device or
resource busy". Noticed during the 08-24 Lua-port session and parked there;
filed here at Craig's direction so the sleep task carries it. Nothing
diagnosed yet — first step is =journalctl -b -1 -u systemd-suspend-then-hibernate=
around those timestamps to see which device reported busy.
*** 2026-08-26 Wed @ 16:16:08 -0600 Diagnosed the hibernate battery drain: three separate faults, one task each
Craig hibernated twice in ten days and found the battery dead both times. Read
all 39 boots since the 08-13 reinstall, upower's charge history
(=/var/lib/upower/history-charge-Framewo-55-03F5.dat=, root-only, starts
08-19), sysfs, and the scripts inside =/efi/EFI/ZBM/zfsbootmenu.efi=.
Hibernate is configured right and has worked: five hibernate+resume cycles
since reinstall (08-13, 08-17 14:31, and three suspend-then-hibernate cycles on
08-20/21). The two fatal events are the two overnight explicit
=systemctl hibernate= runs, 08-17 22:20 and 08-21 19:58. Both journals end at
"PM: hibernation: hibernation entry"; the next power-ons (08-18 10:13, 08-22
17:22) were fresh boots whose resume hook found no image, no later swapon
reported a leftover suspend signature, and on 08-22 the battery read 2% at
power-on. The 08-23/24 night was on AC and not a battery death (three suspends
failed to enter, machine awake all night at the charge limit; the 09:57 end was
three power-key presses and a hard cut at 63%). The 08-19 death was the
caffeine/hypridle one already diagnosed.
Three faults, tracked as the children below:
- Hibernate hard-freezes on entry (documented on Framework 13 AMD incl. Ryzen
AI 300: black screen, never powers off, intermittent, amdgpu-side). Fits
everything: the freeze precedes the swap signature, so the next boot is
fresh, and a frozen laptop at ~5 W empties 44.7 Wh in ~8 h. Unprovable from
logs by nature; the alternative (completed hibernate, unattended power-on to
the ZBM passphrase prompt) predicts a surviving image, which neither boot
had — see the VERIFY.
- ZFS ARC starves the hibernate image: "Image allocation is 8118265 pages
short" today 14:09, "390678 pages short" 08-20 09:14. ARC 58 GB of 93,
=zfs_arc_max=0= so =c_max= = RAM − 1 GiB; the kernel must free RAM −
=image_size= (37.4 GB) ≈ 56 GB. systemd falls back to s2idle and retries
every 90 min, so suspend-then-hibernate never actually hibernates.
- The SD card reader (090c:3350, =sda=, no media) can block suspend entirely:
"Freezing remaining freezable tasks failed after 20s (wq_busy=1)", pending
=disk_events_workfn= on =events_freezable_pwr_efficient=, three times on
08-23/24. On battery that is a dead laptop by morning.
Mistake worth remembering: =journalctl --since … -k= silently limits itself to
the current boot (=-k= implies =-b=); cross-boot kernel facts need
=_TRANSPORT=kernel= or an explicit =-b=.
*** 2026-10-05 Mon @ 22:18:03 -0600 The bluetooth resume hook never ran: it was installed where systemd-sleep doesn't look
Bluetooth came back rfkilled after tonight's suspend-then-hibernate (S4 wake
19:43 MDT, hci0's rfkill entry recreated at resume, bluetoothd "Failed to set
mode: Failed (0x03)"), the same double fault the 08-21 hook was written for.
The hook had never fired: btusb was deregistered only twice since 08-20, both
by hand (the 08-21 07:39 test and tonight's 20:06 relief), across about
fifteen hibernate cycles. Cause: the installer created
=/etc/systemd/system-sleep/= and put the hook there, and systemd-sleep (262,
and 261 before it) scans only =/usr/lib/systemd/system-sleep/=. That is the
one path in the binary and the one the man page names, which is also why Arch
ships no =/etc= directory. TLP's hook lives in =/usr/lib= and does run, and
=tlp resume= restores the rfkill state it saved at suspend, so once bluetooth
is blocked at any sleep edge TLP carries the block across every later cycle.
Fixed tonight: the installer now installs to =/usr/lib/systemd/system-sleep/=
(the step's test pins the full install list, so a copy under =/etc= fails it),
and the live hook on velox was moved there with the dead =/etc= directory
removed. A hand run from the new path exits 0. Ratio has no hook in either
directory and no TLP, so nothing to do there. The real-cycle check rides the
Manual testing task ("Bluetooth resume hook fires on a real sleep cycle").
One caveat for that check: =man systemd-sleep= says the hooks run in parallel,
so the zz- prefix buys no ordering after TLP's hook (the hook's own comment
claims it does and needs correcting). The reload's one-second settle should
make this hook's unblock land after =tlp resume=, and the re-check step is
what shows it if the race goes the other way.
*** 2026-10-05 Mon @ 23:08:31 -0600 Real-cycle check passed: the hook fires from /usr/lib
One suspend-then-hibernate cycle on AC, RTC-woken after thirty seconds
(suspend entry 23:07:01, exit 23:07:32, s2idle). The kernel logged btusb
deregistered at 23:07:32 and registered again at 23:07:34, which is the hook's
unload, settle and load, and rfkill logged "unblock set for type bluetooth"
at 23:07:34, the hook's last command. No system-sleep line named the hook,
and afterwards rfkill read "Soft blocked: no" with the controller "Powered:
yes". The manual check under Manual testing is retired. Not proven by this
cycle: the plain-suspend race with =tlp resume='s replay (it entered with
bluetooth unblocked), and a true hibernate-class wake, which the entry-freeze
test now checks after each of its cycles.
*** TODO Hibernate entry freeze — confirm under observation, then mitigate :bug:velox:hibernate:
Interim rule until this closes: do not hibernate unattended on battery. Shut
down, or suspend on AC.
What is known: the two dead-battery hibernates match the Framework 13 AMD
"hard freeze on hibernate entry" reports (community threads 69516 and 53860,
Arch bbs 293242): screen black, power LED on, never powers off; intermittent
(one report: every 6–7 cycles); TTM/amdgpu warnings; improved by newer
=linux-firmware=; no confirmed fix. Board A9, BIOS 03.05, linux-lts 6.18.46,
=amdgpu.dcdebugmask=0x610= already on the cmdline.
Confirm first: the "Hibernate entry freeze: five observed cycles on AC" test
under Manual testing and validation. A failed cycle there is the proof the
journal cannot give.
Mitigations to try in order once confirmed, one at a time, re-running the
cycles after each: (1) =linux-firmware= at current, then =linux-firmware-git=
if the freeze persists; (2) =/sys/power/disk= = =shutdown= instead of
=platform= (a systemd =HibernateMode=shutdown= drop-in), which skips the ACPI
S4 path some Framework users found hanging; (3) a newer kernel (=linux= vs
=linux-lts=) for the amdgpu delta; (4) unload =mt7925e= in a pre-sleep hook
if the freeze survives the first three. Not =:solo:=: each cycle needs a
person watching the power LED.
*** TODO ZFS ARC starves the hibernate image — cap it or shrink it pre-hibernate :bug:zfs:velox:solo:
The arithmetic: the kernel preallocates RAM − =image_size= pages before
snapshotting; with 93 GB RAM and the default =image_size= (2/5 of RAM,
37.4 GB) that is ~56 GB, and only free memory plus what shrinkers give back
counts. ARC was 58 GB today and the ZFS shrinker released little inside the
preallocation window, so it came up 31 GiB short. Nothing in
=/etc/modprobe.d/= sets =zfs_arc_max=.
Two fixes, either or both:
- Cap the ARC: =options zfs zfs_arc_max=<bytes>= in =/etc/modprobe.d/zfs.conf=
(16 GiB leaves ~70 GB reclaimable) plus =echo <bytes> >
/sys/module/zfs/parameters/zfs_arc_max= for the running system.
- Or a =/usr/lib/systemd/system-sleep/= pre hook for the hibernate class that
lowers =zfs_arc_max=, waits for =size= in
=/proc/spl/kstat/zfs/arcstats= to fall, and restores it post-sleep. Keeps
the big ARC while awake.
- Raising =image_size= toward the kernel's ceiling (about half of RAM) also
shrinks the demand; combine with the cap.
Install it through archsetup so the next rebuild carries it (velox-only: ratio
has no swap partition).
Verify: after the change =arcstats size= drops below the cap within seconds;
then one live suspend-then-hibernate cycle on AC with the delay temporarily
short shows "hibernation exit" and no "Image allocation … short" line in the
journal. That live cycle rides the entry-freeze test above; the ARC half is
checkable without it.
*** TODO SD card reader media polling can block suspend :bug:velox:solo:
The reader (USB 090c:3350 Silicon Motion, =sda=, "Media removed, stopped
polling" at boot yet =events_poll_msecs= = −1 → default 2000 ms) left a
=disk_events_workfn= item pending on the freezable workqueue three times on
08-23/24, and the freezer gives up after 20 s: "Failed to put system to
sleep … Device or resource busy". Same symptom as the flaky expansion slot in
the ribbon task; a stalled poll never completes.
Fix: a udev rule for that vendor/product setting
=ATTR{events_poll_msecs}="0"= (or =block.events_dfl_poll_msecs=0= on the
cmdline if every removable disk should stop polling), shipped by archsetup.
Verify with =rtcwake -m mem -s 20= on AC: journal shows "PM: suspend entry"
and "PM: suspend exit" with no "Freezing remaining freezable tasks failed",
and =/sys/block/sda/events_poll_msecs= reads 0 after a replug. Pulling the
card before sleeping is the manual workaround meanwhile.
*** VERIFY After the 08-17 and 08-21 dead batteries, did the first power-on hang, or boot straight to a fresh login?
Decides between the two mechanisms. An entry freeze leaves no image, so the
next power-on boots straight through. A completed hibernate followed by an
unattended power-on (phantom power button, ZBM passphrase prompt until dead)
leaves the image in place, so the next power-on would try to resume — and the
only way that ends in the fresh boots the journal shows is a hung resume that
got force-cut. If both power-ons went straight to a fresh login, the freeze
is the answer.
** TODO [#A] Port Hyprland config to Lua before 0.57 drops .conf support :hyprland:dotfiles:
SCHEDULED: <2026-09-24 Thu>
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
Hyprland prints "You are using the .conf config format, support for which will be
removed in Hyprland 0.57" at every start. Installed and in =extra= is 0.56.2-1, so
the *next* release breaks the config. Craig's call 2026-08-24: port now, under no
time pressure, rather than pin the package or wait for the upgrade to force it.
STATE (2026-08-24): built and verified in a nested compositor, *not deployed*.
I deployed it to the dotfiles tree this afternoon and rolled it back the same
hour on Craig's call — the switch had not been checked on real hardware and the
machine has to stay usable. The dotfiles repo is untouched at =8f692f5= and the
live config is the original =hyprland.conf=; =hyprctl reload= after the rollback
returned zero configerrors and the velox host override is applied
(=xwayland:force_zero_scaling= false), so the per-host chain is intact.
Everything needed to redeploy is in =working/hyprland-lua-port/= with a README
carrying the step-by-step: the three =.lua= deliverables, plus the two reader
changes saved as patches (=reader-changes-for-lua.patch= for dotfiles'
=dotfiles-validate= and three test suites, =test-desktop-for-lua.patch= for
archsetup's post-install checks). Both patches were verified to apply clean
against their repos, and every assertion in them was mutation-tested — each one
confirmed to go red when the property it guards is removed. Replay them rather
than rewriting the assertions.
Also settled along the way: =themes/dupre/hyprland.conf= is dead. Nothing sources
it, no apply script exists, and it has silently drifted from the live config
(=dab53dff= / =2c2f32ff= against =daa520ff= / =444444ff=). It needs no porting.
HOW IT WAS BUILT. =hyprlang2lua= (github.com/EIonTusk/hyprlang2lua, AUR 0.7.1-1)
converts hyprlang to the 0.55+ Lua format and preserves comments. Built from
source into a scratchpad with the local Go rather than installing the AUR package
— one dependency, and no PKGBUILD executed. Run with =--no-merge=, which emits
each config section as its own =hl.config()= call at its original position; the
default merges them into one hoisted call, which both scrambles comment placement
and puts the =conf.d= source glob BEFORE the config it must override.
THREE DEFECTS THE CONVERTER INTRODUCED, all fixed by hand:
1. Source glob emitted before the merged config block, silently reversing every
per-host override — including velox's =force_zero_scaling = false=, which is
the 2026-08-19 Qt scaling fix. =--no-merge= plus moving the glob to the last
line fixes it.
2. =bind = CTRL $mod, S= became ="CTRL" .. mod .. " + S"= → ="CTRLSUPER + S"=;
same for =CTRL ALT $mod, K=. Proven fatal, not merely odd: Hyprland answers
=hl.bind: failed to parse key string: Unknown keysym: "CTRLSUPER"=. Two dead
keybinds.
3. Super+RETURN is an =exec= of a shell pipeline; the converter pattern-matched
the =hyprctl dispatch layoutmsg= prefix and swallowed =&& sleep 0.05 && ...=
as the layoutmsg argument. That sleep is the one proven load-bearing 19/19.
Also rebuilt the autostart section: the generator hoists every =exec-once= into
one block at the end and leaves the comments stranded where the commands were.
Each command now sits under its own comment again via =at_start= / =at_shutdown=
/ =at_reload= collectors that the =hl.on()= handlers at the bottom replay.
VERIFIED by running both configs in a nested Hyprland on a headless output and
diffing runtime state, not by reading: 38 config keys identical (only type
*rendering* differs — =bool: true= where hyprlang prints =int: 1=);
=xwayland:force_zero_scaling= false on both sides, so the host override still
wins; 103 binds registered on both sides with 100 of 103 matching exactly;
autostart list byte-identical to the 16 =exec-once= lines in order; zero
=configerrors=; and no ERR/WARN line in the ported run that is absent from the
original run.
KNOWN DELTA — the three =bindm= binds. hyprlang reports =mouse: true=, the Lua
path reports =mouse: false=, and the raw bind struct confirms the flag is unset
rather than merely unreported. Not a transcription error: the wiki documents
exactly the spelling used (=hl.bind("ALT + mouse:272", hl.dsp.window.drag(),
{ mouse = true })=), and all four candidate spellings were tested — none sets it.
Reads as a gap in 0.56.2's Lua config manager. Kept the documented spelling: it
is correct upstream, harmless now, and starts working when the gap closes. Per
the wiki that flag is what makes the action fire *while held*, so Super+drag to
move a window may fire once instead of tracking. Settle it in five seconds after
switching; worth an upstream report if it survives 0.57.
WHAT REMAINS:
1. *Craig decides when to switch.* The port is ready to go in; it has not been
run on real hardware. The gate is the "Hyprland Lua config" test under Manual
testing and validation, which is written to be run right after the switch.
2. *Four review gates travel with the redeploy*, all written up in that README:
exclude any in-repo =retired/= dir from =dotfiles-validate= (its find globs
across slashes and would validate the dead config); add tests for the new
=dotfiles-validate= Lua branch (25 lines, currently zero coverage — proven
vacuous, since stubbing both regexes to =NEVERMATCHES= still passes 15 tests);
guard that =hl_source_glob= stays the last statement (the one invariant the
per-host layer rests on, and archsetup's VM cannot catch it); and sweep the
~15 prose comments still naming =hyprland.conf=, of which
=waybar-reserve:12= is the load-bearing one.
3. *Redeploy per =working/hyprland-lua-port/README.org=*, which carries the eight
steps and the two traps that bit on 2026-08-24: =make restow hyprland= aborts
on the pre-existing =obsbot-wb-guard.service= conflict in =common= (restow
=hyprland= and the host package individually), and the running Hyprland
rewrites a stub =hyprland.conf= within a second of the symlink vanishing
(silence it with =hyprctl keyword misc:disable_autoreload 1=, stow, set back
to 0).
4. *Push dotfiles before committing archsetup.* One-directional and load-bearing:
archsetup's post-install suite asserts =~/.config/hypr/hyprland.lua= and the
installer clones the dotfiles *remote* (=archsetup:1481=), so a local commit is
not enough. Confirm with =git ls-tree -r origin/main --name-only | grep
hypr/hyprland.lua=.
5. *Do not leave the =.conf= beside the =.lua= as a rollback.* With both present
Hyprland 0.56.2 loads the =.lua= — proven in a nested instance with a fixture
whose =.conf= set =gaps_in=11= and =.lua= set =77=; the result was 77. A
=.conf= left in place buys nothing and only obscures which file is live. Move
it out of the stow package instead.
6. *=bindm='s missing =mouse= flag is worth an upstream report* if it survives
0.57. Documented spelling, four variants tested, flag never set.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-dated to 09-24; 0.57 still isn't in the repos
velox runs =hyprland 0.56.2-3= with no Hyprland update pending, so nothing has
broken yet. The switch still needs me at the keyboard for one restart and the
manual test.
** TODO [#B] Manual testing and validation :test:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-23
:END:
Craig's standing checklist of everything that isn't agent-verifiable. Each child is one test in the =verification.md= shape (title, what we're verifying, steps, Expected). A child that fails gets its actual behavior written under it and is promoted to a top-level TODO. 62 checks pending as of the 2026-08-23 review — up from 44 at the 2026-07-09 audit, so the queue has gained 18 in six weeks and nothing has drained it. A checklist that only grows is on its way to being where tests get filed rather than run; if the next review finds it higher again, the container needs a scheduled sweep rather than another re-stamp.
Priority and type tag added by that audit: the task carried neither, which kept the project's largest live container out of the agenda entirely.
*** Airplane console key in the net panel: does it engage, confirm, and let you back out?
What we're verifying: that the new AIRPLANE key actually drives the mode both
ways, that engaging asks first, and that the way out is visible from inside
airplane mode. The GTK widget layer has no unit coverage and the AT-SPI smoke
can't run on velox (no weston/sway, no at-spi-bus-launcher), so this is the
only check that exercises the real wiring.
Do it on AC, and not while you need the network — it stops tailscale, the VPN,
syncthing, avahi, cups and inbound ssh, and dims the screen.
- Open the net panel (Super+Shift+N). The CONSOLE row should now show three
keys: DOCTOR, SPEED TEST, AIRPLANE.
- Click AIRPLANE.
Expected: a dialog naming what it will do — wifi off, services stopped, screen
dimmed, CPU to power — with Cancel and an AIRPLANE button.
- Press Cancel.
Expected: nothing happens. Wifi stays up, the key still reads AIRPLANE.
- Click AIRPLANE again, then confirm.
Expected: the key's lamp flashes while it runs, then the faceplate shows the
AIRPLANE badge with a gold lamp, and the key's label changes to LEAVE AIRPLANE.
Wifi is off and the screen is dimmer.
- While engaged, try the faceplate wifi switch.
Expected: it refuses and the status line says to press LEAVE AIRPLANE. It must
NOT name a keyboard shortcut — the old message said Super+Shift+A, which is
push-to-talk.
- Click LEAVE AIRPLANE.
Expected: no confirmation this time, it just runs. Wifi comes back, brightness
returns to where it was, and the key reads AIRPLANE again.
#+begin_src sh :results output
# The services it stopped should be back. Anything listed here is still down.
for s in tailscaled.service avahi-daemon.service cups.service sshd.service fail2ban.service; do
systemctl is-active --quiet "$s" || echo "still stopped: $s"
done
systemctl --user is-active --quiet syncthing.service || echo "still stopped: syncthing (user)"
echo "airplane state: $(cat "${XDG_RUNTIME_DIR}/airplane-state" 2>/dev/null | head -1)"
#+end_src
Expected: no "still stopped" lines, and the state reads mode=off. A service
that was already stopped before you engaged is correctly left alone, so check
it was running first if one shows up.
*** Hyprland Lua config: does the real desktop come up, and does Super+drag track?
What we're verifying: that the Lua port drives a real Hyprland session the way
the .conf did, and specifically whether the one known delta — the three =bindm=
binds losing their =mouse= flag — actually costs anything. A nested compositor
proved 38 config keys, 103 binds and the host-override chain identical, but it
cannot test real input devices or a real DRM display.
PRECONDITION: run this *only after* redeploying the port per
=working/hyprland-lua-port/README.org=. As of 2026-08-24 the port is rolled back
and the live config is the original =hyprland.conf=, so running this now just
confirms the old config — which is not what it is for. Run it on velox; the stub
check in the last block is velox-specific.
- Restart Hyprland (log out and back in, or =hyprctl dispatch exit= from a TTY).
- Confirm the desktop comes up: waybar present and not off-screen, wallpaper
restored, dunst notifications working.
#+begin_src sh :results output
# Which config did it actually load, and did anything fail to parse? The log is
# per-instance under the runtime dir, not in ~/.local/share. Resolve the newest
# instance dir rather than reading $HYPRLAND_INSTANCE_SIGNATURE: Emacs runs as a
# daemon that survives the logout in step 1, so a block run from it can still be
# carrying the PREVIOUS session's signature. That path is gone after the restart,
# grep prints nothing, and an empty result under "Expected: names hyprland.lua"
# reads as "the port failed" when it in fact succeeded -- the worst possible
# wrong answer at exactly the wrong moment.
log="$(\ls -td "$XDG_RUNTIME_DIR"/hypr/*/ | head -1)hyprland.log"
echo "reading: $log"
grep -iE '\[cfg\].*(lua|legacy)' "$log" | tail -3
hyprctl configerrors
#+end_src
Expected: the log names hyprland.lua, and configerrors is empty.
- Hold Super and drag a window with the left mouse button.
Expected: the window tracks the pointer continuously while Super is held. If it
jumps once and stops, the =bindm= =mouse= flag gap is real and costs the drag —
write that here, promote to a top-level TODO, and report upstream.
- Hold Super and drag with the right mouse button (resize), same check.
- Walk the keymap: the launcher, terminal, browser, screenshot chords, the panel
family (Super+Shift+B for bluetooth), workspace switching, layout cycling.
Expected: every chord does what it did before the port.
#+begin_src sh :results output
# The stub .conf should stay gone now that Hyprland started from the .lua.
# Refuse to touch a symlink: on a host that has not been through this port yet,
# ~/.config/hypr/hyprland.conf is still the stow link to the real config, and
# deleting it would report "stays gone" as a pass while having broken the desktop.
f=~/.config/hypr/hyprland.conf
if [ -L "$f" ]; then
echo "REFUSING: $f is a symlink (a live stowed config), not the stub."
elif [ -f "$f" ]; then
rm -f "$f"; sleep 2
[ -e "$f" ] && echo "REGENERATED — still stubbing" || echo "stays gone"
else
echo "already absent — nothing to do"
fi
#+end_src
Expected: "stays gone". If it regenerates, Hyprland is still resolving its config
to the .conf path and the port is not actually live — stop and investigate.
*** 2026-09-13 Sun @ 07:57:32 -0500 Retired the lock-screen clock check: fixed, per Craig
Craig reported on 2026-09-13 Sun that the stale clock after a real sleep no longer
happens on velox, so the three-outcome check never needed running. The
parent bug task is closed with the same note.
*** Lock keybind has no crash or hang recovery
What we're verifying: that a hand-lock is as recoverable as an idle lock.
=hyprland.conf:495= is =bind = $mod, ESCAPE, exec, hyprlock=, which runs the
binary directly. hypridle's =lock_cmd= routes through =screen-lock=, which
relaunches a hyprlock that exits non-zero; the keybind bypasses that entirely.
=~/.local/var/log/screen-lock.log= does not exist on velox, so the watchdog has
never recorded a relaunch — consistent with it rarely being in the path at all.
- Lock with Super+Escape.
- From another tty (ctrl+alt+F3), log in and run: =pkill -x hyprlock=
- Return to the graphical tty.
Expected: with the keybind as written, the session is left locked with no client
and Hyprland draws its "lockscreen app died" screen. If instead a fresh password
prompt appears, something is already relaunching it and the gap is closed.
*** Clock/DNS deadlock: does the next abrupt power loss strand velox again?
What we're verifying: that the machine survives an RTC reset unattended. Not the
coin cell, which is new with the 2026-08-13 mainboard and is ruled out. The RTC
did not drift on 2026-08-19, it was reset to exactly 2025-01-01T00:00:16 by an
abrupt power loss at 01:33:18 that left no shutdown sequence in the journal.
This one can't be scheduled. Run the block the next time velox comes up after an
unexpected power loss, before touching the clock.
#+begin_src sh :results output
echo "--- what did the RTC read at this boot? ---"
journalctl -b 0 | grep -m1 'rtc_cmos.*setting system clock'
echo "--- did systemd have to advance the clock to its build epoch? ---"
journalctl --list-boots | tail -3
echo "--- sources: is an IP-addressed one selected? ---"
chronyc -n sources
echo "--- clock + DNS ---"
timedatectl | grep -iE 'Local time|RTC time|synchronized'
getent hosts gnu.org || echo "DNS DEAD"
#+end_src
Expected: even if the RTC came up at 2025-01-01 and systemd advanced the clock
to 2026-07-23, chrony reached 162.159.200.1 without DNS, stepped the clock to
now, and names resolve. You did nothing.
If instead the clock is still wrong or DNS is dead, the fix did not hold in the
field despite holding under a simulated skew. Capture that whole block and
promote this to a top-level TODO.
*** Floating layout: freeze positions, border flash, glyph, exit to master
What we're verifying: the rebuilt floating mode (Super+Shift+F) floats every window on the workspace via per-window setfloating (the old workspaceopt allfloat was deprecated and no-op'd, which is why nothing floated), freezes each in place, flashes the border gold on entry and exit, flips the waybar glyph to the floating icon, and exits to master. Live-verified on a headless output already (windows floated in place, dragged to overlap, glyph read Floating, toggled back clean); this is the on-your-own-monitor confirmation.
- Go to a workspace with 2-3 tiled windows in master.
- Press Super+Shift+F. Watch: the borders flash gold, the waybar layout glyph changes to the floating icon (), and every window stays exactly where it was but is now floating.
- Drag each window with Super+left-mouse — they should move freely and be able to overlap (tiled windows can't).
- Press Super+Shift+F again. Borders flash; every window re-tiles to master; the glyph returns to the master icon.
- On an empty workspace, press Super+Shift+F — nothing should happen (no flash, no state).
#+begin_src sh :results output
echo "state file:"; cat "${XDG_RUNTIME_DIR:-/tmp}/hypr-float-mode.state" 2>/dev/null || echo "(none — not floating)"
echo "windows on active workspace:"; ws=$(hyprctl activeworkspace -j | jq .id); hyprctl clients -j | jq -r --argjson ws "$ws" '.[] | select(.workspace.id==$ws) | "\(.address) floating=\(.floating) at=\(.at) size=\(.size)"'
#+end_src
Expected: entering floating flashes the border bright gold (a few blinks); the glyph shows ; every window stays where it was, now floating and drag-able/overlap-able with Super+mouse; a second Super+Shift+F flashes again and re-tiles to master (state file gone, glyph back to master). On an empty workspace nothing happens. Note: Super+Shift+F used to open nautilus — that bind is now floating; add nautilus elsewhere if you want it.
*** Settings panel: brightness drum drags
What we're verifying: the paper drums' drag gesture (a Cairo hit-test the e2e suite can't drive) maps drag distance to backing percent with the floor rules. On ratio (no backlight) the drums render dimmed and refuse the drag — that's expected, so the drag half of this test needs velox.
- Open the settings panel (Super+Shift+G).
- Drag the SCREEN drum downward well past the bottom.
- Drag the KBD drum the same way.
#+begin_src sh :results output
brightnessctl -m info; brightnessctl -m -c kbd_backlight info
#+end_src
Expected: the screen backing floors at 5% (drum never shows 0); the kbd backing reaches 0. On ratio both drums are dimmed and undraggable instead.
*** Settings panel: tripper dial tab drags
What we're verifying: the dial's drag gestures — clamping between enabled neighbors, the exact-minutes counter during a drag, the OFF-notch park, and unparking (the e2e suite covers this commit path only at the backing layer).
- Open the settings panel.
- Drag the LOCK tab counterclockwise toward 0; watch the minutes counter during the drag.
- Drag the SUSPEND tab out of the OFF notch onto the scale.
- Drag SUSPEND back down into the bottom OFF notch.
#+begin_src sh :results output
cat ~/.config/hypr/hypridle.conf
#+end_src
Expected: LOCK refuses to land below DIM's minutes (clamps, doesn't cross); the counter shows exact minutes while dragging; SUSPEND leaves/rejoins the conf as it unparks/parks. Finish by restoring the rail to DIM 5 / LOCK 7 / WATCH 8 / SCREEN OFF 10 / SUSPEND parked.
*** Settings panel: matrix pin clicks and letter wheels
What we're verifying: the program matrix's pin and wheel hit-tests (Cairo; the e2e suite drives only the backing calls) — seat/unseat pins, cycle a CPU POWER letter, and the active-is-live rule.
- Open the settings panel.
- Activate FOCUS (its head key).
- Click FOCUS's NIGHT LIGHT pin to seat it.
- Click the pin again to unseat it.
- Click FOCUS's CPU POWER letter wheel a few times.
- Move a control that belongs to no program (e.g. flip TOUCHPAD).
Expected: seating the pin starts the night light immediately (screen warms) and FOCUS stays active; unseating stops it; each wheel click steps P→B→S and applies live; the manual TOUCHPAD change deactivates the scene (no head stays lit).
*** Hypridle is reaped when its compositor goes away
What we're verifying: the orphan that wedged velox on 2026-07-22 can't accumulate — a compositor exit leaves no hypridle behind, and a fresh session starts with exactly one. Agent-untestable: proving it requires ending a live session, which is the thing that costs work.
- Note the current daemon count before doing anything.
#+begin_src sh :results output
pgrep -c hypridle; pgrep -a hypridle
#+end_src
- Exit the session deliberately (Super+Shift+Backspace twice, now that it confirms), landing back at the console.
- From the TTY, before logging back in, check what survived.
#+begin_src sh :results output
pgrep -a hypridle || echo "no hypridle survived — exec-shutdown reaped it"
#+end_src
- Log back in and let the desktop settle.
#+begin_src sh :results output
pgrep -c hypridle
#+end_src
Expected: zero hypridle processes at the TTY between sessions, and exactly one after logging back in — never two. If caffeine is engaged the count is legitimately zero after login too (caffeine works by stopping the daemon); release caffeine and re-check in that case.
*** Exit-confirm submap guards the session-kill chord
What we're verifying: mod+Shift+Backspace no longer ends the session on one press — it arms a confirm submap where a second Backspace exits and anything else backs out. Agent-untestable by design: exercising it on a live session risks the session loss it prevents, so the chord press is yours. Save your work first; the confirm path really does exit.
- Press Super+Shift+Backspace. Nothing should visibly happen (no logout, no dialog).
#+begin_src sh :results output
hyprctl submap
#+end_src
- Press Escape.
#+begin_src sh :results output
hyprctl submap
#+end_src
- Press Super+Shift+Backspace again, then press an unrelated key (say =a=) instead of Escape.
#+begin_src sh :results output
hyprctl submap
#+end_src
Expected: after the first chord the submap reads =exitconfirm=; after Escape it reads =default=; after the stray key it reads =default= again (the catchall backs out). The session survives all three. Only Backspace-then-Backspace exits — confirm that separately when you're ready to end a session anyway.
*** Settings panel: locked-path night-watch swap
What we're verifying: the WATCH stage under a locked session — the kiosk face reveals only after its window maps (no desktop flash), and first input restores hyprlock. The e2e/live checks so far only exercised the unlocked lifecycle. Note: caffeine was found engaged on 2026-07-22 (hypridle deliberately left stopped); release caffeine first or this never fires.
- Release caffeine so hypridle runs with the five-stage conf.
- In the panel, temporarily drag the rail tight: DIM 1 / LOCK 2 / WATCH 3.
- Lock the session (Super+Shift+Q → lock, or wait for the LOCK stage).
- Leave the machine untouched past the WATCH minute mark.
- Watch the transition carefully for any flash of the desktop between hyprlock and the night-watch face.
- Press a key or move the mouse.
Expected: the night-watch board replaces the lock face with no desktop flash between them; first input drops the face and hyprlock is back (password prompt, not the desktop). If the face fails to start, the plain hyprlock stays — never a bare desktop. Restore the rail to DIM 5 / LOCK 7 / WATCH 8 afterward.
*** Net panel: sticky error toast dismisses on the next click
What we're verifying: the stuck enterprise-error banner (dotfiles a157bed) now clears when you click anywhere else in the panel — the fix is a pointer-level gesture that AT-SPI can't drive.
- Open the net panel from the bar.
- Click an enterprise (802.1X) network in the NETWORKS list (Cox Mobile in the usual scan) so the red "Enterprise (802.1X) — join or edit it in nmtui/nmcli." banner appears.
- Click anywhere else in the panel: another network row, a meter card, or empty plate.
Expected: the red banner disappears on that click. A routine (non-red) toast still fades on its own 4s timer as before.
*** maint privilege degrade: PROMPT path on a real tty
What we're verifying: with sudo not passwordless, a maint fix on a terminal prompts for the password (after the wall note) instead of hard-failing. Safe on ratio/velox — run from a real terminal; PANELKIT_SUDO=false makes the model treat the box as non-passwordless without touching sudoers, while the fix still fires through real (prompting) sudo.
#+begin_src sh :results output
PANELKIT_SUDO=false maint fix cache_clean
#+end_src
- Type your password if sudo asks.
Expected: a dim note line "no passwordless sudo — sudo will ask for your password on this terminal", then the RUN line shows plain "sudo paccache -r" (no -n) and the step lands OK with re-probe notes after. On ratio/velox the NOPASSWD grant means sudo won't actually prompt (only the seam is faked); the real password prompt is only observable on a box without the grant — a VM or a fresh install pre-sudoers.
*** maint privilege degrade: GUIDE events on the GUI wall
What we're verifying: the maint panel's CLEAN UP degrades to guide instructions on the results wall (nothing executes) when the box can't elevate silently.
#+begin_src sh :results output
PANELKIT_SUDO=false setsid -f maint panel >/dev/null 2>&1
#+end_src
- In the panel, press CLEAN UP (needs an off-nominal reclaim metric; if the board is quiet, expect the "nothing to clean" note instead).
Expected: each privileged reclaim lands on the wall as a dim instruction line — "cannot elevate here (...) — run yourself: $ sudo ..." — with no RUN/OK pair for it, and nothing actually reclaimed. Close the panel afterward (it inherited the fake PANELKIT_SUDO).
*** Net doctor privileged fixes (net Phase 1) — run in a disposable VM/container
What we're verifying: the three control-plane fixes actually repair a real broken state under real sudo. These states are dangerous on a daily driver (masking NM kills the network), so run them in a throwaway VM or booted nspawn with NetworkManager installed and passwordless sudo, NOT on ratio/velox. Each: break the state, run =net doctor --fix=, confirm the repair.
**** nm-masked → unmask-nm
What we're verifying: a masked NetworkManager is unmasked and started, not met with a doomed nm-restart.
#+begin_src sh :results output
sudo systemctl mask NetworkManager
net doctor --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], d.get("next_action"))'
#+end_src
- Expected (diagnose): the verdict names the masked NM (outcome fixable, action unmask-nm), not the generic "NetworkManager isn't running".
#+begin_src sh :results output
net doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print("fixed=",d["fixed"],"attempts=",[a["id"] for a in d["attempts"]])'
systemctl is-enabled NetworkManager; systemctl is-active NetworkManager
#+end_src
Expected: the fix attempts unmask-nm, NetworkManager is no longer masked (is-enabled != masked) and is active.
**** rival-manager → disable-rival
What we're verifying: a rival manager active alongside NM is disabled, and the doctor names it rather than blindly resetting.
#+begin_src sh :results output
sudo systemctl enable --now dhcpcd # a rival that fights NM for the link
net doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], [a["id"] for a in d["attempts"]])'
systemctl is-active dhcpcd
#+end_src
Expected: the verdict/fix is disable-rival, and dhcpcd reads inactive afterward. Note whether the link comes back on its own or needs a follow-up =net doctor --fix= (the un-chained-reset question — if it needs the reset, chain "disable-rival": ["disable-rival","reset"] in FIX_CHAINS).
**** keyfile-perms → chmod-keyfile
What we're verifying: a profile keyfile with unsafe perms is set back to 0600 root so NM stops ignoring it. Needs root (the system-connections dir is 0700 root:root), so run the doctor as root or via sudo.
- Pick an existing profile name (its keyfile is /etc/NetworkManager/system-connections/<name>.nmconnection).
#+begin_src sh :results output
sudo chmod 0644 /etc/NetworkManager/system-connections/<PROFILE>.nmconnection
sudo net doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], [a["id"] for a in d["attempts"]])'
sudo stat -c '%a %U' /etc/NetworkManager/system-connections/<PROFILE>.nmconnection
#+end_src
Expected: the verdict/fix is chmod-keyfile and the keyfile reads =600 root= afterward.
*** Net doctor sharpened auth verdict (net Phase 2) — needs a real WPA3 / hidden network
What we're verifying: the auth-cause classifier names the specific auth cluster (SAE / hidden / enterprise / generic) instead of a bare "password rejected", and the SAE / hidden one-line profile fixes actually run and take. These need a real network the machine can (fail to) associate with, so they can't be faked; run against an AP you control or a VM bridged to one. The classify logic is already unit-tested — this is the live half.
**** SAE cause named + auth-sae fix
What we're verifying: a WPA3-only network whose saved profile is still WPA2-PSK reports the SAE cause, and =--fix= sets key-mgmt sae so the association can complete.
- Set an AP (or its VM equivalent) to WPA3-Personal only (pure SAE, not WPA2/WPA3 transition).
- Save a profile for it as WPA2-PSK (=nmcli connection add type wifi ... wifi-sec.key-mgmt wpa-psk=), then try to connect so the association fails on auth.
#+begin_src sh :results output
net doctor --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], d.get("message"), "|", d.get("next_action"))'
#+end_src
- Expected (diagnose): outcome fixable, action auth-sae, the message names WPA3 (not just "password rejected").
#+begin_src sh :results output
net doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print("fixed=",d["fixed"],"attempts=",[a["id"] for a in d["attempts"]])'
nmcli -g 802-11-wireless-security.key-mgmt connection show <PROFILE>
nmcli -g 802-11-wireless-security.pmf connection show <PROFILE>
#+end_src
Expected: attempts start with auth-sae (then reset), the profile now reads key-mgmt =sae=, and pmf is accepted (the =pmf optional= keyword took — this is the one value only a live nmcli can confirm). Note whether the link comes back after the chained reset.
**** hidden SSID cause named + auth-hidden fix
What we're verifying: a hidden (non-broadcast) network whose profile lacks the hidden flag reports the hidden cause, and =--fix= sets 802-11-wireless.hidden yes so NM probes for it.
- Set the AP SSID to non-broadcast (hidden). Save a profile for it without =802-11-wireless.hidden yes=, then try to connect.
#+begin_src sh :results output
net doctor --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], d.get("message"))'
net doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print([a["id"] for a in d["attempts"]])'
nmcli -g 802-11-wireless.hidden connection show <PROFILE>
#+end_src
Expected: diagnose names the hidden cause (outcome fixable, action auth-hidden); the fix attempts auth-hidden; the profile now reads hidden =yes=.
**** enterprise / generic stay terminal + named
What we're verifying: an enterprise (802.1X) auth failure stays needs-user-action and names the missing cert rather than offering a bogus fix; a plain wrong-password stays the generic "rejected" message.
#+begin_src sh :results output
# On an enterprise network with a broken/absent CA cert, or a WPA2 network with a wrong saved password:
net doctor --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["outcome"], "|", d.get("next_action"))'
#+end_src
Expected: enterprise → outcome needs-user-action, next_action names the CA certificate / identity (no auth-sae/auth-hidden action); wrong password → needs-user-action with the "re-enter the password" message.
*** Bt doctor persistent-power fix (bt Phase 2) — proves out only across a real reboot
What we're verifying: a deliberately boot-disabled adapter reports =powered-off-persistent= (not the transient =power-on=), =bt doctor --fix= clears the cause and powers it on, and the adapter comes up on its own after a reboot. The reboot-survival is the whole point, so this can't be faked; run on a machine (or VM) you can reboot. Back up =/etc/bluetooth/main.conf= first.
**** AutoEnable=false → persistent verdict → fix → survives reboot
What we're verifying: the full loop — break persistence, confirm the verdict, fix, reboot, confirm it stuck.
#+begin_src sh :results output
sudo cp /etc/bluetooth/main.conf /etc/bluetooth/main.conf.bak
# Force the deliberate disable and power the adapter off:
printf '[Policy]\nAutoEnable=false\n' | sudo tee -a /etc/bluetooth/main.conf
bluetoothctl power off
bt doctor --json | python3 -c 'import sys,json; d=json.load(sys.stdin); p=[s for s in d["steps"] if s["id"]=="powered"][0]; print(p["code"], "|", p["evidence"])'
#+end_src
- Expected (diagnose): the powered step reads code =powered-off-persistent= and the evidence names AutoEnable=false (not the plain "adapter is powered off").
#+begin_src sh :results output
bt doctor --fix --json | python3 -c 'import sys,json; d=json.load(sys.stdin); print("attempts=",[a["id"] for a in d["attempts"]])'
grep -i autoenable /etc/bluetooth/main.conf
bluetoothctl show | grep -i powered
#+end_src
- Expected (fix): attempts include persist-power; main.conf now reads =AutoEnable=true=; the adapter is powered on now.
- Reboot the machine.
#+begin_src sh :results output
bluetoothctl show | grep -i powered # after the reboot, before touching anything
#+end_src
Expected: the adapter is powered on straight out of the reboot (AutoEnable=true held). Restore the backup if desired: =sudo mv /etc/bluetooth/main.conf.bak /etc/bluetooth/main.conf=.
**** service-disabled and TLP causes (optional variants)
What we're verifying: the other two persistent causes are named and fixed the same way.
- Service variant: =sudo systemctl disable bluetooth= (leave it running this boot), power the adapter off, then =bt doctor --json= should read =powered-off-persistent=; =--fix= should re-enable it (check =systemctl is-enabled bluetooth=).
- TLP variant (only if TLP is installed): add =bluetooth= to =DEVICES_TO_DISABLE_ON_STARTUP= in =/etc/tlp.conf=, power off, diagnose (persistent), =--fix=, confirm bluetooth is dropped from the list.
Expected: each variant names the persistent verdict and the fix clears exactly that cause, leaving the others untouched.
*** Maintenance console — in-person checklist
Re-homed here by the 2026-07-09 audit: this was a second top-level "Manual testing and validation" parent. One parent, per verification.md. Priority and the :test: tag now live on the parent.
Promoted from the build parent when it closed 2026-07-08 — Craig's checklist, runs once in person. Collects everything not agent-verifiable; populate per verification.md as phases land. Known so far: panel look-and-feel vs the E5 prototype (Craig's eyeball); arm-press wording reads right at the moment of use; results-wall readability during a real doctor run; a real UPDATE through the armed guard (and the TTY path once); REBOOT offer after an update; velox in-person glyph check (battery %, charging glyph, low-charge red); SET 80% charge limit sticks across a charge cycle; KILL on a real memory hog.
Phase 8 additions: subpanel scroll position survives an arm press on a long digest (MAINT_PANEL_FIXTURE=bad, PACKAGES band, scroll into the 51-orphan list, press REMOVE — the list should stay put and the key read REMOVE?); a KEEP on a real orphan dims it into the kept section and UNKEEP restores it; MERGE on a needs-merge pacnew opens pacdiff in a foot terminal.
Phase 9a additions: MARK KNOWN on a real journal group (ratio's wpa_supplicant bgscan spam is the standing candidate) — arm shows the identifier + sample wording, fire writes curation.toml, the next re-scan drops the group into KNOWN NOISE with "marked <date>", and UNMARK restores it to SIGNAL; OPEN JOURNAL opens journalctl -p 3 -b in a foot terminal; a failed unit's RESTART on a real unit (systemctl restart via the armed key) lands the re-probe on the act line.
Phase 9b additions: MARK EXPECTED on ratio's real unexpected listeners (postgres :5432, docker :8080 — the standing curation material) dims them into the expected list and UNMARK restores them; a CPU-mode press (e.g. BAL·PWR) writes the EPP hint and the row re-reads it; KILL renders disabled (insensitive, tooltip) on a session-critical name in top memory; on velox the battery row shows NO SET 80% key (cros_ec knob absent — correct withholding, not a bug).
Phase 11 additions: glyph left-click opens the console and a second click closes it; right-click still toggles the btop scratchpad; the glyph color matches the board's worst lever-less diagnostic (open the console and check the watch items against the glyph tint); after fixing/curating a diagnostic, the glyph follows within one 30-min scan (or immediately after a manual =maint scan=); on velox in person — battery % + charging icon in the bar, collapsed bar keeps the battery glyph, low-charge red (drain below 15% or temporarily raise battery_low_pct in the TOML); after velox's reboot confirm systemctl --user list-timers shows both maint timers scheduled.
Phase 10 additions: results-wall readability during a real doctor run (press CLEAN UP on the live board — every step should appear as a running line the moment it starts, resolve in place with the result, and the re-probed truth should follow; the wall should feel like watching the run, not reading a report afterward); COPY puts the same lines the CLI wall prints on the clipboard, paste somewhere to check; HIDE collapses the wall to its header and SHOW restores it with the scroll position at the bottom; REVIEW & FIX reads right at the moment of use (each row: what's wrong, the maint fix usage, and either a FIX key or "per-item — on its subpanel"); RECLAIM on a degraded disk row arms with the first step + "(+N more steps)" and the fire streams every step to the wall individually; a guard-tripped UPDATE fired from the REVIEW roster re-arms with the override wording and the second press actually runs (the token fix — verify the loop terminates).
Phase 11b additions: the fidelity eyeball — open the live console beside the settled E5 prototype (headless-Chrome render or the browser) and judge whether the board finally reads as the same instrument: big-number typography, bar fills with the gold threshold tick on the cache card, the scrub/keyring/topgrade rings, status chips, the lit CPU-mode segment, selector count chips + severity left borders, right-aligned attention/ok/fixable/watch header; press an EPP segment on the card (e.g. BAL·PWR) and confirm it arms then fires exactly like the old digest control; on the bad fixture (MAINT_PANEL_FIXTURE=bad maint panel) check a long readout (the BACKUPS card) ellipsizes with a tooltip carrying the full value.
**** 2026-07-08 Wed @ 06:31:00 -0500 Fixed TOPGRADE remedy failure found in testing (dotfiles 9b5384f)
Craig's first testing find: TOPGRADE exited 2 on every launch — topgrade 17.6.1 names the git step git_repos and the remedy passed --disable git, rejected by clap before any step ran. Fixed red-first (test pins git_repos), full make test green, dry-run on the installed binary confirms the step name lands and the repo-rebasing step stays excluded. Velox pulled. RE-TEST: press TOPGRADE again (it's guard-armed on ratio's pending mesa set — TTY is still the safe path for the mesa run).
*** CPU mode survives a reboot
What we're verifying: =maint-epp-restore.service= replays the CPU mode the pill last set. The kernel resets EPP to the driver default on every boot, so this is the only check that can prove the fix; nothing an agent runs before a reboot distinguishes "it works" from "nothing was reset yet."
- Open the maintenance console, MEM·PWR band, and press a CPU-mode segment other than the current one (say BAL·PWR).
- Confirm the act line says the mode was remembered.
#+begin_src sh :results output
cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
grep -A1 '^\[power\]' ~/.config/maint/*.toml 2>/dev/null
#+end_src
- Reboot.
- After logging back in, run the block below.
#+begin_src sh :results output
cat /sys/devices/system/cpu/cpu0/cpufreq/energy_performance_preference
systemctl --user status maint-epp-restore.service --no-pager | head -5
#+end_src
Expected: the EPP value after the reboot is the mode you selected, not the driver default, and the unit shows =Active: inactive (dead)= with =Result=success= (a oneshot that ran and exited).
*** Audio signal metering: apply the package (precondition)
What we're verifying: the signal-metering build is live. Only a =git pull= is needed — the =audio/= package .py files are stowed and no new launcher files were added, so no restow or reboot. The shared panel CSS gained a =.lamp.dim= class (live-idle); it's in the same stowed =panel.css=.
#+begin_src sh :results output
cd ~/.dotfiles && git pull
#+end_src
Expected: the pull succeeds (commits 6054d3d..21437b4).
*** Three-state activity lamp reads live
What we're verifying: the INPUT/OUTPUT lamps show muted / live-idle / live-active from real device state, not just mute.
- Open the audio panel (bar sound glyph / Super+A). With nothing playing and the output unmuted, look at the OUTPUT lamp.
Expected: a dim (not bright) green lamp — live-idle: unmuted but no audio flowing.
- Start playing audio (music, a video).
Expected: the OUTPUT lamp brightens to a bright glowing green — live-active.
- Mute the output (click OUTPUT).
Expected: the lamp goes red — muted.
*** OUTPUT needle tracks real signal, not the fader
What we're verifying: the OUTPUT VU needle moves with actual output level, replacing the old volume-fed needle.
- With audio playing, watch the OUT · PLAY needle; then pause the audio (leave the volume fader where it is).
Expected: the needle deflects and rides the music while playing, and falls to rest when paused even though the fader hasn't moved. (The old behavior held the needle up at the fader position.)
*** INPUT needle registers the mic, including during PTT
What we're verifying: the IN · MIC needle shows real mic level and confirms PTT is capturing.
- With the mic live (INPUT unmuted), speak.
Expected: the IN · MIC needle deflects with your voice.
- Set the mic to muted, then hold the PUSH·TALK key and speak.
Expected: while held, the INPUT lamp reads live and the IN · MIC needle registers your voice; on release it rests again.
*** No parec / CPU cost when the panel is closed
What we're verifying: the meter runs only while the panel is open and leaves no orphan =parec=.
- Open the audio panel, then close it (✕, Esc, or the bar click).
#+begin_src sh :results output
pgrep -af 'parec .*@DEFAULT_(MONITOR|SOURCE)@' || echo "no parec running — clean"
#+end_src
Expected: no matching =parec= process after the panel is closed (the process groups were reaped on teardown).
*** Per-device activity lamp on each device row (option 3)
What we're verifying: each OUTPUTS/INPUTS row leads with a lamp showing whether THAT device is live — red (muted) / dim green (unmuted, idle) / bright green (carrying audio) — so you can see which specific device has sound, not just the default.
- Open the panel. Scan the leading lamp on each output/input row.
Expected: muted devices show a red lamp; unmuted devices show green.
- Play audio to a non-default output (or switch the default to a device, then play).
Expected: the row of the device actually playing shows the brightest (live-active) green; an unmuted-but-silent device shows the dimmer (live-idle) green. Judge whether the dim-vs-bright contrast is clear enough — if not, say so and the CSS is a one-line tweak.
- Note: the row lamps update on the panel's status refresh (~3s), so allow a moment after audio starts.
*** Timer redesign: apply the package + wtimer, clear stale timers (precondition)
What we're verifying: the redesigned =timer/= package + =wtimer= are live. Only a =git pull= is needed — the package .py files and =wtimer= are already stowed, and this build added no new launcher files, so no restow or reboot. Clear any pre-existing timers first so a stale-shape state file (an old pomodoro item lacking the new =cfg=) can't crash =wtimer render=.
#+begin_src sh :results output
cd ~/.dotfiles && git pull
wtimer cancel-all
#+end_src
Expected: the pull succeeds and the bar timer glyph goes idle (no active items).
*** Panel opens hero-on-top; close via the ✕, Esc, and the bar
What we're verifying: the redesigned layout renders (header, hero, CONFIGURE, queue) and every close path works.
- Click the timer module on the bar.
Expected: the panel opens floating top-right — a header faceplate, a HERO block for the primary item, the CONFIGURE strip, then a QUEUE well below.
- Click the ✕ in the header; reopen; press Esc; reopen; click the bar module again.
Expected: each of ✕, Esc, and the bar click closes the panel.
*** Timer with repeat
What we're verifying: the REPEAT toggle re-arms a timer on fire instead of dropping it.
- Pick TIMER, type =10s= (fast to observe), toggle REPEAT on, click +.
Expected: the timer appears carrying a "repeat" badge and a "timer · repeats" sub-line.
- Let it fire.
Expected: it notifies and immediately re-arms to a fresh full countdown rather than vanishing.
*** Alarm: recurring day, snooze, ringing, dismiss
What we're verifying: a recurring alarm rings (not fire-and-vanish), snooze re-arms, and dismiss re-arms a recurring one.
- Pick ALARM, type a clock time about a minute ahead (HH:MM), select today's weekday in the day picker, set SNOOZE to 1, click +.
Expected: the row shows the fire clock time and a weekday badge; the hero donut is a countdown ring to the fire time.
- Let it reach the fire time.
Expected: the hero shows RING and the bar glyph goes urgent (RING); SNOOZE and DISMISS buttons appear on the hero.
- Click SNOOZE.
Expected: ringing stops and the alarm re-arms one minute out.
- Let it ring again, click DISMISS.
Expected: ringing stops; because it is recurring it re-arms to its next matching day (a one-shot alarm would be removed instead).
*** Pomodoro: configurable cycle, auto vs await, cycle dots
What we're verifying: the config grid drives the cycle and the AUTO toggle switches between auto-advance and awaiting a start.
- Pick POMODORO, tap a named cycle (e.g. Deep) to fill the grid, set WORK S to 1 (min) for a fast run, toggle AUTO off, click + ADD CYCLE.
Expected: a pomodoro starts in work; the hero shows cycle dots and "work · cycle 1/N".
- Let the work phase fire with AUTO off.
Expected: it enters the rest phase awaiting a start — the hero shows "ready · start break" and a START BREAK button, and it does not count down until pressed.
- Press START BREAK.
Expected: the rest phase counts down; the glyph/color shifts to the break (sage) state.
*** Stopwatch: sweep dial, lap badge, promote, stop
What we're verifying: the analog sweep dial animates, the last-lap ghost badge shows, promote works, and STOP just stops (run-save deferred).
- Pick STOPWATCH, click +. If it isn't the hero, promote it (the ▲ on its queue row).
Expected: the hero donut is an analog second-hand dial sweeping once per minute (not a percentage ring); the elapsed value counts up.
- Hit LAP a couple of times (name one via the popover).
Expected: a "LAP m:ss" ghost badge appears beside the elapsed value and the sub-line shows the lap count.
- Hit STOP.
Expected: the stopwatch is removed. No =~/org/stopwatch-runs.org= entry is written — run-save is deferred to a later version.
*** Queue sort, promote/cycle, 10-item cap
- Add a 25m timer, a 5m timer, and an alarm a few minutes out.
Expected: the queue orders soonest-first; a ringing alarm jumps above everything.
- Use ‹ / › on the hero (or ▲ on a queue row) to change the primary.
Expected: the hero (bar-slot) item changes accordingly and the bar glyph follows.
- Queue items until 10 exist.
Expected: + disables with a "queue is full (10/10)" reason; running countdowns advance live (the =wtimer watch= subscription).
*** Presets: locked defaults, half-past, custom add/delete
- On TIMER, note the default chips carry no × (locked); add a custom preset via + preset, then delete it with its ×.
Expected: locked defaults can't be deleted; a custom preset adds and deletes cleanly.
- On ALARM, tap the half-past chip, click +.
Expected: an alarm is created at the next :30.
*** Bar tooltip parity
What we're verifying: the bar tooltip mirrors the redesign verbatim.
- With a pomodoro, a paused timer, and a ringing alarm active, hover the bar module.
Expected: each item lists on its own line — the pomodoro as "label cycle/iv countdown" (no phase word), the paused one with a "(paused)" suffix, the ringing one as "RING (ringing)".
*** Audio panel: apply the new shims + configs (precondition for the tests below)
What we're verifying: the new =audio=/=audio-panel=/=waybar-audio= bin shims and the hyprland.conf + waybar config edits are live. New files need a restow (a plain =git pull= doesn't symlink them). Quit Hyprland or run from a TTY — restowing live Hyprland writes a stub hyprland.conf.
#+begin_src sh :results output
cd ~/.dotfiles && git pull
cd ~/.dotfiles && make restow hyprland
#+end_src
- Reload waybar: mod+B (or =killall -SIGUSR2 waybar=).
- Reload the Hyprland config: mod+Shift+R (or =hyprctl reload=).
Expected: =which audio audio-panel waybar-audio= resolves all three to ~/.local/bin symlinks; no stow conflicts reported.
*** Audio panel: opens and reads the live graph
What we're verifying: Super+A opens the instrument-console panel (not the old pulsemixer scratchpad) and it shows the real devices.
- Press Super+A.
Expected: the audio panel opens top-right. OUTPUTS lists every sink, INPUTS every mic (no .monitor entries), the default device in each is emphasized (cream name, gold glyph), each row shows its volume percent, and the twin OUT·PLAY / IN·MIC needles deflect. Esc closes it.
*** Audio panel: set default, volume fader, per-device mute
What we're verifying: the three row interactions drive the real graph and the panel re-reads to confirm.
- With two or more outputs present, click a non-default output row's body.
Expected: that device becomes default (gold DEF emphasis moves to it) and playback follows.
- Drag a device's fader.
Expected: the volume percent tracks the fader and the actual device volume changes (no lag, no jump to the border).
- Click a device's trailing mute glyph.
Expected: that one device mutes/unmutes; the glyph and caption reflect it; other devices unaffected.
*** Audio panel: master quick-mute (faceplate switch + mute key)
What we're verifying: the master switch and the XF86AudioMute key both mute every output at once (not just the default sink).
- Flip the faceplate master switch.
Expected: every output mutes, the state word reads MUTED, the badge shows, the faceplate glyph goes to the slashed speaker. Flip back to restore.
- With two outputs audible, press the hardware mute key (XF86AudioMute).
Expected: both outputs mute (master), not just the default. Press again to unmute all.
*** Audio panel: mic modes + push-to-talk (the meeting case)
What we're verifying: LIVE/MUTED work, and PUSH·TALK holds the mic muted except while the talk key is held — the one genuinely new capability, and the one only a live test can confirm.
- Click LIVE, then MUTED, watching the default mic row.
Expected: LIVE unmutes the mic (needle lifts), MUTED mutes it (needle red); the active mode key shows a gold lamp.
- Click PUSH·TALK. In a call app (or =audio status= in a loop), watch the mic while you hold Space, speak, then release.
Expected: mic is muted at rest, un-mutes for exactly as long as Space is held, re-mutes on release. Switching back to LIVE or MUTED disarms the hold (Space types normally again).
*** Audio panel: visual polish eyeball + bar entry point
What we're verifying: the panel looks right in the dupre instrument-console language, and the bar opens it.
- Look over the whole panel: faceplate spacing, engraved section labels, fader styling, VU needle centering, the muted-vs-audible glyph/color states.
Expected: it reads as a sibling of the net and bt panels; nothing overflows the gold border; the faders and gauges look machined, not stock GTK.
- Right-click the waybar sound module.
Expected: the panel opens (left-click still mutes, scroll still changes volume).
*** SUPERSEDED — Timer fuzzel dialogs (Escape-cancel, 12h alarm entry)
These two tests exercised the old fuzzel creation dialogs, retired when the bar's =custom/timer= on-click became =timer-panel=. The panel redesign checklist above covers the same ground: alarm entry (12h shapes, bad-input reject) is now the panel's ALARM freeform + inline validation, and the whole-flow abort is the panel's Esc/✕/bar close. Nothing to run here; kept as a pointer so the intent isn't lost.
*** Speed test streams in the panel
What we're verifying: the panel's speed test fills in as phases complete instead of dumping everything at the end (the CLI path is live-verified; this is the GTK rendering).
NOTE (2026-07-04 audit): these steps describe the OLD four-tab Blueprint panel ("Diagnostics → Network Performance"). The net panel was rebuilt as a single-screen instrument console (dotfiles 800ef60, f4e688e dropped the Blueprint pages), so the navigation below no longer matches the shipped UI — reword to the console layout before running. Don't delete; the streaming-render behavior is still worth verifying.
- Open the net panel, Diagnostics → Network Performance → Run Speedtest.
Expected: a Ping/Download/Upload checklist appears under the running row; ping lands within seconds, download mid-run, upload near the end; then the final rows (with jitter on Ping, a Packet loss row) replace it. A Tip row appears only if a rule fired, and it names the numbers that triggered it.
*** Proton VPN CLI sign-in (velox now, ratio on its trip)
What we're verifying: the proton CLI has its own account store (separate from the retired GTK app's), so the panel's proton rows can't toggle until you sign in once per machine.
- Run in a terminal: protonvpn signin <your-proton-username> (it prompts for the password). The CLI's action is =signin=, not =login=.
#+begin_src sh :results output
protonvpn info
#+end_src
Expected: Account shows your Proton username instead of 'None'. After that, protonvpn status still says Disconnected — correct, nothing auto-connects.
*** Tunnels round-trip: panel rows + bar badge (first real tunnel-owned route)
What we're verifying: the panel's Tunnels tab drives a real wireguard tunnel up and down, and the bar indicator grows the vpn badge while the tunnel owns the default route (the badge has never rendered live — every prior check ran with the wlan owning the route).
- Open the net panel (left-click the bar's net module), switch Connections to the Tunnels page.
- Confirm the rows: tailscale (up), and the three Proton configs (wg-US-TX-714, wg-US-CA-144, wg-NL-781), all down.
- Select wg-US-TX-714, press Bring Up, wait for the row to land.
Expected: the bar's net glyph gains the small vpn badge; its tooltip names the owner ("Tunnel: default route via wgpvpn (wireguard)").
- Press Bring Down on the same row.
Expected: badge gone, tooltip back to normal, internet still works (the wlan owns the route again).
*** Screenshot View Image option
What we're verifying: the new post-capture menu entry opens the shot and puts its path on the clipboard (dispatch is unit-tested; this is the live end-to-end).
- Take a screenshot the usual way (region or fullscreen).
- Pick "View Image" from the fuzzel menu.
- Paste anywhere (the clipboard should hold the file path).
Expected: the shot opens in feh (the default image/png viewer) and the pasted text is the saved file's path under ~/pictures/screenshots/.
*** Wlogout square cells on velox (and later ratio)
What we're verifying: the exit menu's six buttons read as squares with visible muted borders, and only pointer hover shows gold (measured 361x361 px, but your eyeball is the arbiter).
- Press Super+Shift+Q.
- Look at the six cells' shape and borders; hover a couple of buttons; note the lock button is not gold before you hover it.
- Press Esc to close (careful: the letter keybinds are live — e is logout).
Expected: square cells with subtle borders, gold only under the pointer, muted ring on the focused button. Repeat on ratio after its sync.
*** wtimer: color states + click/scroll interactions on the live bar
What we're verifying: the timer module's interactions and CSS state colors render right on the live bar. The glyph, position (right of battery), countdown, and "+N" badge are already verified live; the per-state colors and the real mouse/scroll bindings are what's left. The logic is unit-tested (86 cases); this is the human-in-the-loop visual + input check.
- Left-click the timer module — a fuzzel menu offers timer / alarm / stopwatch / pomodoro; pick timer, enter =5s=.
- Watch it count down; at under a minute it should turn the urgent color (dupre orange #d47c59).
Expected: the timer reaches 0:00, a persistent notification fires, and the item disappears from the bar.
- Create two timers (e.g. =3m= and =10m=); a =+1= badge shows; scroll over the module.
Expected: scrolling cycles which item is primary (the displayed time/glyph changes); the badge count stays correct.
- Middle-click the module while a timer runs.
Expected: it pauses (dimmed paused color #5f5c52) and the countdown freezes; middle-click again resumes from where it left off.
- Right-click the module with items present.
Expected: a fuzzel menu lists the items; choosing one cancels it.
- Start a pomodoro (left-click → pomodoro); let a work phase elapse (or set short test phases by editing state).
Expected: the glyph + color switch between work (gold #d7af5f) and break (#8a9a5b), a notification fires at each phase change, and the cycle count advances.
*** Sysmon right-click cycles the visible metric (live waybar)
What we're verifying: right-clicking the collapsed sysmon module rotates the visible metric and the bar refreshes at once, left-click still opens btop, and the cpu/temp/mem icons render as real glyphs (not tofu boxes). The cycle logic is unit-tested; this is the live-waybar + visual confirmation.
- Reload waybar so it picks up the new =signal= / =on-click-right= config (Super+B relaunches it, or =pkill waybar; waybar &= from a terminal)
- Right-click the sysmon module several times, watching the visible metric
- Left-click the sysmon module once
Expected: each right-click advances the visible metric battery → cpu → temp → mem → disk → back to battery (velox is a laptop, so battery is in the ring) and the bar updates immediately. Every metric shows a sensible icon plus its value, no tofu. Left-click still opens the btop popup. The tooltip still lists all metrics.
*** Give the README a final read before public release
What we're verifying: =README.md= reads cleanly and accurately for a first-time reader, with no stale personal info and consistent public-fork placeholders.
- Open =~/code/archsetup/README.md=
- Read it end to end as if you've never seen the project
Expected: every section is accurate, the personal-project disclaimer reads right, the placeholders (=<your-domain>=, =github.com/yourusername=) are consistent, and nothing personal leaked into the public-facing draft.
*** Bt console: connect / disconnect a paired device
What we're verifying: a paired row's primary action toggles the real connection and the row's lamp + battery dial follow (the smoke drives the widgets against fakes; this is a real device).
- Open the bt panel (left-click the bar's bluetooth module, or Super+Shift+B).
- On a paired-but-disconnected audio device, click its row's connect action.
Expected: the device connects, the row lamp goes live, and if it reports battery a dial fills in with its level.
- Click the same row again to disconnect.
Expected: the device disconnects, the lamp dims, and its battery dial returns to NO DEVICE.
*** Bt console: rename a paired device
What we're verifying: the ✎ affordance renames a real device through the bluez Alias and the new name persists (mechanism live-probed on the M650 during the build; this is the in-panel path).
- In the bt panel, click the ✎ on a paired device's row.
- Enter a new name in the dialog and confirm.
Expected: the row's name updates to the new alias immediately, and it survives closing and reopening the panel (verify-after read confirms it stuck).
- Rename it back to its original name the same way.
*** Bt console: pair a nearby device (passkey flow)
What we're verifying: pairing a new device from the NEARBY list runs the pair flow into the passkey-confirm dialog and default-deny holds (this can't be auto-driven — it needs a real discoverable device and the passkey compare).
- Put a bluetooth device into pairing mode.
- In the bt panel, press SCAN and wait for the device to appear under NEARBY.
- Click its row to start pairing.
Expected: a passkey-confirm dialog appears; confirming completes the pairing and the device moves to PAIRED; dismissing it leaves the device unpaired.
*** Bt console: arm-forget a paired device
What we're verifying: the ✕ two-click arm-to-fire removes a real pairing (the arm latch is unit-tested; this confirms the real forget lands).
- In the bt panel, click the ✕ on a device you can re-pair later.
Expected: the row arms (tinted, a confirm affordance) rather than forgetting on the first click.
- Click again to confirm.
Expected: the device is unpaired and drops off the PAIRED list.
*** Bt console: discoverable toggle and adapter power switch
What we're verifying: the discoverable chip and the faceplate power switch drive the real adapter state.
- In the bt panel, click the discoverable chip.
Expected: the chip reads "discoverable on" and another device can see this adapter while it's on; clicking again turns it off.
- Flip the faceplate adapter-power switch off, then on.
Expected: off powers the adapter down (faceplate word → OFF, rows empty), on brings it back (POWERED, paired rows return). Airplane mode overrides — if it's on, the switch refuses with the way out.
*** Bt console: LOW BATT badge with a real sub-15% device
What we're verifying: a connected device reporting under 15% battery drives the faceplate LOW BATT badge and the red dial (the threshold is unit-tested; this needs a real device actually low).
- Connect an audio device whose battery is genuinely under 15% (drain one, or catch it low).
Expected: its battery dial renders red, and the faceplate shows the LOW BATT badge. Charge it above 15% and the badge clears on the next refresh.
*** 2026-06-28 Sun @ 12:54:47 -0400 Live-update guard verified on velox (live Hyprland)
Verified the =hypr-live-update-guard= PreTransaction hook end-to-end on velox
with Hyprland running (pid 1997). velox predated the feature, so the guard was
absent — placed =/usr/local/bin/hypr-live-update-guard= (755) and
=/etc/pacman.d/hooks/hypr-live-update-guard.hook= (644), byte-matching the
archsetup hyprland-step install. The guard now ships on velox permanently.
Results:
- Quick contract (=printf 'mesa\nhyprland\n' | guard=) → exit=1, BLOCKED banner,
sorted pkgs, correct TTY remedy + sentinel path.
- Not-running branch (=HYPR_GUARD_RUNNING=0=) → exit=0, silent.
- Env override (=HYPR_ALLOW_LIVE_UPDATE=1=) → exit=0.
- Sentinel (=touch /run/archsetup-allow-live-gpu-update=) → exit=0; removed →
re-armed exit=1.
- Real firing through pacman: =sudo pacman -S mesa= (same-version reinstall =
Upgrade op on a guarded target). pacman ran the hook, fed =mesa= via
=NeedsTargets=, the guard aborted, =AbortOnFail= stopped the transaction
("no packages were upgraded"); mesa unchanged at 1:26.1.3-2. This is the
authoritative proof pacman parses + wires the hook.
- Full-logout end-to-end (guard quiet, upgrade completes after logout): covered
by construction — the not-running branch exits 0, and a 0-exit PreTransaction
hook lets pacman proceed normally (proven by the mesa abort showing the hook
path runs). Not re-run under a real logout; no separate residual.
*** Wallpaper survives relogin (waypaper --restore)
What we're verifying: the hyprland =exec-once= now runs =waypaper --restore= instead of a hardcoded =awww img=, so a wallpaper chosen via =set-wallpaper= / waypaper / dirvish persists across a relogin. The exec-once only fires at Hyprland startup, so this can't be confirmed without a real relogin. (Mechanism already verified: =waypaper --restore= applied the persisted wallpaper via the awww backend, exit 0.)
- Set a wallpaper different from the current one (or pick one in waypaper, Super+Shift+P):
#+begin_src sh :results output
set-wallpaper ~/pictures/wallpaper/trondheim-norway.jpg
#+end_src
- Log out of Hyprland and back in (or reboot)
Expected: the wallpaper you just set is what comes back after login — not whatever was showing before, and never the old hardcoded default unless that's what you set.
*** velox per-host env applies after Hyprland restart
What we're verifying: the velox tier's env lines (GDK_SCALE/QT_SCALE_FACTOR 1.5, XCURSOR_SIZE 36) only apply at Hyprland startup, and the foot font moved to host.ini — neither can be confirmed over ssh.
- On velox, log out of Hyprland and back in (or reboot)
- Open a foot window — text should render at 12pt (same as before the migration)
- Launch Zoom (ideally from a browser link) — it should open at normal size with no per-app patch
- Check the cursor isn't tiny on the HiDPI panel
Expected: foot at 12pt, Zoom normally sized, cursor 36px — all from the velox tier, no local real files involved.
*** Dupre Chrome theme renders correctly
What we're verifying: the new Chrome theme's colors look right in a real browser — palette mapping can't be eyeballed from a manifest.
- Open chrome://extensions in Chrome
- Enable "Developer mode" (top right)
- Click "Load unpacked" and select =~/code/archsetup/assets/color-themes/dupre/chrome-theme/=
- Look at the window frame, toolbar, tab strip, and a new tab page
Expected: near-black frame (#151311), dark toolbar/omnibox (#252321), gold links on the new-tab page, steel-gray inactive tab text — coherent with the rest of the dupre desktop.
*** 2026-06-10 Wed @ 17:46:34 -0500 velox post-trim reboot verified; realtek firmware restored
Craig rebooted velox (passphrase at console); checks ran over SSH after boot. Wifi connected, TLP active, graphics fine. One dmesg hit: r8152 failed to load rtl_nic/rtl8156b-2.fw — the Framework Ethernet expansion card (RTL8156B) is Realtek, so the trim list wrongly dropped linux-firmware-realtek (a Realtek laptop camera is on USB too). Reinstalled the package on velox (its hook rebuilt the initramfs) and removed realtek from archsetup's trim list. The driver worked even without the blob (internal-defaults fallback), so this was correctness, not breakage.
*** Super+F dirvish popup: launch, float, dismiss-on-focus-loss, q
What we're verifying: the physical keychord opens a floating Dirvish popup; opening any file launches it independently (never inside the popup frame) and the popup auto-dismisses when focus leaves; navigating dirs keeps it; q is the manual close. The Wayland focus event that drives the auto-dismiss can't be driven headlessly — only a real keypress + real app launch confirms it.
- Press Super+F
- Expected: a Dirvish frame opens floating and centered, rooted at ~/ (home)
- Navigate into a directory with RET (or right-arrow)
- Expected: the popup stays open and shows that directory (browsing keeps it up)
- Open a video with RET (or o)
- Expected: the video opens in its player and the popup vanishes on its own — no q needed, nothing left in the way, and q never lands on the video
- Press Super+F again, open a PDF or image with RET
- Expected: it opens in zathura / feh (externally), NOT inside the popup frame; popup dismisses
- Press Super+F again, open a .txt or .org file with RET
- Expected: it opens in a NEW emacsclient frame (separate from your working session), not adopted into your current session; popup dismisses
- Press Super+F, then click another window without opening anything
- Expected: the popup dismisses on focus loss
- Press Super+F, then press q
- Expected: the popup closes completely (manual dismiss still works; no empty leftover frame)
- Press Super+F, then press Super+F again while it's still open
- Expected: still exactly one popup — the second press focuses the existing one, no second frame, no stray buffer (for several independent file managers, use C-x d)
- Press Super+Shift+F
- Expected: GUI nautilus opens (the binding nautilus moved to)
*** Network module Phase 1 — indicator states on the live bar
What we're verifying: =custom/net= shows the right state for each real network condition. The engine logic is unit-tested; this is the live-bar + visual check (states can't be faked on the running bar). Phase 2-3 tests get added under this task as those phases land.
- Reload waybar to pick up =custom/net=. Super+B does NOT reload a running bar — it only toggles visibility (SIGUSR1), and the bar reads a generated runtime config, so a stale copy keeps the old module. The correct reload regenerates the runtime config then restarts:
#+begin_src sh :results output
waybar-active-config && killall waybar && waybar-toggle
#+end_src
- On a normal connected network, read the module.
- Expected: wifi glyph + signal + SSID; tooltip shows IPv4, gateway, throughput, and a recent "online" probe result.
- Join the hotel/captive network (or any portal network).
- Expected: the module shows the captive state (distinct glyph + warning color), tooltip names the portal host.
- Unplug to a network with no internet (or block egress).
- Expected: the no-internet state (distinct from captive and from disconnected).
*** Network module Phase 1 — net doctor recovers rfkill from a TTY
What we're verifying: the console-recovery path works with no GUI, and recovers the framework's post-power-loss soft-block.
#+begin_src sh :results output
rfkill block wifi # simulate the soft-block
rfkill list wifi # confirm Soft blocked: yes
#+end_src
- Switch to a TTY (Ctrl+Alt+F3) and log in (no Hyprland).
#+begin_src sh :results output
make -C ~/.dotfiles online # or: net doctor --fix
#+end_src
- Expected: doctor reports the rfkill block, runs =rfkill unblock wifi= + =nmcli radio wifi on=, reconnects, and ends "online" — all from the TTY.
*** Network module — bar clicks + airplane keybind (FINAL scheme)
What we're verifying: the custom/net clicks and the airplane keybind. Clicks (settled with Craig over live use 2026-06-29): left = =net-panel= toggle (the GTK panel), middle = =net portal= (floating terminal), right = =net-fix= (notify the doctor result when one-way; open a terminal only when fixable). Airplane = Super+Shift+A.
- Left-click =custom/net=.
- Expected: the GTK connection panel toggles open (left-click again, or Esc, closes it).
- Right-click =custom/net= while online.
- Expected: a desktop notification "Network / Online" (success), no terminal. When a repair is needed it instead opens a terminal running =net doctor --fix=. (Craig confirmed the notification delivers, 2026-06-29.)
- Middle-click =custom/net= on a captive network.
- Expected: =net portal= runs in the floating terminal — reset + opens the portal page.
- Press Super+Shift+A.
- Expected: airplane engages (wifi off, dim, low-power); =custom/net= shows the airplane glyph in gold. Super+Shift+A again restores everything.
- Check =airplane-mode= is still present (=ls ~/.local/bin/airplane-mode=), and =waybar-airplane= / =waybar-netspeed= / =custom/airplane= are gone.
*** Network module Phase 3 — panel Diagnose / Repair / Speed test tabs
What we're verifying: the four-tab panel works end to end. Left-click =custom/net= to open it.
NOTE (2026-07-04 audit): the "four-tab panel" framing predates the instrument-console rebuild (dotfiles 800ef60, f4e688e dropped the Blueprint pages). Diagnose/Repair/Speed-test now live in the single-screen console, not tabs — reword the steps to the console layout before running. Don't delete; the underlying behaviors still need verification.
- Diagnose tab → "Run diagnose".
- Expected: a list of steps (link, DHCP, gateway, DNS config, DNS resolution, internet) each with a ✓/✗/… glyph + evidence; on a captive network an "Open portal" button appears.
- Repair tab → click Reset (or Bounce, or DNS override test).
- Expected: a confirmation dialog with the exact wording (Reset names the network + new-MAC warning; Bounce "links drop briefly"; DNS test "reverts automatically"). Proceed opens a floating terminal that runs the repair (sudo prompt there) and shows the step output incl. cleanup-verified for the DNS test.
- Speed test tab → "Run speed test" (uses ~30s + data — do it on real wifi, not the metered hotspot).
- Expected: ↓/↑ Mbps + ping + server shown inline.
- Byte-rate→Mbps unit: VERIFIED 2026-06-30 (velox). Raw =speedtest-go --json= dl_speed read ~3.66M, unambiguously bytes/s (29 down / 80 up Mbps); =net speedtest= reported 33.62 / 77.99 through the wired path. =BYTES_PER_SEC = True= + =* 8 / 1e6= are correct, no flip needed. Remaining here is only that the panel renders the inline result.
*** Hibernate entry freeze: five observed hibernate cycles on AC
What we're verifying: whether velox hard-freezes on hibernate entry (black
screen, power LED on, never powers off), the documented Framework 13 AMD
failure that fits both dead-battery events. The journal cannot show it; a
person watching the LED can.
- Plug in AC, lid open, nothing important unsaved.
- Note the cycle number, then hibernate from a terminal:
#+begin_src sh :results output
date; systemctl hibernate
#+end_src
- Watch: the screen goes black; within about two minutes the power LED goes
off and the fans stop.
- Press power, enter the ZBM passphrase, and confirm the same session comes
back (windows still open).
- Check that the cycle was a real hibernate and not a fallback:
#+begin_src sh :results output
journalctl -b -o short-iso | grep -E "systemd-sleep|hibernation (entry|exit)|Image allocation|Failed to put" | tail -6
#+end_src
- Check bluetooth came back. A hibernate re-probes the controller, and the
resume hook's btusb reload is what un-wedges it:
#+begin_src sh :results output
rfkill list bluetooth | grep Soft; bluetoothctl show | grep Powered; journalctl -b -o short-iso | grep -E "btusb|unblock set for type bluetooth|Failed to set mode" | tail -6
#+end_src
- Repeat until five cycles are logged.
Expected: all five cycles power off within two minutes and resume into the
same session, with "hibernation exit" and no "Image allocation … short" line.
After each cycle bluetooth reads "Soft blocked: no" and "Powered: yes", the
journal shows a btusb reload and "unblock set for type bluetooth", and no
"Failed to set mode" line.
A cycle where the screen stays black with the power LED on for more than five
minutes is the entry freeze: hold power for 10 s, and write down the cycle
number and whether the keyboard backlight was lit. A cycle that instead comes
straight back with "Cannot allocate memory" is the ARC task, not a freeze.
*** M-split-console: an everyday run with no compositor defers only kernel-kind entries
What we're verifying: with Hyprland not running, =upgrade-guarded
--no-topgrade= lands the pending GPU/compositor packages on real pacman and
leaves only kernel-kind entries deferred (AC3's held-set claim). It's the
first test of the VM gate.
- Make sure Phase 1's commits are in HEAD (=make test-keep= bundles
archsetup from HEAD).
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'pacman -Q wayland libdrm $(pacman -Qqo /usr/lib/modules/*/vmlinuz)'
#+end_src
- On https://archive.archlinux.org/packages/, find the release before the
installed one for =wayland= or =libdrm= (whichever downgrades without
breaking a dependency), and for the VM's kernel and its headers.
- Paste the three URLs into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
gpu_url='PASTE-URL'; kernel_url='PASTE-URL'; headers_url='PASTE-URL'
vmroot "pacman -U --noconfirm $gpu_url $kernel_url $headers_url" | tail -3
vm 'LC_ALL=C pacman -Qu'
#+end_src
- Confirm Hyprland isn't running in the VM. If the block prints LIVE, stop
and record it under this test: the VM procedure's premise has failed.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'pgrep -x Hyprland >/dev/null && echo "Hyprland LIVE: the test premise fails" || echo "Hyprland not running"'
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'upgrade-guarded --no-topgrade > /tmp/ug-console.log 2>&1; echo "exit: $?"
tail -3 /tmp/ug-console.log
jq -r ".data.packages[] | \"\(.kind) \(.name)\"" ~/.local/state/maint/upgrade_deferred.json
echo "-- pacman -Qu:"; LC_ALL=C pacman -Qu'
#+end_src
Expected: Hyprland was not running; the run printed exit 0; the GPU-pattern
package is back at its current version; every record line reads kernel;
and =pacman -Qu= lists only the kernel and its headers, plus any
=[ignored]= row.
*** M-split-live on ratio: the live everyday run leaves the guard silent
What we're verifying: the everyday split transaction on real pacman with
Hyprland live, before any lever calls it. It should exit 0, print no
BLOCKED banner, and defer exactly what =--dry-run= predicted (AC1). It's
the last item of step (a) on ratio.
- Run it in ratio's live Hyprland session, right after the rest of step
(a).
#+begin_src sh :results output
upgrade-guarded --dry-run > /tmp/ug-dry.txt 2>&1; echo "dry-run exit: $?"
cat /tmp/ug-dry.txt
#+end_src
- Note the predicted deferred set, and whether it lists a GPU-kind entry.
#+begin_src sh :results output
# The real run: refresh, -Su with the held set ignored, then yay -Sua. It
# takes minutes, and Emacs waits on it.
upgrade-guarded --no-topgrade > /tmp/ug-live.log 2>&1; echo "exit: $?"
echo "BLOCKED lines: $(grep -c BLOCKED /tmp/ug-live.log)"
jq -r '.data.packages[] | "\(.kind) \(.name)"' ~/.local/state/maint/upgrade_deferred.json | sort
echo "-- pacman -Qu:"; LC_ALL=C pacman -Qu
#+end_src
- If the dry-run listed no GPU-kind entry, leave this test open and repeat
it on the first day one is pending. The hook-silence half needs one.
Expected: exit 0; zero BLOCKED lines; the record's names equal the
dry-run's predicted deferred set; =pacman -Qu= lists only those names plus
the =[ignored]= bridge-utils row; and on a run with a GPU-kind entry
pending, that entry is in the record and the guard never printed its
banner.
*** M-split-live on velox: the live everyday run leaves the guard silent
What we're verifying: the same everyday split transaction on velox's ZFS
root with Hyprland live, before any lever calls it. It should exit 0, print
no BLOCKED banner, and defer exactly what =--dry-run= predicted, with the
kernel and zfs-dkms among the deferred entries when pending (AC1). It's the
last item of step (a) on velox.
- Run it in velox's live Hyprland session, right after the rest of step
(a).
#+begin_src sh :results output
uname -r
upgrade-guarded --dry-run > /tmp/ug-dry.txt 2>&1; echo "dry-run exit: $?"
cat /tmp/ug-dry.txt
#+end_src
- Note the predicted deferred set, and whether it lists a GPU-kind entry.
#+begin_src sh :results output
# The real run: refresh, -Su with the held set ignored, then yay -Sua. It
# takes minutes, and Emacs waits on it.
upgrade-guarded --no-topgrade > /tmp/ug-live.log 2>&1; echo "exit: $?"
echo "BLOCKED lines: $(grep -c BLOCKED /tmp/ug-live.log)"
jq -r '.data.packages[] | "\(.kind) \(.name)"' ~/.local/state/maint/upgrade_deferred.json | sort
echo "-- pacman -Qu:"; LC_ALL=C pacman -Qu
pacman -Q $(pacman -Qqo /usr/lib/modules/*/vmlinuz)
#+end_src
- If the dry-run listed no GPU-kind entry, leave this test open and repeat
it on the first day one is pending. The hook-silence half needs one.
Expected: exit 0; zero BLOCKED lines; the record's names equal the
dry-run's predicted deferred set; =pacman -Qu= lists only those names plus
any =[ignored]= rows; the kernel package's version is unchanged; and on a
run with a GPU-kind entry pending, that entry is in the record and the
guard never printed its banner.
*** M-boot-armed in the ZFS VM: the armed set lands from cache on tty1 before login, offline
What we're verifying: the completion path on a ZFS root with no network at
boot. =--complete= should pass the real gate (=sudo -n lsinitcpio=
included) and arm without firing the guard or informant's hook. The boot
unit should then apply the armed set before getty@tty1 starts (AC5, and
AC4's gate on a real initramfs). Second test of the VM gate.
- Make sure Phase 1's and Phase 3's commits are in HEAD.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'pacman -Q wayland libdrm $(pacman -Qqo /usr/lib/modules/*/vmlinuz)'
#+end_src
- On https://archive.archlinux.org/packages/, find the release before the
installed one for =wayland= or =libdrm=, and for the VM's kernel and its
headers.
- Paste the three URLs into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
gpu_url='PASTE-URL'; kernel_url='PASTE-URL'; headers_url='PASTE-URL'
vmroot "pacman -U --noconfirm $gpu_url $kernel_url $headers_url" | tail -3
vm 'LC_ALL=C pacman -Qu'
#+end_src
- Arm over SSH with the live branch forced, since Hyprland never runs in
the VM.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'UPGRADE_GUARDED_HYPR_RUNNING=1 upgrade-guarded --complete > /tmp/ug-arm.log 2>&1; echo "exit: $?"
echo "pre-transaction hook runs: $(grep -c "Running pre-transaction hooks" /tmp/ug-arm.log)"
echo "BLOCKED lines: $(grep -c BLOCKED /tmp/ug-arm.log)"
echo "-- flag:"; cat /var/lib/archsetup/apply-upgrade-on-boot
echo "-- record:"; jq -c ".data | {result, gate, held_snapshot, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json'
#+end_src
- Reboot the guest and take its network link down straight away.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; sleep 3; mon set_link net0 off >/dev/null; echo "link off"
#+end_src
- Capture tty1 for three minutes while it boots.
#+begin_src sh :results output
. /tmp/ug-vm.sh
shots 90
#+end_src
- Open the frames in order (=imv /tmp/ug-shots/=).
- Bring the link back.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon set_link net0 on >/dev/null; sleep 20; echo "link on"
#+end_src
- Read the boot's outcome.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'journalctl -b -t archsetup-boot-upgrade --no-pager | head -2
systemctl show archsetup-boot-upgrade -p Result -p ExecMainExitTimestampMonotonic
systemctl show getty@tty1 -p ExecMainStartTimestampMonotonic
ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
jq -c ".data | {result, failed_step, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json
echo "stamp age: $(( $(date +%s) - $(jq -r ".written_at | floor" ~/.local/state/maint/topgrade_run.json) )) s"
echo "uptime: $(cut -d. -f1 /proc/uptime) s"'
#+end_src
Expected: the arm exited 0 with exactly one pre-transaction hook run (stage
1's; the arm is forced live, so no stage 2 runs, and the arm's =-Sw= ran no
hooks) and zero BLOCKED lines. The flag listed the GPU entries as
=name=version= lines, and the record had gate null with held_snapshot
naming a pre-pacman snapshot. A frame shows 'archsetup: applying N deferred
GPU/compositor upgrades — do not power off' on tty1 before any frame shows
the login, and the journal's first line is that banner. Result=success, and
the unit's exit timestamp is below getty@tty1's start timestamp. Afterwards
the flag is gone, the record reads result ok with n 0, and the stamp's age
is below the uptime.
*** M-boot-midtx in the ZFS VM: a timeout inside a real transaction leaves no db.lck
What we're verifying: when the start timeout's SIGINT lands in the middle
of a real transaction, pacman should stop at a package boundary and
release =db.lck=. The record should name the interruption, and
=--complete= from a console should finish the set (AC6). Third test of the
VM gate, on the VM M-boot-armed leaves.
- Use the VM M-boot-armed just left. Nothing reinstalls between the two
tests, so its journal still times a real boot transaction.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'journalctl -t archsetup-boot-upgrade -o short-monotonic --no-pager | grep -E "applying|upgrading|installing|transaction" | tail -12'
#+end_src
- On https://archive.archlinux.org/packages/, find the previous releases of
two or more GPU-pattern packages, mesa among them, so the transaction is
long enough to cut.
- Paste their URLs into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
gpu_urls='PASTE-URL PASTE-URL'
vmroot "pacman -U --noconfirm $gpu_urls" | tail -2; vm 'LC_ALL=C pacman -Qu'
#+end_src
- Arm over SSH with the live branch forced.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'UPGRADE_GUARDED_HYPR_RUNNING=1 upgrade-guarded --complete > /tmp/ug-arm.log 2>&1; echo "exit: $?"
cat /var/lib/archsetup/apply-upgrade-on-boot'
#+end_src
- Work out a start timeout that lands inside the transaction: the seconds
from the banner to about halfway through the 'upgrading' lines above,
scaled up for the larger set.
- Put it in the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
secs=PASTE-SECONDS
vmroot "install -d /etc/systemd/system/archsetup-boot-upgrade.service.d
printf '[Service]\nTimeoutStartSec=%ss\n' $secs > /etc/systemd/system/archsetup-boot-upgrade.service.d/test.conf
systemctl daemon-reload && cat /etc/systemd/system/archsetup-boot-upgrade.service.d/test.conf"
#+end_src
- Reboot the guest.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; echo rebooting
#+end_src
- Capture tty1 for three minutes.
#+begin_src sh :results output
. /tmp/ug-vm.sh
shots 90
#+end_src
- Open the frames in order (=imv /tmp/ug-shots/=).
- Read the outcome once the login shows.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'ls /var/lib/pacman/db.lck 2>&1
jq -c ".data | {result, failed_step, detail, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json
journalctl -b -t archsetup-boot-upgrade --no-pager | tail -4'
#+end_src
- If the journal shows the transaction finished, or never started, change
the seconds and repeat from the downgrade block.
- Confirm Hyprland isn't running, so the next =--complete= applies the set
rather than arming it. If the block prints LIVE, record it under this
test.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'pgrep -x Hyprland >/dev/null && echo "Hyprland LIVE: the test premise fails" || echo "Hyprland not running"'
#+end_src
- Finish the set from a console.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'upgrade-guarded --complete > /tmp/ug-finish.log 2>&1; echo "exit: $?"
LC_ALL=C pacman -Qu
jq -c ".data | {result, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json'
#+end_src
- Remove the drop-in.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'rm -rf /etc/systemd/system/archsetup-boot-upgrade.service.d && systemctl daemon-reload && echo removed'
#+end_src
Expected: no =db.lck= after the cut-off boot, and the journal stopped
between packages. The record read result interrupted with failed_step
boot-transaction and the =--complete= remedy. Then the console
=--complete= exited 0, =pacman -Qu= no longer lists the armed packages, and
the record reads result ok with n 0.
*** M-boot-timeout in the ZFS VM: a hung boot run times out to the login and disarms
What we're verifying: a boot transaction that never ends is cut off by the
start timeout (shortened here to 90 s). The login should still appear, the
flag should be gone, the failure should be visible in the record and in
maint, and the next boot should skip the unit (AC6). Fourth test of the VM
gate.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
- Run the next block for a fresh VM, or skip it to reuse the VM
M-boot-midtx left (the block reinstalls from clean).
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
- On https://archive.archlinux.org/packages/, find the release before the
installed =wayland= or =libdrm=.
- Paste its URL into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
gpu_url='PASTE-URL'
vmroot "pacman -U --noconfirm $gpu_url" | tail -2; vm 'LC_ALL=C pacman -Qu'
#+end_src
- Arm over SSH with the live branch forced.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'UPGRADE_GUARDED_HYPR_RUNNING=1 upgrade-guarded --complete > /tmp/ug-arm.log 2>&1; echo "exit: $?"
cat /var/lib/archsetup/apply-upgrade-on-boot'
#+end_src
- Install the stand-in fakes and the drop-in. The fake pacman hands every
query to the real one and sleeps on the transaction, and the fake sudo
drops =-n=. They do the job the Phase 1 fakes do.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'set -e
d=/usr/local/lib/ug-fakes
install -d "$d" /etc/systemd/system/archsetup-boot-upgrade.service.d
cat > "$d/sudo" <<"EOF"
#!/bin/sh
# Drop -n and run the command as the caller.
[ "$1" = -n ] && shift
exec "$@"
EOF
cat > "$d/pacman" <<"EOF"
#!/bin/sh
# Real pacman for every query; the transaction (-S) sleeps or fails.
if [ "$1" = -S ]; then
[ "$UG_FAKE_MODE" = fail ] && { echo "error: failed to commit transaction (fake)" >&2; exit 1; }
exec sleep 3600
fi
exec /usr/bin/pacman "$@"
EOF
chmod 755 "$d/sudo" "$d/pacman"
cat > /etc/systemd/system/archsetup-boot-upgrade.service.d/test.conf <<"EOF"
[Service]
TimeoutStartSec=90s
Environment=PATH=/usr/local/lib/ug-fakes:/usr/local/bin:/usr/bin
Environment=UG_FAKE_MODE=sleep
EOF
systemctl daemon-reload && echo installed'
#+end_src
- Reboot the guest.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; echo rebooting
#+end_src
- Capture tty1 for four minutes.
#+begin_src sh :results output
. /tmp/ug-vm.sh
shots 120
#+end_src
- Open the frames in order (=imv /tmp/ug-shots/=).
- Read the outcome once the login shows.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
systemctl is-failed archsetup-boot-upgrade.service
systemctl show archsetup-boot-upgrade -p ExecMainStartTimestampMonotonic
systemctl show getty@tty1 -p ExecMainStartTimestampMonotonic
jq -c ".data | {result, failed_step, detail, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json
ls /var/lib/pacman/db.lck 2>&1
~/.local/bin/maint status 2>&1 | grep -i -E "failed|deferred"'
#+end_src
- Reboot the guest once more.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; echo rebooting
#+end_src
- About a minute later, read whether the unit ran.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'systemctl show archsetup-boot-upgrade -p ConditionResult -p ActiveState'
#+end_src
- Remove the drop-in and the fakes.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'rm -rf /etc/systemd/system/archsetup-boot-upgrade.service.d /usr/local/lib/ug-fakes && systemctl daemon-reload && echo removed'
#+end_src
Expected: the login appeared about 90 s into the unit: getty@tty1's start
timestamp is about 90,000,000 µs above the unit's
ExecMainStartTimestampMonotonic. After it, the flag was gone, =is-failed=
printed failed, and no =db.lck= existed. The record read result
interrupted with failed_step boot-transaction, a detail naming
=upgrade-guarded --complete=, and n equal to the armed count. maint's
failed-units row names archsetup-boot-upgrade.service, and the deferred row
lists the armed set at WARN. The next boot shows ConditionResult=no.
*** M-boot-fail in the ZFS VM: a failed boot transaction still reaches the login and disarms
What we're verifying: a boot transaction that fails outright should still
end at the tty1 login. The flag should be gone, the record should name the
failed step, maint should show the failed unit and the deferred set, and
the unit should be left failed (AC6). Last test of the VM gate.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
- Run the next block for a fresh VM, or skip it to reuse the VM the
previous M-boot test left (the block reinstalls from clean).
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
- On https://archive.archlinux.org/packages/, find the release before the
installed =wayland= or =libdrm=.
- Paste its URL into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
gpu_url='PASTE-URL'
vmroot "pacman -U --noconfirm $gpu_url" | tail -2; vm 'LC_ALL=C pacman -Qu'
#+end_src
- Arm over SSH with the live branch forced.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'UPGRADE_GUARDED_HYPR_RUNNING=1 upgrade-guarded --complete > /tmp/ug-arm.log 2>&1; echo "exit: $?"
cat /var/lib/archsetup/apply-upgrade-on-boot'
#+end_src
- Install the stand-in fakes and the drop-in, with the fake pacman set to
answer =-Q= and =-Sp= normally and exit 1 on =-S=.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'set -e
d=/usr/local/lib/ug-fakes
install -d "$d" /etc/systemd/system/archsetup-boot-upgrade.service.d
cat > "$d/sudo" <<"EOF"
#!/bin/sh
# Drop -n and run the command as the caller.
[ "$1" = -n ] && shift
exec "$@"
EOF
cat > "$d/pacman" <<"EOF"
#!/bin/sh
# Real pacman for every query; the transaction (-S) sleeps or fails.
if [ "$1" = -S ]; then
[ "$UG_FAKE_MODE" = fail ] && { echo "error: failed to commit transaction (fake)" >&2; exit 1; }
exec sleep 3600
fi
exec /usr/bin/pacman "$@"
EOF
chmod 755 "$d/sudo" "$d/pacman"
cat > /etc/systemd/system/archsetup-boot-upgrade.service.d/test.conf <<"EOF"
[Service]
TimeoutStartSec=90s
Environment=PATH=/usr/local/lib/ug-fakes:/usr/local/bin:/usr/bin
Environment=UG_FAKE_MODE=fail
EOF
systemctl daemon-reload && echo installed'
#+end_src
- Reboot the guest.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; echo rebooting
#+end_src
- Capture tty1 for two minutes.
#+begin_src sh :results output
. /tmp/ug-vm.sh
shots 60
#+end_src
- Open the frames in order (=imv /tmp/ug-shots/=).
- Read the outcome once the login shows.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
systemctl is-failed archsetup-boot-upgrade.service
jq -c ".data | {result, failed_step, detail, n: (.packages | length)}" ~/.local/state/maint/upgrade_deferred.json
~/.local/bin/maint status 2>&1 | grep -i -E "failed|deferred"'
#+end_src
- Remove the drop-in and the fakes.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'rm -rf /etc/systemd/system/archsetup-boot-upgrade.service.d /usr/local/lib/ug-fakes && systemctl daemon-reload && echo removed'
#+end_src
Expected: the login appeared without waiting out the timeout, the flag is
gone, and =is-failed= prints failed. The record reads result failed with
failed_step boot-transaction, a detail whose last line is the fake's
'error: failed to commit transaction (fake)', and n equal to the armed
count. maint's failed-units row names archsetup-boot-upgrade.service, and
the deferred row lists the armed set at WARN.
*** M-boot-armed on ratio: APPLY arms, the row shows it, and the offline reboot applies the set
What we're verifying: the panel path the VM can't cover. APPLY should open
=upgrade-guarded --complete= in a detached foot terminal, the row should
read armed with REBOOT shown, and an offline armed boot should apply the
set before the login (AC4's APPLY clause, AC5).
- Run it after step (c) on ratio, on a day the dry-run below lists a
GPU-kind entry.
#+begin_src sh :results output
upgrade-guarded --dry-run 2>&1 | tail -20
#+end_src
- Open the maint panel.
- Find the deferred row.
- Press APPLY on the deferred row. The first press arms.
- Press APPLY again to fire it.
- Watch the panel's wall and the foot terminal that opens, until the
terminal shows its 'Reboot now?' prompt.
- Press Enter at the prompt, which answers no.
- Wait up to 30 s for the panel's next probe.
- Read the deferred row and the REBOOT key.
- Read the flag.
#+begin_src sh :results output
cat /var/lib/archsetup/apply-upgrade-on-boot
#+end_src
- Take networking down.
#+begin_src sh :results output
nmcli networking off && echo "networking off"
#+end_src
- Press REBOOT on the panel.
- Confirm the reboot the way the panel asks.
- Watch the monitor through the boot until the tty1 password prompt.
- Log in.
- Bring networking back.
#+begin_src sh :results output
nmcli networking on && echo "networking on"
#+end_src
#+begin_src sh :results output
ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
jq -c '.data | {result, failed_step, n: (.packages | length)}' ~/.local/state/maint/upgrade_deferred.json
maint status 2>&1 | grep -i -E 'topgrade|deferred'
#+end_src
Expected: the second APPLY press opened a foot terminal running
=upgrade-guarded --complete=, and the panel's wall streamed nothing. The
terminal ended at the prompt with no BLOCKED banner. Before the reboot, the
row read 'armed — A apply at next boot' (with ' · <N−A> more deferred' if
anything else stayed deferred) and REBOOT was shown. The banner showed on
the monitor before the password prompt. Afterwards the flag is gone, the
record reads result ok with n 0, and =maint status= shows topgrade_age
fresh when nothing else is deferred.
*** M-boot-ratio: the banner and progress show on ratio's monitor, and REBOOT shows while armed
What we're verifying: ratio's own boot path. Its =/dev/console= is ttyS0,
so the unit's tty1 output has to reach the monitor. Its tty1 shows a
password prompt that the =Before=getty@tty1.service= ordering holds. And
REBOOT shows from the flag alone, since reboot_required stays false there
(AC5, and Phase 4's ratio-path confirmation).
- Run it on the boot M-boot-armed on ratio arms, doing the steps up to the
REBOOT press before that test's own REBOOT press; or arm the same way.
#+begin_src sh :results output
ls -l /var/lib/archsetup/apply-upgrade-on-boot
maint status 2>&1 | grep -i reboot
#+end_src
- Read the panel's REBOOT key.
- Press REBOOT on the panel.
- Confirm the reboot the way the panel asks.
- Watch the monitor from the firmware screen to the tty1 password prompt.
- Log in.
#+begin_src sh :results output
systemctl show archsetup-boot-upgrade -p ExecMainExitTimestampMonotonic
systemctl show getty@tty1 -p ExecMainStartTimestampMonotonic
#+end_src
Expected: before the reboot, reboot_required read clear and REBOOT was
shown anyway. The banner and pacman's per-package progress stayed on the
monitor for the whole transaction, and the password prompt appeared only
after it. The unit's exit timestamp is below getty@tty1's start timestamp.
*** ratio's linux-lts-strix is never moved or gated
What we're verifying: the foreign, headerless GRUB-default kernel stays out
of the pending set and the gate entry on a real =--complete= (Phase 4's
ratio-path confirmation).
- Run it on ratio the first time a =--complete= lands linux or linux-lts.
#+begin_src sh :results output
pacman -Q linux-lts-strix | tee /tmp/ug-strix-before.txt
LC_ALL=C pacman -Qu | grep strix || echo "strix not pending"
#+end_src
- Start =upgrade-guarded --complete= with APPLY, or in a terminal.
- While stage 1 runs, read the gate entry from a second terminal.
#+begin_src sh :results output
jq -c '.data.gate' ~/.local/state/maint/upgrade_deferred.json
#+end_src
- Let the session finish.
#+begin_src sh :results output
pacman -Q linux-lts-strix | diff /tmp/ug-strix-before.txt - && echo "strix unchanged"
grep "$(date +%Y-%m-%d)" /var/log/pacman.log | grep linux-lts-strix || echo "no strix lines today"
#+end_src
Expected: strix wasn't pending; gate.pkgbases during stage 1 listed linux
and/or linux-lts and never linux-lts-strix; strix's version is unchanged;
and no pacman.log line from today names it.
*** M-boot-armed on velox: APPLY arms, the row shows it, and the offline reboot applies the set
What we're verifying: the same panel path on velox's ZFS root with
autologin. APPLY's detached terminal should run the real gate when a
kernel is pending, the row should read armed with REBOOT shown, and an
offline armed boot should finish before autologin starts Hyprland (AC4's
APPLY clause, AC5).
- Run it after step (c) on velox, on a day the dry-run below lists a
GPU-kind entry.
#+begin_src sh :results output
upgrade-guarded --dry-run 2>&1 | tail -20
#+end_src
- Open the maint panel.
- Find the deferred row.
- Press APPLY on the deferred row. The first press arms.
- Press APPLY again to fire it.
- Watch the panel's wall and the foot terminal that opens, until the
terminal shows its 'Reboot now?' prompt.
- Press Enter at the prompt, which answers no.
- Wait up to 30 s for the panel's next probe.
- Read the deferred row and the REBOOT key.
- Read the flag.
#+begin_src sh :results output
cat /var/lib/archsetup/apply-upgrade-on-boot
#+end_src
- Take networking down.
#+begin_src sh :results output
nmcli networking off && echo "networking off"
#+end_src
- Press REBOOT on the panel.
- Confirm the reboot the way the panel asks.
- Watch tty1 from the ZFSBootMenu countdown until autologin starts
Hyprland.
- Bring networking back.
#+begin_src sh :results output
nmcli networking on && echo "networking on"
#+end_src
#+begin_src sh :results output
ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
jq -c '.data | {result, failed_step, gate, n: (.packages | length)}' ~/.local/state/maint/upgrade_deferred.json
journalctl -b -t archsetup-boot-upgrade --no-pager | head -2
maint status 2>&1 | grep -i -E 'topgrade|deferred'
#+end_src
Expected: the second APPLY press opened a foot terminal running
=upgrade-guarded --complete=, and the panel's wall streamed nothing. If a
kernel was pending, the terminal showed the gate passing before the arm.
Before the reboot the row read 'armed — A apply at next boot' and REBOOT
was shown. The banner showed on tty1 before Hyprland started, and the
journal's first line is that banner. Afterwards the flag is gone, the
record reads result ok with gate null and n 0, and =maint status= shows
topgrade_age fresh when nothing else is deferred.
*** M-boot-news on velox: unread Arch news doesn't stall the boot run
What we're verifying: with two or more unread news items at boot, the boot
form's informant clear should keep the transaction from waiting on stdin
(AC7 at boot).
- Run it after step (c) on velox, on a day =upgrade-guarded --dry-run=
lists a GPU-kind entry.
- Open the maint panel at its deferred row.
- Press APPLY on the deferred row. The first press arms.
- Press APPLY again to fire it.
- Press Enter at the terminal's 'Reboot now?' prompt, which answers no.
Arming cleared the news, so the news has to become unread after this.
#+begin_src sh :results output
informant list --unread 2>&1 | head
#+end_src
- If fewer than two items are unread, move informant's state aside so the
whole feed reads unread. The last step puts it back, and no pacman
transaction should run before the reboot.
#+begin_src sh :results output
sudo mv /var/lib/informant.dat /var/lib/informant.dat.bak && informant list --unread 2>&1 | head -3
#+end_src
- Press REBOOT on the panel.
- Confirm the reboot the way the panel asks.
- Watch tty1 until autologin starts Hyprland.
#+begin_src sh :results output
systemctl show archsetup-boot-upgrade -p Result -p ExecMainStatus
journalctl -b -t archsetup-boot-upgrade --no-pager | tail -5
jq -c '.data | {result, failed_step, n: (.packages | length)}' ~/.local/state/maint/upgrade_deferred.json
#+end_src
- If you moved informant's state aside, restore it.
#+begin_src sh :results output
[ -e /var/lib/informant.dat.bak ] && sudo mv -f /var/lib/informant.dat.bak /var/lib/informant.dat && echo restored
#+end_src
Expected: the boot run ended with Result=success well inside its timeout,
the journal runs straight from the banner to the end of the transaction
with nothing waiting on input, and the record reads result ok.
*** D-zbm: recover from a failed zfs-dkms build by rolling back to the held snapshot
What we're verifying: the README's recovery steps work end to end on a ZFS
root. A =--complete= whose zfs build fails should exit 4 with its snapshot
held and named. A ZFSBootMenu rollback (MOD+R) plus the pacman-log
reconcile should then leave a consistent system. This is non-gating; no
criterion waits on it.
- Follow the recovery steps in =maint/README.md= (Phase 4), which carry the
first build task's MOD+R and version fixes.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'pacman -Q less $(pacman -Qqo /usr/lib/modules/*/vmlinuz)'
#+end_src
- On https://archive.archlinux.org/packages/, find the previous releases of
the VM's kernel, its headers and one small leaf package (=less=).
- Paste the three URLs into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
kernel_url='PASTE-URL'; headers_url='PASTE-URL'; leaf_url='PASTE-URL'
vmroot "pacman -U --noconfirm $kernel_url $headers_url $leaf_url" | tail -3
vm 'LC_ALL=C pacman -Qu'
#+end_src
- Make the zfs build fail for any kernel built from now on. The VM is
disposable after this drill.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'for f in /usr/src/zfs-*/dkms.conf; do echo "MAKE[0]=\"false\"" >> "$f"; tail -1 "$f"; done'
#+end_src
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'upgrade-guarded --complete > /tmp/ug-zbm.log 2>&1; echo "exit: $?"; tail -2 /tmp/ug-zbm.log
ls /var/lib/archsetup/apply-upgrade-on-boot 2>&1
snap=$(jq -r .data.held_snapshot ~/.local/state/maint/upgrade_deferred.json); echo "held: $snap"
zfs holds "$snap"'
#+end_src
- Write down the held snapshot's name; a rolled-back root may not keep it
in the record.
- Reboot the guest and catch ZFSBootMenu inside its 3 s countdown by
sending Escape every half second (the VM takes no keyboard input any
other way).
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'sudo systemctl reboot'; sleep 5
for i in $(seq 60); do mon sendkey esc >/dev/null; sleep 0.5; done
shot
#+end_src
- If the shot shows anything but the boot-environment list, change the
=sleep= before the loop and run the block again; if it shows the
firmware's setup screen, which also answers Escape, run
=mon system_reset= first.
- Open the snapshot list with MOD+S (Ctrl+S).
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-s >/dev/null; sleep 1; shot
#+end_src
- If ZFSBootMenu asks to import the pool read/write, press MOD+W.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-w >/dev/null; sleep 1; shot
#+end_src
- Move the selection onto the held snapshot one row at a time, checking
each shot.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey down >/dev/null; sleep 1; shot
#+end_src
- Roll back with MOD+R. Never press ENTER (duplicate), MOD+X (clone and
promote) or MOD+C (clone) here.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-r >/dev/null; sleep 1; shot
#+end_src
- Answer the rollback confirmation the screen shows; for a y/N prompt, the
next block sends y.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey y >/dev/null; sleep 2; shot
#+end_src
- Go back to the boot-environment list.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey esc >/dev/null; sleep 1; shot
#+end_src
- Boot =zroot/ROOT/default=.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ret >/dev/null; sleep 40; shot
#+end_src
- List the pacman.log lines to undo, newest first, with the held name
pasted in.
#+begin_src sh :results output
. /tmp/ug-vm.sh
snap='PASTE-HELD-SNAPSHOT'
vmroot "ls /var/lib/pacman/db.lck 2>&1
t=\$(date -d @\$(zfs get -Hp -o value creation $snap) +%Y-%m-%dT%H:%M:%S); echo \"since \$t\"
awk -v t=\"\$t\" 'substr(\$1, 2, 19) >= t && \$2 == \"[ALPM]\" && \$3 ~ /^(upgraded|downgraded|installed|removed)\$/' /var/log/pacman.log | tac"
#+end_src
- Undo each listed line, newest first, as the README's recovery steps say.
- Paste the reconciled names into the check block and run it.
#+begin_src sh :results output
. /tmp/ug-vm.sh
names='PASTE-RECONCILED-NAMES'
vmroot "pacman -Q $names; pacman -Dk && echo 'Dk clean'; pacman -Qkk $names 2>&1 | tail -4"
#+end_src
Expected: =--complete= exited 4 with the zfs module item as its last line,
left no flag, and named a held snapshot that =zfs holds= shows with the
=upgrade-guarded= tag. After the rollback and the reconcile, =pacman -Q=
shows the old kernel, headers and =less= versions, =pacman -Dk= is clean,
and =pacman -Qkk= over the reconciled names reports no mismatch.
*** D-zbm power-cut variant: a reset during stage 1 leaves the right snapshot held and a reconcilable db
What we're verifying: a hard reset during =--complete='s stage 1, on a VM
with no open gate, should leave the snapshot step 3 took held and named.
Recovery's power-cut steps, including the half-registered package, should
then reconcile the db. This is non-gating.
- Use a fresh =make test-keep FS_PROFILE=zfs= VM, not the one D-zbm left
with its gate open.
#+begin_src sh :results output
# VM helpers; every later block in this test sources them. Root logs in by
# key only after the install, so vmroot uses the key the harness left with
# the newest test results.
cat > /tmp/ug-vm.sh <<'EOF'
cd ~/code/archsetup || exit 1
o="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -p 2222"
key=$(ls -td test-results/*/root_key 2>/dev/null | head -1)
vm() { sshpass -p archsetup ssh $o cjennings@localhost "$@"; }
vmroot() { ssh $o -i "$key" root@localhost "$@"; }
mon() { echo "$*" | socat - UNIX-CONNECT:vm-images/qemu-monitor-zfs.sock; }
shot() { mon screendump /tmp/ug-tty1.ppm >/dev/null; sleep 1
magick /tmp/ug-tty1.ppm /tmp/ug-tty1.png && echo /tmp/ug-tty1.png; }
shots() { d=/tmp/ug-shots; rm -rf "$d"; mkdir -p "$d"
for i in $(seq -w 1 "${1:-90}"); do mon "screendump $d/$i.ppm" >/dev/null; sleep 2; done
for f in "$d"/*.ppm; do magick "$f" "${f%.ppm}.png" && rm -f "$f"; done
echo "$(ls "$d" | wc -l) frames, 2 s apart, in $d"; }
EOF
echo "helpers written to /tmp/ug-vm.sh"
#+end_src
#+begin_src sh :results output
# The full install runs first; this takes a while, and Emacs waits on it.
cd ~/code/archsetup && make test-keep FS_PROFILE=zfs 2>&1 | tail -6
#+end_src
- On https://archive.archlinux.org/packages/, find the previous releases of
the VM's kernel and its headers.
- Paste the two URLs into the next block.
#+begin_src sh :results output
. /tmp/ug-vm.sh
kernel_url='PASTE-URL'; headers_url='PASTE-URL'
vmroot "pacman -U --noconfirm $kernel_url $headers_url" | tail -3
vm 'LC_ALL=C pacman -Qu'
#+end_src
- In a separate terminal, start the session with a tty. pacman prints
'(n/m) upgrading <pkg>' only on a tty; without one it prints 'upgrading
<pkg>...'.
#+begin_src sh :eval no
sshpass -p archsetup ssh -t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222 cjennings@localhost upgrade-guarded --complete
#+end_src
- As soon as stage 1's first upgrading line appears, read the held
snapshot's name.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vm 'jq -r .data.held_snapshot ~/.local/state/maint/upgrade_deferred.json'
#+end_src
- As the kernel headers package's upgrading line appears, run the next
block. It hard-resets the guest, then sends Escape every half second to
catch ZFSBootMenu's 3 s countdown. Never use =system_powerdown= or
=quit=: both end QEMU, and =make test-keep= then restores the clean
install.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon system_reset >/dev/null; sleep 3
for i in $(seq 60); do mon sendkey esc >/dev/null; sleep 0.5; done
shot
#+end_src
- If the shot shows anything but the boot-environment list, run
=mon system_reset= and the Escape loop again with a changed delay.
- Open the snapshot list with MOD+S (Ctrl+S).
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-s >/dev/null; sleep 1; shot
#+end_src
- If ZFSBootMenu asks to import the pool read/write, press MOD+W.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-w >/dev/null; sleep 1; shot
#+end_src
- Move the selection onto the held snapshot one row at a time, checking
each shot. If its name wasn't read in time, it's the
=zroot/ROOT/default@pre-pacman_*= row taken just before stage 1.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey down >/dev/null; sleep 1; shot
#+end_src
- Roll back with MOD+R. Never press ENTER (duplicate), MOD+X (clone and
promote) or MOD+C (clone) here.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ctrl-r >/dev/null; sleep 1; shot
#+end_src
- Answer the rollback confirmation the screen shows; for a y/N prompt, the
next block sends y.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey y >/dev/null; sleep 2; shot
#+end_src
- Go back to the boot-environment list.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey esc >/dev/null; sleep 1; shot
#+end_src
- Boot =zroot/ROOT/default=.
#+begin_src sh :results output
. /tmp/ug-vm.sh
mon sendkey ret >/dev/null; sleep 40; shot
#+end_src
- Read the hold, the lock and the db's state, with the held name pasted in.
#+begin_src sh :results output
. /tmp/ug-vm.sh
snap='PASTE-HELD-SNAPSHOT'
vmroot "zfs holds $snap; ls /var/lib/pacman/db.lck 2>&1; pacman -Dk 2>&1 | tail -3; tail -8 /var/log/pacman.log"
#+end_src
- If pacman.log shows the reset landed outside extraction (every target
has an =[ALPM]= line, or 'running post-transaction hooks' is logged),
stop and repeat on a fresh VM.
- Remove =db.lck= if the read above showed it.
#+begin_src sh :results output
. /tmp/ug-vm.sh
vmroot 'rm -fv /var/lib/pacman/db.lck'
#+end_src
- Delete the local db entry =pacman -Dk= reported as 'description file is
missing', with its directory name pasted in.
#+begin_src sh :results output
. /tmp/ug-vm.sh
entry='PASTE-ENTRY-DIR'
vmroot "rm -rv /var/lib/pacman/local/$entry"
#+end_src
- Find the version the booted root holds for that package: the last
=[ALPM]= line for it before the snapshot's creation (on an upgraded or
downgraded line, the version after =->=; on a removed line, or with no
line, register nothing and skip the next step).
#+begin_src sh :results output
. /tmp/ug-vm.sh
snap='PASTE-HELD-SNAPSHOT'; pkg='PASTE-PACKAGE'
vmroot "t=\$(date -d @\$(zfs get -Hp -o value creation $snap) +%Y-%m-%dT%H:%M:%S)
awk -v t=\"\$t\" -v p=$pkg 'substr(\$1, 2, 19) < t && \$2 == \"[ALPM]\" && \$4 == p' /var/log/pacman.log | tail -1"
#+end_src
- Register that version from the cache, with its package file pasted in.
#+begin_src sh :results output
. /tmp/ug-vm.sh
pkgfile='PASTE-CACHE-FILE'
vmroot "pacman -U --dbonly --noconfirm /var/cache/pacman/pkg/$pkgfile"
#+end_src
- List the later =[ALPM]= lines to undo, newest first.
#+begin_src sh :results output
. /tmp/ug-vm.sh
snap='PASTE-HELD-SNAPSHOT'
vmroot "t=\$(date -d @\$(zfs get -Hp -o value creation $snap) +%Y-%m-%dT%H:%M:%S); echo \"since \$t\"
awk -v t=\"\$t\" 'substr(\$1, 2, 19) >= t && \$2 == \"[ALPM]\" && \$3 ~ /^(upgraded|downgraded|installed|removed)\$/' /var/log/pacman.log | tac"
#+end_src
- Undo each listed line, newest first, as the README's recovery steps say.
- Paste the reconciled names into the check block and run it.
#+begin_src sh :results output
. /tmp/ug-vm.sh
names='PASTE-RECONCILED-NAMES'
vmroot "pacman -Q $names; pacman -Dk && echo 'Dk clean'; pacman -Qkk $names 2>&1 | tail -4"
#+end_src
Expected: =zfs holds= showed the =upgrade-guarded= tag on the snapshot step
3 took before stage 1, and held_snapshot named it. After the reconcile,
=pacman -Q= shows the old kernel set, =pacman -Dk= is clean, and =pacman
-Qkk= over the reconciled names reports no mismatch, including the package
cut off mid-extraction.
** DOING [#B] Prepare for GitHub open-source release
:PROPERTIES:
:LAST_REVIEWED: 2026-08-17
:END:
Remove personal info, credentials, and code quality issues before publishing.
*** 2026-07-21 Tue @ 08:00:00 -0500 Audit reconcile: the four assets/ "& Claude" author lines are fixed
The four =assets/= files the 2026-07-09 note flagged as "Craig's call, still open" all now read =#+AUTHOR: Craig Jennings= (verified). No tracked file outside =todo.org= still carries =Craig Jennings & Claude= (the one in todo.org is a historical dated note quoting the old line). That open item is resolved; the two open children below (scripts personal-info, git-history scrub) remain.
*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: docs/ scrubbed; scripts and history still open
The =docs/= half of "remove personal information" landed (=a8f7e9b=). Four absolute =/home/cjennings= paths became the repo root, a home-relative path, or a named variable — every snippet stays runnable. Three =~/projects/home= references and three =.ai/sessions/= links, all dead in any clone but Craig's, were replaced by what they mean. No =file:= links broke.
The scrub also turned up something the epic didn't name: nine tracked files carried =#+AUTHOR: Craig Jennings & Claude=, a co-author who is not a person and which survives conversion into docx, a wiki page, or a PDF. The five under =docs/= are fixed. *Four remain, and they are Craig's call* (dated records under =assets/=, which the rule says stay as history, but which are tracked and would publish): =assets/2026-06-19-collapsible-waybar-sides-spec.org=, =assets/2026-07-03-instrument-console-panels-build-summary.org=, =assets/outbox/2025-11-08-keyring-fix-next-steps.org=, =assets/outbox/2025-11-08-test-failure-analysis.org=.
The two open children below are untouched: the =scripts/= personal-information pass, and the git-history secret scrub.
*** 2026-06-16 Tue @ 00:55:39 -0500 Six dotfiles-scoped sub-tasks moved to the ~/.dotfiles project
Per the 2026-06-16 task audit, the six sub-tasks targeting files now owned by the standalone =~/.dotfiles= repo were handed off to that project (newly bootstrapped as its own AI project) and removed from this epic: "Remove credentials and secrets from dotfiles", "Remove/template personal info from dotfiles", "Remove binary font files from repo", "Move battery out of waybar sysmonitor group", "Resolution-adaptive scratchpad sizing", and "Dynamic waybar/foot config based on screen resolution". Handoff: =~/.dotfiles/inbox/2026-06-16-0053-from-archsetup-dotfiles-release-prep-handoff.org=. This epic now covers archsetup-proper release work only (scripts personal-info, device-specific config, history scrub, shellcheck, SPDX headers, README/LICENSE). The 2026-06-09 reconciliation note below is the prior state.
*** 2026-06-09 Tue @ 19:21:36 -0500 Reconciliation: six sub-tasks now target the ~/.dotfiles repo, not archsetup
Phase 3.2 removed the in-repo =dotfiles/= tree, so six sub-tasks below no longer describe archsetup content — they target files now owned by the =~/.dotfiles= repo (=git.cjennings.net/dotfiles.git=): "Remove credentials and secrets from dotfiles", "Remove/template personal info from dotfiles", "Remove binary font files from repo", "Move battery out of waybar sysmonitor group", "Resolution-adaptive scratchpad sizing", and "Dynamic waybar/foot config based on screen resolution". Their paths are relative to that repo now. Kept here for tracking per Craig (2026-06-09); he'll re-scope the archsetup-vs-dotfiles split shortly. archsetup-proper release work (scripts personal-info, device-specific config, shellcheck, and scrubbing the pre-=b10cba5= dotfiles secrets from archsetup's own history) stays this task.
*** 2026-05-11 Mon @ 13:01:29 -0500 AI Response: Open-source-prep source audit
Checked each subtask below against the source / git state. Bottom line: almost nothing is fully done. =LICENSE= and =README.md= were added this session (see those subtasks); the rest still stands.
- *Remove credentials and secrets from dotfiles* — NOT DONE. All five named files still tracked: =dotfiles/common/.config/.tidal-dl.token.json=, =.config/calibre/smtp.py.json=, =.config/transmission/settings.json=, =.msmtprc=, =.mbsyncrc=. =.gitignore= lists none of them; no =.example= templates exist.
- *Remove/template personal info from scripts* — PARTIALLY DONE. Repo URLs ARE config-driven (=archsetup:141-146= use =${dwm_repo:-https://git.cjennings.net/...}=, documented in =archsetup.conf.example=). Still personal: =archsetup:2-3= (email/website header), =init:8,21= (=root:welcome=), =scripts/post-install.sh:17-56= (personal repos).
- *Remove/template personal info from dotfiles* — NOT DONE. =.gitconfig= has =c@cjennings.net=, =name = Craig Jennings=, =github user = cjennings=, =safe.directory= and employer creds; =.config/mpd/musicpd.conf= + =mpd.conf= still use =~cjennings/= / =/home/cjennings/= paths; =.ssh/config= has personal/employer hosts; =.config/yt-dlp/config:2= has =c@cjennings.net=; =hyprland.conf:3= has personal attribution.
- *Scrub git history of secrets* — NOT DONE. 275 commits; history not fresh, no filter-repo evidence.
- *Remove device-specific configuration* — NOT DONE. =archsetup:1486-1493= still creates the Logitech BRIO udev rule unconditionally; no config flag.
- *Add README.md for GitHub* — DONE (this session — initial draft, pending review). See subtask below.
- *Add LICENSE file* — DONE (this session — GPL-3). See subtask below.
- *Remove binary font files from repo* — NOT DONE. =dotfiles/common/.local/share/fonts/= still tracks 8 PragmataPro =.ttf= files, =AppleColorEmoji.ttf=, and other commercial fonts (Cartograph, MonoLisa, ComicCode, etc.).
- *Make claude-code installation optional* — NOT DONE. =archsetup:1817-1818= runs =curl -fsSL https://claude.ai/install.sh | sh= unconditionally; no flag.
- *Add input validation for username and paths* — PARTIALLY DONE. =archsetup:326-328= validates =$username= against =^[a-z][a-z0-9_]*$= (plus reserved-names check, marked DONE separately). No validation of =$source_dir= or other path vars.
- *Move battery out of waybar sysmonitor group* — NOT DONE. =dotfiles/hyprland/.config/waybar/config:27-37= still has =battery= inside =group/sysmonitor=.
- *Resolution-adaptive scratchpad sizing* — NOT DONE. No size/move windowrules for scratchpads in =hypr/conf.d=.
- *Dynamic waybar/foot config based on screen resolution* — NOT DONE. No resolution-detection/generation script.
- *Bulk shellcheck cleanup* — PARTIALLY DONE. =shellcheck archsetup= still shows 68 findings: 30×SC2329, 16×SC2174, 15×SC2024, 4×SC2086, 1 each SC2155/SC2129/SC2005. The 4 SC2086 (unquoted) are the ones a reviewer would flag — those are the priority.
- *Document testing process in README* — NOT DONE. =scripts/testing/README.org= exists but isn't the project README. (Now unblocked — root README exists.)
- *Add guard for rm -rf on constructed paths* — DONE 2026-05-20. All three constructed-path deletes routed through a =safe_rm_rf= guard (absolute / no-=..= / inside-allowed-prefix / real-dir checks); unit-tested in =tests/safe-rm-rf/=.
- *Standardize boolean comparison style* — NOT DONE. Mixed: =[ "$var" = "true" ]= at =archsetup:542,544,569= vs bare =if $var;= form ~7 places elsewhere.
- *Replace eval with safer alternatives* — NOT DONE. =archsetup:442= still =if eval "$cmd" >> "$logfile" 2>&1;= in =retry_install=.
*** 2026-06-28 Sun @ 13:34:03 -0400 Cancelled: calendar-feed URL rotation
Craig's call — not rotating. The three private iCal URLs (Google personal, Proton with PassphraseKey, Google DeepSat) sat in git history from =500b1f5= (2026-05-13) until the 2026-05-20 filter-repo scrub, which removed them from local + remote history. The residual exposure is only to anyone who cloned the repo in that 2026-05-13..05-20 window; Craig accepts that window rather than regenerating all three tokens on ratio. The history scrub already happened; the live =calendar-sync.local.el= is owned by the emacs project. Closing without rotation.
*** 2026-05-20 Wed @ 12:09:32 -0500 Scrubbed the calendar secret from git history
=dotfiles/common/.emacs.d/calendar-sync.local.el= (private Google/Proton/DeepSat ical URLs, added in =500b1f5= for stow distribution) was discovered while folding tmux-util into stow. Sent the file back to the emacs project's inbox, =git rm='d it, then =git filter-repo --invert-paths --path= purged it from all 29 affected commits. Force-pushed (=0921e4d...618e6cc=, with lease) and ran =reflog expire= + =gc --prune=now= on the bare repo at =/var/git/archsetup.git=. Verified: the file is in zero commits, the secret tokens return zero matches across all history, and =500b1f5= / =0921e4d= are unreachable on both local and remote. Rotation of the URLs tracked as the sibling TODO above. This also proves =filter-repo= works cleanly here — relevant precedent for the broader [[*Scrub git history of secrets (or start fresh)][history-scrub task]] below (the 5 credential files are still in history).
*** TODO [#B] Remove/template personal information from scripts
- =archsetup= lines 3-4: personal email and website in header
- =scripts/post-install.sh=: personal git repos and server URLs (the old =scripts/gitrepos.sh= was consolidated into this script in =dae7659=, so its personal =git.cjennings.net= clone targets now live here)
- =init= line 9: hardcoded password =welcome=
**** 2026-06-28 Sun @ 13:29:29 -0400 Reconciled: dotfiles repo URLs already config-driven
Dropped the "lines 141-146 hardcoded =git.cjennings.net= URLs" bullet. archsetup:138-140 reads =DOTFILES_REPO= / =DOTFILES_BRANCH= / =DOTFILES_DIR= overrides (defaults only, documented in =archsetup.conf.example=), so that item is already done. Refreshed the stale line numbers on the remaining bullets (header email/site now lines 3-4, init password now line 9, after the SPDX headers shifted the files).
*** TODO [#B] Scrub git history of secrets (or start fresh)
Even after removing files, secrets remain in git history.
Options: =git filter-repo= to rewrite history, or start a fresh repo for the GitHub remote.
Recommend: fresh repo for GitHub (keep cjennings.net remote with full history).
**** 2026-06-28 Sun @ 13:29:29 -0400 Reconciled: 589 commits, 5 credential files still in history
History is now 589 commits (the 2026-05-11 note's "275" is stale). Only the calendar-feed file has been filter-repo'd so far (2026-05-20). The five credential files remain in history at their pre-=b10cba5= paths: =.tidal-dl.token.json= (5 commits), =calibre/smtp.py.json= (6), =transmission/settings.json= (5), =.msmtprc= (8), =.mbsyncrc= (9). None are tracked in the current tree. The scrub-or-fresh-repo decision still stands.
***** 2026-07-04 Sat @ 11:48:24 -0500 Count refresh — history now 565 commits; re-verify the 5-file claim before scrubbing
The 2026-07-04 audit found the history is now 565 commits, down from the 589 recorded above. Because the count dropped, re-verify that the five credential files are still present in history (re-run the per-file =git log --all -- <path>= check) before relying on the scrub scope — the earlier count is stale and the file set may have moved.
***** 2026-08-17 Mon @ 10:20:00 -0700 Corrected the paths — every prior check has been querying paths that never existed
The five filenames recorded above are not the paths these files live at, and =git log -- <path>= answers "no commits" for a path it has never seen rather than erroring. So the checks return a clean result and mean nothing. The real paths, from =git log --all --name-only --diff-filter=A= over the full history, all sit under the pre-migration =dotfiles/= tree:
- =dotfiles/system/.msmtprc= (3 commits)
- =dotfiles/system/.mbsyncrc= (2)
- =dotfiles/system/.config/calibre/smtp.py.json= (2)
- =dotfiles/system/.config/transmission/settings.json= (2)
- =dotfiles/system/.config/.tidal-dl.token.json= (2)
- =dotfiles/system/.config/.tidal-dl.json= (2) — a *sixth* file, never recorded here
Use those paths for any future check, not the bare filenames. History is 891 commits; none of the six are in the current tree. The scrub-or-fresh-repo decision still stands and its scope is six files, not five.
This surfaced while re-verifying on velox, where the check ALSO returned a false clean for a second, unrelated reason: the clone was shallow (7 commits), so it could not see the history either way. Both failures produce the same confident zero. Filed as =[#A] The installer shallow-clones the two repos I develop in=.
***** 2026-07-21 Tue @ 08:00:00 -0500 Re-verified: history now 851 commits; five files still present, per-file counts dropped
2026-07-21 audit re-verification. History is now 851 commits (=git rev-list --all --count=). The five credential files are still in history but at fewer commits each than the 2026-06-28 record: =.tidal-dl.token.json= 3 (was 5), =calibre/smtp.py.json= 4 (was 6), =transmission/settings.json= 3 (was 5), =.msmtprc= 5 (was 8), =.mbsyncrc= 6 (was 9). None are in the current tree. The scrub-or-fresh-repo decision still stands; the scope is smaller than recorded.
*** 2026-06-24 Wed @ 19:41:56 -0400 Gated device-specific udev rules behind a flag
The Logitech BRIO udev rule is now wrapped in =if [ "$install_device_udev_rules" = "true" ]=, fed by a new =INSTALL_DEVICE_UDEV_RULES= key (default yes, opt-out — still mainly a personal project). Added the var default, the config read, a =validate_config= check, and an =archsetup.conf.example= entry. Verified: default/yes writes the rule, no skips it, bogus is rejected; =bash -n= clean.
*** 2026-06-28 Sun @ 13:37:33 -0400 Added README.md — full draft complete, final read filed
=README.md= is substantively done at repo root (10.9 KB), covering project description, features, requirements, installation, the =archsetup.conf= configuration guide, security considerations, contributing, and license, with generic placeholders for the eventual public fork. The 2026-05-11 "first pass" note below is superseded. Craig's final read before public release is filed under "Manual testing and validation"; closing as code-complete pending that human check, per the audit rule.
**** 2026-05-11 Mon @ 13:01:29 -0500 AI Response: Initial README draft
Drafted =README.md= at repo root, modeled on =~/code/chime/README.org=. First pass — review and run a voice/style pass before committing. Personal info (emails, =cjennings.net= URLs, personal repo names) intentionally replaced with placeholders for the eventual public release.
*** 2026-05-19 Tue @ 01:54:29 -0500 Added GPL-3 LICENSE file at repo root
GPL-3 chosen. Canonical GPLv3 text landed at =LICENSE= on 2026-05-11 (commit =f80e664=). README already links to it. SPDX/license headers across source files (or a NOTICE file) split out as a new sub-task below for the eventual public release.
*** 2026-06-24 Wed @ 19:41:56 -0400 Added SPDX headers to all shell scripts
Swept =# SPDX-License-Identifier: GPL-3.0-or-later= in right after the shebang of all 24 shell scripts in the repo (=archsetup=, =init=, =scripts/**/*.sh= incl. =scripts/testing/=). The dotfiles are a separate repo now, so they aren't swept here. Verified the header sits at line 2 (after the shebang) and syntax still passes.
*** 2026-06-09 Tue @ 19:21:36 -0500 Made claude-code install optional
Shipped in =f2dad22= (feat: make the claude-code install optional). The =curl | sh= from claude.ai now sits behind a config flag instead of running unconditionally.
*** 2026-06-09 Tue @ 19:21:36 -0500 Input validation added (validate_config + validate_username)
validate_config + validate_username shipped (detail in the 2026-05-11 note below). The =$source_dir= path check was judged unnecessary — it derives from the now-always-validated =$username= (=/home/$username/.local/src=). Closed as done.
**** 2026-05-11 Mon @ 18:20:49 -0500 AI Response: validate_config + validate_username added
Added two pre-flight validators to =archsetup= (right after =load_config=, before any install step):
- =validate_username()= — the lowercase / starts-with-letter / =[a-z0-9_]= / not-reserved check, extracted from the inline block in =preflight_checks()=. Fixes an existing gap: the inline check only ran on the *prompted* path, so a config with =USERNAME=root= (or =USERNAME=foo bar=) slipped through unvalidated. Now both =preflight_checks= and =validate_config= call it.
- =validate_config()= — runs whenever =--config-file= is used: rejects unknown =DESKTOP_ENV= (must be dwm/hyprland/none) early instead of dying in step 7-9; rejects =AUTOLOGIN=/=NO_GPU_DRIVERS= values that aren't =yes=/=no= (currently silently ignored); basic shape check on =LOCALE=; and a scheme + no-whitespace/no-leading-dash check on the six =*_REPO= URLs that get passed to =git clone= (rejects e.g. =--upload-pack=…= injection). Plain =echo …>&2; exit 1= (the logging helpers aren't defined that early). =$source_dir= needs no separate check — it's =/home/$username/.local/src=, derived from the now-always-validated =$username=.
Not a security boundary (=load_config= sources the config as bash; a hostile config can already run anything) — it's typo-catching. Verified with =bash -n= and a smoke-test matrix of good/bad inputs through both functions. The next =make test= run confirms valid configs still install. Leaving as DOING for review.
*** 2026-05-20 Wed @ 06:50:25 -0500 Swept shellcheck across the shell scripts
Census across the 16 shell scripts (=archsetup=, =init=, =scripts/*.sh=, =scripts/testing/=): 124 findings, zero errors. Triaged against "what matters for public review" and confirmed the 2026-01-24 read — most are intentional or documented-acceptable:
- SC2024 (14, sudo redirects), SC2174 (16, =mkdir -p -m=), SC1091 (13, unfollowable sources), SC2329 (32, functions invoked indirectly via the =STEPS= dispatch array), SC2153 (1, =DISK_PATH= sourced from =vm-utils.sh=) — all false positives or accepted.
- SC2086 on =$SSH_OPTS= in =vm-utils.sh= (×4) and =$TEMP_DISKS= in =cleanup-tests.sh= — intentional word-splitting; quoting would break them. The SSH_OPTS-as-array refactor is the proper fix, deliberately deferred (codebase-wide, one atomic change).
- SC2086 integer tests in =[ ]= (=archsetup=, =cleanup-tests=) — safe, note-level style; left to avoid churn in the just-fixed =retry_install=.
- SC2015 (×2, =vm_exec && success || warn=) — =success=/=warn= return 0, so C won't spuriously fire. Idiomatic.
Fixed the four that are genuine: =init= (a =#!/bin/sh= script) used =$(</etc/hostname)= (SC3034 bashism → =$(cat ...)=) and an unquoted =$interface_up= (SC2086 → quoted); =shellcheck init= now clean, =sh -n= passes. Suppressed the two =VM_IP= SC2034 warnings with documented =# shellcheck disable= directives (consumed by the sourced =validation.sh=, which shellcheck can't follow). 124 → 120; the remaining 120 are the triaged-acceptable set above.
*** 2026-05-20 Wed @ 06:32:17 -0500 Documented the testing process in the README
The README only covered the VM integration harness; the unit-test layer under =tests/= (Python =unittest=, fake-binary-on-PATH, one dir per script — =layout-navigate=, =tmux-util=) was undocumented. Added a =make test-unit= target that runs every =tests/*/test_*.py= suite explicitly (=unittest discover= can't find them — hyphenated dir names aren't valid package paths), then rewrote the README Testing section into "Unit tests" and "Integration tests (VM harness)" subsections, including how to add a suite for a new script. Updated Contributing to point at =make test-unit= for script changes. 61 unit tests pass via the new target.
*** 2026-05-20 Wed @ 18:22:42 -0400 Added safe_rm_rf guard on constructed-path deletes
Added a self-contained =safe_rm_rf <path> <allowed_prefix>= helper to =archsetup= and routed all three constructed-path deletes through it. The guard refuses to run unless the target is absolute, free of =..=, deeper than a bare top-level dir, strictly inside the allowed prefix (not the prefix itself), and a real directory (not a symlink); otherwise it prints the reason and returns non-zero without deleting. On the happy path it delegates to =rm -rf=.
Sites converted (the line numbers in the original task body were stale — actual sites located by grep):
- =--fresh= state-dir wipe — prefix =/var/lib/archsetup=.
- =git_install= clone-retry cleanup (=build_dir= under =$source_dir=).
- =aur_installer= yay clone-retry cleanup (same prefix).
The helper is defined before the top-level =--fresh= handler (which runs at load time, before the logging helpers exist), so it carries no =error_warn= dependency and reports refusals to stderr itself. The two in-function sites keep their existing =|| error_warn= / =|| error_fatal= handling.
Tests: =tests/safe-rm-rf/test_safe_rm_rf.py= sources the real function out of the script and exercises Normal/Boundary/Error cases (13 tests) against real temp dirs. =make test-unit= green (61 tests), =bash -n= clean, no new shellcheck warnings.
*** 2026-06-24 Wed @ 19:41:56 -0400 Standardized boolean comparisons on the explicit form
Rewrote the bare =if $var= boolean conditionals (=show_status_only=, =fresh_install=, =skip_gpu_drivers=, =detected_intel/amd/nvidia=, plus two =! $var= negation chains) to the explicit =[ "$var" = "true" ]= / =!= "true"= form, and quoted the one unquoted =install_claude_code = true=. Left =if $step_func= alone — that's the STEPS function-dispatch, not a boolean. Verified: only =step_func= remains bare, all comparisons are quoted, =bash -n= clean.
*** 2026-05-26 Tue @ 15:27:09 -0500 eval task moot — the line-434 eval is gone, the survivor is deliberate
Verified: the only =eval= left in =archsetup= is line 578 in =retry_install=, and it's intentional and documented — it captures =$?= directly from =eval "$cmd"= to dodge the if-compound-swallows-exit-code trap. Replacing it with an array would reintroduce that bug. The line-434 eval this task pointed at no longer exists. Nothing to change.
** TODO [#C] The audio doctor never checks the microphone :bug:audio:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
The classifier is output-only. =diag.probe_semantic= already collects =default_source= and =default_source_present=, and =classify.py= reads neither: the word "source" appears once in the whole module, in the graph row that counts them. So a muted mic, a default source naming an unplugged device, or a mic at zero volume all classify as =healthy=, and the verdict prints "the default output is present and audible" while the input side goes unexamined. Found 2026-07-10 while asking whether the doctor would have caught Chrome losing the mic. It would not have.
Not a scope decision. The spec's Non-Goals never say the doctor is output-only, and its Summary calls it a doctor for "a broken sound stack".
Work: mirror the sink rules onto the source. =probe_semantic= gains =default_source_muted= / =default_source_volume=; the classifier gains the source cases; =classify.findings()= gains an input row beside its =defaults= row, so the CLI wall and the GUI wall both grow it for free.
Two things not to get wrong. An absent microphone is legitimate on a desktop, so "no input devices" must never be a fault the way =no-output-devices= is. And a monitor source is a legitimate default source (recording desktop audio), which is why =probe_semantic= passes =include_monitors=True= — inheriting the panel's display filter here would call a working setup broken.
Specced 2026-07-10 after discussion with Craig, and the design grew past the original gap: [[file:docs/specs/2026-07-10-audio-doctor-input-side-spec.org][docs/specs/2026-07-10-audio-doctor-input-side-spec.org]] (DRAFT, three decisions open as of 2026-08-25). A doctor key per direction, a kernel-level capture probe below PipeWire, PTT-aware muting, and a direction-aware guard. The precedence question the build would have faced is gone: a doctor per direction means the user's press says which side they came to fix.
Parent spec: [[file:docs/specs/2026-07-09-audio-doctor-spec.org][docs/specs/2026-07-09-audio-doctor-spec.org]] (IMPLEMENTED). This is a v1 gap found after the fact, not a phase of it.
Grading (2026-08-25 review): Major severity — the doctor's verdict is silently wrong for a whole direction, workaround is checking the mic by hand — × "some users, sometimes" (mic faults are occasional) = P3 = [#C]. Was held at [#B] ungraded; regraded by the matrix.
** TODO [#C] Weather chip color signals unclear + unenforced :bug:dotfiles:waybar:weather:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
From the roam inbox (2026-07-20): the shipped Waybar weather chip's comfort coloring reads as noise — it shows amber for no clear reason, and some items are bolded, which isn't a legible signal. Craig's intended scheme (every item except the arrow key colored by whether the weather is comfortable; NO bold or italic anywhere):
- Normal — all text white: temp in 60-85; condition sunny/clear/etc.
- Poor conditions — colored glyph: rain = blue glyph; snow = bright-blue glyph; fog = silver glyph.
- Bad conditions — red number: temp over 90 → the temp number is red; wind over a comfort level → the wind glyph displays and its number is red.
Open design question (Craig): which other conditions/combinations to signal, and how a forecasted severe event (e.g. an upcoming hurricane) should display.
Grading: Minor severity (legibility on a shipped chip, nothing broken) × frequent (every glance at the bar) = P3 = [#C]. Mostly buildable from the rules above; the severe-event display needs Craig's call. Waybar is archsetup-owned per the dotfiles standing rule.
** TODO [#C] Add a time selector to the timer panel :feature:timer:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-08
:END:
Offer a period-appropriate selector for timer duration, likely drawing on the
tape-counter idiom, while preserving the existing direct-entry path.
** TODO [#C] Order network-panel connections by availability :feature:network:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-09
:END:
Present saved and currently available networks in this order: available saved
profiles, available unsaved networks, then saved profiles that are unavailable.
*** 2026-08-09 Sun @ 11:13:15 -0500 Design conflict resolved; now :solo:
Craig, 2026-08-09: sort available-first within the Saved group (available
saved MRU-first, unavailable saved below), keeping the spec's three labelled
groups intact — no merged list. Escalate to a merge later only if it still
reads wrong in use.
** TODO [#C] Indicate hotspot or metered WiFi in amber :feature:network:waybar:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-09
:END:
Detect hotspot/metered connectivity and render the WiFi icon plus SSID amber,
while ordinary WiFi stays white.
*** 2026-08-09 Sun @ 11:13:15 -0500 Both design calls answered; now :solo:
Craig, 2026-08-08/09: amber means connected to a phone's hotspot (this
machine running an AP does not trigger it), and the one-nmcli-call fast-path
contract stays intact. Build shape: NetworkManager's metered flag is the
detector (Android tethering auto-flags via the DHCP vendor hint; NM's
GENERAL.METERED yes/guessed-yes on the active wifi device), read by the
slow-path probe and written into the connectivity cache the fast path
already consumes, so the indicator pays nothing new. Where NM can't guess
(some iPhones), the per-connection metered flag is the manual override; a
panel affordance for it can come later. Live phone-hotspot check is Craig's
manual-testing entry; everything else verifies with fakes.
** TODO [#C] Net panel speedtest history :feature:dotfiles:network:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-25
:END:
From the roam inbox (routed 2026-07-13): the networking panel should track speedtests over time with appropriate info. Shape: persist each SPEED TEST result (timestamp, down/up, latency, server) to a small local store and surface history in the net panel. Design questions for work time: retention window, which fields matter, and presentation within the panel's ~400px width (recent-results list vs trend readout). Point-in-time results exist today; the gap is comparison across days and venues.
** TODO [#C] zfs base VM image build failure: ZFS DKMS module missing :bug:zfs:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-17
:END:
=FS_PROFILE=zfs make test-vm-base= fails inside the VM at initramfs time: archangel reports "ZFS module not found! DKMS build may have failed" against the installed kernel (linux-lts 6.18.38 at the 2026-07-08 attempt). Consequences: the maint scenario harness's zfs lane (Phase 12) is filtered but unexercised, and a real zfs bare-metal install via archangel would plausibly hit the same wall. Priority per the bug matrix: Major severity (zfs install path broken) × some-users-sometimes = P3. When fixed, run =FS_PROFILE=zfs bash scripts/testing/run-maint-scenarios.sh --list= and add zfs scenario files (zpool scrub / autotrim / snapshot destroy) to the harness.
*** 2026-08-17 Mon @ 10:08:51 -0700 Rechecked: archzfs still on 2.3.3, still blocked
Ran the unblock check from the diagnosis below: archzfs' x86_64 index still serves only =zfs-dkms-2.3.3=. The first release supporting 6.18 is 2.4.0, so the blocking condition is unchanged and there is still nothing on our side to fix. Recheck again with the same one-liner.
*** 2026-07-14 Tue @ 01:40:48 -0500 Diagnosed: OpenZFS/kernel version skew, blocked on archzfs
Reproduced in ~1 minute of install: =dkms install zfs/2.3.3 -k 6.18.38-2-lts= exits 1 during pacstrap. Root cause confirmed: OpenZFS 2.3.3's META declares Linux-Maximum 6.15, and the VM installs linux-lts 6.18.38. The first release supporting 6.18 is 2.4.0 (2.4.1 covers 6.19), and archzfs currently serves only zfs-dkms 2.3.3-1 — nothing on our side to fix. Unblock condition: archzfs publishes zfs-dkms ≥2.4.0; recheck with =curl -s https://archzfs.com/archzfs/x86_64/ | grep -o 'zfs-dkms-[0-9.]*'=, then rerun =FS_PROFILE=zfs make test-vm-base=.
** TODO [#C] Waybar collapse control: replace the triangle glyph :feature:waybar:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
From the 2026-07-04 roam capture. The waybar collapse mechanism (click the triangle, the bar sections redisplay shortened) works, but the triangle glyph doesn't match the instrument-console aesthetic the panels now use. Replace it with something in keeping with the console look. Aesthetic decision — bring Craig two or three concrete glyph/style options (a machined chevron, a console-key style expander, an engraved caret) before wiring. Dotfiles waybar config (handled per the archsetup-owns-dotfiles rule). Raised alongside the net-panel/audio speedrun; deferred from it because the glyph choice is a taste call.
** TODO [#C] Net panel: driver-health diagnostic tier :feature:network:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-26
:END:
Follow-up from the 2026-07-04 net-panel hardening speedrun (Craig's cj question on the no-WiFi item). The shipped no-wifi-hardware verdict covers "no adapter at all." This tier covers "adapter present but the driver is wedged": read-only health signals — =ip link= (device present but no-carrier / down), =dmesg= / =journalctl -k= for firmware-load failures, =rfkill= for a hard block, =modinfo= / =lsmod= for the driver module — classified before a generic reset. Remedy actions: a privileged =modprobe -r <mod> && modprobe <mod>= reload of the wifi driver, and a firmware-package pointer when the failure is a missing/failed firmware load. Dotfiles net-package work (handled per the archsetup-owns-dotfiles rule). Design pass first to decide whether it's worth a repair tier vs a needs-user-action pointer.
** TODO [#C] Voice dictation / speech-to-text input :feature:tooling:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-08
:END:
Push-to-talk dictation that types transcribed speech into the focused Wayland window — usable at any text field, including the Claude Code terminal prompt and Emacs buffers. Claude Code has no built-in voice input; dictation has to happen at the OS level and inject text. Raised 2026-07-03.
Tool choice is the open decision (needs Craig): =nerd-dictation= (Vosk, lighter, lower accuracy) vs a =whisper.cpp=-based daemon (heavier, higher accuracy, optional GPU). Wayland typing backend is =wtype= or =ydotool=. Scope once chosen: install + model download, a push-to-talk keybind (Hyprland), and an autostart entry; fold into archsetup so it lands on both daily drivers. Consider an Emacs-native path (=whisper.el=) as a complement for in-buffer dictation.
*** 2026-07-21 Tue @ 08:40:00 -0500 Decided (Craig): whisper.cpp + wtype, system-wide
STT engine = =whisper.cpp= (accurate offline, optional GPU on ratio's Radeon). Typing backend = =wtype= (Wayland-native virtual-keyboard injection into the focused window, no root/daemon), with =ydotool= (uinput) held as a fallback only if a specific app — some XWayland/Electron surface — won't accept wtype's synthetic input. One system-wide path that also covers Emacs buffers and the Claude Code prompt; the Emacs-native =whisper.el= route was NOT chosen. Build scope: whisper.cpp + a model (start with a mid-size English model, tune later), a Hyprland push-to-talk keybind driving a record→transcribe→wtype pipeline, and an autostart/service entry, folded into archsetup so it lands on ratio + velox. Now unblocked (agent-buildable; verification includes a live dictation check).
** TODO [#C] Osbot camera configuration :chore:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
Re-graded [#C] → [#B] and scheduled at the 2026-08-08 review: Craig leaves on
vacation in a week (~2026-08-15), so this needs to land before then. Blocked
only on the camera being physically plugged in (no /dev/video node as of
2026-08-08). Likely same sitting as the camera-passthrough udev rule task.
Craig's roam capture 2026-07-20, routed via .emacs.d sentry inbox-zero as archsetup-owned device setup: "configure osbot camera." Scope to define at pickup (device model, what "configure" covers — kernel module, v4l settings, default framing).
*** 2026-07-21 Tue @ 08:10:00 -0500 Scoped (Craig): tiny — it works, just needs configuring via a panel
Craig: the camera works (bought for being Linux-friendly), it just needs configuring. There IS a config panel — =cameractrls= 0.6.10 is installed (a GTK GUI for camera controls: exposure, white balance, PTZ, focus, framing) plus =v4l-utils= for the CLI path. Caveat found 2026-07-21: no =/dev/video*= device is present right now, so the camera isn't currently plugged in / its UVC node isn't enumerated. Task: with the camera connected, confirm it enumerates as a /dev/video node, then set defaults in cameractrls. Small, mostly a live-hardware step.
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-graded [#B] → [#C] and unscheduled
The 08-08 raise was only for the vacation deadline, which has passed, and the
camera is at home. Pick it up the next time the camera is plugged in.
** TODO [#C] Re-check python-lyricsgenius --skipinteg workaround :chore:solo:
:PROPERTIES:
:LAST_REVIEWED: 2026-08-17
:END:
archsetup installs =python-lyricsgenius= with =--mflags --skipinteg=, skipping makepkg integrity + PGP checks — a workaround originally for an expired-signature issue upstream (surfaced by the 2026-06-23 --noconfirm audit). Periodically test whether the cause has cleared: if a plain =aur_install python-lyricsgenius= builds without complaint, drop the =--skipinteg= workaround. Removal needs a real AUR build to confirm, so it isn't a blind change.
*** 2026-08-17 Mon @ 10:08:51 -0700 Rechecked: still needed, cause unchanged
Fresh AUR clone, =makepkg --verifysource= on 3.7.0-1: the PyPI tarball passes, =LICENSE.txt= still FAILS its b2sum. The PKGBUILD still pins the license at github master, so its checksum drifts whenever upstream touches the file. =--skipinteg= stays.
*** 2026-07-23 Thu @ 02:20:00 -0500 Rechecked: still needed, unchanged
Fresh AUR clone, =makepkg --verifysource= on 3.7.0-1 (PKGBUILD still unchanged since the last check): the PyPI tarball passes, =LICENSE.txt= still FAILS its b2sum. Same structural cause — the source pins the license at github master, so its checksum drifts whenever upstream touches the file. =--skipinteg= stays. Nothing to change in the installer.
*** 2026-07-14 Tue @ 01:40:48 -0500 Rechecked: still needed, same cause
Fresh AUR clone, =makepkg --verifysource= on 3.7.0-1 (PKGBUILD unchanged): tarball passes, =LICENSE.txt= still FAILS its b2sum — the github-master pin, structural as diagnosed 2026-06-24. =--skipinteg= stays.
*** 2026-07-02 Thu @ 05:08:37 -0400 Rechecked: still needed, same cause
=makepkg --verifysource= on 3.7.0-1: tarball passes, =LICENSE.txt= still FAILS
its b2sum — the PKGBUILD still pins a hash for a file fetched from github
master. Structural, as diagnosed 2026-06-24; =--skipinteg= stays.
*** 2026-06-24 Wed @ 17:55:34 -0400 Rechecked: still needed, but the cause changed
Ran =makepkg --verifysource= on the current AUR PKGBUILD (3.7.0-1). The package tarball =lyricsgenius-3.7.0.tar.gz= now passes its b2sum — the original expired-PGP-signature problem is gone (the PKGBUILD no longer carries any =validpgpkeys=). But integrity still FAILS, on a different file: =LICENSE.txt=, which the PKGBUILD fetches from the project's github master and pins a b2sum for. github master is a moving target, so that b2sum drifts and =--skipinteg= is still required. This is structural (not a transient upstream fix away), so it likely won't clear until the maintainer pins the LICENSE to a tagged release. Updated the archsetup comment to the real cause. Keep rechecking, but lower expectations of it clearing.
** TODO [#C] Review theme config architecture for dunst/fuzzel
:PROPERTIES:
:LAST_REVIEWED: 2026-08-08
:END:
The active dunst config is stowed from dotfiles/common/ but theme templates
live in dotfiles/hyprland/.config/themes/. set-theme copies the templates to
the stowed locations at runtime, so edits to the common file get overwritten
on theme switch. This split between stowed configs and theme templates is
error-prone — changes must be made in both places. Consider:
- Having set-theme be the single source of truth (remove common dunstrc from stow)
- Or symlinking the stowed config to a theme-managed location
- Same situation applies to fuzzel.ini
The goal is a single place to edit each config, not two.
*** 2026-07-21 Tue @ 08:00:00 -0500 Audit reconcile: single-theme now, so the switch-revert pressure is lower
The theme system went single-theme — Hudson was retired (dotfiles e03436e; documented archsetup 98142bb, 2026-07-18), leaving Dupre as the only theme. So "edits get overwritten on theme switch" now bites only rarely (switches are effectively nonexistent). The structural two-places-to-edit problem still stands and is worth fixing, but the urgency the original grading implied has dropped.
** TODO [#D] Installer + scripts refactor opportunities :refactor:solo:
Grading: no behavior change; parking lot. 10 refactors remain from the sentry audit — duplicated GPU-modalias scan, triple hand-rolled retry loop, stow x4, display_server/window_manager dispatch dup, Maia ELO range x3, per-script log helpers, GRUB/snapper/fsck sed clusters, waybar-battery positional sed. Full list with line numbers in [[file:docs/design/2026-07-19-sentry-code-findings.org][sentry code findings]] (High/Medium/Low tagged). Pull individual ones out as their own tasks when tackled. The system-mutation sed clusters (snapper/fsck/GRUB/waybar) want characterization coverage before any rewrite.
*** 2026-07-20 Mon @ 16:35:00 -0500 Extracted validate_yesno and the NVIDIA_MIN_DRIVER constant
In 67d0b6e: the four yes/no config-validation blocks collapse into validate_yesno (TDD), and the driver-floor literal 535 becomes NVIDIA_MIN_DRIVER. The safe, purely-testable slice; the remaining 10 (structural / system-mutation) stay parked above.
*** 2026-07-21 Tue @ 08:00:00 -0500 Audit reconcile: the GRUB cmdline sed slice shipped
f9da097 (2026-07-21) rewrote the boot-critical =GRUB_CMDLINE_LINUX_DEFAULT= sed into a guarded awk+mv merge with a backup and added characterization tests (tests/installer-steps/test_grub_cmdline.py). So the "GRUB sed clusters want characterization coverage before rewrite" caveat no longer covers the cmdline line — only the 4 cosmetic GRUB sed lines (timeout/default/terminal/gfxmode, which overwrite fixed values with nothing to preserve) remain in that slice.
** TODO [#D] Net doctor vNext :feature:dotfiles:network:
Deferred from the [[file:docs/specs/2026-07-11-net-doctor-expansion-spec.org][net doctor expansion spec]] (IMPLEMENTED, v1 shipped): event-log correlation for the flaky/drops cluster (powersave, roaming stalls, USB autosuspend, no-reconnect-after-resume, firmware crashloop drop signatures — needs history a one-shot probe can't see); a DoT/DNSSEC-specific verdict distinguishing "the venue resolver mangles DNSSEC" from the generic DNS-not-resolving; per-profile autoconnect/duplicate-profile hygiene.
** TODO [#D] Bt doctor vNext :feature:dotfiles:bluetooth:
Deferred from the [[file:docs/specs/2026-07-11-bt-doctor-expansion-spec.org][bt doctor expansion spec]] (IMPLEMENTED, v1 shipped): the stale-bond re-pair-offer signature (v1 keeps re-pair strictly user-initiated; the signature must be designed and validated before the doctor ever offers it); a connection-parameter/coexistence hint tail for a "keeps dropping" verdict; the bt-audio-profile expansion beyond the current a2dp repair (codec fallback, default-sink, absolute-volume) — wants coordination with the audio doctor so the two panels don't claim the same A2DP/HFP diagnosis with divergent verdicts.
** TODO [#D] Widget catalogue vNext :feature:design:
Deferred from the [[file:docs/specs/2026-07-12-component-generation-spec.org][component-generation spec]] (DOING): framework wrappers for the web library (React or otherwise — demand-gated, none until a real framework consumer exists); Level-2/3 widget codegen if the spec's Phase 5 decision point says go (a go spawns its own spec); ports beyond the demand matrix as new consumers appear.
** TODO [#D] Maintenance console vNext :feature:
Deferred from the [[file:docs/specs/2026-07-07-maintenance-console-spec.org][spec]] (v1 ships determinate remedies only): AI/workflow assistance on read-only metrics (the vLater tier — failed-unit diagnosis, journal-error fix suggestions, OOM investigation); SIGKILL escalation for TERM-survivors on the KILL lever; live streaming meters on evidence rows where a count could be a level.
** TODO [#D] Retention-repair remedy unreachable from the panel GUI :bug:dotfiles:
snapshot_retention_repair (WRITE SANE LIMITS — =snapper -c <config> set-config TIMELINE_CREATE=yes TIMELINE_LIMIT_*=<TOML values>=) sits in the maint remedy table but is wired into no GUI layer (no reference in panel.py, viewmodel.py, or gui.py) — it's reachable only via =maint doctor review= / =maint fix=. Worse, the REVIEW & FIX roster's wording for item-bearing remedies says "per-item — on its subpanel", which for this remedy points at a key that doesn't exist. Found 2026-07-08 while walking the SNAPSHOTS subpanel with Craig.
Two fix shapes, decide at work time: (1) wire a digest key — e.g. on the timeline row when the config's installed TIMELINE_LIMIT_* drift from the TOML values (needs the probe to read the installed limits for comparison); or (2) keep it CLI-only deliberately and special-case the roster wording for remedies with no panel key ("CLI only — maint fix"). Option 2 is a few lines; option 1 makes limit drift visible on the board, which is the 2026-05-26 pile-up's root cause. Severity minor × rare edge = P4 per the bug matrix.
** TODO [#D] Consider Customizing Hyprland Animations
Current: windows pop in, scratchpads slide from bottom.
Customizable animations:
- windows / windowsOut / windowsMove - window open/close/move
- fade - opacity changes
- border / borderangle - border color and gradient angle
- workspaces - workspace switching
- specialWorkspace - scratchpads (currently slidevert)
- layers - waybar, notifications, etc.
Styles: slide, slidevert, popin X%, fade
Parameters: animation = NAME, ON/OFF, SPEED, BEZIER, STYLE
Speed: lower = faster (1-10 typical)
Example tweaks:
#+begin_src conf
animation = windows, 1, 2, myBezier, popin 80%
animation = workspaces, 1, 4, default, slide
animation = fade, 1, 2, default
animation = layers, 1, 2, default, fade
#+end_src
** TODO [#D] Parse and improve AUR error reporting
Parse yay errors and provide specific, actionable fixes instead of generic error messages
** TODO [#D] Improve progress indicators throughout install
Enhance existing indicators to show what's happening in real-time
** TODO [#D] Telega coredump recurrence tell :bug:maint:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
*** 2026-09-17 Thu @ 08:59:59 -0400 Re-graded C → D: the tell has been quiet on both machines since at least 08-28
Grading: Minor severity (a crashed telega-server restarts; the cost is coredump
noise and a chat client that blinks) x rare edge case (no occurrence on either
machine in the ~3 weeks the records cover) = P4 = [#D].
This moves the letter by moving an input, not by overruling the 2026-07-21 raise
below. That raise was correct on its own frequency row: the tell had just fired,
repeatedly, across five days. It has since stopped, so the row moved from
most-users-frequently to rare-edge-case and the letter follows it. If the
assertion reappears the row moves back and so does the grade.
Checked at the 2026-09-17 review. ratio: =coredumpctl list telega-server= finds
nothing, and its coredump records reach back to 2026-08-28 (the oldest is an
unrelated Hyprland SIGABRT); =~/.telega/telega-server.log= (3.0 MB, last written
09-13) holds zero =tdat_plist_value= assertions. velox: no telega-server
coredumps either. Back to a watch item; the tell and the durable escape are
unchanged.
*** 2026-07-21 Tue @ 08:10:00 -0500 Re-graded D → C (Craig): treat as the actionable version-skew recurrence
Craig's call: the fired tell counts as the version-skew recurrence this task predicts (zevlg =:latest= outran the installed elisp again), not just benign host noise — so it bumps [#D] → [#C]. Action: re-pin / upgrade the TDLib side (upgrade the elisp telega package on ratio+velox, or move to a host-native pinned TDLib build) so the server's plist parser and the installed elisp agree again. The durable escape remains the host-native pinned TDLib build.
*** 2026-07-21 Tue @ 08:00:00 -0500 Audit reconcile: the tell FIRED — coredumps recurred
The named tell has tripped. =~/.telega/telega-server.log:11413= now carries a fresh =Assertion failed: false (telega-dat.c: tdat_plist_value: 500)=, and =coredumpctl= shows telega-server SIGSEGV recurring on ratio: a burst of 8 on 2026-07-15, then 07-16, 07-18, 07-19, newest 2026-07-20 23:41. The 2026-07-09 "nothing recurred" verdict below is superseded.
Open question for Craig (re-grade): is this the version-skew recurrence this task predicts (zevlg =:latest= outran the installed elisp again — actionable: re-pin/upgrade TDLib) or the separately-known benign dockerized-musl SIGSEGV class (the health-check known-issues entry, cosmetic host-coredump noise)? The answer decides whether this bumps back [#D] → [#C]. Note the health-check on 2026-07-21 classified this boot's telega coredumps as KNOWN musl-build noise, which argues against a version-skew regression — but the fresh tdat_plist_value assertion is the skew signature, so the two need reconciling.
*** 2026-07-09 Thu @ 16:32:54 -0500 Audit reconcile: the fix is holding; re-graded C → D
Verified rather than assumed: =~/.telega/telega-server.log= carries zero =tdat_plist_value= assertions, and the newest telega-server coredump is 2026-07-08 16:57 — before .emacs.d upgraded the elisp. Nothing has recurred.
Re-graded =[#C]= → =[#D]= per the bug matrix. There is no defect to fix here; it is a watch item with a named tell, and the severity × frequency read is cosmetic (host coredump noise on a metric we own) × rare edge case → P4 → =[#D]=. It stays on the list only so the tell isn't lost.
The maintenance console's coredump metric flagged telega-server on ratio (8 coredumps) and velox (18). Root cause was a version skew: the Dockerized =zevlg/telega-server:latest= is frozen at the 2026-06-05 build while the installed elisp lagged at 20260513, so the newer server's plist parser choked on the older elisp's output. .emacs.d fixed it by upgrading telega to 20260706 on both machines (docker kept, =docker pull= is a no-op against the frozen image). Host-coredump pollution should stop. If zevlg later pushes a =:latest= that outruns the installed elisp, the skew and the coredumps recur — the tell is a fresh =tdat_plist_value:500= assertion in =~/.telega/telega-server.log=. The durable escape is a host-native pinned TDLib build, at the cost of an AUR source build.
** TODO [#B] Proton static WireGuard profiles pass no traffic :chore:network:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-09
:END:
wg-US-CA-144, wg-US-TX-714 and wg-NL-781 (all on wgpvpn) complete a WireGuard handshake and answer ICMP at 10.2.0.1, then forward nothing: no DNS on any transport, no HTTPS payload, no IPv6. The same account over the Proton CLI works, so the static configs are what Proton stopped honoring (the shape of an expired certificate on the profile). Diagnosed 2026-09-01.
The net doctor now names these as a dead tunnel and brings them down (dotfiles f56fd1a), which gets the machine back online but doesn't restore the tunnels. Two ways out: re-download the WireGuard configs from the Proton dashboard and re-import them (nmcli connection import type wireguard file ...), or drop the static profiles and use the Proton CLI only. Needs the Proton account, so not solo.
*** 2026-09-12 Sat @ 23:26:50 -0500 The NM tunnel DoT drop-in already covers wgpvpn
=/etc/NetworkManager/conf.d/tunnel-dns-over-tls.conf= (live on both daily
drivers since 2026-09-10/12, and now written by the installer) matches
=interface-name:wgpvpn= as well as =proton0=, so a re-imported static profile
gets per-link DNS over TLS turned off automatically. Their dead-forwarding
problem is separate and unchanged.
** TODO [#C] Declined dot-link-restore branch untested :test:network:dotfiles:solo:quick:
:PROPERTIES:
:LAST_REVIEWED: 2026-09-09
:END:
repair_tunnel_dot_off (dotfiles net/src/net/repair.py) puts a tunnel link back to its DoT mode when turning DoT off didn't bring names back, and the evidence says "put back to <mode>" only when that restore succeeded. The restore-declined branch has no direct test. Give fake-resolvectl a second failure switch (FAKE_RESOLVECTL_DOT_RESTORE_FAIL) so the wording can be asserted absent as well as present. Follow-up from the f56fd1a review.
* Archsetup Resolved
** DONE [#B] Function keys issue media actions instead of F-keys :bug:velox:
CLOSED: [2026-09-01 Tue]
:PROPERTIES:
:CREATED: [2026-08-19 Wed]
:LAST_REVIEWED: 2026-08-19
:END:
From the roam inbox, Craig's words: "function keys should issue F+number
functionality rather than their media functionality when the button is hit.
currently it's reversed and I have to hit function and the f button for F+number
functionality."
Check first whether this belongs to archsetup at all. On a Framework the Fn-lock
is a firmware-level toggle held in the keyboard itself (Fn+Esc on most
revisions), not something the OS sets, in which case this is one keystroke
rather than a change here. If it is instead a hid/keyboard-module quirk, it is
ours.
Grading: Minor severity (the keys work, they are on the wrong layer, and there
is a workaround) x every user every time (every F-key press) = P2 = [#B].
Resolved 2026-09-01: not ours, as the body suspected. The Fn layer is decided
in the EC (the keyboard reaches Linux as a plain AT keyboard on i8042), so no
OS-side knob exists. One keystroke: Fn+Esc toggles Fn Lock; Craig confirmed
F1-F12 now send F-keys by default. The EC holds the state across reboots; it
reverts only if the EC loses power (battery disconnect or mainboard reset),
which is likely why it flipped around the August reinstall.
** DONE [#A] Lock-screen clock stale after a real sleep :bug:hyprland:dotfiles:velox:
CLOSED: [2026-09-13 Sun] SCHEDULED: <2026-08-25 Tue>
:PROPERTIES:
:CREATED: [2026-08-25 Tue]
:LAST_REVIEWED: 2026-08-25
:END:
After waking velox from a real sleep, the hyprlock clock shows a stale time
(Craig confirmed 2026-08-24: the wake-from-sleep case, not an idle-locked
screen). Three isolated tests on 2026-08-24 failed to reproduce it — hyprlock
0.9.6 repainted within a second of a display power-cycle, a three-minute
SIGSTOP, and both together with the screenshot background — so it needs a real
suspend on the real hardware.
Grading: Minor severity (cosmetic-to-confusing, the screen still unlocks) ×
most users frequently (every wake) = P3 = [#C] by the matrix; held at [#A] at
Craig's direction on 2026-08-25 so it gets run while velox is the daily driver
on the road. Revisit the letter once the manual check has an answer.
Not :solo: — the distinguishing observation is Craig's. The check lives under
Manual testing and validation: "Lock screen after a real sleep: is the clock
frozen, or is all of hyprlock frozen?". Its three outcomes each name a
different fix: stale-then-corrects → repaint interval; frozen with live input
→ clock rendering; frozen with dead input → hyprlock hung, a crash/hang
recovery bug the =screen-lock= watchdog doesn't cover.
*** 2026-09-13 Sun @ 07:57:04 -0500 Closed: fixed, per Craig
Craig confirmed on 2026-09-13 Sun that the stale clock after a real sleep is fixed on
velox. I could not identify the commit from here (nothing in the dotfiles
or archsetup log since 2026-08-24 names hyprlock, the lock clock, sleep or
resume), so this closes on his report rather than a cited change; the
manual-testing check for it is retired with it.
** DONE [#A] Velox reinstall — DR test of archangel + archsetup :velox:chore:
CLOSED: [2026-09-13 Sun] DEADLINE: <2026-08-15 Sat>
:PROPERTIES:
:CREATED: [2026-08-13 Thu]
:LAST_REVIEWED: 2026-08-13
:END:
Mainboard swapped Intel→AMD (Ryzen AI 9 HX 370); new NVRAM has no boot entry.
Decision: full reinstall via archangel+archsetup, run deliberately as a
disaster-recovery drill before the Sunday flight. Runbook (live checklist):
[[file:docs/2026-08-13-velox-reinstall-runbook.org][docs/2026-08-13-velox-reinstall-runbook.org]]
Done 2026-08-13: ISO rebuilt (archangel-2026-08-13, archsetup baked with AMD
microcode detection, velox profiles at /root/, .ai/inbox excluded — build.sh
edits pending commit in archangel), contents verified, dotfiles swept clean of
Intel assumptions.
Finding folded in: velox's truenas backups silently stopped ~Jul 6 (newest is
DAILY.0 Jul 6; wolf.conf.gpg from Jul 29 is in NO backup). Salvage pass in the
runbook is therefore REQUIRED before partitioning, and the fresh install must
fix + verify the backup timer (runbook Phase 5).
*** 2026-09-13 Sun @ 07:16:27 -0500 Applied the two live convergence steps on velox
Ratio got both by hand on 2026-09-12 while velox was off the tailnet; velox
came back on 2026-09-13 and got them over ssh: =systemctl disable --now
wsdd.service= (no Samba host to advertise; 43acf51 stops the installer
enabling it) and =systemctl mask passim.service= (the unit is static, so the
09-12 disable was a no-op; 38b1758 masks it in the installer, but
supplemental_software is a completed step there and doesn't re-run).
Verified after: wsdd inactive/disabled, passim inactive/masked, zero
listeners on 5357 and 27500. Same pass fast-forwarded velox's dotfiles to
f56fd1a and archsetup to dc00a62, and confirmed the headless Proton Bridge
service is still disabled there.
*** 2026-09-13 Sun @ 07:56:37 -0500 Closed the drill and filed its working-dir artifacts
The reinstall itself finished on 2026-08-14; every finding it surfaced is
its own task, and the live convergence steps landed on 2026-09-13, so nothing
was left in this task but the record. Filed per the working-files
convention: the runbook to docs/2026-08-13-velox-reinstall-runbook.org (with
an Outcome section, since the checklist was never ticked during the run),
the boot-entry reference to docs/2026-08-15-velox-uefi-boot-entry-reference.org,
the three gap reports to docs/design/2026-08-14-velox-reinstall-gaps-1 to 3,
and the wttrin bundle to working/emacs-wttrin-rescue/ under the task that
owns it. working/velox-reinstall/ is gone and every inbound link repointed.
** DONE [#B] Land the rescued emacs-wttrin commit :chore:velox:
CLOSED: [2026-09-13 Sun]
:PROPERTIES:
:CREATED: [2026-08-14 Fri]
:LAST_REVIEWED: 2026-08-17
:END:
bf0457f "feat: add wttrin-hide-follow-line to hide the wttr.in follow line"
(2026-06-24) was the only genuinely unpushed commit anywhere on the old
velox — 3 files, 103 insertions, with a test file. Rescued as a verified
git bundle before the disk was wiped (wttrin-bf0457f.bundle, deleted on
2026-09-13 once the commit was on the remote):
To land it: clone emacs-wttrin, =git fetch <bundle> --branches=, review the
commit, then push to git@cjennings.net:emacs-wttrin.git. Delete the bundle
once it's on the remote.
*** 2026-08-17 Mon @ 19:57:42 -0700 Re-checked: still unlanded, and the bundle is still the only copy
Cloned the remote bare and asked it for the object directly: =git cat-file -t
bf0457f= returns "Not a valid object name", so the commit has never reached
=git@cjennings.net:emacs-wttrin.git=. Remote =main= is =ee8fdeb=.
That makes =working/emacs-wttrin-rescue/wttrin-bf0457f.bundle= (moved there 2026-09-13 when the reinstall task filed its artifacts) the sole surviving
copy of 103 insertions across three files, on one laptop that is travelling.
Worth doing sooner than its =[#B]= suggests for that reason alone. It also
pinned the reinstall working directory open until 2026-09-13, when the bundle
moved into its own working dir here and the reinstall task closed.
*** 2026-09-13 Sun @ 09:47:20 -0500 Landed on emacs-wttrin release/0.4.0 and deleted the bundle
Cherry-picked unchanged onto release/0.4.0 (which already contains main; the
remote's default branch is still main, so main doesn't carry it yet) as
cb70193, patch-id identical to bf0457f. Review turned up two
defects the rescued commit carried, both fixed test-first and pushed with it:
f9449f6 makes the existing-F check case-sensitive (case-fold-search defaults
to t, so a lowercase f read as F), and 9c8d23e keys the cache on the effective
display options (toggling the setting kept serving a cached buffer with the
Follow line, against the 944a52f rule). Pushed 37e1c94..9c8d23e; full suite
green at 73 files.
Two things checked before landing: an old emacs-wttrin note that F broke ANSI
colour is stale (wttr.in sends identical colour codes with and without F), and
the suite's smoke failure was only missing Emacs 31.1 eask deps. The bundle's
main and release/0.4.0 heads are on the remote as-is, and its only other commit
(bf0457f) is there as the patch-identical cb70193, so the bundle and its
working dir are deleted.
emacs-wttrin has a handoff note in its inbox covering all of it.
** DONE [#A] Pre-vacation fix list — morning review
CLOSED: [2026-09-17 Thu]
:PROPERTIES:
:LAST_REVIEWED: 2026-09-17
:END:
Reviewed 2026-08-08; the decisions it drew are recorded in the tasks below.
Closed at the 2026-09-17 review because the ~08-15 departure it ranked work for
has passed. Where each of the seven items went:
1. Velox reliability → the [#A] sleep/suspend task.
2. Velox machine health for travel → moot: velox was wiped and reinstalled on
2026-08-13.
3. Remote access from outside the LAN → the wolf WireGuard rider under the
sleep/suspend task. Tailscale to ratio works off-LAN (checked 2026-09-17);
truenas and truenas-kvm weren't re-checked.
4. cgit secrets audit → the [#A] audit task and its rotation VERIFY.
5. Osbot camera → its own task; the podman socket and camera udev rule are
installed (sleep/suspend task, 08-17 entry).
6. Hotspot/metered WiFi and network ordering → the held design calls under
Next Session Focus.
7. The orchestrator sequence-pin gap → filed 2026-09-17 as [#C] Orchestrator
sequence pin misses an added step.
|