Supervision for a long unattended packet run.
One compact line per interval, of raw facts:
WATCH 16:57Z runner=UP prompt=11 quiet=0min repos=3 dirty=0 commits=27runner—UPwhen.prompt_runner/run.pidnames a process that still exists. The runner writes that file for the duration of a run. Liveness is deliberately not a process-name match: such a pattern matches any command line containing it, including the supervisor's own shell, and an earlier implementation of exactly this check reported a healthy run forever.prompt— the id in the newestprompt-*.logunder the packet's log directory, ornone.quiet— minutes since the newest file mtime across the packet's log directory and every configured repository. Mtime, not JSON: the event schema differs betweenevents_mode: compact({"t": epoch_ms}) andfull({"ts": "ISO8601"}), and an earlier implementation parsed one of them and silently reported zero quiet time for the other. An mtime cannot be the wrong schema.?means no file was found to measure.repos,dirty,commits— the number of configured repositories, the totalgit status --porcelainline count across them, and the total number of commits reachable from eachHEAD.
Nothing here decides anything. It reports what is on the machine, in a shape a human or an agent can read at a glance, and lets the reader judge. A watcher that greps for known failure signatures only catches failures someone predicted, and its silence is indistinguishable from health.
What The Quiet Scan Skips
.git, _build, deps, node_modules.
All four are derived output or internal bookkeeping whose mtimes say nothing about whether a session is making progress, and on a large repository they dominate the walk — a build directory alone can outnumber the source tree by an order of magnitude. Pruning them trades a rarer false "quiet" for a scan that stays cheap enough to run on an interval, which is the right trade: the 15-minute default and a generous staleness threshold absorb the former, and an O(repo) walk every interval is a cost that never goes away.
Summary
Functions
Renders a sample as one JSON object.
Renders a sample as the compact one-line form.
Returns the interval in seconds resolved from opts.
Emits one sample per interval until the process is stopped.
Collects one sample of the packet's supervision facts.
Types
@type sample() :: %{ packet: String.t(), root: String.t(), timestamp: String.t(), runner: :up | :down, pid: pos_integer() | nil, prompt: String.t() | nil, quiet_minutes: non_neg_integer() | nil, repos: non_neg_integer(), dirty: non_neg_integer(), commits: non_neg_integer() }
Functions
Renders a sample as one JSON object.
Renders a sample as the compact one-line form.
@spec interval_seconds(keyword()) :: pos_integer()
Returns the interval in seconds resolved from opts.
Emits one sample per interval until the process is stopped.
Options:
:interval— seconds between samples (default 900).:once— emit a single sample and return.:json— emit each sample as one JSON object instead of the compact line.
Collects one sample of the packet's supervision facts.