yog 0.0.1

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

Status: normative. This is the repo's living architecture document, tracked and
amended like code. It synthesizes three competing designs and three judge
verdicts (unanimous convergence); the decisions here are settled unless amended
here. Deliberate interpretations of the requirement that the user may veto are
collected in §13; rejected alternatives in §14.

---

## 0. What yog is

yog is a desktop manager for lernie loops organized around **named
workspaces**; balls are the work items workspaces pick up. One yog window
shows every project on the machine, every ball in each project, every lernie
workspace, every agent and subagent in each workspace, and every byte of
diagnostic data lernie produces — and lets the operator start work from
nothing, a path, or a ball (§3.4),
drive agents, and edit brazen and lernie configuration, all through the
substrates' own write paths (`bl`, `lernie`, `bz`-validated file writes).

**yog is an application that owns a nested world, not a layer draped over the
user's.** The naïve reading — yog as a thin viewer over the ambient
`bl`/`lernie`/`bz` state a human already runs — inverts: yog composes its own
nested environment (§16.2) for itself and every child it spawns, so the
substrate state yog drives is *yog's*, under yog's data root. Playing on top of
the user's *direct* tool usage stays possible — the balls store branch is
shared by default (§16.3) and redirection knobs tune the overlap — so
**compatibility with an ambient workflow is the user's decision, not a
structural given.** The world is the subject of §16.

**Governing invariant (I0): two yog instances running side-by-side faithfully
replicate the same data, with nothing RAM-only except unsubmitted input text.**
yog is a pure renderer of disk plus a small set of user-action dispatchers. The
only durable state yog itself owns is one UI-state document (`ui.json`) and one
action-outcome log (`ops.jsonl`). Everything else already has an authoritative
home in balls, lernie, brazen, or git, and yog derives it.

Crate `yog`, bin `yog`, repo `github.com/mudbungie/yog`, version 0.0.1 with
crates.io metadata wired; publication is a separate deliberate act.
**New runtime dependencies: zero in phase 1.** (clap, eframe/egui 0.29, libc,
notify, serde_json, thiserror — unchanged; tempfile dev-only.) Phase 2 (§16.5)
embeds `balls`, `brazen`, and `lernie` as **exact-pinned** crates — the sole new
dependencies, and the version mechanism; the crate direction is the end state,
gated per substrate on upstream lib-readiness.

---

## 1. Taxonomy

