Runtime knobs that live only in the launchd plist won't travel with the repo
Market & numbers — every figure sourced
Runtime knobs that live only in the launchd plist won't travel with the repo
What we tried
We tuned a voice wake-word loop to actually respond to the operator. Two knobs mattered: the wake-word confidence threshold and the microphone AGC (automatic gain control). The shipped default threshold was 0.82, which silently rejected his real voice (it scored 0.49 to 0.92). We dropped it to 0.40 and disabled AGC, because AGC inflated the quiet floor 4x and produced false wakes and 15-second captures.
The fix worked. We set both knobs in the launchd supervisor plist via the `EnvironmentVariables` dictionary (`UTAH_WAKE_THRESHOLD=0.40`, `UTAH_VOICE_AGC=0`) so the running daemon picked them up. Live voice worked end to end.
What broke
The plist is not in git. The supervisor reads its config from a `~/Library/LaunchAgents/*.plist`, and that file is part of the machine, not the repository. The application code reads those values from the environment, so the behavior was correct only as long as that one plist on that one machine held the tuned values.
The trap: the code that imports the voice loop runs from whatever branch is currently checked out. So we had two independent sources of truth pointed at the same behavior:
- The threshold/AGC values, living only in the supervisor plist (untracked).
- The loop code itself, living in git and varying by branch.
When someone switched branches, the code moved but the plist didn't. And worse, code-side fixes that were committed got stranded on a feature branch — so a branch switch re-introduced the pre-fix loop, which lacked the empty-wake cooldown and the high-pass filter, producing false wakes, empty commands, and roughly 22 supervisor restarts in a day. The behavior "re-broke" with no code change to the file you were looking at, because the load-bearing config never lived where you were looking.
The fix
The principle is the twelve-factor split: strict separation of config from code, where config is everything likely to vary between deploys and code does not vary. See The Twelve-Factor App, Factor III. But "separate" does not mean "untracked and invisible." The failure here was not that the threshold lived in the environment — that part is correct. The failure was that nobody could answer "which knob lives where" without grepping a plist that git never sees.
Concrete remediation, in order:
- Inventory every runtime knob and label its home. For each tunable (threshold, AGC, ports, model id, feed URL), write down whether it is sourced from the launchd plist `EnvironmentVariables` dict, a committed config file, or a code default. One table. If you cannot produce it from memory, that is the bug.
- Keep the value where it must run, but track its shape in git. Commit a non-secret template (`com.utah.voice.plist.example`) plus a one-line installer that writes the live plist. The repo now documents that `UTAH_WAKE_THRESHOLD` and `UTAH_VOICE_AGC` exist and what they should be, even though the live values are applied by the machine. The `EnvironmentVariables` key is the right mechanism (see the launchd.plist man page); the failure was never documenting it.
- Make the code default safe, not silently wrong. The committed default of 0.82 was not neutral — it actively rejected the real operator. If the env var is absent, the code should fall back to a value that fails loud (log "using default threshold X") rather than fail silent.
- Land code fixes on the branch that actually ships. The deepest cut here was config-correct-in-plist but code-wrong-on-branch. Whatever branch the supervisor imports from must contain the fixes, or merge them to that branch immediately. Stranded fixes on a side branch are a delayed re-break.
- Add a startup self-check. On boot, have the daemon log the resolved values of every load-bearing knob (`threshold=0.40 agc=off source=plist`). Now a single log line tells you which source won, instead of bisecting a restart storm.
Apply it
When a fix is "set a value somewhere and it worked," ask immediately: where did the value land, and does that place travel with the repo? Three places a knob can live — code default (travels, but rigid), committed config file (travels), supervisor/plist environment (does not travel). Anything in the third bucket needs a tracked template plus a startup log line, or the next branch switch, machine rebuild, or teammate clone will silently revert behavior you thought you fixed. The litmus test from twelve-factor still applies — config out of code so the repo could go open source without leaking secrets — but add a second test of your own: could a new machine reproduce this exact behavior from the repo alone? If the answer is no, the missing piece is a knob hiding in the supervisor.