yog 0.0.4

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
# yog — Agent Operating Guide

You are working in **yog**, a single published binary crate: an egui desktop
window that drives `lernie` loops over `balls` tasks. Two authorities govern
your work and they do not overlap:

- **`docs/DESIGN.md` is the architecture authority** — what yog *is*, its
  invariants, module map (§12), and the world substrate it composes (§16). When
  code and DESIGN disagree, one of them is a bug; never invent a third answer.
  Do not implement a deviation silently — fix the doc (see the global AGENTS.md
  guidance: "Nothing is set in stone").
- **This file is the code-style authority** — the machine-enforced rules below
  and the repo discipline that surrounds them.

yog composes a **nested world** (DESIGN §16): it overrides `LERNIE_HOME` and
`XDG_STATE_HOME` under `$XDG_DATA_HOME/yog/world` and hands that env to every
child it spawns, so yog's `bl`/`lernie` substrate never collides with the
user's ambient tools (brazen's config/credentials/cache resolve **per
workspace** since the blast-radius ruling — nothing brazen-shaped
stays ambient, §16.2). If you touch spawn paths, env folds (`src/xdg`), or
`src/world/*`, read §16 first — the nesting is the point.

---

## Code-style rules (Rust Bootstrap v3, adapted to yog)

The house standard is **contained Rust**: complexity lives in function bodies
(local, compiler-caught), not in type signatures (viral). Prefer
clones / `Arc` / `Box<dyn Trait>` / enums over borrow-based APIs — the perf given
up was never why we chose Rust. The rules are flat-numbered so they are
mechanical to follow; most are machine-enforced by a pinned ast-grep rule
(`rules/*.yml`), the clippy manifest (`Cargo.toml [lints]`), or `cargo-deny`.
**Where yog deviates from the standard, the deviation and its reason are
recorded verbatim in the rule — read it before assuming a rule is absolute.**

1. **No named lifetimes.** `'a`/`'ctx`/… on a signature, struct, or impl leaks
   internal storage into the interface. Borrow on the way IN (elided), hand back
   OWNED on the way OUT. `'static` and `'_` are fine (they name nothing).
   Enforced: `rules/no-named-lifetimes.yml`.

2. **A `pub fn` returns an owned, concrete type** — never `&T`/`&mut T`/`&[T]`
   nor an opaque `impl Trait` (edition-2024 implicit capture makes `impl Trait`
   smuggle borrows invisibly). Return `String`/`Vec<T>`/a named struct, or an
   index the caller resolves. If the accessor is internal, demote to
   `pub(crate)` rather than clone-to-own. Enforced:
   `rules/no-pub-borrow-return.yml`.

3. **`unsafe` is confined, not forbidden. yog ADAPTATION:** the standard's
   `unsafe_code = "forbid"` is replaced by an ast-grep *location* rule pinning
   every `unsafe` to `src/cli_outbound/sys.rs` — one irreducible SIGTERM syscall
   in `Stream`'s drop. `forbid` is unoverridable and reaches test code, and a
   `nix`/`rustix` dependency or a crate split for ~10 lines of FFI is worse than
   a confinement rule. Enforced: `rules/unsafe-outside-sys.yml`.

4. **No panic paths outside tests.** `unwrap`/`expect`/`panic!`/`todo!`/
   `unimplemented!`/`dbg!` and unchecked `indexing_slicing`/`string_slice` are
   `deny` in the manifest; `assert!`/`assert_eq!`/`assert_ne!` are banned in
   prod (a `panic!` in disguise); `debug_assert!` is fine. Locks use
   `unwrap_or_else(PoisonError::into_inner)`; fallible reads use `.get()`/`?`.
   Tests get carve-outs via `clippy.toml` (`allow-*-in-tests`) and
   `rules/no-assert-outside-tests.yml`.

5. **No `#[allow]` in prod.** An inline `#[allow]`, a `#![allow]`, or a
   `#[cfg_attr(…, allow(…))]` hides a warning where it fires. Policy lives in
   the manifest (`Cargo.toml [lints]`) where it is reviewable and justified in
   one place — **that manifest is the only home for a suppression.** Test code
   may relax a lint. Enforced: `rules/no-lint-suppression.yml`.

