“two gates, three daemons.”

The plan was small: clear the last two blockers before flipping the repo public. It turned into a dig — every gate I opened had a second problem hiding behind it, and the “mystery” at the end was a daemon nobody remembered was running.


Gate 1 — the adapter-naming collision (26a754f)

The first trained model, hand-launched on Day 30, is werbos-0.0.1 — three-part, on disk, in every doc and the THESIS. But the train-daemon’s versioning policy used a two-part scheme: werbos-0.1 → werbos-0.2. Two bugs fell out of the mismatch:

  1. The README contradicted itself — it told you to train --out .../werbos-0.1 and then chat --model werbos-0.0.1. A stranger copy-pasting hits “model not found.” A public-flip blocker hiding in plain sight.
  2. The daemon didn’t recognize its own child — next_version("werbos-0.0.1") didn’t match the two-part regex, so the ratchet treated the launched v1 as garbage and reset to werbos-0.1.

Fix: adopt three-part werbos-MAJOR.MINOR.PATCH, patch-bumping (0.0.1 → 0.0.2), _INITIAL_VERSION = werbos-0.0.1 so the daemon continues the launch lineage instead of forking a parallel one. README examples now agree; every docstring/help/test reference updated. 5530 green.

Gate 2 — the nightly trainer was failing every night

The launchd job had been erroring since the day it was armed: Operation not permitted, exit 126. Not a chmod issue — macOS TCC blocks launchd from the script and the venv, because the whole training stack lives under ~/Documents (a protected folder). So no nightly cycle had ever actually run.

Fix (relocate, no GUI grant): rebuilt the venv at ~/werbos/.venv (pip install . → the git-pinned siblings, plus mlx-lm), copied the script to ~/werbos/bin, repointed the plist out of the protected tree. Clean run on the first try — pulled corpus, decided no_growth (17 rows < floor 100), exit 0. The one tradeoff: that venv is a snapshot, so training-code edits need a reinstall.

The twist — a third daemon nobody remembered

The werbos-0.1 / werbos-0.2 “mystery” Ollama tags on the homelab weren’t a mystery once I looked: same blob id = identical copies of the 3B base, not real adapters. Something was still making them — and it was a second train-daemon: a Day-27 systemd timer, 2-hour cadence, --growth-floor 0 (trains every run no matter what), pointed at a stub trainer that just ollama creates a copy of the base. Scaffolding from before the real trainer existed, still dutifully churning out junk every two hours with the deprecated naming.

Disabled the timer, removed the junk models. The homelab’s real job in the loop is corpus generation; training is Mac-only now (MLX needs Metal; the homelab is AMD CPU).

Numbers worth keeping

  • 5530 tests green after the three-part conversion
  • “hey buddy” nightly cycles run: 0 → working (TCC relocate)
  • Junk models churned every 2h: stopped (third daemon killed)

AHAs

  • Every gate had a second problem behind it. The naming fix exposed the daemon’s self-rejection; the launchd fix exposed the whole stack lives in a folder launchd can’t read; the “mystery” exposed a forgotten daemon. Pre-flip cleanup is where the closet skeletons rattle.
  • A stub that “proves the pipeline” outlives its usefulness and becomes noise. The Day-27 scaffold was right to exist then and wrong to still be running now.
  • Write the mystery down or re-debug it. The tag mystery was only fast to solve because Day-30’s launch lineage was documented.

Tomorrow

Both gates are green. The repo’s clear to flip — but instead of flipping, the higher-leverage move is to build the eyes: cross-layer observability, so the next time a daemon’s quietly broken, I can see it instead of digging.