lernie bans the term "session" (TAXONOMY §3: "underdefined, per-framework
overloaded, and colliding with the transport/connection sense"). yog does not
re-mint it — **the concept dissolves into existing nouns**: "start a session"
is *prompt into a workspace (created if none exists — §3.4; claim optional)*;
"the session" is *the
workspace*;
"the session list" is *workspace enumeration*. The word "session" appears
nowhere in yog code or UI. yog's vocabulary, exhaustively:

| Noun | Definition | Authority |
|---|---|---|
| **project** | A repo path with a balls clone: one entry under `$XDG_STATE_HOME/balls/clones/<pct-enc-path>/`, percent-decoded | balls |
| **ball** | A task: `tasks/<id>.md` in a project's store, read via `bl list --json` / `bl show <id> --json` | balls |
| **workspace** | A lernie workspace: a directory containing `repo.git`; yog-started workspaces live at `$XDG_DATA_HOME/yog/workspaces/<name>/` (§3.1) | lernie (contents), yog (location) |
| **name** | A yog-minted two-word identity (embedded wordlist, hyphenated): the workspace's dir leaf **and** the `--as` identity of every ball claim the workspace makes (§3.1) | yog (minted once; thereafter the dir leaf) |
| **binding** | The derived association between a ball and a workspace: ball claimant = workspace name (§3.2). Balls-owned metadata, explicitly late-mutable via `bl claim`/`bl unclaim` — never a yog-stored fact | balls (claimant field); yog joins |
| **agent** | `agents/<id>` branch + descent arithmetic on the hyphenated id | lernie |
| **exchange** | lernie's presentational span (ARCH §2.4: root agent's history between a user message and the terminal response) | lernie |
| **attention** | A derived per-agent predicate (§6): unacked notify / stop / budget / conflict, or pending mail with no driver | yog (pure function) |
| **seen / pin / collapse** | The operator's durable, converging UI facts (§4.1) | yog (`ui.json`) |
| **draft** | Text typed but not sent | RAM (the requirement's carve-out) |
| **world** | The nested substrate environment yog composes under its data root — the `LERNIE_HOME` / `XDG_STATE_HOME` override fold that redirects `lernie` and `bl` state into yog-owned roots (§16.2; brazen's config stays ambient) | yog (composed) |

**Rejected:** a first-class session/loop record (registry file, ball field, or
git ref mapping ball↔workspace) — a second name for a workspace path, i.e. a
stored duplicate of a derivable fact, which drifts. balls' unknown-key
writeback seam was considered for storing the workspace path in the ball and
rejected: machine-local paths in a shared store are the same mistake balls
itself refuses for worktree paths (balls arch §11). The claimant field is
neither: it is balls' own first-class metadata under balls' own merge
discipline, and a *name* is a machine-neutral identity, not a path — which is
why binding lives there (§3.2).

---

## 2. Invariants (the durability skeleton)

- **I1 — Disk is the app.** Every rendered fact is a pure function of (files
  on disk, probe observations). Restart is equivalent to re-read. This already
  holds for the read path (`tests/pluggability.rs` proves N concurrent
  `GitTree::from_repo` calls converge); yog extends it to all state. The files
  are the *nested world's* files: every path derivation resolves through the
  composed world env (§16.2), so "disk" means yog's world, not the ambient one.
- **I2 — Two durable yog artifacts, no more.** yog owns exactly
  `$XDG_STATE_HOME/yog/ui.json` (§4.1) and `$XDG_STATE_HOME/yog/ops.jsonl`
  (§4.2) — these `$XDG_STATE_HOME` paths resolve through the composed world env
  (§16.2), i.e. nested inside yog's data root, which the world leaves anchored
  to the ambient `$XDG_DATA_HOME`. Every other write goes through the owning
  substrate's contract: task
  state via `bl` verbs; workspace state via `lernie` verbs only (never a direct
  write inside a workspace — ARCH §3.5); brazen config as a
  `bz --dump-config`-validated atomic file replace; lernie global config as an
  atomic file replace (lernie declares these hand-edited).
- **I3 — All yog file writes are temp-in-destination-directory + `rename`.**
  Never in-place truncation, never a temp on another filesystem (EXDEV). Temp
  names are dotfiles (`.<name>.yog-tmp-<pid>`) so no substrate reads them;
  leftovers older than 24 h are swept at startup. `ops.jsonl` is the one
  exception: O_APPEND lines ≤ 4096 bytes (PIPE_BUF), atomic per line.
- **I4 — Watches are latency, polls are correctness.** fs-watch (notify:
  inotify/FSEvents) triggers re-derivation fast; a periodic sweep re-derives
  regardless (§7.2: 2 s cheap sweep, 15 s full sweep, clock-injected). A
  dropped event, a stale watch, an overflowed inotify queue never causes
  divergence — only delay bounded by the sweep interval.
- **I5 — Convergence discipline by state class.** `ui.json`: last-writer-wins
  whole-file with echo suppression. `ops.jsonl`: append-only, order-free
  union. Config files: optimistic hash guard (refuse to overwrite a file
  changed since load, §9). `bl` operations: converge-on-retry (balls' own
  recovery rule, arch §13). lernie repo state: lernie's writer/driver
  discipline (ARCH §2.11); yog is a pure reader.
- **I6 — The RAM whitelist is closed.** Only the items in §5.3 may exist
  without a disk home. Every addition requires amending this document.
- **I7 — yog never mutates any substrate except on an explicit user action.**
  No auto-prime, no auto-scan, no auto-push, no background repair. Two
  instances can never race a spontaneous mutation because neither has any.
  *Composing* the world env (§16.2) is pure — no mutation, always safe;
  *materializing* the world (creating the subtree, seeding `LERNIE_HOME` via
  lernie's own bootstrap verb, priming the nested balls clone, setting the
  no-marks knob) is a mutation and so happens only on an explicit action — the
  first Start seeds a missing world exactly as it seeds a missing workspace
  (§3.4), never at idle.
- **I8 — A probe never perturbs the observed.** Liveness probing is read-only
  observation (lsof / procfs scans), never lock acquisition (§10, §14).
- **I9 — Determinism substitutes for persistence.** All ordering is derived:
  projects sort by path, workspaces by ball id, agents by descent order. Two
  instances render identical order without sharing any ordering state.

---

## 3. The organizing unit: the named workspace

Both roots below — `yog_data_root`, `lernie_data_root` — resolve through the
composed world env (§16.2), so the paths name locations *inside yog's nested
world*. The balls state root enters only through `bl` verbs (claims and
listings), never through path arithmetic: **the workspace tree encodes no
project paths and no ball ids** (§3.2 supersedes the original path-convention
binding).

### 3.1 Names: a minted identity, a flat root

**A workspace is long-lived and low-volume** — a sphere of work (personal,
corporate, a client) whose wall is lernie's isolation boundary; conversations
are root agents *inside* it (lernie §7.3), and balls flow through it (§3.2).
A new user never meets the concept: the first start bootstraps one (§3.4),
and further workspaces are minted only to wall spheres off from each other.

Every yog-started workspace is created at:

```
$XDG_DATA_HOME/yog/workspaces/<name>/
e.g.  ~/.local/share/yog/workspaces/cobalt-gecko/
```

- **The name** is minted at creation: two words from an embedded wordlist,
  hyphenated. The mint is a pure function over an injected RNG and the
  occupied set — existing dirs under the root plus every claimant visible in
  `bl list --json` across enumerated projects — so a fresh name collides with
  neither a live workspace nor a live claim. Every wordlist entry matches
  `^[a-z]{3,9}$` and is neither a given name, a surname, nor `unknown` (bl's
  `--as` fallbacks are `$USER` then the literal `unknown`, and bl validates
  nothing), so a minted name is path-safe by construction and can never be
  mistaken for a human claimant (curation + provenance: bl-ccf7,
  `src/names/words.txt` header). **The dir's existence is the
  registration**: the name namespace *is* the readdir; no registry file.
- **Enumeration / reverse derivation:** readdir the root for directories
  containing `repo.git`; the leaf is the name. Workspaces under
  `<lernie-data-root>/workspaces/` are **foreign** (lernie's auto-id
  territory — rendered, unnamed, never created by yog);
  `<lernie-data-root>/replays/*` render as read-only replay workspaces. Three
  roots, one shape, classification by path alone.
- **Severability:** the root is yog's own territory, *not* lernie's
  machine-populated `workspaces/` tree. Deleting `$XDG_DATA_HOME/yog` erases
  yog's entire workspace footprint and leaves lernie and balls untouched —
  the same choice balls made in placing delivery worktrees under its own
  plugin territory (arch §1). **With the nested world (§16.2) this widens:** the
  nested `LERNIE_HOME`, the nested balls state root, and yog's own artifacts
  all live under `$XDG_DATA_HOME/yog`, so one `rm` erases the whole world and
  leaves the *ambient* lernie/balls/brazen untouched.
  Rejected: `<lernie-data-root>/workspaces/yog/…`
  — squats in lernie's retention-governed, auto-id-populated territory.
- lernie accepts any path for `lernie new [path]` (CLI §1); yog `mkdir -p`s
  the root (outside any workspace — the ban is on writing *inside* one) and
  passes `<root>/<name>`.

**Superseded (was §3.1):** the path-convention binding
`$XDG_DATA_HOME/yog/balls/<mirrored-project-path>/<ball-id>/`. It encoded a
**congenital, immutable, 1:1** ball↔workspace association — fixed at `mkdir`,
unchangeable thereafter — and the actual workflow is the opposite: **agents
author balls mid-flight and pick up several**, and assignment must be
**late-mutable**. A location cannot be reassigned; a claim can. "Id is the
path" survives where it belongs — the *name* is the path leaf — while the
*assignment* moves to metadata balls already owns (§3.2). Y7/Y11/Y14/Y17
landed against the superseded convention; the Z-wave (§15 M6) reworks them.

### 3.2 Binding is the ball's claimant

The ball↔workspace association is **metadata on the ball**: a ball is bound
to a workspace iff its claimant equals the workspace's name.

- **Forward derivation:** ball → workspace = the dir named by the claimant.
  **Reverse:** workspace → balls = `bl list --json` filtered on claimant =
  name. Both are joins over facts balls already owns, mutates, and syncs —
  yog stores nothing (I2 intact).
- **Every claim a workspace makes is stamped with its name:** yog's own start
  flow claims `bl claim <id> --as <name>`, and the composed preamble (§3.3)
  tells the agent its name, so the balls an agent authors and picks up
  register the same way — **"agents write the balls" is the normal case, not
  a deviation.** The ball-pickup event *is* the assignment record.
- **Explicitly late-mutable, by design:** assign = `bl claim <id> --as
  <name>`; move = `bl unclaim <id>` + `bl claim <id> --as <other-name>`;
  release = `bl unclaim <id>`. All are first-class UI verbs (§8.2), legal
  whenever balls allows them, at any point in a workspace's life.
- **N balls per workspace** over its life; a ball has one claimant, so at
  most one workspace at a time. Stop-scope, budget-scope, and retention-scope
  ride the workspace — the conversation — not the ball.
- **Cross-machine caveat (accepted):** the store branch is shared (§16.3), so
  claimants minted by another machine's yog are visible here; a name
  collision would false-join. The mint's occupied-set check plus the
  wordlist's combinatorics make this negligible, and a false join only
  renders — it never mutates (I7).

**Justification:** single source of truth — the assignment's one
authoritative home is the ball's claimant field, owned by balls, mutated only
through `bl` verbs, synced by the store branch, and visible to the ambient
`bl list`. **Rejected:** (a) a yog-side registry file — drifts, needs its own
merge discipline (the claimant is not a registry: it is balls' first-class
metadata under balls' discipline); (b) storing the workspace *path* in the
ball — machine-local paths in a shared store (balls arch §11); a name is a
machine-neutral identity, not a path; (c) binding in `goal.md` content only —
per-root prose, unusable as an enumeration source; (d) workspace inside the
delivery worktree (`<worktree>/.lernie/`) — captured by close's squash into
the project repo, disqualifying.

Parallel attempts, reprompts, and follow-ups remain *multiple root agents in
the one workspace* — exactly lernie §7.3's concurrent-exchange model ("new
question → `lernie prompt` forks new root"); the workspace stays lernie's
isolation boundary (§2.2).

**Two altitudes of ball attribution, both derived, honestly scoped.** The
claimant equality above binds a ball to a *workspace* — the enumeration source
for the workspace's bound balls (the roster/header). A *conversation* (a root
agent, §1) is finer than a workspace, and its association is derived from a
different fact, with a different reach:

- **Conversation → ball (start-flow only):** the start flow composes the ball
  header into the conversation root's `goal.md` (`Ball <id>:`, §3.3); parsing
  that stamp back is the conversation↔ball join. It is the **inverse of the
  compose**, so one module owns both (compose and parse live together) and the
  format has a single home. A start-flow conversation stamps **exactly one**
  ball, so a conversation carries **at most one** derived ball — never a set.
- **Agent-picked balls have no conversation-level record.** When an agent runs
  `bl claim` mid-conversation (the normal case, §3.2 above), the claim stamps
  the *workspace* name — there is no fact recording *which conversation* picked
  it up. Such balls therefore bind at the workspace altitude only. **This is a
  real limit, not a rendering choice:** per-conversation badges come *only* from
  the goal stamp; every other bound ball renders in the workspace header. A
  conversation-level pickup record does not exist yet, and until a fact carries
  it, none is invented (single source of truth — no yog-side registry, §3.2).

Both joins are pure reads over facts already owned elsewhere (the claimant on
the ball; the `Ball <id>:` line in `goal.md`), stored nowhere by yog (I2).

### 3.3 Work-target and identity ride in the goal — through an editable composer

lernie has no target-repo concept: tools inherit the driver's cwd, which is
unreliable across detached revivals (CLI ground truth: message→advance via
setsid inherits lernie's own spawner cwd). The durable facts are therefore
**content**: the composed goal embeds the workspace's name and, when a work
target exists, its absolute path.

The start flow (§3.4) opens a **prompt composer prefilled** per payload rung.
**Identity is stamped by the harness, not the model:** every workspace-scoped
spawn carries `YOG_NAME=<name>` in its env (§8), and the world's bl agent
tool injects `--as $YOG_NAME` whenever the caller omits `--as` (W9, §16.4) —
the agent cannot forget what it never had to remember. The preamble still
opens by telling the agent who it is, for self-reference:

```
You are <name>.
```

*Phase-1 interim (host `bl`, no shim yet):* the preamble instead carries
`You are <name>. Stamp every bl verb you run with --as <name>.` —
load-bearing until W9 lands and deleted with it. A forgotten stamp falls back
to bl's `$USER` default and renders claimed-elsewhere: visible as a stray,
never corrupting.

The path rung appends a target preamble naming the given directory verbatim;
the ball rung composes ball title, ball body verbatim, and the worktree
preamble:

```
Ball <id>: <title>

<ball body verbatim>

The project repository checkout for this work is the git worktree at:
<abs worktree path>   (branch work/<id> of <project-path>)
Do all repository work there, by absolute path. Do not rely on the current directory.
```

**Transparency, scoped by ownership** *(amended with the STORIES ladder)*:
the operator sees and edits exactly the **payload** that is sent — their own
text and the target/ball prefills — before `lernie prompt` fires; the
**identity preamble is the harness's fact, stamped at fire time** (the same
ownership line as `YOG_NAME`/W9: the harness stamps identity, the model and
the operator never carry it). The composer *previews* the identity line
greyed above the box — the mint is a pure read (names-root readdir +
already-fetched claimants), so the predicted name renders before submit with
nothing spawned (I7 intact) — and on the rare lost race (another instance
registered the name between preview and Enter) the mint re-derives and stamps
the fresh name; the preview is a prediction, the stamp is the truth. The
binding mechanic stays transparent, and no goal-template config file exists
(the visible editable prefill is the severable version; deleting nothing
changes no code path). Belt-and-suspenders: yog also sets `current_dir` per
the §3.4 cwd column on every spawn.

The worktree path is never stored (balls arch §11: "computed, never stored");
it is recomputed by the bl-delivery formula
(`$XDG_STATE_HOME/balls/plugins/bl-delivery/<mirrored-project-path>/<id>/`)
and cross-checked against `bl claim` stdout at claim time; `git worktree list`
in the project repo is the standing ground truth. Because the path is a pure
function of (project, id), an unclaim/re-claim re-materializes at the same
path — the preamble never goes stale.

### 3.4 Lifecycle: the start flow

**Two orthogonal axes, one composer.** *Where* a prompt goes: the focused
workspace — and a world with zero workspaces mints one first, so **bootstrap
is the empty case of the general path**, never a wizard or a concept the new
user meets. *What* it carries: the payload ladder, each rung the one below
plus inputs.

| Payload rung | Input | Extra steps | Composer prefill (the identity line is *not* prefill — it is previewed grey and harness-stamped at fire, §3.3) | Driver cwd |
|---|---|---|---|---|
| **bare** | — | (none) | (none — the empty composer) | `~` |
| **path** | a directory | (none) | target preamble, path verbatim | the path |
| **ball** | a ball, picked or freshly created | `bl claim <id> --as <name>` | target preamble + ball title + body + worktree preamble | the work worktree |

- **Creating a workspace is the rare, deliberate verb** (+ New workspace,
  §11): raising a sphere wall — a client, corporate vs. personal — not a
  per-conversation act. The everyday gesture is the composer: a new prompt is
  a new root in the focused workspace (lernie §7.3), and a ball pickup claims
  `--as` that workspace's name (§3.2).
- The path rung's directory need **not** be a bl-primed project — it is
  simply where the agent works.
- Re-opening is the same path as opening: an existing dir skips `lernie new`,
  dissolving any "resume" special case. During work, everything is lernie's
  normal surface; yog renders and dispatches verbs (§8.2), including
  late assignment of further balls (§3.2).
- **`bl close`:** the ball file is deleted; the closed listing's claimant
  still names the workspace, so *that query is the "delivered" status* —
  delivered balls group under their claimant workspace on demand
  (`bl list -s closed --json`; obituary via `bl show <id> --json`;
  delivered-in commit via `git log --grep "[<id>]"`). yog deletes nothing:
  workspace retention is lernie's (§9.2, 30-day default); `lernie bundle` is
  the archival verb (deferred, §8.3).

### 3.5 Join states (the edge-case-dissolving enumeration)

The join is the claimant equality (§3.2), enumerated once, as a table; every
combination renders as a row state, never an ad-hoc branch. All derived, none
stored:

| Ball (derived status) | Claimant | Rendered as |
|---|---|---|
| ready | — | ready ball: ▶ Start (ball rung) or Assign to an existing workspace |
| blocked | — | blocked ball (blocker edges shown from bedrock JSON) |
| claimed | = a local workspace name | **bound** — the normal working row, grouped under its workspace |
| claimed | ≠ every local workspace name | **claimed-elsewhere** — badge shows the claimant verbatim (a human, another machine, or a deleted workspace) |
| closed (absent from live set) | (from the closed listing) | **delivered** — grouped under the claimant workspace when one matches, else visible in the on-demand closed listing |
| (none claim it) | workspace exists, zero bound balls | **unassigned workspace** — the bare/path-rung general case: full rendering, no ball column |
| any | project clone gone | **orphaned-project** — the project's balls unlistable, marked missing; workspaces unaffected (they encode no project path) |

**Conversation-level rendering is an overlay on this same table, keyed by the
goal stamp (§3.2).** A conversation row's ball badge is the ball its `goal.md`
stamps (`Ball <id>:`, §3.3), coloured by *that ball's* row-state above — bound
green, delivered ash, blocked/claimed-elsewhere brazen, orphaned ichor. The
join table is unchanged: the overlay looks the stamped id up in it. Two honest
gaps follow from §3.2's scoping, each a plain `None`, never a fabricated row:
(a) a conversation with no start-flow stamp (bare/path, or a hand-typed root)
shows no badge; (b) a stamped id the join does not know here (its project
unfetched, or the ball since deleted) still shows the id from the stamp, but
uncoloured — the badge is source-1 truth, the colour is the join's when it has
one. The **grouped-by-ball** organizing view is this overlay inverted: each
stamped ball heads its conversations, the stamp-less ones trailing in one
unassociated group — a pure, stable partition of the recency-sorted list.

---

## 4. Durable yog state

### 4.1 `$XDG_STATE_HOME/yog/ui.json`

`ui.json` holds **only genuinely-converging data** — user assertions with no
other authoritative home, where both instances *should* agree. Live focus,
selection, and scroll are deliberately **not** here: they are per-instance
viewport ephemera (§5.3, reasoning in §13.1).

```json
{
  "v": 1,
  "seen": {
    "/abs/ws/path": {
      "a-b": { "notify": "<ref-oid>", "stopped": "<tip-oid>",
               "budget": "<ref-oid>", "conflicted": "<ref-oid>" }
    }
  },
  "pinned":   ["/abs/ws/path", "..."],
  "collapsed": ["proj:/home/mark/dev/brazen", "ws:/abs/ws/path"],
  "show_internal": false,
  "identity_last_used": "orionriver@gmail.com"
}
```

- **`seen`** — the attention-acknowledgement watermarks (§6), one per signal
  kind, keyed to ref oids (notify/budget/conflicted = the `refs/lernie/*` ref
  target; stopped = the branch tip oid at acknowledgement). lernie's marks are
  level-triggered and yog may not delete refs ("the UI is a pure reader"; no
  ack verb exists) — so "the user has seen this" is a yog fact: **the mark is
  lernie's, the acknowledgement is yog's.** A moved ref re-notifies.
- **`pinned`** — ordered float list of workspaces (a user assertion, no other
  home).
- **`collapsed`** — explicit user expansion overrides only; default expansion
  is derived from attention, so the file stays tiny. A persisted *view* (§13.0),
  not durable data.
- **`show_internal`** — the global nested-delivery ("internal") clone view
  filter (§5.1 #1): a boolean, `false`/absent ⇒ hidden. A persisted view
  (§13.0), not durable data.
- **`identity_last_used`** — prefills `--as` for verbs dispatched *outside*
  any workspace (manual `bl create`/`bl update` from a project row; default
  `$USER` when absent). Workspace-scoped verbs never consult it: they stamp
  the workspace's name (§3.2). The severability showcase: no yog config file exists;
  deleting `ui.json` restores defaults and deletes no code path.

**Write discipline:** debounced (≤1 write 250 ms after last change; flushed on
any dispatched action and window close), temp-in-dir + rename (I3).
**Convergence:** both instances watch `$XDG_STATE_HOME/yog/`; on an external
change, read; if the content hash equals our own last write it is an echo —
ignore; otherwise adopt wholesale (LWW at file granularity). A missing/corrupt
`ui.json` is the fold identity — all defaults, never an error (brazen's
forgiving-read stance for its model cache). Unknown keys are preserved on
writeback (additive schema, balls' discipline).

**Startup focus derivation:** with focus out of `ui.json`, each instance
derives its initial focus deterministically — the next-attention workspace
(§6), else the first workspace in derived order (I9). Nothing is lost on
crash; focus re-derives.

### 4.2 `$XDG_STATE_HOME/yog/ops.jsonl`

One JSON line per **attempted** yog-initiated action *(amended from
"completed": a spawn failure — missing binary, exec error — appends a
synthetic line with the intended argv and the failure in `stderr`, extending
the precedent the detached prompt's spawn-failure line already set in §8.1;
a non-spawn step failure — mint pool exhaustion, `mkdir`, worktree
cross-check drift — appends a line whose `argv` is the logical step name,
e.g. `["yog-step","mint"]`, with a sentinel `exit`. **The sentinels are the
`src/opslog` consts (the authority): `-1` a piped verb whose status was
unobservable, `-2` a detached spawn (`lernie prompt` — clean when `stderr` is
empty, a spawn failure or a post-launch death when not), `-3` a synthetic
failure line (a piped spawn that never launched, or a non-spawn
`["yog-step",…]` step).** An error class with no ops row is an error the UI
cannot render — the §7.3 failed-action row depends on this)*:

```json
{"ts":"2026-07-17T12:00:00Z","argv":["bl","close","bl-4db6"],"cwd":"/home/mark/dev/brazen","exit":0,"stdout":"…","stderr":"…"}
```

- Written with O_APPEND; **each line is capped at 4096 bytes (PIPE_BUF)** so
  concurrent appends from two instances are atomic and never interleave;
  `stdout`/`stderr` are truncated to fit with an explicit
  `"truncated":true` marker. A pathological `argv` element is the one field the
  capper cannot shrink, so the caller pre-clips the sole large one — the detached
  `lernie prompt`'s composed goal — to a bounded head with an explicit
  `… [+N bytes elided]` marker before logging (the *spawned* goal is unclipped;
  full fidelity is never the log's job, the goal being derivable from the
  workspace).
- Both instances append; both tail it (fs-watched). The ops pane renders the
  shared history.
- This closes the one durability leak of a pure derive-everything stance:
  gate/close output, `lernie scan` summaries, and error text are *not* on disk
  anywhere else — without this log they would be RAM-only and non-convergent
  (and the current shell's "stderr printed and dropped" hole would persist).
- No rotation in v1 (documented; balls' own multi-MB per-clone `log` sets the
  precedent). Detached long-lived drivers (§8.1) do **not** stream here —
  their outcomes are derived from disk, not parsed from pipes.
- **One field is not stored here: a detached spawn's `stderr`.** The `-2` line
  is written at fire, when the child has said nothing yet. Its stderr is
  captured to the per-spawn sink `detached/<ts>-<workspace leaf>.err` (§8.1,
  §5.2) and folded into the row **at read time**, on the tail the ops sweep
  re-reads. The sink is the authority and the row a projection, so the text is
  never stored twice and no line is ever rewritten; the sink's name derives from
  the `ts` and workspace the line already carries, so the schema gains no field
  to join them. A row whose folded `stderr` is non-empty is a rendered failure by
  the `-2` rule above — that is how a driver that died *after* launching stops
  being invisible.

---

## 5. The complete state inventory (normative)

Every piece of state in the application, classified. **This table is
normative: code review rejects any state not placeable in it.**

### 5.1 Derived-from-disk (fact → home → derivation; never stored by yog)

Every path fold below resolves through the composed world env (§16.2):
`LERNIE_HOME` and `$XDG_STATE_HOME` name nested locations, while
`$XDG_DATA_HOME` / `$XDG_CACHE_HOME` / `$BRAZEN_CONFIG` stay ambient — so
brazen config (#19), credentials (#22), and model cache (#23) all read the
*shared* ambient world, with no change to the fold expressions themselves.

| # | Fact | Source of truth | Derivation |
|---|---|---|---|
| 1 | Project list | `$XDG_STATE_HOME/balls/clones/*` | readdir + percent-decode basename; decoded paths under `plugins/bl-delivery/` are nested-delivery clones, hidden behind an "internal" toggle |
| 2 | Balls per project | project store | `bl list --json` with cwd = project path (the bedrock projection; serde_json). **Process-failure ≠ empty:** a non-zero/failed `bl` (clone gone, unlistable) leaves the project *unkeyed* in the cache → the §3.5 orphaned-project row; a clean exit with `[]` keys it to an empty vec → a listable project with no balls (the two are distinct states, not both "no balls") |
| 3 | Ball status | ball frontmatter | balls §3 ladder: claimant ⇒ claimed; else unresolved claim-blocker ⇒ blocked; else ready. Closed = absent from live set |
| 4 | Delivered/closed balls | store history | `bl list -s closed --json` / `bl show <id> --json` on demand |
| 5 | Work-worktree path | formula + git | bl-delivery formula recompute; `git worktree list` ground truth; `bl claim` stdout cross-check |
| 6 | Workspace list + names + foreign + replays | `$XDG_DATA_HOME/yog/workspaces/*`, `<lernie-data>/workspaces/*`, `replays/*` | readdir for `repo.git`; leaf = name per §3.1 |
| 7 | Join state per (ball, workspace) | #2–#6 | the §3.5 claimant join, a pure function |
| 8 | Agent set, descent, tips | `repo.git` refs | `git for-each-ref agents/*` + hyphen-prefix arithmetic (existing `git_tree`) |
| 9 | Agent state {Live, InFlight, Quiescent, Stopped} | inbox flock + response.json framing | LockProbe + WriterProbe (tri-state, §10) + `last_segment_complete` (ARCH §3.5/§4.4) |
| 10 | Streaming text, tool calls | `steps/<id>/NNN/` | existing `streaming.rs` / `tools.rs` |
| 11 | Pending messages, inbox contents | `inbox/<id>/*.md` | count + parse `---from/deposited_at/epitaph---` frontmatter |
| 12 | Transcript | `agents/<id>/messages/NNN-<origin>.*` | readdir + sort; origin from filename; "tool in progress" = tool_use with no tool_result |
| 13 | Step diagnostics | `steps/<id>/NNN/{meta,request,response,staging}.json`, `tools/` | full-file reads, jsonview rendering — every byte inspectable. The §7.3 **no-response wound** derives from the same bytes: an empty-or-absent `response.json` with no `meta.json`, on an agent nobody is driving (#9) |
| 14 | Marks ×4 | `refs/lernie/{conflicted,budget-exhausted,abandoned,notify}/*` | `for-each-ref` (marks.rs extended from 2 to 4 namespaces) |
| 15 | Attention (per agent / rollups / totals) | #9, #11, #14 + `ui.json.seen` | §6 predicate — pure |
| 16 | Budget *spent* | Usage events across `steps/<root>*/` | fold; limits displayed only as raw `workflow.yaml` text (no YAML dep) |
| 17 | Governing config per agent | git ancestry | nearest ancestor of agent tip reachable from any `config/*` ref (merge-base over config refs, ARCH §2.2) |
| 18 | Config branches + contents | `repo.git` `config/*` refs | `for-each-ref` + `git show <ref>:<path>` |
| 19 | brazen file config | brazen's exact path fold: `$BRAZEN_CONFIG` > `$XDG_CONFIG_HOME/brazen/config.toml` > `~/.config/…` (pure XDG on all platforms) | raw text |
| 20 | brazen effective config | `bz` itself | `bz --dump-config` stdout verbatim (bz is the authority on the value fold; yog never re-implements TOML semantics) |
| 21 | brazen built-in rows | compiled into bz | static read-only hint labeled "compiled into bz <`bz --version`>" |
| 22 | Credential presence | `credentials/<provider>.json` existence (per-OS dir) | existence only; contents never read, never written |
| 23 | Model cache | `$XDG_CACHE_HOME/brazen/models/*.json` (per-OS) | read-only display; refresh = `bz --list-models` |
| 24 | lernie global config | `<config-root>/models.yaml`, `workflows/*.yaml` | raw text |
| 25 | Action history | `ops.jsonl` | tail + parse (§4.2); ambient error prominence is the §6 retirement projection over that tail — never a stored flag |

### 5.2 Durable-on-disk, yog-owned

Exactly `ui.json` (§4.1), `ops.jsonl` (§4.2), and the detached-spawn stderr
sinks `$XDG_STATE_HOME/yog/detached/<ts>-<workspace leaf>.err` (§8.1, §13.3) —
each written *by the detached child itself*, not by yog, and each the sole
authority for what that driver said, projected into its `-2` ops row at read
time. Like `ops.jsonl` they are not rotated in v1.

Plus two *transient scratch* artifacts that exist only inside an operation and
are swept: config staging temps (`.<name>.yog-tmp-<pid>` in the destination dir)
and the scripted-editor staging directory `$XDG_STATE_HOME/yog/stage/<nonce>/`
(§9.3). Neither is an authority; leftovers >24 h old are swept at startup.

### 5.3 Legitimately RAM (the closed whitelist, I6)

| Item | Why RAM is legitimate |
|---|---|
| Unsubmitted input text (prompt/message bars, claim-dialog fields, the goal composer, config editor buffers before Apply) | the requirement's explicit carve-out: "text typed in a box can live in RAM until sent" |
| **Live focus/selection and scroll position** | per-instance viewport ephemera — *which data you look at*, not data; loses nothing on crash; re-derives at startup (§4.1). Deliberate interpretation, §13.1. Scroll is *represented* as content anchors (topmost visible message ordinal / step number), never pixels, so the viewport stays stable across live re-derivation of the tree beneath it |
| Subprocess handles, drain threads, `Stream`s | a process is not data; the fact each represents lives on disk (driver running = flock held; op outcome = `ops.jsonl` line + substrate state). Long-lived drivers are spawned fully detached (§8.1) so yog's death cannot kill or starve them |
| Watcher registry, notify channels, dirty flags | reconstructible plumbing; the sweep (I4) makes their loss harmless |
| Memoized derived snapshots (`HashMap<PathBuf, GitTree>`, ball lists, parsed transcripts) | caches of §5.1 facts; discarded and rebuilt at will |
| Live window geometry, egui layout/font caches, GPU state | instance-physical, not data (§13.0: window arrangement is a view, not data) |
| Probe result TTL cache on macOS (§10) | a cache of an observation with a 2 s bound |
| Live streamed-verb output (§8's streamed-piped class: the `bz --login` device code/URL lines) and the last failure outcome held at its originating surface | instance-local by nature (a device code is for the human at *this* keyboard); both converge to their `ops.jsonl` line — the stream to its outcome line at exit, the failure to the entry it already appended — so the other instance renders the durable fact from the pane, never diverges |

**The left panel's `collapsed` overrides (§4.1) are the deliberate
counter-example:** a persisted *view* — which sections (the balls section)
you keep folded — that converges benignly for convenience, intentionally
*unlike* this RAM-only jsonview collapse set and the RAM-only activity
accessory (§13.0). All are "collapse" state; only the section override is
worth persisting. Don't "fix" the asymmetry.

---

## 6. The attention model

`attention(agent)` is a derived predicate, true when any of:

1. `refs/lernie/notify/<id>` exists and its oid ≠ `seen[ws][agent].notify`.
2. State = Stopped, no `refs/lernie/abandoned/<id>`, and tip oid ≠
   `seen[ws][agent].stopped`.
3. `refs/lernie/budget-exhausted/<branch>` oid ≠ `seen[ws][agent].budget`.
4. `refs/lernie/conflicted/<id>` oid ≠ `seen[ws][agent].conflicted`.
5. Pending inbox > 0 **and** lock Free — mail nobody is driving (the
   writer/driver stall case). **Not seen-gated**: it is actionable (flush via
   `lernie scan`), self-clears when a driver picks it up, and hiding it would
   hide a stall.

Signals 1–4 are seen-gated on the `ui.json` watermarks (§4.1); focusing an
agent records the current evidence oids as seen. Because `seen` converges,
**acknowledging in one instance acknowledges in both — attention is data, and
it converges.** Live focus does not.

Rollups: workspace attention = max over its agents; the top strip shows totals
across all workspaces with a **jump-to-next-attention** control (also the
startup focus derivation, §4.1). Sort within each group (conversation list,
keyboard order): **attention > running > idle**, then derived order (I9) —
the conversation list refines "idle" to recency (§11).

**Ops error prominence retires the same way — derived, never stored.** The
activity surface (§4.2's tail, §11's chip) is the ops-side analogue of this
model. `ops.jsonl` is append-only and keeps every failure forever, so the
*ambient* ⚠ count is a **projection over the tail at read time, never a stored
flag**: walking newest-first, a failed line is a **live failure** unless a later
line with the same (`cwd`, verb) did not fail. The verb is the leading two argv
tokens — binary plus subcommand (`bl close`, `lernie prime`, `yog-step mint`) —
because the argv tail carries per-run operands (a ball id, a composed goal) that
never repeat, so keying on the whole argv would retire nothing; `cwd` scopes it,
so a clean `bl close` in one project leaves a failed one in another alone.
Success is the pane's own failure classifier negated (`OpRow::failed`), so no
second definition of success can drift from the one it paints. A retired failure
keeps its row and its ⚠ in the expanded accessory — it loses only ichor and the
chip's count. **Absence of a live failure is the record; the log is the
history.** The wound this closes: a three-day-old `lernie prime` failure, since
fixed and re-run green, read as THE error when an unrelated action failed,
sending diagnosis down a false trail.

A conversation whose latest step **failed** stirs the strip through rule 2: a
failed or killed latest `response.json` classifies the agent Stopped
(§4.4/§3.5) — an auth-failed step included — so an unseen dead conversation is
never "nothing stirs". Acknowledging it clears the *signal*, not the fact: the
conversation list's state badge and the §11 Login affordance keep rendering
the settled failure (the badge is state, not attention).

**The prompt that never became a conversation (bl-a649).** Rules 1–5 are all
per-*agent*, so they can only stir once a conversation root exists. A detached
`lernie prompt` that dies before writing one — a tool version-skew refusal at
startup — has no agent to attach a signal to, and used to stir nothing at all.
It is not a sixth rule: that failure is an **action** outcome, not an agent
state, and it surfaces on the action path it already belongs to — the child's
stderr sink folded into its `-2` ops row (§4.2, §8.1), which makes the row
`failed()` and therefore lights the §11 activity chip's ⚠ count and the §7.3
ichor-red banner at the firing surface. The two paths stay disjoint on purpose:
attention is about agents that exist; the ops surface is about actions that were
attempted. A prompt whose driver dies mid-life crosses over — it *has* an agent
by then, and rule 2 fires. Rule 2 only says *something is wrong here*, though:
the **cause** is not attention's job. What that conversation shows once focused
is the §7.3 no-response wound (§11), and what the driver actually said is its
stderr sink on the ops surface.

---

## 7. Watch / re-render architecture

### 7.1 Watch roots

A `WatchSet` (new module `src/watch/`) owns one `fs_watcher::Watcher` per
root, with a per-root-kind allowlist (generalizing the existing hardcoded
single allowlist):

| Root kind | Path | Allowlist |
|---|---|---|
| Workspace (×N) | each workspace dir | existing: `steps/`, `inbox/`, `agents/<id>/{goal.md,soul.md,summary,messages,descriptions,skills}`, `repo.git/HEAD`, `repo.git/refs` |
| NamesRoot | `$XDG_DATA_HOME/yog/workspaces/` (top-level — flat by construction) | dir create/remove (new/removed named workspaces) |
| WorkspacesRoot | `<lernie-data>/workspaces/`, `replays/` (top) | dir create/remove |
| BallsClones | `$XDG_STATE_HOME/balls/clones/` | clone dir create/remove; per-clone `tasks/tasks/*.md` and `config/config/**`; the per-clone `log` (multi-MB, no rotation) is **filtered out** to avoid event storms |
| BrazenConfig | dir of the resolved config.toml (the ambient file, §16.2/§9.1) | the file name only (atomic rename = Remove+Create on the dir watch) |
| LernieConfig | `<config-root>/` | `models.yaml`, `workflows/` |
| YogState | `$XDG_STATE_HOME/yog/` | `ui.json`, `ops.jsonl` (the `detached/` sinks are **not** watched: a chattering driver would storm the watch, and the 15 s sweep's re-read of the ops tail folds them in anyway, §8.1) |

Rejected: one recursive watcher over the whole lernie data root — an agent
building a large tree in its worktree inflates the inotify watch count and
fires on every git object write; per-workspace scoped watchers with allowlists
are the existing, tested shape.

### 7.2 Repaint and re-derivation

eframe is repaint-on-interaction; today nothing re-reads disk after startup
(fs_watcher is built, tested, and unwired). The wiring:

- A single **bridge thread** blocks on the aggregated notify channels; on an
  allowlisted event it records the dirty root in a `Mutex<DirtySet>` and calls
  `egui::Context::request_repaint()`. This is the only cross-thread signal.
- `App::update` each frame: drain `DirtySet` → re-derive only dirty roots →
  render. Re-derivation granularity v1 is **whole-root rebuild with a 100 ms
  coalescing debounce** (a streaming `response.json` append storm collapses to
  ≤10 rebuilds/s of one workspace). Rebuild is always correct; incremental
  streaming-only refresh is a listed optimization task, not a correctness
  feature. `GitTree: PartialEq` suppresses no-op replacements.
- **Poll floor (I4):** every frame schedules `request_repaint_after(2 s)`.
  The **2 s cheap sweep** re-runs the cheap enumerations (readdir of clones/,
  workspace roots, config dirs), reconciles the WatchSet (a watcher whose
  directory was deleted/recreated — e.g. a re-primed clone — is rebuilt), and
  performs the **targeted liveness re-probe: only agents currently
  Live/InFlight are re-probed** — a released flock emits *no* fs event, so
  silent driver death is only observable by polling, and probing only the
  agents that could have died bounds the cost. Every **15 s** the sweep marks
  *everything* dirty. Correctness never depends on an event arriving; a stale
  watch costs ≤15 s of latency, never divergence. `bl list --json` per
  project runs on its root's dirtiness or the 15 s sweep, never per frame.
- **All sweep/debounce/heartbeat timing is clock-injected** (a `Clock` trait,
  same injection pattern as LockProbe/WriterProbe) so every time-gated branch
  is testable to 100% without sleeps.
- The existing ~30 fps tool-pulse repaint is unchanged; effective repaint
  delay each frame is `min(pulse, sweep)`.

Rejected: a dedicated derivation thread pushing snapshots — an extra
concurrent stateful component; deriving on the frame thread with dirty-set
coalescing keeps "pure function of disk at this tick" inspectable.

### 7.3 Failure modes (enumerated)

| Failure | Handling |
|---|---|
| inotify queue overflow / dropped events | 15 s full sweep re-derives; bounded staleness |
| Watched dir replaced (clone re-primed, workspace deleted) | 2 s reconcile rebuilds the watcher from the enumerated root list |
| Editor atomic-rename inode swap on config files | dir-level watch sees Remove+Create; allowlist matches by name |
| Event storm from streaming response.json | 100 ms coalescing debounce per root; balls `log` files excluded from allowlists |
| Concurrent `ui.json` writers | LWW + echo-hash (§4.1) |
| Concurrent `ops.jsonl` appenders | O_APPEND + ≤PIPE_BUF lines — kernel-atomic, no interleave |
| Concurrent config-file editors (two instances, or instance + vi) | optimistic hash guard: Apply refuses if on-disk content ≠ content loaded into the buffer; user reloads and re-applies (§9) |
| Crashed `bl` op | converge-on-retry: re-run the verb (balls arch §13); stderr in `ops.jsonl` |
| Crashed/killed driver | agent classifies Stopped from framing; attention rule 2 fires; `lernie scan` deposits died epitaphs and flushes inboxes |
| yog crash mid-write | rename atomicity: whole-old or whole-new; dotfile temp debris swept at startup |
| yog crash with drivers running | drivers are detached into their own process group, holding no yog-owned pipe (stdin/stdout null, stderr on a file) — unaffected; next launch re-derives their state from locks/refs |
| Detached driver dies right after launch (tool version skew, missing model config) | its stderr sink (§8.1) is non-empty; the ops sweep folds the tail into the `-2` row, which becomes a rendered failure — banner + ⚠ chip. Without the sink this was invisible: exit `-2`, empty stderr, a prompt that "does nothing" (bl-a649) |
| Probe backend unavailable (lsof missing) | tri-state `Unknown` → uncertainty badge, never a false definite state (§10) |
| Driver dies leaving an empty step (version skew, OOM, kill before the first event) | the step is a **no-response wound**, not a quiet one: an empty-or-absent `response.json` **and** no `meta.json` **and** no driver on the agent (§3.5) renders "driver produced no response" in ichor beside the step and banners it at Altitude 1 (§11). Framing alone reads this `Killed` — the ash "stopped" badge over a `0 attempts · 0 tok` row, which is how it read as a quiet step (bl-7f2e). The *cause* lives on the ops surface: the driver's stderr sink, folded into its `-2` row (§8.1) |
| Failed action (short verb or start step) | **a rendered fact, never stderr-only**: the full `ops.jsonl` entry (argv, cwd, exit, stderr) is expandable at the ops pane, *and* the originating surface (start pane, input bar) renders the failure in ichor red with argv + stderr tail. No `eprintln!`-only error path may exist in `src/shell/` (STORIES INV-2) |

---

## 8. The action surface (v1) and exact argv

All spawns go through `cli_outbound` (generalized: binary resolution
parametric over env var — `LERNIE_BINARY`, `BL_BINARY`, `BZ_BINARY`, default
PATH names — plus `current_dir` support and a detached-spawn mode). **Every
spawn carries the composed world env (§16.2)** — the overrides layer over the
inherited environment via the existing `run_env` seam — so every child, the
detached driver included, runs *inside* the nested world; an agent's own tool
processes inherit the driver's nested `$XDG_STATE_HOME` and so a host `bl` they
invoke computes the right nested paths (§16.4, phase-1 correctness).
Workspace-scoped spawns additionally carry `YOG_NAME=<name>` (§3.3) — the
identity the W9 tool shim stamps onto unstamped bl verbs. Binary
resolution is unchanged. Every attempted action appends its line to
`ops.jsonl` (§4.2). Spawns come in exactly three classes: **short-piped**
(run to completion, outcome logged), **detached** (own process group,
stdin/stdout→null, stderr→a per-spawn sink file, spawn logged with the `-2`
sentinel), and **streamed-piped** *(added with the §8.3
login amendment: line-buffered stdout rendered live at the invoking surface —
a §5.3-whitelisted instance-local stream — with the outcome line appended at
exit; sole v1 member: `bz --login`)*.

### 8.1 Start (the composite verb — §3.4's axes, as argv)

1. Resolve the target workspace: the focused one. **Zero workspaces in the
   world → mint a name (§3.1), `mkdir -p` the names root, `lernie new
   <root>/<name>`** — the bootstrap is this empty case, not a separate flow.
   The explicit **+ New workspace** verb (§11) runs the same mint + `lernie
   new`, deliberately.
2. Open the **editable goal composer** (§3.3), prefilled per payload rung. On
   confirm: `lernie prompt <root>/<name> <composed-goal>` — **spawned
   detached** (own process group, stdin/stdout→null, **stderr→the per-spawn
   sink file**, `YOG_NAME=<name>` layered per §8), cwd per
   the §3.4 column. The agent id on stdout is
   **deliberately not read**: the new root materializes in the watched repo
   within a tick — *derive, don't parse*. Detachment also removes the failure
   mode where yog holding a driver's stdout pipe ties the driver's lifetime
   to yog's (`Stream`'s Drop SIGTERMs; a SIGPIPE after yog death would kill
   the loop).

The ball rung inserts, between 1 and 2: (optional) `bl create <title> …`
(cwd = project; stdout = id), then `bl claim <id> --as <name>` (cwd =
project; stdout = worktree path). **The order is load-bearing:** every
substrate step (the seed, `lernie new`) precedes every `bl` mutation, so a
failed or missing substrate aborts before anything half-commits — *the start
flow* can never mint an orphaned claim. (A claimed ball whose workspace was
later deleted remains a legal state; §3.5 renders it claimed-elsewhere.)

The planner (`start::plan`) is a pure function returning the command sequence;
the executor runs it step-by-step with per-step outcomes in `ops.jsonl`.
Steps are individually idempotent-or-convergent: re-running after a crash at
any step converges (double-claim refuses benignly; `lernie new` skipped when
the dir exists; prompt just adds a root; **a ball already claimed by a local
workspace name re-plans as a prompt into that workspace — resume, not a
second mint**). A **new** ball defers the id: the plan is a single
`bl create`, and the freshly-minted (ready, unclaimed) ball is re-planned as
an existing one — the new→existing transition *is* the convergence, not a
special case. The claim's stdout worktree path is cross-checked against the
bl-delivery formula (both the `<id>` and `<id>-<claimant>` variants match;
anything else is a convention drift surfaced loudly, never silently
accepted). The short steps (`lernie prime`, `bl create`, `bl claim`,
`lernie new`) log their piped outcome; the detached `lernie prompt` logs only its **spawn** — argv,
cwd, and a `-2` sentinel exit, since a detached child in its own process group
has no waitable status — with a spawn failure riding the same line in `stderr`.

**The detached child's stderr sink (bl-a649, amending §13.3).** A spawn failure
is not the only way a prompt fails: the child can launch cleanly and *then* die
(a tool version-skew refusal, a missing model config), which under the original
stdio→null left `-2` + empty `stderr` — indistinguishable from a healthy launch,
and the operator saw a prompt that did nothing. So the detached child's stderr
is bound to a **per-spawn sink file**,
`$XDG_STATE_HOME/yog/detached/<ts>-<workspace leaf>.err`, and the ops row's
`stderr` is **derived from that file at read time** (§4.2, §7.2) instead of
being copied into `ops.jsonl`. Consequences, each load-bearing:

- **The sink is the authority; the row is a projection.** The fact lives once.
  `ops.jsonl` records the *launch* and is never rewritten; a driver that keeps
  writing surfaces more on each sweep, with no second durable copy to diverge.
- **The join key is computed, not stored.** The sink's name derives from the ts
  and workspace the ops line already carries, so no field is added to the §4.2
  schema to point a row at its file — the path *is* the id.
- **A file, not a pipe.** yog holds no descriptor on it, so §8.1's whole reason
  for detaching is untouched: the child outlives yog and keeps writing.
- **Only the tail is folded**, bounded, from a line boundary — a long-running
  driver's sink is unbounded and this read runs every sweep.
- **Nothing new stirs.** A non-empty capture makes the row `failed()` by the
  rule already written for `-2` (§4.2), which is what the §7.3 banner and the
  §11 activity chip's ⚠ count read. No new signal, no new surface.
- **An unopenable sink degrades to `/dev/null`** and the launch proceeds: the
  driver is the point, the capture is the diagnosis.

### 8.2 Per-workspace / per-agent verbs

| UI action | argv (cwd) | Spawn mode |
|---|---|---|
| New prompt (new root) | `lernie prompt <ws> <text>` | detached |
| Message agent (also the resume gesture — no resume verb exists, ARCH §2.9) | `lernie message <ws> <agent> <text>` | short, piped (it self-detaches its driver) |
| Stop | `lernie stop <ws> <agent>` (+ `--stop-children` toggle) | short, piped |
| Scan / flush | `lernie scan <ws>` | short, piped; summary line surfaced |
| Close ball | `bl close <id>` (project) | short, piped; capture/fold/gate/squash output in `ops.jsonl`, gate failures verbatim (claim+worktree stay up, bl's own semantics) |
| Assign ball → workspace (§3.2) | `bl claim <id> --as <name>` (project) | short, piped |
| Move ball → other workspace | `bl unclaim <id>` then `bl claim <id> --as <other-name>` (project) | short, piped ×2, both logged |
| Release ball | `bl unclaim <id>` (project) | short, piped |
| New ball | `bl create "<title>" [--body B] [flags]` (project) | short, piped; new id captured on stdout |
| Update ball | `bl update <id> …` (project) | short, piped |
| Refresh models | `bz --list-models --provider <row> --json` | short, piped |
| Login provider | `bz --login --provider <row>` (toolchain pane; also offered beside an auth-failed step) | streamed-piped (§8.3) |

Short verbs show a busy indicator (RAM — the underlying fact is the
`ops.jsonl` line plus substrate state both instances see).

**Identity rider (Z4).** Every `bl` claim/close/unclaim yog issues is stamped
`--as <workspace name>`, **not** the operator's `$USER` — the claimant delivers
its own ball (§3.2's ownership line, the same fact as the start flow's `bl claim
--as <name>` and W9's `YOG_NAME`). Concretely: **close** and **release** stamp
the ball's *bound* workspace name (its claimant); **assign** and a **move**'s
claim stamp the *target* workspace name; a **move**'s unclaim stamps the ball's
current (source) workspace name. The operator identity survives only as the
*author* of a standalone `bl create`/`bl update` (§8.2 New ball / Update ball),
where a workspace is not the reporter. `lernie message` (the resume gesture)
additionally layers `YOG_NAME=<ws leaf>` on the revived driver, so its agents'
own tool subprocesses stamp `--as` the same name — the detached `lernie prompt`
already does (§8.1); message is a workspace-scoped spawn too (§8).

### 8.3 Deliberately not in v1 (with reasons)

- **Manual `lernie dispatch`** — workflow-driven dispatch is the designed
  path; a manual role-dispatch button invites mis-goaled children. If ever
  surfaced: `lernie dispatch <role> <ws> <branch> --goal <text>`, role list
  derived from governing `providers.yaml` roles that carry souls.
- **Fork-from-history** — no lernie CLI verb exists; yog may not write refs
  (ARCH §3.5). Upstream gap, tracked as a lernie ball, not worked around.
- **`bundle` / `replay` actions** — v1.1; replay *results* (`replays/*`)
  already render read-only in v1 since they are just workspaces.
- **`bl conf` editing, `bl prime` of new projects** — v1.1; v1 scope is
  projects already primed (present in `clones/`).
- **brazen `[ingress]`/`--serve`, credentials editing** — out of scope;
  credentials are constitutionally untouchable. **Amended (STORIES S0):**
  `bz --login --provider <row>` *is* v1 — it is bz's one interactive surface
  and its headless device flow needs no TTY input, so yog runs it as a
  **streamed-piped** verb (§8's third spawn class) from the toolchain pane,
  its stdout lines (device code, URL, poll status) rendered live in the pane
  verbatim. yog renders the flow;
  credentials remain bz-stored, never read or written by yog. Showing the
  exact command stays as the fallback when the piped flow exits non-zero.

### 8.4 World escape hatches (`yog env`, `yog exec`)

Two subcommands of the yog binary — the multi-call pattern beside
`--editor-apply` (§9.3) — expose the composed world to a human at a shell:

- `yog env` prints the world's `export` lines (`LERNIE_HOME`,
  `XDG_STATE_HOME`); `eval "$(yog env)"` drops the current shell *into* the
  world, where the ambient `bl`/`lernie`/`bz` then operate on yog's nested
  state.
- `yog exec <cmd…>` runs one command inside the world (world env layered,
  optional cwd) without touching the caller's shell.

Both are pure entrypoints of the yog binary, not substrate spawns — the
operator's hand-hold on an otherwise-encapsulated world (§16.2), and the human
counterpart to the embedded-crate agent tools (§16.4). **They stay hatches:**
yog never fires `yog exec` at itself as a reproduction affordance beside a
failure — the driver's stderr sink already carries the cause (§8.1), so a
re-run button would re-create a held fact and start a second driver (§14).

---

## 9. Config editing write paths

One shared discipline for all three editors: **load → edit in RAM buffer
(carve-out) → Apply = stage → validate (where a validator exists) → hash-guard
→ atomic rename → watcher propagates to the other instance.** The hash guard
is the concurrent-edit discipline: Apply refuses if the on-disk content no
longer matches what was loaded into the buffer (another instance, or vi, wrote
meanwhile); the user reloads, re-diffs, re-applies. Rejected: blind LWW on
operator-authored config — silently discarding a concurrent edit.

### 9.1 brazen `config.toml`

- Path: brazen's exact fold, reproduced — `$BRAZEN_CONFIG` else
  `$XDG_CONFIG_HOME/brazen/config.toml` else `~/.config/brazen/config.toml`
  (pure XDG on all platforms, per brazen `env.rs`). This is the **ambient**
  file — the world leaves `BRAZEN_CONFIG` unoverridden (§16.2), so the file
  yog edits, validates, and watches is the same one the user's own `bz` reads.
  That is the intent: one `bz`, one config.
- Editor: **raw TOML text**, not form fields. Apply: write buffer to
  `.config.toml.yog-tmp-<pid>` in the same dir → run
  `bz --config <temp> --dump-config`; non-zero exit (MalformedFile/BadValue/
  IncompleteProvider… all exit 78) rejects with stderr shown, draft kept in
  RAM → hash-guard → rename into place. A malformed config can therefore
  never land, so `bz` (and every lernie loop calling it) never breaks.
- Alongside the editor: a read-only "effective config" pane =
  `bz --dump-config` stdout verbatim (the merged, redacted, authoritative
  view including env-layer effects yog could never compute from the file),
  plus the built-in-rows hint (§5.1 #21).
- **Structured view = `bz --dump-config`; no TOML dependency.** brazen's
  schema is versionless, forward-additive, and full of open valves (top-level
  passthrough, `body_defaults`) that a form would corrupt or reject; any
  yog-side parse is a second authority that drifts. bz *is* the parser, kept
  in lockstep with brazen by being brazen. This deliberately contradicts the
  brief's "toml parsing is probably justified" leaning — all three judges
  concurred it is not.

### 9.2 lernie global config (`models.yaml`, `workflows/*.yaml`)

Text editor per file; Apply = hash-guard + temp-in-dir + rename (lernie
declares these hand-edited; yog is the hand, minus torn writes). No validator
exists and yog adds no YAML dep — the operator's risk is identical to `vi`.
Documented gap; a future `lernie config --check` slots into the same pipe.
New workflow = same path, new name; templates copyable.

### 9.3 Per-workspace config branches (the scripted `$EDITOR`)

Browsing: `for-each-ref refs/heads/config/` + `git show <ref>:<path>`
(read-only, via the existing env-scrubbed `git_tree::cmd`), including each
agent's derived governing config (§5.1 #17, "policy frozen at `<short-oid>`").

Editing — `lernie config <ws> [name]` is the only lawful writer of `config/*`
and is $EDITOR-interactive, so yog drives it:

1. User edits the branch's files in RAM buffers; Apply writes the full
   drafted file set to `$XDG_STATE_HOME/yog/stage/<nonce>/`.
2. Spawn `lernie config <ws> <name>` (plus `--from <src>` / `--orphan` when
   forking) with `EDITOR="<yog-binary> --editor-apply"` and
   **`YOG_EDIT_SRC=<staging-dir>` in the environment** — the staging dir
   rides in env, the checkout path arrives as the shim's argv, because lernie
   composes the editor line through `sh -c` and its exact arg-passing shape
   is a flagged open question. The shim tolerates both `$EDITOR <dir>` and
   per-file invocation.
3. `yog --editor-apply` (a tiny non-GUI mode of the yog binary; its copy
   logic is a pure, fully-tested lib function) copies **only the drafted
   files** over the materialized checkout — **never a full-tree sync**:
   `lernie config` has just refreshed `descriptions/**` from the data-root
   pools at commit time and the shim must not clobber that. Exits 0; lernie
   commits and tears down. Empty diff is declined by lernie; surfaced as "no
   change".
4. Staging dir deleted on completion; leftovers swept (§5.2).

**Diligence task 0 for this feature: source-read lernie's exact `$EDITOR`
invocation shape** (how the checkout path is passed through `sh -c`) before
building the shim, and record the finding in the task.

This is the only path that advances a config branch, honoring "never write
inside a lernie workspace except via the lernie CLI" — yog writes only its own
staging dir; lernie performs the commit.

---

## 10. Portability (Linux + macOS/aarch64)

- **Already portable:** notify (inotify/FSEvents), eframe/glow, libc::kill,
  all git/CLI spawning, the XDG folds (balls/lernie/yog paths are pure-XDG on
  both platforms, matching those tools; brazen's *per-OS* credential/cache
  dirs are reproduced for the read-only displays).
- **The gap:** both probes scan `/proc/<pid>/fd` (Linux-only). The fix:
  - Probe traits return a **tri-state `Probe::{Held, Free, Unknown}`**
    (replacing bool).
  - Linux impls: existing procfs scans, behavior unchanged.
  - macOS impls: parse `lsof -F` output over the inbox dir / response.json
    (writer filter from the fd access-mode field). The **parser is a pure,
    platform-independent function compiled and tested everywhere**
    (recorder-fixture inputs, 100% covered on Linux CI); only the ~20-line
    spawn shim is `#[cfg(target_os = "macos")]`. lsof is slow, so macOS probe
    results carry a 2 s TTL cache (RAM, §5.3), refreshed eagerly on watcher
    events touching the agent, and re-probed only for Live/InFlight agents on
    the sweep (§7.2).
  - `lsof` missing/failing ⇒ `Unknown` ⇒ classification degrades to
    framing-only: closed-with-`end` = quiescent, closed-without = stopped,
    open-file undetectable ⇒ rendered with an explicit **uncertainty badge
    ("live?")**, never a false definite state.
  - **Rejected: flock-acquire probing** (`flock(LOCK_SH|LOCK_NB)` then
    release) — portable and dependency-free but **perturbs the substrate**:
    during yog's transient hold, a `lernie message` writer's probe sees the
    lock taken, concludes a driver exists, and strands the deposit until the
    next scan (writer/driver totality, ARCH §2.11). A probe must never affect
    the observed (I8). Also rejected: the libproc crate (a dependency for one
    probe) and hand-rolled FFI (unsafe, untestable on Linux).
- **Coverage mechanics:** brazen's per-OS creds/cache path folds take
  **`target_os` as a runtime-injected parameter**, so the macOS branch is
  exercised by Linux tarpaulin — no cfg-gated coverage hole. The same rule
  applies to any future per-OS branch.
- **Known upstream limit, documented, not worked around:** `lernie stop` is
  itself /proc-based (Linux-only). On macOS yog surfaces the Stop failure
  verbatim in `ops.jsonl`; fixing stop portability is lernie's ball.
- **CI:** Linux runs the full gate (fmt, clippy -D warnings, tarpaulin 100%
  pinned 0.35.2). macOS (aarch64) job: `cargo build` + `cargo test`, no
  tarpaulin (Linux sees every line because nothing but the lsof spawn shim is
  cfg'd out). Known macOS test issues — `/tmp` vs `/private/tmp`
  canonicalization in probe fixtures, FSEvents timing in fs_watcher tests —
  are already filed as **bl-592b**.

---

## 11. UI structure — three altitudes

Single window; the organizing frame is three information altitudes, each one
click apart. The organizing *unit* on screen is the **conversation** — a root
agent in the focused workspace (§1, STORIES) — and the workspace is a regime
wall (personal / work / client): totally separate blast radius, almost
invisible — nothing in the UI but a small tab bar under the top right.

**Altitude 0 — glance (always visible).**
`TopBottomPanel::top`, left: the wordmark and the **attention strip** — totals
per signal kind across all workspaces, jump-to-next-attention. Right: the
**workspace tab bar** — one tab per named workspace (pinned first, in pin
order, then name order), each badged with its attention count; a slim **+**
tab (the deliberate sphere-wall mint, §3.4 — minting is not the everyday
gesture; the composer is); and an overflow menu (⋯, shown only when needed)
holding foreign and replay workspaces — real but not regimes, so they never
widen the wall row; pinning hoists one into the tabs, and the menu button
carries their aggregate attention.

`SidePanel::left`: the focused workspace's **conversation list** — headed by a
**+ conversation** affordance that clears the agent selection and focuses the
composer, then one row per root agent: state badge (aggregated over the
conversation's subtree — InFlight > Live > the root's settled state, with the
§10 "?" uncertainty suffix), first-line preview, age, and the shared
streaming pulse while any member is in flight. Sort: **attention > running >
recency** (§6). Below the list: a minimal collapsible **balls** section — the
start affordances (▶ Start / ▶ Continue / Assign per §3.5, the new-ball
forms, the empty-project hint, the nested-delivery "internal" toggle) and the
focused workspace's bound-ball rows with join badges; the full per-project
ball views return in the ball-views wave — then the Config entry and the
toolchain pane. Answers "does anything need me?" and "what's running?"
without interaction.

**Altitude 1 — the selected conversation (center).**
The center renders the selected conversation, **transcript first**: a header
(conversation id, aggregated state badge, age, whole-tree budget-spent
figures), then — **only when the conversation has children** — the compact
descent tree (one selectable row per member, state badge + `✉n` pending;
selecting a member is the §6 acknowledgement gesture and retargets the
inspector), then the Altitude-2 inspector for the selected member, Transcript
tab by default with the live streaming tail appended and visually distinct
(§5.1 #10/#12). A conversation whose **latest step is an auth-shaped failure**
(the §8.3/Z8 detection over the derived step facts) banners in ichor red and
renders the **Login** affordance inline — the same streamed `bz --login`
machinery as the toolchain pane, one click away where the wound is. A
conversation whose latest step is the §7.3 **no-response wound** — its driver
died before the model said anything — banners in ichor red the same way,
naming where the cause lives (the driver's own stderr, in the activity
accessory below) rather than offering to re-run the prompt (§14). Both banners
sit above the inspector, so the cause is on the conversation surface whichever
tab is open — including the default Transcript, which for a step that produced
nothing has by construction nothing to show. yog's own plumbing never reads as
conversation content: the ops trail lives in the bottom activity accessory
(below), never as inline rows between conversation content.

**Altitude 2 — the inspector (per selected agent, tabbed; every tab has a Raw
toggle showing verbatim bytes).**
- **Transcript** — `messages/NNN-*` in filename order; `.md` deliveries with
  origin header; model `.json` as content blocks (text/thinking/tool_use);
  `NNN-tool.json` results; committed tool_use without tool_result = "tool in
  progress"; live tail from the open response.json appended, visually
  distinct.
- **Steps** — `steps/NNN` table: framing status (in-flight/complete/failed/
  stopped, plus the §7.3 no-response wound, which outranks the framing read and
  paints the ichor ✗ with its sentence beside the row), commit, started/ended,
  attempts (segment count), tokens. Drill-in:
  meta.json, request.json, response.json event list, staging.json, per-tool
  input/output — all rendered through **jsonview**, a small hand-rolled pure
  collapsible `serde_json::Value → row tree` widget (zero-dep, uniformly used,
  fully testable): every byte inspectable.
- **Inbox** — deposits with from/deposited_at/epitaph; explains `✉n`; Flush =
  `lernie scan`.
- **Files** — the agent worktree read-only, bounded previews: goal.md,
  soul.md, summary/, skills/, descriptions/, work products.
- **Config** — governing config files with "policy frozen at `<short-oid>`";
  links to the workspace config editor (§9.3).

**Bottom accessories (`TopBottomPanel::bottom`, stacked):**
- The **composer**, docked bottom whenever a non-replay workspace is focused:
  one text box (RAM until sent). The target follows the selection — a selected
  agent ⇒ **message** (the resume gesture); none ⇒ **new conversation** (a
  detached prompt into the focused workspace; §3.4's rungs, minting only when
  no workspace exists). **Enter fires the targeted verb** — the S0/S1 gesture,
  one box, one Enter — with the greyed identity preview above the box (§3.3).
  The dir (path-rung) field, Stop (+children checkbox), Scan, and the ball
  actions (Close/Release/Move, §8.2) ride beside it unchanged.
- The **activity accessory** — the demoted ops pane: one collapsed chip
  (`activity · N ops · M ⚠`, the **live**-failure count in ichor when M > 0 —
  §6's retirement rule, so a failure a later clean run of the same verb
  superseded is not counted) expanding on demand to the `ops.jsonl` tail; a row
  expands to the full entry — argv, cwd, exit, stderr — because a trail that
  hides *why* is not a trail (§7.3 failed-action row); a retired failure still
  renders its ⚠ row, weak instead of ichor. Default collapsed; per-instance
  viewport state (§13.0).
- Config mode (left-panel entry) swaps the center to the brazen /
  lernie-global / config-branch editors (§9).

**Keyboard navigation.** ↑/↓ step the focus through the flattened
conversation order (workspace path order across, §6 sort within), landing via
the seen-acknowledgement path (§6); the
digit keys 1–5 select an Altitude-2 inspector tab (RAM, §5.3). The bindings are
a pure key → intent table (`src/keymap`, tested); the `egui::Key` lift and
dispatch are thin shell glue, excluded like the rest of the tree.

Widget split discipline (unchanged religion): pure view-model modules (no
egui) + pure render functions (headless shape-walk-tested) + interaction glue
(`.clicked()` branches) confined to `src/shell/*`, coverage-excluded alongside
`main.rs`: shell-level clicks are unreachable in the headless harness, so the
split keeps everything a click *calls* covered. The exception, proven by Y13
(jsonview's tested collapse toggle): a self-contained widget whose interaction
is *intrinsic* may own a tested click, exercised under a simulated-pointer
render test — the exclusion is for the shell tree, not a claim that no click
can be driven headlessly.

**Visual identity — the congeries palette (`src/theme`).** Yog-Sothoth
manifests as *a congeries of iridescent globes*; the UI's identity is exactly
that — luminous sphere-hues against a violet-black void. `src/theme` is the
**single colour authority**: every hue the UI paints is a lore-named constant
there (hydra green = liveness/ok, spectral blue = in-flight/streaming, brazen
bronze = pending/warn, ichor red = error, ash = stopped, sigil magenta = the
uncertainty "?", gate violet = yog's own selection/wordmark hue), renderers
import the name and never restate an RGB triple. The module also derives the
whole-app `egui::Visuals` (installed once at eframe bring-up), owns the one
shared in-flight pulse (every pulsing indicator beats in step), maps each
driven integration to its hue (`lernie`→hydra, `bz`→brazen, `bl`→gate — used
by the toolchain rows and config-editor headings), and renders the wordmark
(three iridescent spheres + "yog") seated in the attention strip and the
empty-workspace placeholder.

---

## 12. Module map and line budgets

New dependencies: **none in phase 1.** Percent-decoding, XDG folds, jsonview,
lsof parsing are small pure functions; serde_json covers every machine contract
(`bl --json`, step files, model cache, `ui.json`, `ops.jsonl`). The existing
trait-injection pattern (LockProbe/WriterProbe) is **the template for every
new effect**: lsof runner, bl runner, bz runner, editor shim, clock. Phase 2
(§16.5) embeds `balls`/`brazen`/`lernie` as exact-pinned crates — the only new
dependencies, and the version mechanism. The world epic adds `src/world/*`
(§16.6); its line budgets live in those task specs.

tarpaulin excludes: `src/main.rs`, `src/shell/*`. Budgets include inline
tests; anything projected ≥250 is pre-split at design time, not at the cap.

| Module | Est. lines | Responsibility |
|---|---|---|
| `src/main.rs` (excl.) | 140 | entry, `--editor-apply` dispatch, eframe boot |
| `src/lib.rs` | 160 | module decls, Args (`--workspace` optional initial focus; `--repo` deleted), test_support |
| `src/shell/{mod,navigator,workspace,conv_ball,activity,config_edit,input_bar,inspector,start_pane}.rs` (excl.) | 230+290+215+60+60+290+230+160+140 | interaction glue only (navigator = top bar tabs + conversation panel with the grouping toggle; workspace = conversation center + ball header; conv_ball = the shared ball-badge painters, §3.5 hue; activity = the demoted ops accessory; inspector = tab-strip/controls + VM build; config_edit = config-mode editors; acceptance.rs is the excluded full-window smoke test) |
| `src/app/mod.rs` | 230 | AppModel: snapshots map, ui-state integration, tick |
| `src/app/dirty.rs` | 150 | Change→dirty-root mapping, debounce/sweep scheduling (clock-injected) |
| `src/xdg/mod.rs` | 150 | env folds: balls state root, lernie roots (`LERNIE_HOME` collapse), brazen paths (per-OS via runtime `target_os` param), yog roots; percent-decode |
| `src/projects/mod.rs` | 160 | clone enumeration, nested-delivery detection |
| `src/projects/balls.rs` | 240 | `bl list/show --json` parse, status ladder, groupings, join-state table |
| `src/binding/mod.rs` | 150 | names-root enumeration (§3.1), claimant join (§3.2), worktree formula, workspace classification |
| `src/names/mod.rs` (+ `words.txt` data) | 100 | two-word mint: pure over injected RNG + occupied set; wordlist embedded via `include_str!` |
| `src/ui_state/mod.rs` | 230 | ui.json schema, forgiving load, atomic save, echo-hash, adopt, seen API |
| `src/opslog/{mod,line,rows,live,detached}.rs` | 190+125+180+160+110 | mod = O_APPEND append + tail parse + the sentinels; line = the pure ≤4096 capper (truncation marker, argv clip); rows = OpRow/SurfaceFailure; live = §6's retirement projection (live-failure flags per row) + the §11 activity summary built on it (op + live-error counts, chip label); detached = the per-spawn stderr sink's computed name and its read-time fold into the `-2` row (§8.1, §13.3) |
| `src/attention/mod.rs` | 230 | predicates, seen-gating, rollups, sort ranks, next-attention |
| `src/nav/{mod,tabs,convs,convs/group}.rs` | 40+170+220+125 | §11 altitude-0 view-models: `ws_key`/BoundBall; the workspace tab bar (pin hoist, named tabs, foreign/replay overflow); the conversation list (per-root subtree aggregation, sort, age labels, conversation-root lookup, the derived goal-stamp ball overlay §3.5); `convs/group` = the grouped-by-ball partition (§3.5, stable, unassociated-last) |
| `src/watch/mod.rs` | 210 | WatchSet reconcile, bridge thread, repaint hook |
| `src/fs_watcher/roots.rs` | 150 | per-root-kind allowlists (existing mod.rs unchanged) |
| `src/cli_outbound/{mod,detach}.rs` | 265+80 | mod = parametric binary resolution, current_dir, the piped `run` family; detach = the fire-and-forget spawn and its stderr-sink opening (degrading to null) |
| `src/cli_outbound/tests/{run,stream,spawn}.rs` | ≤250 each | split of the current 274-line tests.rs (pre-graft hygiene) |
| `src/theme/{mod,tests}.rs` | 185+135 | the congeries palette — single colour/visuals authority (§11): lore-named hues, whole-app egui Visuals, shared in-flight pulse, integration-hue map, the §3.5 ball-status hue, wordmark |
| `src/transcript/{mod,render}.rs` | 240+180 | messages enumeration/order/parse; render |
| `src/steps_view/{mod,render,wound}.rs` | 230+195+60 | step inspector VM; render; the §7.3 no-response wound |
| `src/jsonview/mod.rs` | 190 | pure collapsible JSON row tree + render fn |
| `src/inboxview/{mod,render}.rs` | 230+150 | deposit parsing; render |
| `src/budgets/{mod,render}.rs` | 230+65 | Usage fold across subtree steps; spend render |
| `src/files_view/{mod,render,tests}.rs` | 190+180+185 | agent-worktree bounded walk + file preview VM (depth/entry/byte caps, absent-worktree state); read-only tree + selected-file preview render (§11 Files tab) |
| `src/inspector/{mod,tests}.rs` | 100+300 | tested per-agent tab-content dispatch (§11) over the landed render fns (incl. the Files walk/preview) + governing-config view |
| `src/git_tree/probe.rs` | 110 | tri-state traits (moved from fd/lock_probe headers) |
| `src/git_tree/lsof.rs` | 210 | lsof -F parser (pure, tested everywhere) + cfg(macos) shim |
| `src/git_tree/marks.rs` | +50 | add abandoned + notify namespaces |
| `src/actions/verbs.rs` | 180 | message/stop-children/scan/close/unclaim/create/update dispatchers + opslog wiring |
| `src/start/mod.rs` | 210 | plan (pure) + goal composition + step executor |
| `src/config_edit/brazen.rs` | 230 | path fold use, staged bz validation, hash-guard, rename |
| `src/config_edit/lernie_global.rs` | 160 | file enumeration, hash-guard atomic write |
| `src/config_edit/branch.rs` | 240 | config-ref browse, governing-config derivation, edit plan (EDITOR env + argv) |
| `src/config_edit/apply.rs` | 100 | `--editor-apply` copy logic (pure: only drafted files) |

Testing per the house pattern: real-git tempdir fixtures (extended with a
balls-clone-layout and yog-ball-root fixture builder), argv-recorder scripts
under the binary-wide SPAWN_LOCK, fake `/proc` and fake `lsof` output
injection, injected clocks for every debounce/sweep branch, headless
shape-walk for every render fn, forgiving-read cases for every file parser,
`--test-threads=1`, tarpaulin 0.35.2 pinned, 100%.

### 12.1 Code style is governed by Rust Bootstrap v3 (see AGENTS.md)

DESIGN is the *architecture* authority; **code style is governed by the "Rust
Bootstrap v3" standard, whose yog-adapted, flat-numbered rules live in
`AGENTS.md`** at the repo root, machine-enforced by `rules/*.yml` (pinned
ast-grep 0.44.1), the clippy manifest (`Cargo.toml [lints]`, pedantic=deny), and
`cargo-deny` (`deny.toml`). `make check` runs the whole gate; the pre-commit
hook and CI mirror it. Read AGENTS.md before writing code — this note only
records that the standard exists and which parts of it yog deliberately skips.

**Surfaced skips (deliberate, not oversights — reasons in AGENTS.md/rules):**

- **No workspace/crates split** — yog is a single published binary crate; the
  module tree (§12) plus the 300-line cap already contain complexity, and a
  split would fight the 100% coverage floor (Bootstrap rule 11 adapted).
- **No musl static target** — yog is a native GL desktop app needing dynamic
  platform libs; `rust-toolchain.toml` carries no `targets`. The TLS bans stay
  in `deny.toml` regardless.
- **`unsafe` confined, not `forbid`** — one irreducible SIGTERM syscall lives in
  `src/cli_outbound/sys.rs`, pinned there by `rules/unsafe-outside-sys.yml`;
  `forbid` is unoverridable and reaches tests (Bootstrap rule 3 adapted).
- **No `anyhow`** — `main.rs` is a thin entry with no error-plumbing layer;
  `thiserror` already carries the error enums (rule 10 adapted).
- **Async rules vacuous** — no async/tokio today; the async rule (8) is installed
  but matches nothing.
- **No pre-commit framework / nextest / bacon / sccache / mold** — one hook
  system (`.githooks`) carries the same checks; `cargo test` + pinned tarpaulin
  remain the runners.

---

## 13. Deliberate interpretations (user-vetoable)

These are deliberate readings of the requirement, flagged for veto rather than
silently assumed.

### 13.0 The rule is no *state* in RAM, not nothing in RAM

The user clarified the durability requirement: **"no STATE in RAM" — not
"nothing in RAM"; views are fine in RAM.** This is the primary rule the rest
of §13 applies. A *view* is which data you look at and how the window is
arranged: focus, selection, scroll position, which nodes or panel sections you
have folded, live window geometry. *State* (durable data) is a user assertion
with no other authoritative home: the seen watermarks, pins, the last-used
identity. State must replicate so two instances agree (§4.1); a view may live
in RAM and be lost on crash. §13.1's focus/scroll ruling was the first
statement of this rule and is now just its leading instance. The persisted-view
keys (`collapsed`, `show_internal` — §4.1) are the allowed converse: a view is
*permitted* to be kept durable for convenience, but is never *required* to
replicate the way state is.

### 13.1 Live focus/selection and scroll are per-instance viewport ephemera

The requirement: "effectively nothing is allowed to exist in only-RAM… two
yog instances side-by-side faithfully replicate the same data, excepting
unsubmitted user inputs." **This design interprets the requirement's motive as
data durability and replication: no *data* is RAM-only, and both instances
replicate the same *data*. Live focus/selection and scroll are *which data
you look at*, not data:** they lose nothing on crash (focus re-derives
deterministically at startup — next-attention, else first; scroll re-anchors),
and mirroring them makes side-by-side instances actively hostile — every
click in one yanks the other's view, which two of three judges found defeats
the point of running two instances. `ui.json` therefore keeps the
genuinely-converging *state* — the four seen watermarks, pins,
`identity_last_used` — plus the persisted *views* kept for convenience
(`collapsed`, `show_internal`; §13.0).
Everything the operator would call data — including acknowledgements, which a
zero-durable-state stance structurally cannot represent — is durable and
converges. **Veto path:** if the strict-literal reading is wanted (mirrored
focus/scroll), add `focus`/`scroll` keys back into `ui.json` with content-
anchor scroll representation; the write/adopt machinery already supports it.

### 13.3 Detached prompt defers error immediacy to disk

`lernie prompt` is spawned detached (§8.1), so a prompt-time failure surfaces
from disk rather than from a pipe yog holds. This is the
robustness-over-immediacy trade: yog's death must never kill a running loop,
and the short piped steps (`bl claim`, `lernie new`) still surface their errors
directly in `ops.jsonl`.

**Amended (bl-a649): deferred to disk, not discarded.** The original
"stdio→null" cost more than immediacy — a driver that *launched* and then died
(the brazen 0.0.2/0.0.3 version-skew refusal, 2026-07-22) left exit `-2` and an
empty `stderr`, byte-identical to a clean launch, so the operator saw a prompt
that "does nothing" and yog had nothing to render. **stdin and stdout stay
null; stderr goes to a per-spawn sink file** (§4.2 / §5.2:
`$XDG_STATE_HOME/yog/detached/<ts>-<workspace>.err`). The sink is a *file*, not
a pipe: yog holds no fd, so the §8.1 lifetime guarantee is untouched — the child
outlives yog and keeps writing to an inode nobody must be alive to drain. The
ops row's `stderr` is folded in from that file **at read time**, on the ops
sweep (§7.2), and a non-empty capture makes the row a rendered failure — the
existing §7.3 machinery, fed a fact it was previously denied. Only the
*immediacy* is still deferred: the death is visible on the next sweep, not at
fire.

**The conversation surface owes its own half (bl-7f2e).** The sink answers
*why* on the ops surface; it cannot answer *where*, because a driver that dies
**mid-life** has already written a step, and that step — `response.json` at zero
bytes, no `meta.json` — read as a quiet one (§4.4 framing has no vocabulary for
"produced nothing", so it says `Killed`, the same ash badge a mid-stream kill
gets). The **no-response wound** (§7.3, §11) is that vocabulary: derived at read
time from the two files plus the agent's §3.5 liveness, stored nowhere, rendered
in ichor beside the step and bannered at Altitude 1. The division is the one §6
already draws — the conversation surface says *this died*, the ops surface says
*what it said on the way out* — and the wound's own text points across it.

---

## 14. Rejections

Recorded so they are not relitigated:

- **`toml_edit` or any TOML/YAML dependency** — the brazen structured view is
  `bz --dump-config`; balls data is `bl list --json`; a yog-side parse is a
  second authority that drifts.
- **flock-acquire liveness probing** — perturbs the substrate: a transient
  yog hold makes a `lernie message` writer skip launching a driver and strand
  the deposit (I8).
- **Direct `tasks/*.md` parsing** — re-implements bl's frontmatter parser and
  status/admits ladder; `bl list --json` is the sanctioned bedrock contract.
- **Any yog-side ball↔workspace registry file** — drifts and needs its own
  merge discipline. The claimant field is not a registry: it is balls' own
  first-class metadata under balls' own merge discipline, which is why binding
  lives there (§3.2).
- **Path-convention binding**
  (`yog/balls/<mirrored-project-path>/<ball-id>/`, the original §3.1, with its
  verbatim-vs-percent-encoding debate) — *superseded by the claimant join
  (§3.2)*: a location is congenital and immutable where the requirement is
  late-mutable, and it hard-coded 1:1 where agents author and pick up several
  balls per workspace.
- **Zero-durable-state stance** — structurally cannot represent notification
  acknowledgement (no lernie ack verb exists; marks are level-triggered) and
  violates the governing requirement.
- **Ops-log-free design** — gate/close/scan output would be RAM-only and
  non-convergent; the current stderr-and-drop hole would persist.
- **The word "session" in code or UI** — lernie bans it for cause; the
  concept dissolves (start = claim+new+prompt, the unit is the workspace, the
  list is workspace enumeration).
- **Daemon/socket/IPC between instances** — disk is the bus (every
  substrate's religion).
- **Linking lernie/brazen crates *as a blanket rule*** — *superseded by
  §16.5.* The original stance (CLI + disk is the whole contract; brazen's
  library API is explicitly unstable) is now the *phase-1* posture only; the
  phase-2 end state embeds `balls`/`brazen`/`lernie` as **exact-pinned** crates.
  Instability is answered by exact-pinning (brazen's own README posture), and
  process semantics stay non-negotiable regardless of linking — drivers are
  processes holding flocks; plugin dispatch stays subprocess (§16.5).
- **A reproduction hatch at the wound** (a "re-run this prompt with stderr
  attached" button beside a §7.3 no-response step, running `yog exec lernie
  prompt …`) — *rejected on the evidence, not the ergonomics*. `yog exec` is how
  the bl-8e07 skew was diagnosed, but that was **before** the detached child's
  stderr sink (§8.1/§13.3): the driver's own words are now captured and rendered
  on the ops surface, so the button would ask the operator to re-create a fact
  yog already holds — and would fire a *second* driver at a conversation that
  already has one, with a goal yog would have to re-compose. The wound instead
  names where the cause lives, and the two surfaces stay disjoint the way §6
  keeps them: the conversation says *this died*, the ops trail says *why*.
  `yog exec` stays exactly what §8.4 makes it — a hatch for a human at a shell,
  not a verb yog fires at itself.
- **Async runtime** — the eframe loop + notify threads + repaint scheduling
  is the entire concurrency story.
- **yog aping lernie's seeding** — the nested `LERNIE_HOME` is seeded by
  lernie's own bootstrap verb (**`lernie prime`**, landed upstream as
  bl-6d83: `LERNIE_HOME=<dir> lernie prime`, seed-if-absent, idempotent,
  silent on success; `models.yaml` at the home root is the seeded marker),
  never by yog reproducing lernie's seed logic; a second seeder drifts from
  the first (§16.2).
- **Overriding `XDG_DATA_HOME` / `XDG_CACHE_HOME` in the world env** —
  `$XDG_DATA_HOME` is the world's *anchor* (the world lives under
  `$XDG_DATA_HOME/yog`; overriding it recurses), and leaving both ambient is
  *exactly* what shares brazen's credentials and model cache with the ambient
  world — creds are secrets not schema-fragile state, and the cache is
  regenerable and forgiving (§16.2).
- **yog shipping or installing tool binaries as the end state** — a yog-owned
  pinned bin dir with `cli_outbound` preferring it is phase-1 scaffolding,
  retired wholesale by phase 2 (§16.4). In the end state the version mechanism
  is the exact-pinned crate, and the only binaries are the user's ambient CLIs
  (orthogonal to yog) and the embedded-crate agent-tool shims (§16.4).

---

## 15. Implementation epic

Ordered, bl-sized tasks; each lands independently through the pre-commit gate
(fmt, clippy -D warnings, 300-line cap incl. inline tests, tarpaulin 100%
pinned 0.35.2) via the worktree/merge/no-ff flow. Cross-cutting rules baked
in: `cli_outbound/tests.rs` splits **before** cli_outbound grows (Y1);
`target_os` is a runtime parameter in per-OS path folds (Y2); all timing is
clock-injected (Y6); every new effect is trait-injected on the
LockProbe/WriterProbe template.

Already landed (do not re-file): **bl-a86c** (ETXTBSY SPAWN_LOCK fix),
**bl-fed1** (rebrand lernie-ui-egui → yog), **bl-d5f3** (delivery wiring, CI
linux + macos-arm64, crates.io publish wiring). Already filed and open:
**bl-592b** (macOS test portability: /tmp canonicalization + FSEvents timing —
the macOS CI job) — slots into M5 beside Y21.

### M1 — the existing single-workspace view goes live

**Y1 — Split cli_outbound/tests.rs into submodule files.**
Scope: `src/cli_outbound/tests.rs` is at 274/300 and every subsequent
cli_outbound extension adds tests there. Split it into
`src/cli_outbound/tests/{run,stream,spawn}.rs` (+ tiny `mod.rs`), preserving
every test verbatim and the SPAWN_LOCK/ENV_LOCK usage. Pure refactor, zero
behavior change, green gate.
Deps: none. Files: `src/cli_outbound/tests/*` (≤250 each).

**Y2 — xdg module: env-snapshot path folds with runtime-injected target_os.**
Scope: one module owning every path derivation from an injected env snapshot:
balls state root, lernie config/data roots including the `LERNIE_HOME`
collapse, brazen config path fold (`$BRAZEN_CONFIG` > XDG > `~/.config`),
brazen per-OS credentials/cache dirs with **`target_os` passed as a runtime
parameter** (so the macOS branch is covered by Linux tarpaulin), yog data/state
roots, and percent-decode (hand-rolled, ~25 lines). Table-driven tests for
every fold and both OS branches. No env reads anywhere else in the crate.
Deps: none. Files: `src/xdg/mod.rs` (~150).

**Y3 — cli_outbound generalization: parametric binaries, current_dir, detached spawn.**
Scope: binary resolution parametric over env var (`LERNIE_BINARY`,
`BL_BINARY`, `BZ_BINARY`, PATH-name defaults), `current_dir` support on
`run`, and a detached-spawn mode (setsid via libc, stdio→null, no retained
`Stream` — the caller gets only a spawn result). Recorder-script tests for
cwd/env propagation and a detach test proving the child survives the parent.
Deps: Y1. Files: `src/cli_outbound/mod.rs` (→ ~240), tests in the Y1 split.

**Y4 — Probe tri-state: {Held, Free, Unknown}.**
Scope: change `LockProbe`/`WriterProbe` to return a tri-state
`Probe::{Held, Free, Unknown}`; Linux procfs impls keep behavior identical
(never Unknown); `classify` maps Unknown to an explicit uncertain state that
renders as a "live?" badge instead of a false definite. Move the trait
declarations to `src/git_tree/probe.rs`.
Deps: none. Files: `src/git_tree/probe.rs` (~110), `state.rs`, `render.rs`
touch-ups.

**Y5 — fs_watcher root-kind allowlists.**
Scope: parameterize the hardcoded single allowlist into per-root-kind
allowlists (Workspace, BallRoot, WorkspacesRoot, BallsClones, BrazenConfig,
LernieConfig, YogState) per DESIGN §7.1, with the balls per-clone `log` file
explicitly filtered. Existing `fs_watcher/mod.rs` behavior unchanged for the
Workspace kind.
Deps: none. Files: `src/fs_watcher/roots.rs` (~150).

**Y6 — Watch registry + repaint bridge + clock-injected sweeps; wire the single-workspace view live.**
Scope: `WatchSet` owning one Watcher per root with reconcile (desired vs live
watchers); a bridge thread draining notify channels into a `Mutex<DirtySet>`
and calling `request_repaint()`; frame-side drain → re-derive dirty roots with
a 100 ms coalescing debounce; the 2 s cheap sweep (enumerations + WatchSet
reconcile + **targeted liveness re-probe of only Live/InFlight agents**) and
15 s full sweep — all timing through an injected `Clock` trait. Wire the
existing single-workspace view to re-render on disk change. **Milestone M1:
the current UI finally re-renders live.**
Deps: Y4, Y5. Files: `src/watch/mod.rs` (~210), `src/app/dirty.rs` (~150),
shell wiring.

### M2 — browse everything

**Y7 — binding module: yog ball root arithmetic + workspace enumeration.**
Scope: `workspace_path(yog_data_root, project, ball_id)` and its inverse
(walk `$XDG_DATA_HOME/yog/balls/**` for `repo.git` dirs; last segment = ball
id, rest = project path); enumeration/classification over the three roots
(bound / ad-hoc / replay); the bl-delivery work-worktree formula. Pure
functions over injected paths; fixture builder for the yog ball root.
Deps: Y2. Files: `src/binding/mod.rs` (~150).

**Y8 — ui_state document: ui.json load/save/echo/adopt + seen API.**
Scope: the §4.1 schema (seen watermarks ×4 kinds, pinned, collapsed,
show_internal, identity_last_used), forgiving load (missing/corrupt = default,
unknown keys preserved), debounced atomic save (temp-in-dir + rename),
echo-hash suppression, wholesale adopt on external change, and the
seen-watermark API (record evidence oids on focus). Startup focus derivation
(next-attention else first) as a pure function.
Deps: Y2. Files: `src/ui_state/mod.rs` (~230).

**Y9 — marks completion + inbox view + budgets fold.**
Scope: extend `git_tree/marks.rs` to all four `refs/lernie/*` namespaces
(add abandoned + notify, with oids exposed for watermark comparison); inbox
deposit parsing (`---from/deposited_at/epitaph/terminal_ref---` frontmatter)
as a view-model; budget-spent fold over Usage events across
`steps/<root>/` + `steps/<root>-*/` response.json files.
Deps: Y6. Files: `src/git_tree/marks.rs` (+50), `src/inboxview/mod.rs`
(~130), `src/budgets/mod.rs` (~170).

**Y10 — attention module: predicates, rollups, roster sort.**
Scope: `attention(agent)` per DESIGN §6 (unacked notify / unacked stop
without abandoned / unacked budget / unacked conflicted, each oid-gated on
ui.json seen; plus pending-mail-with-lock-Free, not seen-gated); workspace
rollups; strip totals; jump-to-next-attention; roster sort
attention > running > idle then derived order. Pure over injected snapshots.
Deps: Y8, Y9. Files: `src/attention/mod.rs` (~230).

**Y11 — multi-workspace AppModel + navigator + attention strip.**
Scope: AppModel holding the snapshot map (`HashMap<PathBuf, GitTree>` +
workspace classification), fed by the WatchSet; the roster panel (pinned →
projects → ad-hoc → replays, ball ids from path arithmetic only — no bl calls
yet), the attention strip with totals and jump; per-instance focus with
startup derivation; pins/collapsed durable via ui_state. `--repo` deleted;
optional `--workspace` initial-focus flag. **Milestone M2 shell: browse every
workspace, attention-sorted, two instances converging on seen/pins.**
Deps: Y6, Y7, Y8, Y10. Files: `src/app/mod.rs` (~230),
`src/shell/{navigator,workspace}.rs` (excl.), `src/lib.rs`.

**Y12 — transcript view-model + render.**
Scope: `messages/NNN-<origin>.*` enumeration and ordering (order lives in the
filename), origin classification (`.md` delivery / `NNN-<model>.json` content
blocks / `NNN-tool.json` tool_result), tool-in-progress derivation (committed
tool_use without tool_result), live streaming tail merged from the open
response.json, Raw toggle. Headless shape-walk tests.
Deps: Y6. Files: `src/transcript/{mod,render}.rs` (~240+180).

**Y13 — jsonview + steps inspector.**
Scope: `jsonview` — a pure hand-rolled collapsible `serde_json::Value → row
tree` with a thin render fn (the uniform "every byte inspectable" widget);
the steps inspector VM: per-step framing status, meta/request/response/
staging drill-in, per-tool input/output, attempts and token counts.
**Milestone M2 complete: browse everything.**
Deps: Y12. Files: `src/jsonview/mod.rs` (~190),
`src/steps_view/{mod,render,wound}.rs` (~230+195+60).

### M3 — ball-to-loop lifecycle

**Y14 — projects + balls view-models + join-state table.**
Scope: clone enumeration with nested-delivery detection (decoded-path prefix
match, "internal" toggle); `bl list --json` / `bl show <id> --json` invocation
(cwd = project) and parse; the derived status ladder; the §3.5 join-state
classification joining balls to enumerated workspaces (detached,
claimed-elsewhere, delivered, orphaned-project); ball detail (full bedrock
frontmatter + body); closed listing on demand. Roster gains ball rows and
join badges.
Deps: Y3, Y7, Y11. Files: `src/projects/mod.rs` (~160),
`src/projects/balls.rs` (~240).

**Y15 — ops.jsonl: durable action-outcome log.**
Scope: O_APPEND writer producing one JSON line per completed CLI action
`{ts, argv, cwd, exit, stdout, stderr}`, hard-capped at 4096 bytes
(PIPE_BUF) with stdout/stderr truncation and a `"truncated":true` marker;
tail parser (forgiving per line); fs-watched so both instances render the
shared history; ops pane VM.
Deps: Y2. Files: `src/opslog/mod.rs` (~150).

**Y16 — action verbs: message/stop/scan/close/unclaim/create/update + opslog wiring.**
Scope: dispatchers for `lernie message/stop/scan` and
`bl close/unclaim/create/update` (exact argv per DESIGN §8.2, cwd = project
for bl), each short-piped with its outcome appended to ops.jsonl; enablement
predicates extending the existing `actions` pattern (Stop iff Live/InFlight;
Close iff claimed ∧ bound; Message always — it is the resume gesture).
Replaces the stderr-and-drop `spawn_detached` for short verbs.
Deps: Y3, Y14, Y15. Files: `src/actions/verbs.rs` (~180),
`src/actions/mod.rs` growth, `src/shell/input_bar.rs` (excl.).

**Y17 — start flow: claim + new + editable goal composer + detached prompt.**
Scope: the composite verb as a pure planner (`start::plan`) + step executor:
optional `bl create`, `bl claim <id> --as <identity>` (prefilled from
identity_last_used, default `$USER`; stdout cross-checked against the
worktree formula), `mkdir -p` + `lernie new <bound-path>` (skipped if
existing), then the **editable goal composer** prefilled with ball title +
body + absolute work-worktree preamble — operator edits before
`lernie prompt` fires **detached** (setsid, stdio→null, stdout deliberately
unread — the new root materializes in the watched repo). Per-step outcomes in
ops.jsonl; every step idempotent-or-convergent. **Milestone M3: ball to
running loop from the UI.**
Deps: Y14, Y16. Files: `src/start/mod.rs` (~210), shell wiring.

### M4 — config editing

**Y18 — brazen config editor: raw text + bz validation + hash guard.**
Scope: locate via the Y2 fold; raw-TOML text editor; Apply = stage to
`.config.toml.yog-tmp-<pid>` in the destination dir →
`bz --config <temp> --dump-config` gate (non-zero exit blocks with stderr,
draft kept) → content-hash guard against the loaded snapshot → atomic rename.
Read-only effective pane (`bz --dump-config` verbatim) + built-in-rows hint +
credential-presence booleans + model-cache display with
`bz --list-models --provider <row> --json` refresh.
Deps: Y2, Y3, Y15. Files: `src/config_edit/brazen.rs` (~230),
`src/shell/config.rs` (excl.).

**Y19 — lernie global config editor.**
Scope: enumerate and edit `<config-root>/models.yaml` and
`workflows/*.yaml` as raw text with the same hash-guard + temp-in-dir +
rename pipe (no validator exists; no YAML dep); new-workflow = new file name;
copy-from-existing affordance.
Deps: Y18 (shared editor widget). Files:
`src/config_edit/lernie_global.rs` (~160).

**Y20 — config-branch browse + governing-config derivation.**
Scope: read-only config surface: branch list via
`for-each-ref refs/heads/config/`, file trees and contents via
`git show <ref>:<path>` through the env-scrubbed cmd wrapper; per-agent
governing config = nearest ancestor of the agent tip reachable from any
`config/*` ref (merge-base fold), rendered as "policy frozen at
`<short-oid>`" in the inspector Config tab.
Deps: Y13. Files: `src/config_edit/branch.rs` (browse half, ~140 of 240).

**Y21 — config-branch edit via the editor shim.**
Scope: **task 0: source-read lernie's exact `$EDITOR` invocation shape**
(how the checkout path passes through `sh -c`) and record the finding in this
task before building. Then: staging dir `$XDG_STATE_HOME/yog/stage/<nonce>/`;
drive `lernie config <ws> <name>` (with `--from`/`--orphan` options) with
`EDITOR="<yog-binary> --editor-apply"` and `YOG_EDIT_SRC=<staging>` in env;
the shim reads the checkout from argv and copies **only the drafted files**
(never a full-tree sync — lernie refreshes `descriptions/**` at commit and
must not be clobbered); pure, fully-covered copy fn; sweep of stale staging
dirs. **Milestone M4: all three config surfaces editable.**
Deps: Y3, Y20. Files: `src/config_edit/branch.rs` (edit half),
`src/config_edit/apply.rs` (~100), `src/main.rs` dispatch.

### M5 — portability + polish

**Y22 — macOS lsof probe: pure parser + cfg shim + TTL cache.**
Scope: `lsof -F` output parser as a pure, platform-independent function
implementing both probe traits' evidence extraction (writer filter from the
fd access-mode field), 100% covered on Linux via recorder-fixture inputs and
a fake `lsof` on PATH; the ~20-line `#[cfg(target_os = "macos")]` spawn shim
selects it at construction; `Unknown` on lsof absence/failure (renders the
Y4 uncertainty badge); 2 s TTL cache refreshed eagerly on agent-touching
events and swept only for Live/InFlight agents. Note: macOS *test* failures
(path canonicalization, FSEvents timing) are bl-592b, a sibling task.
Deps: Y4. Files: `src/git_tree/lsof.rs` (~210).

**Y23 — polish wave: replays, keyboard nav, docs.**
Scope: replay workspaces section verified end-to-end (`replays/*` render
read-only); nested-clone "internal" toggle polish; keyboard navigation
(roster ↑/↓, digit tabs); README architecture section rewritten to point at
docs/DESIGN.md; `make run` without REPO.
Deps: Y17, Y21. Files: shell + docs touches.

**Y24 — (optional, only if measured) incremental streaming refresh.**
Scope: replace whole-root rebuild with a streaming-text-only re-read on
`steps/**` events for the focused workspace, if profiling shows the 100 ms
debounced rebuild costing frames. Correctness is already owned by Y6; this
is purely an optimization and lands only with a measurement in the task.
Deps: Y6. Files: `src/app/dirty.rs`, `src/git_tree/streaming.rs`.

### M6 — conversation-first rework (the §3 claimant-join amendment)

Y7/Y11/Y14/Y17 landed against the superseded path-convention §3.1; this wave
reworks them in place. Same gate, same worktree/merge flow. **The acceptance
ladder for this wave is `docs/STORIES.md`** (paved-path stories S0–S9, each
with its integration tests); a rung is done when its story tests pass against
the fake substrate and the flow works against the real one.

**Z1 — names module: embedded wordlist + mint.**
Scope: `src/names/` — an embedded wordlist (`words.txt` data file via
`include_str!`; source and license recorded in the task), the two-word
hyphenated mint as a pure function over an injected RNG and an occupied-name
set, and the occupied-set assembly (readdir of the names root + claimants
from `bl list --json` across enumerated projects). Table-driven tests incl.
collision retry and pool-exhaustion error.
Deps: none. Files: `src/names/mod.rs` (~100), `src/names/words.txt` (data).

**Z2 — binding rework: names root + claimant join.**
Scope: replace `src/binding`'s path arithmetic with §3.1 enumeration (flat
readdir of `$XDG_DATA_HOME/yog/workspaces/`, leaf = name; foreign + replay
classification unchanged) and the §3.2 claimant join; rework the §3.5
join-state table and the roster grouping (balls group under their bound
workspace); migrate the watch root (BallRoot → NamesRoot, flat, §7.1).
Delete the mirrored-path code.
Deps: Z1. Files: `src/binding/mod.rs`, `src/projects/balls.rs`,
`src/watch/mod.rs`, `src/app/mod.rs` touches.

**Z3 — start flow: generalize `start::plan` to §3.4's axes.**
Scope: target-workspace resolution (the focused workspace; zero workspaces →
mint + `lernie new <root>/<name>` — the bootstrap as the empty case; the
explicit + New workspace verb runs the same pair deliberately);
`YOG_NAME=<name>` layered on workspace-scoped spawns (§8); the ball rung
inserting create/claim `--as <name>` with the resume-on-already-claimed
convergence (§8.1); per-rung composer prefills (harness-stamped identity
preamble with pre-mint preview, §3.3 as amended — phase-1 interim stamp
instruction included; target preamble for path; ball + worktree for ball);
per-rung driver cwd (`~` / the path / the worktree). **Two standing defects
die here:** `prepare()` re-implements the sequence while `start::plan` sits
dead — §8.1 mandates the planner as the one source and the executor runs its
output; and the shipped step order ran `bl create`/`claim` *before* the seed
(the §8.1 amendment's load-bearing order is seed → `new` → bl mutations —
the orphaned-claim wound).
Deps: Z2. Files: `src/start/mod.rs`, `src/start/exec.rs`, shell start-pane
wiring.

**Z4 — assign / move / release verbs + + New affordances.**
Scope: the §8.2 additions (assign = `bl claim <id> --as <name>`, move =
unclaim + claim, release = unclaim) with enablement predicates from the join
state; the roster's + New workspace verb (the explicit mint, §11) and the
composer's payload affordances (bare / path / ball, §3.4).
Deps: Z2, Z3. Files: `src/actions/verbs.rs`, shell roster wiring.

**Z5 — failed actions are rendered facts.**
Scope: the §7.3 failed-action row over the amended §4.2 (*attempted*, not
completed: synthetic lines for spawn failures — deleting `verbs.rs`'s
missing-binary un-logged carve-out — and `["yog-step",<name>]` lines for
non-spawn step failures). Ops-pane rows expand to the full entry (argv, cwd,
exit, stderr; today `OpRow` drops everything but ts/argv/exit); start and
short-verb failures render at their originating surface in ichor red with
argv + stderr tail (the surface holds its last failure as the §5.3
whitelisted RAM item; the durable fact is the ops line); every `eprintln!`
in `src/shell/` is deleted (STORIES INV-2's mechanized form: the grep count
is zero). The proven wound: a failing seed step printed to stderr and the
composer silently never opened.
Deps: none (parallel to Z2). Files: `src/opslog/mod.rs`,
`src/actions/verbs.rs`, `src/shell/*` touches, view-model modules for the
failure rows. Tracked: **bl-7687**.

**Z6 — capability gate (W5 as amended).**
Scope: §16.6 W5's capability probe — the normative driven-verb list probed
per verb via `--help`, verdicts naming the missing verb + remediation
command, `pub` verdict types, and gate consultation **in the dispatch
layer**: a `start::prepare` precondition and an `actions::verbs` check, so
every mutating path refuses identically (today only `input_bar` checks
`permits()`, and shell-glue checks are untestable — tarpaulin excludes
`src/shell/`). Pre-split: `src/world/toolgate/probe.rs`.
Deps: Z3 (the precondition lands in the reworked `prepare()`; both tasks
edit `src/start/run.rs`). Files: `src/world/toolgate.rs`,
`src/world/toolgate/probe.rs`, `src/start/run.rs`, `src/actions/verbs.rs`,
`src/shell/toolchain.rs` touch. Tracked: **bl-e324**.

**Z7 — story-test fixture (the fake substrate).**
Scope: a `tests/` recorder fixture — fake `lernie`/`bl`/`bz` script binaries
recording argv+env+cwd with canned stdout/exit per verb, injected as
`Cli::new(path)` / `Deps{…}` at the dispatch API (the actual
`editor_roundtrip.rs` idiom; the `*_BINARY` env vars stay production wiring,
covered by the existing `resolve_with` unit tests — no process-global env
mutation under the parallel test runner) — and the story tests green-able
against the **current** dispatch API: S1-T2, S1-T3, INV-3, and S0-T2's
seed-skip half via `seed::ensure_seeded`. Every other story test lands
red-first *inside the worktree of the task that turns it green* (written
first, red in-worktree, green at close — the precommit gate forbids landing
red on main); **the test→enabling-task map lives in STORIES.md alone**
(single source — do not restate it here).
Deps: none. Files: `tests/support/*`, `tests/stories_*.rs`. Tracked:
**bl-412d**.

**Z8 — the login flow (§8.3 as amended).**
Scope: the streamed-piped `bz --login --provider <row>` runner (provider
rows from the §5.1 #20/#21 config derivations), its live-line view-model
(covered; shell paints it), the exit-non-zero fallback (show the exact
command), and the **detection affordance**: an auth-failed step in the
already-derived steps/response.json facts (§5.1 #10/#13, per §13.3 a
prompt-time failure surfaces as derived agent state) renders Login one
click away, beside the failed step and in the toolchain pane.
Deps: Z5 (failure rows), Z6 (pane verdicts). Files: `src/cli_outbound/`
streamed-spawn addition, `src/world/toolgate.rs` or a small
`src/login/mod.rs`, view-model module. Tracked: **bl-02bf**.

**Z9 — top-level rework: workspace tabs + conversation-first center.**
Scope: §11 as rewritten — the workspace tab bar under the top right (named
tabs + + mint + foreign/replay overflow), the conversation list replacing the
workspace roster (one row per root agent; attention > running > recency), the
conversation-centred center (transcript first, descent tree only with
children, inline Login on an auth-failed latest step), the ops pane demoted to
the collapsed activity accessory, and the composer targeting selection
(message) or new conversation (prompt) on Enter. New VMs: `nav::tabs`,
`nav::convs`, the `opslog` activity summary, `login::latest_step_auth_failed`.
S0/S1 gestures unchanged. Deps: Z1–Z8 (landed). Tracked: **bl-abd9**.

---

## 16. The yog world

yog owns a **world** — a nested substrate environment it composes under its own
data root and hands to every child it spawns. This section is the world's
normative home; §0, §1, §2 (I1/I2/I7), §3, §5.1, §8, §12, and §14 are amended
to point here.

### 16.1 Application, not a layer

The naïve framing has yog as a thin viewer over the user's ambient
`bl`/`lernie`/`bz` state. It inverts: yog composes its own nested world
(§16.2), and the substrate state yog drives is *yog's*, under yog's data root.
Playing on top of the user's *direct* tool usage stays possible — the balls
store branch is shared by default (§16.3) and redirection knobs tune the
overlap — so **compatibility with an ambient workflow is the user's decision,
not a structural given.** The world encapsulates lernie, balls, and brazen
state so completely that yog and the human's own shell never collide unless the
user chooses overlap.

**Rejected:** yog as a pure ambient overlay (no nested state) — the coordination
point would be the user's live working tree and clones, so every yog action
would perturb the user's own `bl`/`lernie` work; encapsulation is what makes yog
safe to run beside a working human.

### 16.2 The composed world environment

yog reads the ambient environment once (`xdg::Env::from_env`), computes its
data-root anchor `$XDG_DATA_HOME/yog`, and composes **one** world `Env` — the
ambient snapshot plus a fixed override set — used both to derive every substrate
path yog reads *and* to spawn every child. Overridden, to nest:

| Var | World value | Nests |
|---|---|---|
| `LERNIE_HOME` | `<yog-data-root>/world/lernie` | lernie config **and** data (the `Env::lernie_home` collapse) |
| `XDG_STATE_HOME` | `<yog-data-root>/world/state` | balls clones/worktrees/op-logs **and** yog's own `ui.json`/`ops.jsonl` |

Left ambient, deliberately:

| Var | Consequence |
|---|---|
| `XDG_DATA_HOME` | the **anchor** — the world lives under `$XDG_DATA_HOME/yog`; overriding it would recurse. Brazen credentials (`$XDG_DATA_HOME/brazen/credentials`) stay **shared** with the ambient world |
| `XDG_CACHE_HOME` | brazen's model cache (`$XDG_CACHE_HOME/brazen/models`) stays **shared** |
| `BRAZEN_CONFIG` | brazen's config (`$BRAZEN_CONFIG` else `$XDG_CONFIG_HOME/brazen/config.toml`) stays **shared** — in phase 1 yog spawns the one host `bz` binary, so there is no version skew for a nested config to protect against, and the provider rows are credential-adjacent (oauth endpoints/client ids for the credentials already deliberately shared). *Amended after live proof (2026-07-21):* the nested path named a file nothing ever creates, so `bz` fell back to built-in defaults and every in-app conversation died at auth while the same prompt against the ambient config went green. **Phase-2 revisit:** re-nest only if the embedded-crate phase (§16.5) reintroduces config-schema skew |

The sharing is the decision, not an accident of which var each fold reads:
**credentials are secrets, not schema-fragile state; the model cache is
regenerable and forgiving** (brazen's own read stance); **and the config is
read by the one shared host `bz`, so nesting it would only orphan the
credentials it points at** — all three are reused from the ambient world;
everything version-fragile (lernie's home, balls' store layout) is nested.
Because `$XDG_DATA_HOME` is *not* overridden, re-deriving the anchor through
the world `Env` yields the same path — the composition is self-consistent, one
lens, no bootstrap special case.

**I1/I2 hold against the nested disk:** every §5.1 derivation resolves through
the world `Env`, so "disk is the app" means yog's world; two instances compose
the identical world from the identical ambient env and converge exactly as
before. **Severability widens (§3.1):** one `rm -rf $XDG_DATA_HOME/yog` erases
the entire world — nested lernie home, nested balls state, and yog's own
artifacts — and leaves the *ambient* substrates untouched.

The `LERNIE_HOME` seed is written by lernie's own bootstrap verb (upstream
lernie **bl-6d83**, in flight), never by yog — yog composes the env and calls
the verb; it never apes lernie's seeding (§14). **Diligence (task 0, W1/W3):**
confirm bl-delivery derives its worktree territory from *its own*
`$XDG_STATE_HOME`, so a nested child `bl` lands clones and worktrees in the
nested state root; record the finding in the task.

### 16.3 The balls store branch: shared by default, with a no-marks knob

The nested balls clone tracks the project's **shared `balls/tasks` store branch
by default**. The store branch is the stable contract, and sharing it is the
coordination point with the user's own `bl`: a ball yog claims or closes is
visible to the ambient `bl list`, and vice versa. Single source of truth — the
branch schema — with two clones (nested and ambient) reading it.

A per-project **no-marks knob** serves operators who want yog without leaving
marks in the shared store: **stealth** (`bl conf task-remote none` — a
local-only store) and/or a **custom task-branch**, both wired through **bl's own
config surface** and exposed in yog's UI as a project setting that *drives
`bl conf`* — never a yog config file (severability: the policy lives in balls'
capability, not yog's core). The trade is rendered at the knob: **stealth makes
yog's claims invisible to the ambient `bl list`** — the user trades coordination
for invisibility.

### 16.4 Binaries are agent tools, not yog's payload

**yog ships and installs no tool binaries in the end state.** Binaries matter
only as the plane on which lernie's worker agents drive tools by bash — an
essential surface — and there they split cleanly:

- **Ambient-world work** uses the user's own installed CLIs. It is orthogonal
  to yog — not yog's concern, not yog's to pin.
- **Embedded-world work** — an agent operating on yog's nested state, mostly
  balls (e.g. closing the ball yog claimed in the nested state root) — needs a
  tool that (a) resolves the **nested** clone/worktree paths and (b) shares
  yog's **exact** balls implementation and state view. An ambient `bl` fails
  (a): it reads a *different* `$XDG_STATE_HOME` and computes the wrong
  clone/worktree paths. No host binary guarantees (b): none is built from yog's
  pinned crate.

The correctness argument, not mere convenience, drives the mechanism. In
**phase 1**, (a) is met by env inheritance — yog spawns the driver in the world
env (§8), so a host `bl` an agent runs inherits the nested `$XDG_STATE_HOME` and
computes the right paths — while (b) is only softly met and is what the phase-1
**capability gate** guards (W5 as amended: per-driven-verb `--help` probes —
presence gating shipped first and was proven blind): surface the toolchain
state as a read-only UI pane, and **mutating verbs refuse on a detected
mismatch while rendering continues** (the read path is never gated). In **phase 2** both are
met structurally: yog seeds the nested world's `<yog-data-root>/world/tools/`
with **`lernie-tool-bl`** — a re-exec shim of the yog binary (the
`--editor-apply` multi-call pattern) speaking bl's argv contract and dispatching
to the **embedded balls crate against the nested roots**, discovered by lernie's
external-tool convention, and stamping `--as $YOG_NAME` onto any bl verb the
caller left unstamped (§3.3). The tool *is* yog: no version and no path is left to
drift, and the phase-1 version gate is deleted (the exact-pinned crate is the
version, §16.5).

The human counterpart to these agent tools is `yog env` / `yog exec` (§8.4).

### 16.5 Crate adoption, phased by upstream readiness

The end-state direction embeds all three substrates as **exact-pinned** crates —
the pin *is* the version mechanism (brazen's own pin-exact README posture,
generalized) — retiring the phase-1 version gate. Phasing is gated per upstream:

- **balls** — a `[lib]` target ships (balls **0.5.7**); adopt for typed store
  reads where the lib serves them (W8), then expose it as the `lernie-tool-bl`
  agent tool (W9).
- **brazen** — a lib exists (pin-exact per its README); adopt for canonical
  Event types and config validation (W10). Where brazen declares its API
  unstable, the `bz --dump-config` subprocess (§9.1) stays the authority.
- **lernie** — gated on upstream lernie **bl-231c** (library port +
  driver/successor exec parametrization). If linked, yog re-execs **itself** as
  the driver binary — exactly bl-231c's exec parametrization (W11).

**Process semantics are non-negotiable regardless of linking:** drivers are
processes holding flocks, and plugin dispatch stays subprocess. Linking changes
what code yog *calls*, never the concurrency model — a linked lernie still runs
drivers as flock-holding processes, and yog re-execing itself as the driver is
exactly that process, not an in-process task.

### 16.6 Phase 1 — binaries (implement now)

Ordered like §15; each lands through the same gate (fmt, clippy -D warnings,
300-line cap incl. inline tests, tarpaulin 100% pinned 0.35.2) via the
worktree/merge/no-ff flow. yog shells to host binaries throughout phase 1
(lernie is not lib-ready); the version gate and any install convenience live
and die with this phase.

**W1 — world-env module: compose the nested `Env` and the world layout.**
Scope: a new module that, from the ambient `xdg::Env`, computes yog's data-root
anchor and derives the world subtree
(`<yog-data-root>/world/{lernie,state,tools}`), then composes
the world `Env` = ambient + `{LERNIE_HOME, XDG_STATE_HOME}`
overrides (`XDG_DATA_HOME`/`XDG_CACHE_HOME`/`BRAZEN_CONFIG` left ambient — the
anchor and the brazen config/creds/cache share, §16.2). Pure over an injected ambient `Env`; every
override and both the nested and shared derivations table-tested; yog re-derives
all substrate roots and its own two artifacts through the world `Env`. Task 0:
confirm bl-delivery derives worktree territory from its own `$XDG_STATE_HOME`
and record it (§16.2).
Deps: none (extends `xdg`). Files: `src/world/mod.rs` (~150).

**W2 — world-env injection at every spawn.**
Scope: the composed world overrides (§16.2 — `world::overrides`, the single
source shared with `compose`, so the dir yog watches and the dir a spawned `bl`
writes are one fact) stand on every child. **The seam is the `Cli` itself, at
construction** (`Cli::resolve_in_world`): a world `Cli` carries the overrides and
layers them under any per-call `env` on every
`run`/`run_in`/`run_env`/`spawn_detached`, so nesting is impossible to forget at
a new call site (a new call reuses an existing world `Cli`) and `cli_outbound`
stays generic — opaque pairs, no world knowledge. `main.rs` composes the world
once and derives **both** sides through it: every read (`Roots`, `ShellState`)
via the world `Env` and every spawn (the `bl`/`lernie`/`bz` runners — incl.
`BlRunner`, `BlConf`, `RealBzRunner`, and the detached `lernie prompt`) via the
standing overrides, so an agent's own tool processes inherit the nested
`$XDG_STATE_HOME` and reads/spawns land on the same paths (§16.4 phase-1
correctness; agreement regression-tested). W3's explicit `LERNIE_HOME` collapses
into the standing env. No scrub — overrides layer over the inherited
environment. (`cli_outbound/mod.rs` split its `Stream` half to `stream.rs` to
stay under the 300-line cap.)
Deps: W1. Files: `src/cli_outbound/{mod,stream}.rs`, `src/world/mod.rs`
(`overrides`), `src/main.rs`, the config-pane resolvers.

**W3 — lernie home seeding via the upstream bootstrap verb.**
Scope: on the first Start against an unseeded world, yog invokes lernie's own
bootstrap verb to populate `LERNIE_HOME`; yog never reproduces lernie's seed
logic (§14). Skipped when the world is already seeded — the general path with
the seed present, not a bootstrap special case (§3.4). **The contract landed
(upstream bl-6d83, 2026-07-18): the verb is `lernie prime`** —
`LERNIE_HOME=<dir> lernie prime`, seed-if-absent, idempotent, silent on
success, `models.yaml` at the home root as the marker — exactly what
`src/world/seed.rs` drives and `seeded()` probes. *(History: yog shipped
against this contract before it landed; a stale installed lernie then failed
every start at the seed step — the incident behind the W5 capability
amendment.)*
Deps: W1. Files: `src/world/seed.rs` (~90), start-flow wiring.

**W4 — the no-marks knob (shared store default + stealth / custom branch).**
Scope: the nested balls clone tracks the project's shared `balls/tasks` store
branch by default (the coordination point with ambient `bl`); a per-project knob
repoints it through bl's own config surface — stealth (`bl conf task-remote
none`) and/or a custom task-branch — surfaced in yog's UI as a project setting
that *drives `bl conf`*, never a yog config file (§16.3). Renders the stealth
trade (claims invisible to ambient `bl list`).
Deps: W1. Files: `src/world/marks.rs` (~120), a config-pane shell touch.

**W5 — phase-1 capability gate + toolchain pane.** *(Amended: presence
gating shipped and was proven blind — a stale lernie predating `prime` passed
the gate, then every Start died at `lernie prime` exit 2 with the composer
never opening. A binary's existence says nothing about its verbs.)*
Scope: at startup (before first render; the gate is a read-only probe, the
class I1 already admits — see STORIES INV-1) and on toolchain-pane refresh,
probe each host tool by **capability** against the normative driven-verb
list, which is exactly this: lernie `prime new prompt message stop scan
config`, bl `create claim unclaim close update list show conf`, bz
`--version`. For
`bl`/`lernie` (no `--version`) each verb must answer `<tool> <verb> --help`
with exit 0 — one short spawn per verb. The read-only premise is per-tool:
lernie is clap, where `--help` is contractual; bl's convention (`<verb>
--help` ⇒ exit 0, unknown verb ⇒ exit 2) is pinned empirically on the tested
tuple — and a future bl whose probe mutates or exits non-zero classifies
Mismatch, which is the gate *working*, not a false negative. Any missing verb
classifies the tool Mismatch **with the verb named in the verdict and the
remediation beside it** (the §8.3 show-the-exact-command pattern: the
phase-1 install/upgrade command per tool; phase 2 dissolves the install
story entirely, §16.4); **mutating dispatches refuse on Mismatch while
read derivation continues — the gate is consulted inside the dispatch layer
(`start::prepare` precondition, `actions::verbs`), never only in shell
glue** (a gate only some verbs honor is not a gate). Verdict types are `pub`
so `tests/` asserts refusals directly. Effect- and clock-injected per the
house pattern. Pre-split for the verb-probe growth: `src/world/toolgate.rs`
(classification) + `src/world/toolgate/probe.rs` (per-verb capability
probe), each under the cap. **Explicitly phase-1-scoped:** phase 2's
exact-pinned crates make the version definitional and delete this gate
(§16.4).
Deps: W1. Files: `src/world/toolgate.rs` + `src/world/toolgate/probe.rs`
(the pre-split above; each under the cap), a shell pane (excl.).

**W6 — `yog env` / `yog exec` world escape hatches.**
Scope: two multi-call subcommands of the yog binary (beside `--editor-apply`):
`yog env` prints the world's `export` lines; `yog exec <cmd…>` runs a command
inside the world (world env layered, optional cwd), §8.4. Pure argv → plan; the
exec spawn reuses `cli_outbound`.
Deps: W1, W2. Files: `src/world/hatch.rs` (~100), `src/main.rs` dispatch.

**W7 — (optional, doomed) `make install-tools` convenience.**
Scope: a Makefile target pinning the tested tool tuple into
`<yog-data-root>/world/tools/bin` via `cargo install --locked --root`, with
`cli_outbound` optionally preferring that dir when present — a convenience so
the W5 gate passes on a fresh machine. **Retired wholesale by phase 2** (§16.4):
the target, the bin dir, and the preference all delete when the embedded crates
land. File only if the manual-install friction is felt.
Deps: W1. Files: `Makefile` target, small `src/cli_outbound` resolution touch.

### 16.7 Phase 2 — crates (gated placeholders, per upstream readiness)

Each names its upstream dependency and lands only when that dependency is ready;
together they retire W5 and W7 and establish the crate end state (§16.5).

**W8 — embed `balls` as an exact-pinned crate (typed store reads).**
Gated on: balls **0.5.7** `[lib]` (available). Scope: adopt the balls lib for
typed store reads where the lib serves them, exact-pinned in `Cargo.lock` (the
pin is the version mechanism; retires W5 for balls); replaces `bl list/show
--json` parsing where the lib exposes the same projection.
Deps: W1; balls 0.5.7 lib.

**W9 — expose the embedded balls crate as an agent tool (`lernie-tool-bl`).**
Gated on: W8. Scope: seed `<yog-data-root>/world/tools/lernie-tool-bl` — a
re-exec shim of the yog binary (the `--editor-apply` multi-call pattern)
speaking bl's argv contract and dispatching to the **embedded** balls crate
against the **nested** roots, so an agent operating on yog's world gets a `bl`
that shares yog's implementation and state view (the correctness argument,
§16.4); lernie's external-tool convention discovers it. The shim also
**injects `--as $YOG_NAME`** (the per-workspace spawn env, §3.3/§8) whenever
the caller omits `--as` — identity is the harness's job, not the model's;
landing this deletes the phase-1 preamble instruction (§3.3).
Deps: W8; lernie external-tool convention.

**W10 — embed `brazen` as an exact-pinned crate (Event types + config validation).**
Gated on: brazen lib (exists; pin-exact per its README). Scope: adopt brazen's
canonical Event types and config validation from the linked crate, exact-pinned;
retire the `bz`-subprocess validation where the lib serves it — the subprocess
stays wherever brazen declares its API unstable (§9.1).
Deps: W1; brazen lib.

**W11 — embed `lernie` as an exact-pinned crate + driver re-exec.**
Gated on: upstream lernie **bl-231c** (library port + driver/successor exec
parametrization). Scope: link lernie as an exact-pinned crate; if linked, yog
re-execs **itself** as the driver binary (bl-231c's exec parametrization).
**Process semantics stay non-negotiable regardless of linking:** drivers are
processes holding flocks; plugin dispatch stays subprocess (§16.5).
Deps: W1; lernie **bl-231c**.