Skip to content

feat(rig): provision tg-ctl inbound daemon as a boot LaunchAgent#30

Draft
alex-mextner wants to merge 4 commits into
mainfrom
rig-tg-ctl-boot
Draft

feat(rig): provision tg-ctl inbound daemon as a boot LaunchAgent#30
alex-mextner wants to merge 4 commits into
mainfrom
rig-tg-ctl-boot

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What

rig init / rig apply now provision the tg-ctl inbound control daemon (tg-cli's
long-poll / inject-into-tmux / voice→text daemon, run as tg-ctl run) as a macOS boot
LaunchAgent
so it auto-starts at login/boot — exactly like rig already does for the tmux
boot service. On a clean machine, rig init alone sets it up (the block is default-ON).

Design — mirrors tmux-boot

The whole pipeline follows the existing tmux pattern (one source of truth shared by plan,
apply, drift):

  • riglib/tg_ctl.py — a pure, stdlib-only, effect-free TgCtlPlan (analog of
    riglib.tmux) that renders ~/Library/LaunchAgents/ai.hyperide.tg-ctl.plist. It uses
    plistlib.dumps(..., sort_keys=False) so the key order matches the hand-created, working
    live plist byte-for-byterig apply against the live file is a true no-op (skipped),
    never a spurious rewrite. Paths are derived from $HOME; bun is discovered (which bun
    ~/.bun/bin/bun fallback); config dir honors $TG_CTL_CONFIG_DIR (default ~/.config/tg-cli).
  • runner._do_provision_tg_ctl — render → back up a differing prior (timestamped) → write →
    (re)load. Unlike tmux-boot (which only writes the plist), this agent is (re)loaded via
    launchctl bootout/bootstrap in the per-user gui/<uid> domain, so a clean rig init
    starts the daemon without a reboot. Reuses _timestamped_backup_path and the fsutil
    conflict engine.
  • drift._check_tg_ctl — flags missing / divergent / written-but-not-loaded, a leftover
    plist when boot:false, and the stale predecessor (extra). rig status surfaces it in the
    GLOBAL section (installed / drifted / disabled / unsupported-off-darwin).

Config block (GLOBAL layer)

Per-machine concern → belongs in ~/.config/rig/config.yaml (like harness/tmux/git_hooks),
NOT a committed repo rig.yaml. Default ON; enabled/boot default true. Follows the tmux:
schema style (fail-closed validation). See docs/config-schema.md#tg_ctl.

Stale-predecessor removal

If ~/Library/LaunchAgents/com.ultra.codex-tg-bot.plist (the dead predecessor) exists,
rig apply boots it out, backs it up (timestamped), and removes it.

Idempotency — proven against the live plist

A real rig apply (no dry-run) against this machine's live, loaded ai.hyperide.tg-ctl.plist
is a skipped no-op: the plist sha is unchanged and no bootstrap/bootout fires (the
byte-identical-AND-loaded early-return). Verified directly.

Tests / smoke

  • pytest: 510 passed (uv run --extra test pytest tests/).
  • smoke: exit 0 (bash tests/smoke.sh), including a new focused, HOME-isolated,
    RIG_TG_CTL_DRY_RUN tg-ctl leg.
  • Hard isolation: no test/smoke ever touches the real ~/Library/LaunchAgents or runs real
    launchctl — conftest neutralizes the default-on provisioner + drift check and stubs the
    gui-domain launchctl seams suite-wide; dedicated tests restore the real ones with their own
    temp HOME + a RIG_TG_CTL_DRY_RUN-style dry-run seam. The real tg-ctl plist sha was unchanged
    by the entire test run.

Review

Ran multi-model review on the diff; addressed its findings — fixed a dry-run disk-mutation
leak in the stale teardown, boot:null/label:null defaulting, off-darwin status wording,
collapsed the launchctl bootout/bootstrap helpers, and added regression tests for each.

Do NOT merge — draft.

🤖 Generated with Claude Code

alex-mextner and others added 4 commits June 17, 2026 01:00
Add the tg_ctl config block (validate + plan) and the pure, effect-free
TgCtlPlan that renders the ai.hyperide.tg-ctl.plist LaunchAgent XML
byte-exact to the working hand-created file (sort_keys=False preserves the
insertion order so a re-apply is a true no-op). Default-on, per-machine
(GLOBAL layer), macOS-only. Mirrors the tmux block's schema style.

boot:null and label:null resolve to their defaults (not bool(None)=False /
str(None)="None").

Reviewed via multi-model `review`; findings addressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Runner: _do_provision_tg_ctl writes the byte-exact plist, backs up a
differing prior, ensures the log dir, tears down the stale predecessor
(com.ultra.codex-tg-bot: bootout + timestamped backup + remove), and
(re)loads via launchctl bootout/bootstrap in the gui/<uid> domain. A
re-apply against the already-correct loaded plist is a skipped no-op.
RIG_TG_CTL_DRY_RUN writes the plist but skips every live/destructive
mutation (launchctl AND the stale teardown) so tests/smoke never touch the
real launchd domain.

Drift: _check_tg_ctl flags missing / divergent / written-but-not-loaded, a
leftover plist when boot:false, and the stale predecessor (extra). CLI:
GLOBAL status line shows installed / drifted / disabled / unsupported
(off-darwin), resolved through the shared plan builder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_tg_ctl.py mirrors test_tmux.py: config validation, byte-exact plist
render (incl. against the live machine plist when present, read-only),
create/idempotent/conflict/dry-run states, stale-predecessor teardown, drift
(missing/modified/extra/not-loaded), status states, and the boot:null /
label:null / dry-run-no-stale-removal / off-darwin regressions.

conftest neutralizes the default-on tg_ctl provisioner + drift check and
stubs the gui-domain launchctl seams suite-wide (dedicated tests restore the
real ones with their own HOME-isolated tmp dirs); no test ever touches the
real ~/Library/LaunchAgents or runs real launchctl. smoke.sh gains a
focused, HOME-isolated, RIG_TG_CTL_DRY_RUN tg-ctl leg and prefers
`uv run --extra test pytest`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docs/config-schema.md: the tg_ctl section (keys, defaults, the byte-exact
no-op contract, gui-domain (re)load, stale-predecessor teardown, drift, the
RIG_TG_CTL_DRY_RUN seam, and the enabled:false vs boot:false distinction) +
the validation paragraph. AGENTS.md: refine the "never mutate a LIVE service"
rule — the stateless background daemons (models cron, tg_ctl) are the
documented (re)load exceptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant