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

Watch a live run, and change how it renders, without attaching to it.

A packet run used to be a black box while it ran: you could read its output
and you could kill it, and that was the whole interface. This is the other
half — a process-addressable API with no IO, no terminal, and no assumption
about who is calling. The CLI's `control` commands are written entirely
against this module, and a Phoenix LiveView would use exactly the same
functions.

## Addressing a run

A `run_ref` is `{packet_dir, run_id}`. Explicit ids rather than an implicit
"current run" cost nothing now and avoid a rewrite if the runner ever goes
concurrent. `current_run/1` finds the newest one for a packet:

    {:ok, run_ref} = PromptRunner.Control.current_run("packets/demo")
    {:ok, snapshot} = PromptRunner.Control.snapshot(run_ref)

## Reading

`snapshot/1` and `log/1` read files the runner writes. They never touch the
session, so polling them cannot slow, block, or crash the run.

## Writing

`set_view/2` writes a request into `control/requests/`. The runner consumes
it at an event boundary — never mid-event — applies it, and records the
outcome in `control/log.jsonl`. `:ok` means the request was accepted for
delivery, not that it has been applied yet; the log is where the outcome
lives. That asynchrony is deliberate: it is what stops an in-VM caller from
quietly getting a privileged synchronous path the CLI does not have.

## Subscribing

`subscribe/3` follows the run's canonical event stream:

    {:ok, ref} = PromptRunner.Control.subscribe(run_ref, self())

    receive do
      {:prompt_runner_event, ^ref, event} -> IO.inspect(event["type"])
      {:prompt_runner_control, ^ref, {:run_finished, status}} -> status
    end

Events arrive as they were written, so maps with string keys.

# `run_ref`

```elixir
@type run_ref() ::
  {store_root :: PromptRunner.Control.Store.root(), run_id :: String.t()}
```

# `amend`

```elixir
@spec amend(run_ref() | String.t() | PromptRunner.Plan.t(), String.t(), keyword()) ::
  :ok | {:error, term()}
```

Adds a requirement to a prompt's verify contract.

Amendment changes what "done" means. It is the one capability here that can
make a completed prompt mean something other than what the packet says, so it
is governed more tightly than steering, which cannot.

Adding a requirement is the routine direction. Removing or relaxing one takes
`relax/4`, a different verb with an explicit confirmation — never a different
argument to this one.

Run-local: the packet file stays authoritative and a future re-run from clean
state uses the original contract. `persist: true` writes it back, which is a
separate explicit act because a packet is a versioned artifact and editing it
is a commit, not a side effect.

Required options:

- `:clause` — one of `PromptRunner.Verifier.contract_keys/0`
- `:entries` — what to add to that clause
- `:reason` — mandatory. An amendment with no stated reason is refused, not
  defaulted.

Optional:

- `:author` — defaults to the OS user
- `:persist` — also write the change back to the packet file

# `contract`

```elixir
@spec contract(String.t() | PromptRunner.Plan.t(), String.t()) ::
  {:ok, map()} | {:error, term()}
```

The packet's contract for a prompt, the contract actually being enforced, and
the difference between them.

If you cannot show the diff, you do not have the audit.

# `current_run`

```elixir
@spec current_run(PromptRunner.Control.Store.root()) ::
  {:ok, run_ref()} | {:error, term()}
```

The newest run recorded for `packet_dir`, running or not.

Returns `{:error, :no_run}` when the packet has no control directory yet,
which is the ordinary state of a packet nothing has ever run.

# `log`

```elixir
@spec log(run_ref()) :: {:ok, [PromptRunner.Control.Entry.t()]}
```

Every command the plane has seen for this packet, oldest first.

Includes refused commands. A refusal that leaves no trace is
indistinguishable from a command that was never sent.

# `relax`

```elixir
@spec relax(run_ref() | String.t() | PromptRunner.Plan.t(), String.t(), keyword()) ::
  :ok | {:error, term()}
```

Removes or weakens a requirement in a prompt's verify contract.

The risky direction, and deliberately a different verb. Requires
`confirm: true`; without it this refuses and says why.

An amendment that weakens a contract *after* a verify failure is exactly the
move pre-registration exists to prevent, so the record says when it happened
relative to verification. It is not forbidden — sometimes a requirement was
simply wrong — but it is never quiet.

# `set_view`

```elixir
@spec set_view(run_ref(), map() | keyword(), keyword()) :: :ok | {:error, term()}
```

Changes how a running run renders, from outside it.

Accepts `log_mode`, `tool_output`, `thinking`, and `diff`. These are already renderer
state; this makes them mutable at runtime rather than only at launch. An
unknown key or value is refused here rather than written and ignored.

Options:

- `:author` — recorded on the log entry, defaults to the OS user

# `snapshot`

```elixir
@spec snapshot(run_ref()) ::
  {:ok, PromptRunner.Control.Snapshot.t()} | {:error, term()}
```

The current state of the run: prompt, attempt, mode, elapsed, provider,
model, tool count, token totals, and the view settings in force.

# `steer`

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

Says something to the agent while it is working.

Steering changes *how* the agent works toward an unchanged definition of
done — "you're down a rabbit hole, check `dependency_sources.exs` before you
keep editing mix files". The verify contract is untouched: the prompt still
passes or fails on exactly the criteria it started with, which is what makes
steering safe to allow freely and amendment (`amend/4`) not.

A steer is never evidence. A contract asserting a document contains X is not
satisfied by a human having said "put X in the doc" — the verifier sees what
the session produced, not what it was told.

A steer is always recorded, twice: on the control log, and as an append-only
artifact at `packet/.prompt_runner/interventions/<prompt>.jsonl` that is
committed with the work. The prompt's result records that it was steered and
how many times, so a human-guided result is distinguishable from an
autonomous one — flagged, not disqualified.

Options:

- `:author` — recorded on both records, defaults to the OS user

# `subscribe`

```elixir
@spec subscribe(run_ref(), pid(), keyword()) :: {:ok, reference()} | {:error, term()}
```

Sends this run's events to `pid` until the run finishes or `pid` dies.

Options:

- `:from` — `:start` (default) replays the run from its first event, then
  follows; `:current` delivers only what arrives after subscribing
- `:interval_ms` — poll interval, default 200

# `unsubscribe`

```elixir
@spec unsubscribe(run_ref(), reference()) :: :ok
```

Stops a subscription started by `subscribe/3` from the same process.

A subscription also ends on its own when the run finishes or the subscribing
process dies, so this is for a consumer that wants to stop watching earlier.

---

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