{"slug": "show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies", "title": "Show HN: Supervice – process supervisor for agentic processes. zero dependencies", "summary": "Supervice, a zero-dependency, pure-Python process supervisor for Unix-like systems built on asyncio, has been released on GitHub, offering automatic restart, health checks, process grouping, hot config reload, and a Unix socket RPC interface for Python 3.10+ environments. The tool supports daemon mode, graceful shutdown, process group kill, log rotation, uptime tracking, retry with backoff, and user switching, with a command-line interface (supervicectl) for status, start/stop/restart, group operations, and hot reload. It is positioned as a production-ready alternative to traditional supervisors like supervisord, with no external dependencies beyond the Python standard library.", "body_md": "A modern, lightweight, and fully async process supervisor for Unix-like systems.\n**Zero dependencies. Pure Python. Production-ready.**\n\nSupervice manages long-running processes with automatic restart, health checking,\nprocess grouping, hot config reload, and a Unix socket RPC interface — all built\non Python's `asyncio`\n\nwith no external packages.\n\n**Async-first**— Built entirely on`asyncio`\n\nfor efficient I/O multiplexing**Zero dependencies**— Pure Python stdlib; nothing to install beyond Python 3.10+** Process groups**— Organize related processes and control them as a unit** Health checks**— TCP connectivity and script-based health monitoring with auto-restart** Hot reload**— Add/remove programs without restarting the daemon (`supervicectl reload`\n\n)**Daemon mode**— Proper double-fork daemonization with PID file locking** Graceful shutdown**— SIGTERM/SIGINT triggers orderly stop of all child processes** Process group kill**— Stops entire process trees, not just the main PID** Log rotation**— Built-in`RotatingFileHandler`\n\nwith configurable size and backup count**Uptime tracking**— Per-process wall-clock uptime displayed in status output** Retry with backoff**— Configurable start retries with automatic FATAL state on exhaustion** User switching**— Run processes as a specific user (requires root)** Type-safe**— Fully type-hinted, passes`mypy --strict`\n\n**Requirements:** Python 3.10+ on a Unix-like OS (Linux, macOS).\n\n```\n# From source\ngit clone https://github.com/yourusername/supervice.git\ncd supervice\npip install .\n\n# Development install (includes docs dependencies)\npip install -e \".[docs]\"\n# supervisord.conf\n[supervice]\nloglevel = INFO\nlogfile = supervice.log\npidfile = supervice.pid\n\n[program:webapp]\ncommand = python3 -u app.py\nautostart = true\nautorestart = true\nstdout_logfile = webapp.log\nstderr_logfile = webapp_err.log\n\n[program:worker]\ncommand = python3 -u worker.py\nnumprocs = 4\nautostart = true\nautorestart = true\nstdout_logfile = worker_%(process_num)s.log\nstderr_logfile = worker_err_%(process_num)s.log\n# Foreground (development)\nsupervice -c supervisord.conf -n\n\n# Background (production — default)\nsupervice -c supervisord.conf\n# Check status\nsupervicectl status\n\n# Output:\n# NAME                 STATE      PID        UPTIME\n# --------------------------------------------------------\n# webapp               RUNNING    12345      1:23:45\n# worker:00            RUNNING    12346      1:23:44\n# worker:01            RUNNING    12347      1:23:44\n# worker:02            RUNNING    12348      1:23:44\n# worker:03            RUNNING    12349      1:23:44\n\n# Start / stop / restart individual processes\nsupervicectl stop worker:00\nsupervicectl start worker:00\nsupervicectl restart worker:00\nsupervicectl restart worker:00 --force   # SIGKILL instead of graceful\n\n# Group operations\nsupervicectl stopgroup workers\nsupervicectl startgroup workers\n\n# Hot reload (add/remove programs without restart)\nsupervicectl reload\n\n# Use a custom socket path\nsupervicectl -s /var/run/supervice.sock status\n```\n\n| Option | Default | Description |\n|---|---|---|\n`logfile` |\n(stdout) |\nDaemon log file; empty logs to stdout in foreground, `supervice.log` when daemonized |\n`loglevel` |\n`INFO` |\nLog level: `DEBUG` , `INFO` , `WARNING` , `ERROR` , `CRITICAL` |\n`pidfile` |\n`supervice.pid` |\nPath to the PID/lock file; set to `none` (or empty) to disable |\n`socket` |\n(runtime dir) |\nRPC socket; defaults to `$XDG_RUNTIME_DIR/supervice.sock` (root: `/run/supervice.sock` , else `~/.supervice.sock` ) |\n`shutdown_timeout` |\n`30` |\nSeconds to wait for graceful shutdown |\n`log_maxbytes` |\n`52428800` |\nMax log file size before rotation (bytes, 0 = no rotation) |\n`log_backups` |\n`10` |\nNumber of rotated log backup files to keep |\n\n| Option | Default | Description |\n|---|---|---|\n`command` |\n(required) |\nCommand to execute (supports shell-style quoting) |\n`numprocs` |\n`1` |\nNumber of instances to run (creates `NAME:00` , `NAME:01` , ...) |\n`autostart` |\n`true` |\nStart automatically when daemon starts |\n`autorestart` |\n`true` |\nRestart automatically when process exits |\n`startsecs` |\n`1` |\nSeconds a process must run to be considered successfully started |\n`startretries` |\n`3` |\nMax consecutive start attempts before entering FATAL state |\n`stopsignal` |\n`TERM` |\nSignal to send when stopping (`TERM` , `INT` , `QUIT` , `KILL` , etc.) |\n`stopwaitsecs` |\n`10` |\nSeconds to wait after stop signal before sending SIGKILL |\n`stdout_logfile` |\n(none) |\nFile for stdout (rotated by the daemon; supports `%(process_num)s` ) |\n`stderr_logfile` |\n(none) |\nFile for stderr (rotated by the daemon; supports `%(process_num)s` ) |\n`stdout_logfile_maxbytes` / `stderr_logfile_maxbytes` |\n`50MB` |\nChild log rotation threshold (0 disables) |\n`stdout_logfile_backups` / `stderr_logfile_backups` |\n`10` |\nRotated child log backups to keep |\n`pdeathsig` |\n`true` |\nLinux/FreeBSD: SIGKILL the direct child if the supervisor dies. One generation only — grandchildren are never covered; see\n|\n`reconcile` |\n`auto` |\nOrphans of a crashed supervisor found at startup: `auto` , `kill` , `warn` , `off` . Matches on identity, not pid — see\n|\n`environment` |\n(none) |\nEnvironment variables: `KEY=VAL,KEY2=\"val with,comma\"` |\n`env_file` |\n(none) |\nComma-separated `KEY=VALUE` secrets files (`#` comments, quotes stripped); read as the supervisor before the privilege drop. Later files win; `environment` overrides `env_file` |\n`directory` |\n(none) |\nWorking directory for the process |\n`user` |\n(none) |\nRun as this user (requires root privileges) |\n\n| Option | Default | Description |\n|---|---|---|\n`programs` |\n(required) |\nComma-separated list of program names |\n\n| Option | Default | Description |\n|---|---|---|\n`healthcheck_type` |\n`none` |\nHealth check type: `none` , `tcp` , `script` |\n`healthcheck_interval` |\n`30` |\nSeconds between health checks |\n`healthcheck_timeout` |\n`10` |\nSeconds to wait for health check response |\n`healthcheck_retries` |\n`3` |\nConsecutive failures before marking unhealthy |\n`healthcheck_start_period` |\n`10` |\nSeconds to wait before starting health checks |\n`healthcheck_port` |\n(none) |\nTCP port to check (required for `tcp` type) |\n`healthcheck_host` |\n`127.0.0.1` |\nTCP host to check |\n`healthcheck_command` |\n(none) |\nScript to run (required for `script` type) |\n\n**Example with health checks:**\n\n```\n[program:api]\ncommand = python3 -u api_server.py\nautostart = true\nautorestart = true\nhealthcheck_type = tcp\nhealthcheck_port = 8080\nhealthcheck_interval = 15\nhealthcheck_retries = 3\nhealthcheck_start_period = 5\nSTOPPED ──┐\nEXITED  ──┼──> STARTING ──> RUNNING ──> STOPPING ──> STOPPED\nFATAL   ──┤                    │                        │\nBACKOFF ──┘                    │                     EXITED\n                               ▼\n                           UNHEALTHY (health check failures)\n                               │\n                               ▼\n                         auto-restart (if autorestart=true)\n```\n\n| State | Description |\n|---|---|\n`STOPPED` |\nProcess is not running (initial or manually stopped) |\n`STARTING` |\nProcess has been spawned, waiting for confirmation |\n`RUNNING` |\nProcess is running and healthy |\n`BACKOFF` |\nProcess exited too quickly, waiting before retry |\n`STOPPING` |\nStop signal sent, waiting for process to exit |\n`EXITED` |\nProcess has exited (normally or abnormally) |\n`FATAL` |\nProcess failed to start after exhausting retries |\n`UNHEALTHY` |\nProcess is running but health checks are failing |\n\n```\n┌─────────────────────────────────────────────────┐\n│                   supervice                      │\n│                                                  │\n│  ┌──────────┐    ┌───────────┐    ┌──────────┐  │\n│  │  Config   │───▶│ Supervisor │───▶│ Process  │  │\n│  │  Parser   │    │   (core)   │    │ Manager  │  │\n│  └──────────┘    └─────┬─────┘    └────┬─────┘  │\n│                        │               │         │\n│                   ┌────▼────┐    ┌─────▼─────┐  │\n│                   │   RPC    │    │  EventBus  │  │\n│                   │  Server  │    │  (pub/sub) │  │\n│                   └────┬────┘    └───────────┘  │\n│                        │                         │\n└────────────────────────┼─────────────────────────┘\n                         │ Unix Socket\n                    ┌────▼────┐\n                    │supervice│\n                    │  ctl    │\n                    └─────────┘\n```\n\n| Platform | Status |\n|---|---|\n| Linux | First-class — full feature set, including `pdeathsig` via `prctl(2)` (direct child only) |\n| FreeBSD | Supported (15.x, 13.x) — all features, including `pdeathsig` via `procctl(2)` (direct child only); see the FreeBSD notes below |\n| macOS | Supported for supervision, without — no kernel equivalent exists; supervice logs a warning if you request it. Children survive an abrupt supervisor kill`pdeathsig` |\n\nField notes from the first production FreeBSD deployment live in\n[ PORTABILITY-FREEBSD.md](/rodmena-limited/supervice/blob/main/PORTABILITY-FREEBSD.md).\n\nRun the supervisor itself as root from `rc.d`\n\nwith **no** `${name}_user`\n\nand\n**no** `daemon -u`\n\n— FreeBSD's `rc.subr`\n\nwraps the whole command in `su -m`\n\nwhen `${name}_user`\n\nis set, so combining it with `daemon -u <user>`\n\nruns\n`setuid`\n\ntwice and fails with `EPERM`\n\n(and `daemon -f`\n\nswallows the error).\nLet supervice drop privileges per program with its `user =`\n\ndirective\ninstead.\n\nA worked `/usr/local/etc/rc.d/supervice`\n\nunit (adapted from production):\n\n``` bash\n#!/bin/sh\n# PROVIDE: supervice\n# REQUIRE: LOGIN\n# KEYWORD: shutdown\n\n. /etc/rc.subr\n\nname=\"supervice\"\nrcvar=\"supervice_enable\"\n\nload_rc_config $name\n\ncommand=\"/usr/local/bin/supervice\"\ncommand_args=\"-c /usr/local/etc/supervice.ini\"\n\nrun_rc_command \"$1\"\n; /usr/local/etc/supervice.ini\n[supervice]\nlogfile = /var/log/supervice/supervice.log\n; daemon(8) is NOT used with -p here; let supervice own its pidfile.\n; If an outer supervisor already owns it, use: pidfile = none\npidfile  = /var/run/supervice/supervice.pid\nsocket   = /var/run/supervice/supervice.sock\n\n[program:api]\ncommand = /usr/local/bin/myapp\ndirectory = /usr/local/myapp\nuser = myapp\nstartsecs = 3\nstartretries = 3\nstopsignal = TERM\nstopwaitsecs = 10\nhealthcheck_type = tcp\nhealthcheck_port = 8080\n```\n\nNotes for FreeBSD operators:\n\n- If something else (e.g.\n`daemon(8) -p`\n\n) already writes the pidfile, set`pidfile = none`\n\n—`daemon(8)`\n\nwrites it as root before dropping privileges, so a second writer fails with`EPERM`\n\n/`EACCES`\n\n. - If you run supervice under\n`daemon(8)`\n\n, start it with(restart on death) and`-r`\n\n(pidfile holds daemon's`-P`\n\n*own*pid, not the child's): pointing the pidfile at the child means`service stop`\n\nkills supervice and daemon immediately restarts it. - The pidfile and socket parent directories must exist and be writable before\n`supervice`\n\nstarts; config load now fails with a clear message otherwise. - Export\n`HOME`\n\nif your program reads client certificates from`$HOME/.postgresql`\n\nor similar (asyncpg does; a wrapper script can set it).\n\nmacOS has no kernel pdeathsig equivalent. `pdeathsig = true`\n\nis accepted but\ninactive, and supervice logs one warning at config load naming the affected\nprograms.\n\n**What this means in practice.** There are two separate guarantees, and macOS\nhas one of them:\n\n| Linux / FreeBSD | macOS | |\n|---|---|---|\nChild dies when the supervisor is killed |\nyes (`pdeathsig` , direct child only) |\nno |\nOrphan is cleaned up at the next start |\nyes (`reconcile` ) |\nyes (`reconcile` ) |\n\nSo after an abrupt kill (`SIGKILL`\n\n, OOM, panic) the children keep running until\nyou start supervice again. **That window is real and unbounded** — if the crash\nhappens at 02:00 and nobody restarts until morning, a queue consumer keeps\nconsuming for those hours. There is no kernel mechanism on macOS to close it;\nthe honest mitigation is to make restarts fast and automatic (a `launchd`\n\n`KeepAlive`\n\njob), not to assume the gap is small.\n\nWhat macOS *does* get is that the restart no longer makes it worse.\nReconciliation identifies the orphans and kills them before spawning\nreplacements, so a crash-restart cycle does not accumulate duplicate workers.\nMeasured on Darwin arm64: without it, four orphans became five across a restart;\nwith it, the count returns to one.\n\nThis replaces the `pkill -u ... -f`\n\nlauncher snippet previously recommended\nhere, which matched on a command string rather than on identity and would kill\nany process whose command line merely looked similar. If you still use a\nlauncher, note the `exec`\n\n— without it the supervisor is a child of the script,\nadding a generation. The same rule applies to your own program wrappers; see\n[ pdeathsig scope](/rodmena-limited/supervice/blob/main/docs/configuration.md#pdeathsig-scope).\n\n**Testing orphan behaviour on macOS:** use a **silent** child. A program with a\n`stdout_logfile`\n\nis reaped by `SIGPIPE`\n\nwhen the supervisor dies — by accident,\nnot by pdeathsig — so the obvious test passes while the guarantee is absent.\n`python3 tests/orphan_harness.py`\n\ndoes this correctly and keeps the\nfalse-positive case beside it.\n\nFull documentation is available at [supervice.readthedocs.io](https://supervice.readthedocs.io).\n\n```\n# Run tests\npython3 -m pytest tests/ -v\n\n# Type checking (strict mode)\nmypy --strict supervice/\n\n# Linting\nruff check supervice/\n\n# Formatting\nruff format supervice/\n\n# Build documentation locally\npip install -e \".[docs]\"\ncd docs && make html\n```\n\nApache License 2.0 — see [LICENSE](/rodmena-limited/supervice/blob/main/LICENSE) for details.", "url": "https://wpnews.pro/news/show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies", "canonical_source": "https://github.com/rodmena-limited/supervice", "published_at": "2026-08-15 03:48:15+00:00", "updated_at": "2026-08-15 04:11:40.523513+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Supervice", "Python", "asyncio", "GitHub", "supervicectl", "Unix"], "alternates": {"html": "https://wpnews.pro/news/show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies", "markdown": "https://wpnews.pro/news/show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies.md", "text": "https://wpnews.pro/news/show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies.txt", "jsonld": "https://wpnews.pro/news/show-hn-supervice-process-supervisor-for-agentic-processes-zero-dependencies.jsonld"}}