Dev setup
Set up Untether for local development, run checks, and test changes safely via the dev instance before releasing to production.
Set up Untether for local development, run checks, and test changes safely via the dev instance before releasing to production.
Clone and install
git clone https://github.com/littlebearapps/untether
cd untether
# Run directly with uv (installs deps automatically)
uv run untether --help
Install as a local tool (optional)
uv tool install .
untether --help
Two-instance model
systemd is optional The two-instance systemd model below is a Linux power-user setup. On any platform (including Linux), you can just run
untetherin a terminal — Ctrl+C and re-run to pick up changes.
Untether runs two separate instances on the same machine:
| Production | Dev | |
|---|---|---|
| Service | untether.service | untether-dev.service |
| Bot | @your_production_bot | @your_dev_bot |
| Source | PyPI wheel (frozen) | Local editable (src/) |
| Config | ~/.untether/untether.toml | ~/.untether-dev/untether.toml |
| Binary | ~/.local/bin/untether (pipx) | .venv/bin/untether (editable) |
Never restart production to test local changes
systemctl --user restart untetherdoes NOT pick up local code changes — production runs a frozen PyPI wheel. Restarting production during development is always wrong and risks disrupting live chat.
Development cycle
The standard workflow:
# 1. Edit source code
vim src/untether/telegram/commands/my_feature.py
# 2. Run checks
uv run pytest && uv run ruff check src/
# 3. Restart to pick up changes
uv run untether # Ctrl+C first if already running
# Or on Linux with systemd:
# systemctl --user restart untether-dev
# journalctl --user -u untether-dev -f
# 4. Test via @your_dev_bot in Telegram
$ journalctl --user -u untether-dev -f
Mar 10 09:15:23 lba-1 untether[12345]: untether.started version=0.34.0 engine=codex projects=3
Mar 10 09:15:23 lba-1 untether[12345]: telegram.connected bot=@untether_dev_bot
Mar 10 09:15:23 lba-1 untether[12345]: telegram.polling started
Always test via the dev bot before merging. Never send test messages to the production bot.
Run checks
# Individual checks
uv run pytest # tests (Python 3.12+, 80% coverage threshold)
uv run ruff check src tests # linting
uv run ruff format --check src tests # formatting
uv run ty check . # type checking (warnings only, not blocking)
# All at once
just check
Format before committing Always run
uv run ruff format src/ tests/before committing — CI checks formatting strictly.
CI pipeline
GitHub Actions runs these checks on every push and PR:
| Job | What it checks |
|---|---|
| format | ruff format --check --diff |
| ruff | ruff check with GitHub annotations |
| ty | Type checking (warnings only — 11 pre-existing warnings) |
| pytest | Tests on Python 3.12, 3.13, 3.14 with 80% coverage |
| build | uv build wheel + sdist validation |
| lockfile | uv lock --check ensures lockfile is in sync |
| install-test | Clean wheel install + import smoke-test (catches undeclared deps) |
| pip-audit | Dependency vulnerability scanning |
| bandit | Python security static analysis |
| docs | Documentation site build |
Test conventions
- Framework: pytest + anyio for async tests
- Coverage: 80% threshold enforced in
pyproject.toml - Patterns: Stub subprocess runners with fake CLI scripts, mock transport with
FakeTransportdataclass - Key test files:
test_claude_control.py(56 tests),test_callback_dispatch.py(28 tests),test_cost_tracker.py(56 tests)
Run specific test files:
uv run pytest tests/test_claude_control.py -x # stop on first failure
uv run pytest tests/test_export_command.py -v # verbose output
uv run pytest -k "test_approve" # run tests matching pattern
Promoting to production
Only after code is merged and released to PyPI:
# Upgrade the package
uv tool upgrade untether # or: pipx upgrade untether
# Restart to apply:
/restart # from Telegram (preferred — drains active runs)
# Or from terminal: Ctrl+C first if running, then: untether
# Or on Linux with systemd: systemctl --user restart untether
Graceful restart Sending
/restartin Telegram lets active runs finish before the service exits. This avoids interrupting in-progress tasks.
Branch naming
Follow conventional branch names:
feature/*— new featuresfix/*— bug fixesdocs/*— documentation changes
Related
- Troubleshooting — common issues and debug mode
- Operations and monitoring —
/ping,/restart, hot-reload - Contributing guide — full contribution guidelines