6. **Dependencies are pre-approved + `cargo-deny`. yog ADAPTATION (amended
   §16.7 W10):** the standard's "rustls-only, no openssl" is now yog's rule
   **verbatim**. The former adaptation — "no TLS surface at all", justified by
   yog being a native GL desktop app with no network — died when the
   batteries-included wave embedded `brazen`, the LLM network adapter: yog's
   own process now makes the HTTPS calls, so `ureq`/`rustls`/`ring`/
   `webpki-roots` are load-bearing, not incidental. **`rustls` is a DIRECT
   dependency since bl-b6fa** — approved by operator ruling 2026-08-13 for
   REMOTE §9.5's wire, and cheap because it was already in the graph behind
   `ureq`: the lockfile gained one line, no crate, no license and no advisory.
   Two conditions came with it and both are load-bearing. `default-features =
   false` with `ring`, because rustls' defaults select `aws-lc-rs`, whose
   `aws-lc-sys` builds C — `deny.toml` now bans that sys crate beside the two
   below so a manifest edit that dropped the flag fails the gate instead of
   quietly re-acquiring a C toolchain. And **no `tokio` and no `rcgen`**: the
   listener is synchronous `std::net` (rule 8 stays vacuous — do not add tokio
   to satisfy it), and certificates are minted by shelling to `openssl`
   (`src/wire/provision.rs`, the one recipe) rather than by a library yog
   links. **Since bl-ae05 the engine's own boot performs that mint** when a box
   has none, which is not an in-channel bootstrap and does not lift REMOTE
   §1.4: the act is the operator's tooling, run on the operator's box, before
   anything is dialled. `yog wire-certs` is the same recipe reached by a verb.
   `deny.toml` still bans
   `openssl-sys` AND `native-tls`, which was always the standard's point — a C
   toolchain dep and a non-portable system bridge, either of which breaks the
   single-binary musl/macOS/Windows story rustls keeps. The license allow-list
   is exhaustive over the committed `Cargo.lock`; transitive advisories may be
   ignored only with a reason recorded beside the entry, and **`deny.toml` is
   the authority for which ones and why — no advisory, count or rationale is
   restated here**, an entry that stops matching being a warning telling you to
   drop it. **Zero new dependencies without explicit user approval.**
   Sources are **registry-only, with no exception in force**: since
   bl-89a4 all three embedded substrate crates (`balls`, `brazen`, `lernie`)
   are plain crates.io pins — **`Cargo.toml` is the pin authority and no
   version is restated here**, the restatement having gone stale once already —
   `deny.toml` has no `allow-git` list, and `make publish` works. The phase-2
   ruling (DESIGN §16.7) still permits ONE interim exception — an embedded substrate crate
   pinned `version = "=x.y.z"` **plus** an exact `git`/`rev` while an upstream
   publish is in flight — but taking it re-blocks `make publish` (crates.io
   refuses git deps) and re-exposes yog to a rewritten upstream history
   orphaning the rev, so it is a last resort with a named exit. The pin must
   always be exact and lockfile-fixed; a `path` dependency is never lawful.

7. **`Mutex`/`RwLock` only in `src/state.rs`; no `Rc`/`RefCell` anywhere. yog
   ADAPTATION:** the lock chokepoint (`state.rs`) has five sanctioned
   carve-outs — the test scaffolding locks (`SPAWN_LOCK`/`ENV_LOCK` in
   `test_support`), `src/git_tree/probe_cache.rs` (a macOS 2 s TTL cache whose
   `Mutex` is uncontended single-thread interior mutability),
   `src/fs_watcher/hub.rs` (the process's one `notify` instance and its fan-out
   registry, `OnceLock` singletons that are never dropped or handed out —
   bl-908c), `src/registry/presence.rs` (REMOTE §5's live-connection map,
   bl-4e08), and `src/registry/mailbox/slots.rs` (REMOTE §5's invocation
   mailbox, bl-024b — the queue per client a routed tool call crosses through,
   which is presence's map in every respect that matters: same lifetime, same
   client key, same connection rate of change). All four code carve-outs share
   one reason: folding them into `state.rs` breaks llvm-cov's per-line coverage
   there, mis-attributing phantom uncovered regions onto its `impl` headers and
   type aliases. The last two are the ones to read closely — both *are*
   cross-thread hand-off state, so the rule says they belong in the chokepoint,
   and the presence map was written there first; it moved out only because it
   cost `state.rs` the 100% floor (four phantom lines, measured, and not
   dissolved by moving the addition to the end of the file), and the mailbox
   sits beside it rather than paying for a fifth measurement of the same
   hazard. `Rc`/`RefCell` are banned everywhere, tests
   included (bare `Cell` counters are fine). Enforced:
   `rules/locks-outside-state.yml`, `rules/no-rc-refcell.yml`.

8. **Async is tokio-only, `#[async_trait]` mandatory. yog ADAPTATION:** yog runs
   **no async and no tokio today** — it is a synchronous egui frame loop over
   subprocess spawns. This rule is installed but **vacuous**; honor it if async
   is ever introduced, do not add tokio to satisfy a rule that currently matches
   nothing.

9. **No trait bounds on a `pub` item** — no `pub struct S<C: Clock>`,
   `pub fn f<T: Into<String>>`, nor a bounded `where`. A bound on the public
   surface forces monomorphization onto every consumer. Dissolve with a trait
   object or a concrete param; demote an internal bounded helper to
   `pub(crate)`. An *unbounded* `pub fn f<T>(x: T)` is fine. **yog ADAPTATION:**
   the shared time source is `Arc<dyn Clock>`, not `Box<dyn Clock>` — the caller
   that injects it (a test advancing a `FakeClock`, `main.rs` handing over a
   `SystemClock`) keeps a handle while the §7.2 sweep schedule holds its own, so
   it must be shared, not owned. Enforced: `rules/no-pub-generic-bounds.yml`.

10. **`thiserror` in libs, `anyhow` in the app. yog ADAPTATION:** `thiserror` is
    in place for the error enums; **`anyhow` is NOT a dependency** — `main.rs` is
    a thin entry with no error-plumbing layer that would justify it. Do not add
    anyhow.

