yog
yog is the desktop manager for
lernie loops: an egui/eframe desktop
window that browses every lernie workspace, surfaces what needs attention, and
drives the loop lifecycle — start work from nothing, a path, or a
ball, message or stop agents, assign and close balls — over the lernie
and bl command-line tools.
It is its own package (mudbungie/yog),
deliberately outside the lernie and balls workspaces: both ship as composable
components and yog composes on top of them. It takes no Cargo dependency on
either today — it spawns the lernie and bl binaries and reads the
workspaces' git repos directly. The intended direction is a fuller GUI with
direct library dependencies on both; lernie and balls take neither back.
Architecture
docs/DESIGN.md is the authority — the state inventory, the attention
model, the write paths, the module map, and the line budgets live there, and it
is kept in sync with the code. This section is only the map.
yog is a stateless renderer: every frame is a pure function of on-disk state at
that tick, so two instances against the same repos converge with no
coordination. The only durable, yog-owned state is ui.json (pins, collapse
overrides, attention watermarks — DESIGN §4.1) and the ops.jsonl action log
(§4.2); everything else is derived from disk. The code is split the house way:
pure view-model modules with no egui — the per-tick git_tree, the nav
roster, attention, projects/binding, start, ui_state, the inspector
VMs (transcript, steps_view, jsonview, inboxview, budgets) composed by
the tested inspector tab dispatch, and config_edit — fronted by thin egui
glue in shell (coverage-excluded), with the keyboard bindings a pure
keymap table.
Visually, the window wears the congeries palette (src/theme, DESIGN §11):
Yog-Sothoth is a congeries of iridescent globes, and the UI is exactly that —
lore-named sphere-hues against a violet-black void (hydra green for liveness,
spectral blue for in-flight, brazen bronze for pending/warn, ichor for errors,
gate violet for yog itself), with every colour authored once in the theme
module and each driven tool keyed to its own hue.
The window is three information altitudes (DESIGN §11): the attention strip and
roster (does anything need me? what's running?), the focused workspace (its
agent descent tree with per-agent state badges, the four refs/lernie/* marks
— conflicted, budget-exhausted, abandoned, notify — budgets, streaming tails,
and tool pulses), and a per-agent tabbed inspector. Two composite flows sit on
top: the start flow (DESIGN §3.4) — prompts land as new roots in the
focused workspace (a fresh world bootstraps one automatically; creating more
is a deliberate act, walling off spheres like clients or corporate vs.
personal) and may carry a path or a ball payload (bl create + bl claim --as <workspace-name>), through an editable goal composer whose
Send fires lernie prompt detached — and the config editors stage-and-validate
brazen config.toml, lernie global config, and per-workspace config branches
(DESIGN §9). Replays (<lernie-data>/replays/*) render through the same view,
read-only.
Running
yog # browse every workspace, attention-sorted
yog --workspace /path/to/workspace # start focused on one workspace
yog enumerates every lernie workspace (named workspaces under
$XDG_DATA_HOME/yog/workspaces/, foreign workspaces and replays under the
lernie data root) and opens on the first one that needs attention. Balls bind
to workspaces by claimant: every claim a workspace makes is stamped --as its
minted name, so assignment is late-mutable metadata, not a location (DESIGN
§3.2). The optional
--workspace <path> flag overrides that startup focus. There is no --repo
flag — the whole roster is the view.
From a source checkout, make run does the same via cargo run; pass
WS=<path> to preset the initial focus.
egui is winit-based; its only runtime deps are the X11/Wayland libs
already present on any Linux desktop. No apt install step is required.
The window drives the lernie and bl binaries over PATH (or LERNIE_BINARY
/ BL_BINARY); install lernie from its repo (make install there).
The world
yog composes its own nested world — a substrate environment under
$XDG_DATA_HOME/yog/world that overrides LERNIE_HOME and XDG_STATE_HOME
and hands it to every child it spawns, so yog's bl/lernie state never
collides with your ambient tools (brazen's config, credentials, and model
cache all stay shared — one host bz, one config, its secrets and its
regenerable cache). The world is the authority in DESIGN §16; one rm -rf $XDG_DATA_HOME/yog erases all of it and leaves your ambient substrates
untouched.
Two escape hatches (DESIGN §8.4) let a human — or a foreign frontend — join that world from a shell with one prefix:
eval "$(yog env)" # drop THIS shell into the world; ambient bl/lernie/bz
# now operate on yog's nested state
yog exec bl list # run one command inside the world (its exit is yog's)
yog exec --cwd /path bl close bl-1234 --as me
yog env prints the world's shell-quoted export lines; yog exec [--cwd DIR] <cmd…> layers the world env over your inherited environment, inherits stdio,
and propagates the child's exit code (a terminating signal maps to 128 + signum). Both work headless — no display required.
By default the nested balls clone tracks the project's shared balls/tasks store
branch, so a ball yog claims is visible to your ambient bl list and vice versa.
A per-project no-marks knob (stealth bl conf task-remote none, and/or a
custom task-branch) trades that coordination for invisibility — driven through
bl's own config surface, never a yog config file (DESIGN §16.3).
Building and contributing
| Target | What it does |
|---|---|
make build / make release |
Debug / release build |
make test |
Run the test suite (parallel; see the Makefile note on spawn discipline) |
make coverage |
Enforce the 100% coverage floor via pinned tarpaulin (see tarpaulin.toml) |
make lint / make fmt |
Clippy -D warnings + ast-grep rules audit + cargo deny check / rustfmt |
make rules-audit |
Run the pinned ast-grep rules (rules/) over src and verify the rules/fixtures negatives still fire |
make check |
fmt-check + lint + coverage — the complete gate CI and the pre-commit hook mirror |
make run [WS=<path>] |
Launch the window (optionally focused on one workspace) |
make install [INSTALL_PREFIX=<p>] |
Release-build and drop yog into $INSTALL_PREFIX/bin (default ~/.local/bin) |
make install-hooks |
Point core.hooksPath at .githooks — do this once per clone |
Code style is governed by the Rust Bootstrap v3 standard — the flat-numbered,
yog-adapted rules in AGENTS.md, machine-enforced by rules/*.yml
(pinned ast-grep), the clippy manifest, and cargo-deny; read it before writing
code.
Task tracking uses balls (bl);
never commit directly on main — all changes land via a claimed worktree
and are delivered by bl close. The pre-commit hook enforces this, a
300-line ceiling on source files, and the 100% coverage floor.
Delivery
Delivery to the upstream (mudbungie/yog)
is automatic. bl close squash-merges the worktree onto main, but it seals
that commit itself — outside git commit — so git's post-commit hook never
fires on a close. Delivery instead rides balls' own extension seam: the
scripts/bl-push-main plugin, wired into close.post, pushes main to
origin right after the close lands it. A push failure only warns — it never
fails the close, so a landed change is never lost to a transient network error;
re-push manually with git push origin main. Balls task state travels its own
path, pushed by the bl-tracker plugin; the two are independent.
Wiring the plugin into a checkout is checkout-local, not a repo edit: symlink
scripts/bl-push-main into the landing's config/plugins/bin/, then
bl conf append close.post bl-push-main. The .githooks/post-commit hook is
retained for the rare legitimate manual commit made directly on main; run
make install-hooks once per clone to arm the git hooks.
Local install (CICD)
The local half of delivery is a sibling close.post plugin,
scripts/bl-install-main: when a close lands on main it recompiles that tip
and make installs yog into $PATH. It builds from an ephemeral
git worktree --detach main, never the root checkout — a plumbing delivery
leaves the root working tree stale and possibly holding uncommitted work, so the
main ref is the only trustworthy source of "what landed". The build shares the
repo's target/ (CARGO_TARGET_DIR) to stay incremental, runs detached
(setsid) so bl close returns at once, and logs to target/cicd-install.log.
Like bl-push-main it always exits 0 — a build failure only lands in the
log, never rolls back the close. Wire it the same way: symlink
scripts/bl-install-main into the landing's config/plugins/bin/, then
bl conf append close.post bl-install-main.
On every push to main, GitHub Actions (.github/workflows/ci.yml) runs
make ci on Linux (fmt-check, clippy -D warnings, tarpaulin 100% floor)
and cargo build + make test on macOS (Apple silicon).
Publishing
The crate is published to crates.io with
make publish. That target runs cargo publish --dry-run unconditionally;
the real upload runs only when you opt in explicitly with
make publish CONFIRM=yes. Publishing a release is a deliberate human
decision, never a side effect of CI or delivery.