PromptRunner.Watch (PromptRunnerSDK v0.9.0)

Copy Markdown View Source

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=27
  • runnerUP when .prompt_runner/run.pid names 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 newest prompt-*.log under the packet's log directory, or none.
  • quiet — minutes since the newest file mtime across the packet's log directory and every configured repository. Mtime, not JSON: the event schema differs between events_mode: compact ({"t": epoch_ms}) and full ({"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 total git status --porcelain line count across them, and the total number of commits reachable from each HEAD.

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

sample()

@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

format_json(sample)

@spec format_json(map()) :: String.t()

Renders a sample as one JSON object.

format_line(sample)

@spec format_line(map()) :: String.t()

Renders a sample as the compact one-line form.

interval_seconds(opts)

@spec interval_seconds(keyword()) :: pos_integer()

Returns the interval in seconds resolved from opts.

run(packet_dir, opts \\ [])

@spec run(
  String.t(),
  keyword()
) :: :ok | {:error, term()}

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.

sample(packet_dir, opts \\ [])

@spec sample(
  String.t(),
  keyword()
) :: {:ok, sample()} | {:error, term()}

Collects one sample of the packet's supervision facts.