feat(justfile): operator recipes for managing STT LaunchAgents - #9
Merged
Conversation
Add a design/handoff doc proposing an optional session-config field "initial_prompt" for STT prompt/vocabulary biasing. Explains why work must start server-side (wire protocol lacks the field), current backend capabilities, and the proposed protocol-first changes: add an "initial_prompt" string to protocol.py, extract and thread it through server config to backends, use it for mlx_whisper.transcribe(..., initial_prompt=...), and have parakeet/nemotron accept-and-ignore. Also documents constraints (≈224-token cap with defensive truncation+logging, soft-bias semantics, session-static, backward compatibility), rollout steps, and verification notes for library support.
Add a development plan (docs/dev_plans/20260607-feature-stt-agents-justfile.md) proposing a repo-root justfile to operate pipecat.stt-server* LaunchAgents. The plan outlines objectives, requirements, phased implementation (stt-list/stt-status; stt-disable/stt-enable and delegated stt-install/uninstall; README/docs), tests (stubbed launchctl/uv harness), CI guards (map mirrors README, Koda-safety diff check), and operator UX decisions (bootout vs uninstall, explicit --socket-path probing, idempotency). This is a planning doc for branch feat/stt-agents-justfile and does not modify scripts or implementation code.
install_stt_agent.sh manages exactly one agent per invocation, so running
whisper/parakeet/nemotron side by side leaves no single way to see them or
stop the idle ones. Add a repo-root justfile (macOS/launchctl) as a thin
operator layer:
- stt-list cross-agent prefix sweep: state, pid, live backend per socket
- stt-status wire health probe for one backend (explicit --socket-path)
- stt-disable launchctl bootout (down until next login; keeps the plist)
- stt-enable bootstrap + kickstart from the existing plist
- stt-install / stt-uninstall delegate to install_stt_agent.sh (no plist
reimplementation)
The backend->(label,socket) map is a checked mirror of the README per-ASR
table; a test fails CI on drift. Koda-safe: touches neither stt_server/, the
wire protocol, nor install_stt_agent.sh's existing subcommands, so no client
pin bump is required.
Tests use the repo's hermetic stub-launchctl/id/uv harness: disable-keeps-
plist, README mirror, probe --socket-path (ignores stale STT_WS_SOCKET),
custom-label sweep, stopped-socket tolerance, exact delegation env,
idempotency, and a Koda-surface diff guard.
stt-list identified agents by backend/label, but consumers (e.g. onoats config.toml [stt] ws_socket) select by socket path — no shared vocabulary to correlate the two. Print each canonical agent's socket in the same ~-form the config uses, so an operator can match a config line to a running agent. Worst case is whisper, whose socket is stt.sock (not whisper.sock); the socket line removes that guesswork. Custom labels show '(custom label — not in the canonical map)' since their socket isn't derivable from the label.
Security:
- Shell-escape the {{backend}} recipe arg via just's quote() at every call
site and inside _resolve, closing a command-injection vector (an
attacker-influenced backend was interpolated as raw shell). The case arms
remain the allowlist; unknown backends still exit non-zero.
Correctness:
- _resolve now emits LABEL/SOCKET/BACKEND one field per line; callers parse
with three reads (bash-3.2-safe — macOS system bash has no mapfile) so a
socket path containing spaces no longer mis-splits.
Tests:
- Drive the resolution + README-mirror tests through the public stt-install
recipe (delegation stub) instead of the private _resolve helper.
- Make the uv stub dispatch argv-position-aware (run / stt_server / status)
rather than a substring match on the whole arg string.
- Anchor _readme_map on the table header and assert column order before
indexing, so a future README column change fails loudly.
Docs:
- Add a CHANGELOG [Unreleased] entry for the operator recipes.
- Mark the dev plan Complete and fill Final Results.
- Document the cache_dir/la_dir runtime-eval + override behaviour and the
deliberate stt-list `exit 0`.
set -uo pipefail does not abort on a failed simple command, so a failed launchctl bootout/bootstrap/kickstart was masked by the subsequent success echo — operators could see "booted out" / "bootstrapped + kickstarted" with exit 0 while the agent was still running or never started. Guard each state change explicitly: print an error to stderr and exit 1 on failure, only print success after the operation succeeds. stt-enable skips kickstart once bootstrap fails. Tests: extend the launchctl stub with a fail_actions hook and add coverage for failed bootout, failed bootstrap (kickstart skipped), and failed kickstart — each asserting non-zero exit and no success line. Found by adversarial review.
- Dev plan: check all acceptance-criteria boxes, fill Issues & Solutions (injection, spaced-path split, launchctl masking, test coupling), and add the launchctl failure-propagation fix to Final Results. - CHANGELOG: add a Fixed entry for stt-disable/stt-enable propagating launchctl failures instead of masking them.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds a repo-root
justfile(macOS /launchctlonly) as a thin operator layer for managing the multiplepipecat.stt-server*LaunchAgents that run side by side.scripts/install_stt_agent.shmanages exactly one agent per invocation — once whisper/parakeet/nemotron are all installed there's no single way to see them or stop the idle ones. This fills that gap.Recipes (
<backend>∈ whisper/parakeet/nemotron)just stt-list— cross-agent prefix sweep: state, pid, and live backend/model per canonical socketjust stt-status <backend>— wire health probe for one backend (explicit--socket-path)just stt-disable <backend>—launchctl bootout: down until next login, keeps the plistjust stt-enable <backend>—bootstrap+kickstartfrom the existing plistjust stt-install/just stt-uninstall <backend>— delegate toinstall_stt_agent.sh(no plist reimplementation)Why this shape
RunAtLoad+KeepAlive, so the script'sstop(SIGTERM) respawns.stt-disable(bootout) is the session-scoped stop;stt-uninstallremoves the plist for good. Documented in the README.statuscall passes--socket-pathexplicitly so it can't fall back to a staleSTT_WS_*/default socket and probe the wrong agent.Koda-safety (no pin bump)
Touches neither
stt_server/(the imported client + wire protocol) norinstall_stt_agent.sh's existing subcommands — purely additive launchd tooling. A test asserts this negative invariant against the branch diff, so the no-pin-bump claim is mechanically enforced.Post-review hardening
Two follow-up commits fold in deep-review + adversarial-review findings (see the plan's Issues & Solutions for the full list):
{{backend}}is shell-escaped viajust'squote()at every call site (it was interpolated as raw shell;just stt-status '$(touch X)'ran the payload). Thecasearms remain the allowlist.launchctlfailures no longer masked —set -uo pipefaildoes not abort on a failed simple command, so a failedbootout/bootstrap/kickstartwas overwritten by the successecho(exit 0 while the agent was still running or never started). Each state change is now guarded and exits non-zero on failure;stt-enableskipskickstartoncebootstrapfails._resolveemits one field per line so spaced socket paths parse correctly (bash-3.2-safe, nomapfile); the resolution + README-mirror tests drive the publicstt-installrecipe instead of the private_resolve; theuvstub dispatch is argv-position-aware; the README-table parser anchors on its header before indexing columns.Tests
tests/test_justfile_recipes.py(21 tests) uses the repo's hermetic stub-launchctl/id/uvharness (same pattern astest_install_migration.py): disable-keeps-plist, README mirror, probe--socket-path(ignores staleSTT_WS_SOCKET), custom-label prefix sweep, stopped-socket tolerance, exact delegation env, idempotency guards, failed-bootout/bootstrap/kickstartpropagation, and the Koda-surface diff guard. Full suite:314 passed, 2 skipped;ruff format+ruff checkclean.Test plan
uv run python -m pytest tests/test_justfile_recipes.py -q— recipe suite green (21 passed)uv run python -m pytest -q— full suite green (314 passed, 2 skipped)uv run ruff format --check . && uv run ruff check .— cleanjust --list— shows the sixstt-*recipes;_resolvestays privatejust stt-status '$(touch /tmp/x)'exits non-zero and creates no filejust stt-listverified live against three running agentsPlan
docs/dev_plans/20260607-feature-stt-agents-justfile.md(reviewed via 5-lens/review-plan; all Critical/Important findings folded in before implementation; Status: Complete).Note
This branch also carries an unrelated docs-only commit (
17f003e, "Add STT initial_prompt protocol design") that was auto-committed onto it from a separate workstream. It's docs-only (no code) and intentionally left in place.