11. **One crate per module boundary. yog ADAPTATION:** yog is a **single
    published binary crate**, not a workspace. The module tree plus the 300-line
    cap (below) already contain complexity; a crates split buys nothing here and
    would fight the 100% coverage floor. No `[workspace]`.

**What "pub" means for rules 2 and 9:** the `pub` surface is the *real* library
surface consumed by the `tests/` integration crate and `src/main.rs`. Anything
internal is `pub(crate)` — the ast-grep rules scan only bare `pub`, so an honest
demotion removes an internal API from the boundary's obligations. Reach for
`pub(crate)` before cloning-to-own or de-generifying a purely internal type.

---

## Repo discipline

- **Task tracking is `bl` (balls).** Run `bl skill` before using it. Session
  start is `bl prime --as YOUR_IDENTITY`, then `bl list`.
- **Claim → work → close, in the worktree.** `bl claim <id> --as ID` prints a
  `work/<id>` worktree; **every edit goes there**, never on `main`. `bl close
  <id> --as ID` folds `main` in, runs the pre-commit gate, squash-delivers, and
  tears the worktree down. Always pass `--as ID` — never let the model invent a
  name. A stray edit on `main` is invisible to the squash and is left behind.
- **`main` is the only durable line, and the remote says so.** Four kinds of ref
  may exist on `origin`, each with an owner; anything else is a defect, and
  which one it is should be answerable from this list alone (bl-7066).
  - `main` — the trunk. **Work lands ONLY by `bl close` squashing a claim
    worktree onto it.** A pushed branch is never how work lands.
  - `balls/tasks` — the task store: structural, owned by `bl`, not a line of
    development. It publishes with the source (see "What may never enter a ball
    body").
  - `release-plz-*` and `speculation/**` — machine-owned and machine-deleted.
    release-plz pushes a new timestamped branch on every refresh;
    `scripts/speculate-gate` pushes one candidate per build and deletes it in a
    trap, on every path a signal can reach. Leave both alone.
  - **anything else is a PROBE, and the agent that pushed it deletes it.** The
    only reason to push a branch at all is to buy a **runner verdict**, and some
    defects exist only on a runner (bl-e492: 1969/1969 locally, five failures on
    CI). That is legitimate; leaving it is not. Read the verdict, land through
    `bl close`, then delete the branch and close its PR in the same breath.

  **The machine collects what you forget, but do not lean on it.**
  `prune-stale-branches` (release-plz.yml) runs after every push to `main` and
  deletes every branch with no open PR, excepting `main`, `balls/tasks` and
  `speculation/**`. That test is exact rather than a heuristic: `ci.yml`
  triggers on `push: branches: [main]` and on `pull_request`, never on a push to
  another branch, so a probe without a PR is getting no verdict and is therefore
  not a live probe. It cannot save you from a SIGKILL'd `speculate-gate`, which
  is the one leak nothing collects.

  **A probe is not free, and deleting it does not make it free.** GitHub keeps
  `refs/pull/<n>/head` forever: the public repo still serves the head refs of
  two branches that no longer exist anywhere. Whatever you push is a permanent
  public artifact of that tree even after the branch is gone — so push a probe
  when you need a verdict, and not to "save work somewhere".
- **Every child process is spawned through `git_env::command`.** `git` exports
  `GIT_DIR`/`GIT_INDEX_FILE` into every process it starts, and those OUTRANK
  `-C <repo>` and `current_dir` — so a child that inherits them forks its *own*
  `git` against the hook's repo. bl-0dff closed that for yog's direct `git`
  forks; bl-916a moved the scrub to the **spawn boundary**, where one
  `env_remove` clears the whole descendant process tree (`bl`, `lernie`, `bz`,
  an `$EDITOR` shim, the suite's fake substrate scripts). A bare `Command::new`
  outside `src/git_env.rs` is an error — `rules/no-bare-command.yml`.
  **`git commit` inside a `work/<id>` worktree is safe again:** the hook runs
  the suite, and the suite no longer writes to the outer repo (regression:
  `tests/git_env_scrub.rs`; verified by running the whole suite with
  `GIT_DIR`/`GIT_INDEX_FILE` pointed at a decoy repo, which stays untouched).
  Two residuals, both narrow: `make rules-audit` scans `src` only, so a bare
  `Command::new` added under `tests/` is on you; and a test binary that drives
  the embedded substrate **in-process** (`multiplex::dispatch`) must scrub its
  own process env from `git_env::INHERITED`, as `tests/multiplex_bl.rs` and
  `tests/multiplex_lernie.rs` do — no spawn boundary exists to do it for them.
- **300-line hard cap on every source file, inline tests included.** Docs and
  config (`.md`/`.toml`/`.yml`/`.json`/lock, `Makefile`, `LICENSE`) are exempt.
  Anything projected ≥200 is pre-split at design time (DESIGN §12), not at the
  cap — 200 is the aspiration the tree was swept to (bl-52f8), 300 the wall. **`make line-cap` is the one definition of the cap and of the exempt
  set**; the pre-commit hook and `make lint` both call it, neither restates it.
  It scans the **whole tree**, not the staged diff — the hook once checked only
  the files you happened to touch, which made the cap a sampling rather than an
  invariant (`src/app/balls.rs` rode at 308 lines undetected until an unrelated
  task edited it, bl-12dc). Over the cap? Split along a real seam and add the
  row to DESIGN §12; never shave lines to duck the limit.
- **100% test coverage, `cargo-tarpaulin` pinned 0.35.2.** `tarpaulin.toml`
  holds the config (excludes `src/main.rs` and `src/shell/*`); the hook and CI
  both run `--fail-under 100`. If it can't be tested, it mustn't be built.
- **The clippy pedantic allow-list lives ONLY in the manifest.** `Cargo.toml
  [lints.clippy]` runs `pedantic = deny` with a justified allow-list (currently
  13 entries in three tiers: the bootstrap five, six empirically-warranted for
  this egui GUI, and two site-specific false positives). Each entry carries a
  one-line justification. Never inline a suppression to dodge it (rule 5) — add
  a justified manifest entry via review instead.
- **`docs/DESIGN.md` is the architecture authority.** Amend the doc when reality
  diverges; do not code around a stale doc.
- **Never credit AI or tooling** in commit messages, code, or docs.

---

## The local gate

`make check` is the complete local gate and mirrors CI exactly:

    fmt-check → lint (line-cap + beat-audit + leak-scan + clippy + ast-grep scan + cargo-deny) → scripts/check-coverage.sh

- `make lint` — `make line-cap` (sub-second, so it fails first), then
  `make beat-audit` (milliseconds), then `make leak-scan` (~5s), then
  `cargo clippy --all-targets -- -D warnings` (picks up the manifest
  `[lints]`), then `make rules-audit`, then `cargo deny check`.
- `make line-cap` — the 300-line cap over every tracked non-exempt file. Prints
  every offender at once, and fails if it enumerates *nothing* (a broken
  pattern must not pass silently — the same two-direction discipline as
  `rules-audit`'s fixtures). The cap is a parameter, so **`make line-cap
  LINE_CAP=199` lists the ≥200 pre-split band** — run that before you extend a
  module, not after. It is deliberately *not* a gate: 94 of 395 source files sit
  in that band today. A warning that fires on a fifth of the tree is noise, and a
  gate there is just the cap moved to 200. ≥200 is a **design-time projection
  rule** — it fires on the author about to add to a file, not on the file's
  existing state. The band is the aspiration, not the limit: bl-52f8 swept the
  tree so nothing rides the 300 wall, and nothing has since — **ask `make
  line-cap LINE_CAP=n` for today's census rather than trusting a count written
  here** (this line has been wrong before: it named `transcript/rows.rs` at 266
  as the tree's one ≥250 file long after bl-2335 split that file in two).
- `make beat-audit` — the two mechanically checkable shapes of a **drive beat
  that proves nothing** (bl-70b8, `scripts/beat-audit.sh`). *(a)* Every label
  handed to `pass` is also handed to `fail`: a `gesture … || pass "…"` emits a
  row only when it succeeds, so the one outcome it exists to catch deletes the
  beat from the verdict instead of reddening it, and a ladder counts the rows
  it has rather than the rows it should have had. No allowlist — a one-armed
  beat is never right. *(b)* No `grep -q` whose whole pattern is one
  interpolation: an empty subject makes it `grep -q ""`, true of every stream,
  and it bites hardest when the beat ABOVE it is the thing that failed to
  produce the id. `grep -qx` is exempt structurally, not by allowlist — `-x`
  anchors to a whole line, so an empty pattern matches only an empty line.
  Two-direction, like `leak-scan`: the harness must be clean AND the script's
  own fixture must still fire, so an edited pattern that matches nothing cannot
  pass as green forever. The **third** shape of this family is structural and
  lives in the harness itself — `one_name_one_definition` (`harness.sh`),
  which refuses a duplicate top-level beat name outright, because bash's flat
  sourced namespace lets a later definition silently delete an earlier stage
  and a beat that never runs writes no row at all (bl-0e44).
- `make leak-scan` — the disclosure gate (bl-fd5a, reworked bl-167d).
  `scripts/leak-rules.sh` is the one definition of what may not be committed:
  private keys, vendor API tokens, credential assignments, routable
  IPv4/IPv6/MAC addresses, **absolute paths under any home root on any
  platform** (`/home/…`, `/Users/…`, `C:\Users\…` — the house synthetic roots
  `/home/u`, `/home/op`, `/home/x` are the only account names that pass),
  email addresses outside the reserved documentation space, dialogue behind a
  speaker label, agent-session artifacts (vendor resource ids, Claude Code
  transcript keys), credential-shaped file paths, and **content no rule can
  read**. `scripts/leak-scan.sh` is the mechanism; findings are truncated to 12
  characters, because a finding must LOCATE a leak, never reprint it into a
  terminal or a CI log.

  **It reads index BLOBS, not the worktree.** `git checkout-index`
  materializes the index into a scratch tree and the scan reads that, so the
  bytes scanned are the bytes committed. The index rather than the diff, for
  the same reason `line-cap` reads it. Until bl-167d the scan enumerated `git
  ls-files` — path NAMES — and grepped the WORKTREE files they pointed at, so a
  leak that was `git add`ed and then overwritten with a clean copy on disk was
  committed unread.

  **Unreadable is rejected, not skipped.** `grep -I` silently passes binary
  files, which is the class most likely to carry a dump (archives, databases,
  PDFs, HAR captures, screenshots, executables). A tracked binary must be a
  regenerable derivation with a byte-for-byte test — `BINARY_ALLOWED` names
  exactly `assets/yog-*.png`, which `make icon` emits and
  `src/theme/icon/tests/artifacts.rs` pins.

  **The scan is never cached.** `scripts/pre-commit` runs it BEFORE consulting
  bl-speculate's verdict cache, so no stored verdict — including one imported
  from the remote builder — can let a leak through unread. (The cache's gate
  fingerprint is a fixed file list compiled into `bl-speculate` that cannot
  name the scanner; not being cacheable dissolves that rather than waiting on
  upstream.)

  **Two scopes, because they answer different questions (bl-1007).** Bare, it
  scans the whole tracked tree — the right question for a commit hook (the tree
  IS your change) and for CI over a published ref. `--commit REV` scans what
  ONE commit publishes: the blobs it adds or rewrites, read out of that commit,
  plus its MESSAGE. That is the store gate's question, and only it can read a
  `-m` note, which lands in no file at all. A tree scan is wrong for a shared,
  long-lived checkout many agents write: run at every store op it judged every
  agent for every other agent's text, so one polluted ball body refused every
  `bl` op in the checkout — `create` included, so the defect about the wedge
  could not be filed. Prevention is per-author and local; the standing-state
  question stays with `store-scan.yml`, daily, over the whole ref.

  Its regression half is `--self-test` (the harness lives in
  `scripts/leak-selftest.sh`, sourced so it exercises the same functions the
  gate runs), which the target runs first and which is stricter than
  `rules-audit`'s: every rule owns a fixture in
  `scripts/leak-fixtures/` where **every non-comment line** must be flagged
  **by that rule** — so one dead alternative inside a nine-way pattern cannot
  hide behind the eight still working — and must carry `FIXTURE_MARKER`
  (`notreal`), because no regex can tell a real secret from a fabricated one
  and only the value can say so. Plus `clean.txt` / `clean-paths.txt`,
  near-misses that must NOT be flagged. Both directions, because a leak gate
  dies by matching nothing, and a noisy one dies by being bypassed.
  `tests/leak_gate.rs` holds the other half: seven tests that drive the real
  scanner over throwaway repositories.

  **There is no allowlist and no per-rule path exemption, and nothing is
  exempt from the tree scan.** There was one — `docs/drive-logs/` was exempt
  from the home-path rule, on the argument that a drive log is evidence of a
  run on a real box and the path *is* the evidence. bl-244f burned those logs
  instead. bl-167d then removed the last two skips: the scanner and its rule
  table are scanned (they stay clean because **no pattern may match its own
  text** — see `leak-rules.sh` on the one that did), and each fixture is
  scanned by every rule EXCEPT the one it is the fixture of, a structural
  exemption keyed to the file's own name rather than an allowlist. **Fix the
  rule, not the coverage.**

  Writing a drive log is still QUALITY.md §3 step 6 — the log now stays where
  it is written, beside its run's evidence under `$DRIVE_ROOT` and outside the
  checkout, and what comes back into the tree is the balls it filed. It stays
  lawful wherever it is quoted because `scripts/drive/logskel.sh` folds `$HOME`
  to `~` in every path it emits, at the one place the text is written.
  Hand-finish a log the same way.

  `.githooks/commit-msg` runs the same scanner over the commit MESSAGE, which
  `pre-commit` never sees. Run `make install-hooks` once to seat it.

  **The scanner scans the tree it is RUN IN, which need not be this repo**
  (bl-1043). It resolves its rule table from its own directory and the tree
  from `git rev-parse` in the working directory, so `cd <any git checkout> &&
  <repo>/scripts/leak-scan.sh` judges that checkout's index by this table. That
  is how the task store is gated below without a second copy of the rules.
- `make rules-audit` — `ast-grep scan src` (must be clean) AND a negative check
  that `ast-grep scan rules/fixtures` *fails* (proving every rule still bites).
  Most rules govern production style; **`no-hand-rolled-paint-walk.yml` governs
  the tests** — `Galley::text()` is the string that went IN, so a galley egui
  truncated to `…` still reports the whole label and every assertion against it
  is blind to elision, the one defect the paint layer is the only witness for.
  Painted glyphs come from `crate::paint_probe`, the one walk. This has been
  fixed three times (bl-bc06 found it in `paint_probe` itself, where **1815
  tests passed while covering no truncation at all**; bl-36c3 found two copies;
  bl-70b8 found a third still live, aiming every pointer test's click by input
  text) — which is why it is a rule now and not a memory. Geometry off a galley
  (`.size()`, `.rect`, `.rows`) is unaffected: it is the text that lies.
- `make coverage` — pinned tarpaulin, `--fail-under 100`. The bare invocation,
  always verbose; `check` does not call it directly.
- `scripts/check-coverage.sh` — the coverage STEP, and the one every caller
  shares: the hook, `make check` and therefore CI (`make ci`). It holds
  tarpaulin's stdout and replays it only on a failure (bl-0dff), and since
  bl-673a it answers with **three outcomes, not two**, because the gate's exit
  code is what two callers write a cached verdict from:
  - **0** — the tree passed. `scripts/pre-commit` records a PASS for
    `(tree, gate)`.
  - **75** (`EX_TEMPFAIL`) — **no verdict.** tarpaulin reported *being signaled*
    on both of its two attempts, so something outside the gate killed the run.
    `.github/workflows/speculate.yml` records NOTHING on this code.
  - **any other non-zero** — the gate failed on the tree's own merits, and that
    is recorded as a FAIL. **A FAIL is permanent**: balls' `speculate_run` stops
    the candidate chain at a stored FAIL on every later pass *without
    rebuilding*, and no re-run can dislodge it because the tree is unchanged.
    That is why an infrastructure death must not borrow this code — five
    sightings of a runner signalling tarpaulin mid-suite became five permanently
    false-negative trees (one of them with the macOS leg of the same tree green).
  The retry is once and only for the signaled class — a real failure is never
  re-run, and neither is an interrupt (tarpaulin *catches* SIGINT and exits with
  the same message, so the script traps INT/TERM/HUP into 75 itself). The
  runner evidence the five sightings lacked — memory, top-RSS processes, the
  kernel log's OOM lines when it is readable and a statement that it is not when
  it is not — prints from the signaled arm, on every caller.

**Tool pins (must match, or the gate/CI is not reproducible):** rustc `1.95.0`
(`rust-toolchain.toml`), ast-grep `0.44.1` (`sgconfig.yml`), cargo-deny `0.20.2`
(`deny.toml`), cargo-tarpaulin `0.35.2` (`tarpaulin.toml`). Bump a pin only as a
deliberate, isolated change.

---

## What may never enter a ball body

`bl` keeps this project's tasks in a **separate git repository** — `tasks/*.md`
on the `balls/tasks` branch, pushed to the *same remote as the source*. A ball
body is therefore published text on a ref that goes public with the crate
(operator ruling 2026-08-13, bl-dd1d: the store is **scrubbed and published**,
not moved to a private remote). Nothing you write in one is private, and the
source gate has never seen a byte of it — `make leak-scan` reads the index of
*this* tree, and the store is not in it.

Write the reasoning; leave out the identity, the chronology and the machine
state — the same editorial rule bl-2368 applied to the source tree. None of the
following may enter a task title, body, comment or `-m` note:

- **Other people's names, handles and addresses.** Third parties, other
  operators, anyone who did not publish themselves. **The maintainer's own
  `mudbungie` identity and `mudbungie@gmail.com` are explicitly permitted** —
  that is the companion ruling on bl-dd1d, not an oversight: the handle is
  already public in `LICENSE`, `Cargo.toml`, the README and the release-plz
  owner guard, and `leak-rules.sh`'s `personal-email` rule excepts that one
  address on purpose. Every *other* address is a leak.
- **Verbatim transcript prose.** Operator dialogue, model output, an agent's
  own reply pasted back in. Cite the conclusion and the ball it came from — a
  conversation is content somebody said, and quoting it publishes them.
- **Live machine state.** Process ids, load figures, absolute paths under a
  real home (`/home/<account>`, `/Users/<account>`, `C:\Users\<account>`),
  workspace and wall names off a live world, host and device names. Cite the
  *shape*, not the instance: `/home/u` is a house synthetic root and passes.
- **Provider auth state.** Who is signed in to what, which credential exists or
  does not, billing and account-status text quoted from a provider. "The
  account cannot run jobs" is the fact; the provider's sentence about it is
  disclosure.
- **Conversation and session ids.** Vendor resource ids, Claude Code transcript
  keys, and the identifiers of a specific run on a specific box.

The gate below enforces the mechanical half of this list. The half no regex can
reach — an unlabelled paragraph of somebody's conversation, a third party named
in ordinary prose — is yours, and it is the half that actually leaked: bl-dd1d's
audit recovered no credential of any kind. What it recovered was private
context.

### The task store gate

`scripts/yog-leak-gate` is a **balls plugin** that runs `scripts/leak-scan.sh`
— the same script and the same `leak-rules.sh` table as `make leak-scan`,
because two copies of the rules drift within a week — over **the op's own store
commit**, and exits non-zero. A non-zero exit is the balls protocol's abort: the
op is refused and the plugins that already ran roll back in reverse, so the
store commit is un-sealed before `bl-tracker` can push it.

It hangs at `<op>.post`, not `pre`. A `pre` plugin runs **before** `bl` writes
the task file, so it would scan the previous state and wave through the very
body being added; `post` is the one window in which the ball exists and has not
yet been published.

**It scans the op's commit, not the store (bl-1007).** The §7 post payload
carries `commit`; the gate hands it to `leak-scan.sh --commit`, which reads the
blobs that commit adds or rewrites plus its message. The first version scanned
the whole store checkout, and a store checkout is shared, long-lived and
written by concurrent ops: one polluted ball body then refused every agent's
every `bl` op — `create` included, so the defect about the wedge could not be
filed — and reported findings in a body no commit had ever carried, off another
op's in-flight worktree. The author who writes a bad body is now the one who is
told, at the moment of writing; whether the store carries a finding in total is
`store-scan.yml`'s question, and it is asked daily over the published ref.
Fail-closed on either field: a payload naming no store, or no commit, refuses
and names the unwire.

**The machine layer runs the same scanner and must pass the same scope.**
`bl-leak-gate` (the operator's `~/.config/balls/plugins.toml` entry, balls
bl-053a) runs `<project>/scripts/leak-scan.sh` over the store of *every*
opted-in project on the box. Bare, that is the whole-tree scan and the wedge
above; it wants `--commit "$commit"` off the same payload field. That plugin is
not a yog artifact and yog cannot edit it — this note is the record of what it
needs.

Wiring is one act per checkout, and this repo cannot perform it — the plugin
schedule lives in the balls landing (`balls/config`), not in yog's tree:

    bl install --bin yog-leak-gate=<repo>/scripts/yog-leak-gate
    for op in create update claim unclaim close drop; do
      bl conf prepend $op.post yog-leak-gate
    done

`prepend`, never `append`: plugins run in list order and only the irreversible
belongs last, so the gate must sit ahead of `bl-tracker` (which pushes) and
`bl-delivery` (which squashes). Those six ops are exactly the ones this
landing runs `bl-tracker` on — *the gate goes immediately before the publisher,
everywhere the publisher runs*. It is severable: `bl conf remove <op>.post
yog-leak-gate` deletes config, not code.

**Wire it only once the store scrub (bl-dd1d) has landed.** When this gate was
written the store tip tripped `home-path` on nine live task files — bl-dd1d's
own body among them, because it cites the paths as evidence — so wiring it
first would refuse every `bl` op on the box, the scrub's own edits included.

**What it cannot do.** It stops the accident, not the author. The same agent can
`bl conf remove` it, or commit and push inside the store clone by hand, exactly
as `git commit --no-verify` defeats the source hook. There is no unbypassable
*preventive* placement to move it to: a git hook inside the store clone is
strictly worse (untracked, per-clone, re-founded by `bl prime`, absent on every
other box and silently so), and GitHub cannot interpose a check on a direct
push to `balls/tasks` — there is no pull request to require a status check on,
and a server-side hook is not a repo artifact, the same boundary bl-167d drew
for protected refs. The check the author *cannot* switch off is
`.github/workflows/store-scan.yml`, which scans the published ref daily, on
dispatch, and whenever the rule table itself changes. It runs after the push,
so it **detects**; the remedy for a hit is a history rewrite. Prevention is
local and bypassable, enforcement is remote and late, and stating that is worth
more than a gate that implies otherwise.

---

## Before making the repo or a crate version public

**A commit hook scans one tree.** `make leak-scan` reads the index it is about
to commit, and that is the whole of what it can promise. Everything below is
outside any hook's reach, cannot be made an invariant by one, and is therefore
a checklist run by a person once per publication — not a gate, and not
something the gate should imply it has covered (bl-167d).

Nothing here is automated on purpose: each item is a one-time judgement whose
remedy is destructive (a history rewrite, a yank, a rotation), so a green
checkbox would be a worse answer than a person looking.

This list was RUN, once, on 2026-08-13 (bl-4f96). What it found and what that
cost is recorded per item; the notes are the checklist's evidence that it works.

1. **History.** The gate has only ever seen the tip. Sweep every reachable
   commit for the same material before the first public push —
   `git log -p --all` through `scripts/leak-scan.sh FILE...` on a checkout of
   each commit, or a purpose-built history scanner. A hit means a rewrite
   (`git filter-repo`) *and* rotation of whatever it named; the commit is
   public the moment the repo is.

   The 2026-08-13 sweep read all 13,801 objects and hit: the operator address
   in 241 blobs of `docs/DESIGN.md`, operator-home paths in 562 places over 22
   files, and the drive logs bl-244f had deleted from the tip. **A filter was
   the wrong remedy and a squash was the right one**, because the same blobs
   also held verbatim operator dialogue and a third party's name — prose no
   pattern can find, so a filtered history would have been unverifiable. A
   squashed initial commit publishes exactly the tree the gate certifies. Its
   one real cost is that balls binds a task to the repo's ROOT COMMIT: a new
   root orphans every ball, `bl list` answers nothing while `bl list
   --everywhere` answers in full, and every live `tasks/*.md` needs its
   `root_commit` rewritten. Closed balls keep the old binding in store history.
2. **Other refs.** Tags, `speculation/**` branches left by a crashed
   `scripts/speculate-gate` driver, and any `work/bl-*` branch that was pushed.
   Delete what should never have been pushed rather than scanning it twice.
   **`balls/tasks` and `balls/config` are on this same remote and publish with
   it** — the task store is a ref, not a private sidecar (bl-dd1d). Run
   `store-scan` by `workflow_dispatch` rather than trusting the schedule was
   alive, and note that both the workflow and the store gate only ever see the
   TIP: the store's history is item 1's problem too, and rewriting it
   invalidates every existing clone.

   **A rewrite of an existing GitHub repository does not clean it, so do not
   plan one.** GitHub keeps `refs/pull/<n>/head` forever: closing the pull
   request and deleting its branch leaves the ref, and every object it reaches
   stays fetchable. Objects orphaned by a force-push stay reachable by sha as
   well. The only publication that can be certified is into a **fresh object
   store** — rename the private repository to an archive (nothing is deleted,
   the old refs and branches stay where they are), create the public one under
   the original name so `repository =` in Cargo.toml and balls' `task-remote`
   stay correct, and push only the refs that were scanned.

   Nothing about a repository travels with its NAME. The new one starts with no
   secrets (`CARGO_REGISTRY_TOKEN` must be set again or the release job cannot
   publish), and with "Allow GitHub Actions to create and approve pull
   requests" off, which 403s release-plz's PR job.
3. **Commit messages.** `.githooks/commit-msg` covers messages written with the
   hook seated (`make install-hooks`). Messages older than the hook, and any
   written elsewhere, are not covered.
4. **Repository text nobody committed.** Pull-request titles and bodies, issue
   text, review comments, release notes, and the crates.io description and
   metadata. None of it is in the tree.
5. **Actions logs and artifacts.** A failed gate prints paths, hostnames and
   sometimes the offending line into a public log; `.github/workflows/
   speculate.yml` also uploads a `verdicts` artifact. Both survive the run.
   A fresh repository starts with no run history, which is the other reason
   item 2's rename-and-recreate is cheaper than it looks.
6. **Already-published versions.** `cargo publish` is irreversible: a yanked
   version stays downloadable. Audit the packaged file list
   (`cargo package --list`) before `make publish CONFIRM=yes`, not after.

   This is the item that was learned the hard way. **0.0.1 was published on
   2026-07-26 carrying everything item 1 found** — the operator address, eleven
   operator-home paths across `src/world/mod.rs`, `src/opslog/tests.rs`,
   `src/binding/tests.rs` and `docs/DESIGN.md`, and three `docs/drive-logs/`
   files — because `Cargo.toml` declares no `include`/`exclude` and so packages
   the whole tree. It was yanked on 2026-08-13, which changes nothing about who
   can download it. A private repository is not a private crate: the moment a
   version ships, this checklist's items 1 and 6 are the same deadline.

## The merge queue — speculative closes, builds on GitHub Actions

The gate above costs minutes and closes serialize on it. yog rides balls'
speculative merge queue (balls `docs/design/bl-24e7-speculative-merge-queue.md`,
adopted in bl-1a5b): the gate consults a tree-keyed **verdict cache** first —
`scripts/pre-commit` exits in seconds when this exact worktree tree already
passed this exact gate — and speculative builds warm that cache ahead of the
queue, on GitHub Actions (`.github/workflows/speculate.yml`), so the local
machine never pays the build. After your last commit in the claim worktree:

    bl-speculate enqueue bl-XXXX                      # seal work/bl-XXXX into the queue
    bl-speculate run --gate scripts/speculate-gate    # builds run on GH Actions
    bl close bl-XXXX --as YOU                         # cache hit → seconds

Facts the queue derives from (do not fight them):

- **Sealing is the tag** `merging/bl-XXXX` on your tip. Any new commit unseals
  you; re-running `enqueue` re-seals at the *bottom* of the queue. That is the
  whole eviction mechanism.
- **A conflict or FAIL verdict ahead of you stops the chain** — the fix belongs
  to that branch's owner. Your close still works; it just pays the stock gate.
- **A FAIL verdict is forever, so an infrastructure death must never become
  one** (bl-673a). `speculate_run` reads a stored FAIL and stops the chain there
  on every later pass *without rebuilding*; the key is the tree, so re-running
  answers from cache and only a new commit escapes. Two writers exist and each
  is guarded on its own side. `.github/workflows/speculate.yml` records a FAIL
  only when the gate's exit code says it judged the tree (see "The local gate":
  75 means no verdict, and a job killed outright writes no output at all).
  `scripts/speculate-gate` cannot be guarded that way, because
  **`speculate_run::build` takes `status.success()` and writes `pass = false`
  from anything else** — the gate has no third answer to give it. So it does the
  only thing it can: an empty `verdicts` artifact is a REBUILD, not an answer,
  and it pushes the candidate a second time rather than answering. **The
  residual is upstream's**: two consecutive infrastructure deaths still store a
  FAIL, and what balls would need is a gate exit code meaning *no verdict,
  record nothing* — the same 75 the workflow already honors.
- **Everything degrades to the stock local gate.** No binary, no verdict, no
  network, no runner: the cache misses honestly and `bl close` builds locally.
  Never wait on the remote to close.
- **The gate fingerprint is content**: `scripts/pre-commit`,
  `scripts/check-line-lengths.sh`, `scripts/check-coverage.sh`, `Makefile`,
  plus local `rustc -V`. Editing any of them invalidates every stored verdict
  (deliberately). `rust-toolchain.toml` pins the toolchain on both sides —
  bump it in lockstep or remote verdicts silently stop matching.
- A crashed `scripts/speculate-gate` can strand a `speculation/<sha>` branch
  on origin; sweep with `git push origin --delete speculation/<sha>`.