# `PromptRunner.Watch`
[🔗](https://github.com/nshkrdotcom/prompt_runner_sdk/blob/v0.12.1/lib/prompt_runner/watch.ex#L1)

Supervision for a long unattended packet run.

One compact line per interval, of raw facts:

```text
WATCH 16:57Z runner=UP prompt=11 quiet=0min repos=3 dirty=0 commits=27
```

- `runner` — `UP` when `.prompt_runner/run.pid` names the same process
  identity that acquired the packet lock. On Linux the identity includes
  `/proc` start time, so PID reuse cannot revive a dead run. 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.

# `sample`

```elixir
@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()
}
```

# `format_json`

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

Renders a sample as one JSON object.

# `format_line`

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

Renders a sample as the compact one-line form.

# `interval_seconds`

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

Returns the interval in seconds resolved from `opts`.

# `run`

```elixir
@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`

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

Collects one sample of the packet's supervision facts.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
