# `ClaudeWrapper.Runner`
[🔗](https://github.com/genagent/claude_wrapper_ex/blob/v0.15.2/lib/claude_wrapper/runner.ex#L1)

How `claude` subprocesses are executed.

Two shapes cover the non-duplex paths: a bounded one-shot `run/4` and
an NDJSON `stream_lines/4`. The default implementation is
`ClaudeWrapper.Runner.Port`, the `System.cmd`/`Port` code the library
has always used.

For leak-free execution -- where a timeout, a halted stream, or BEAM
death kills the whole `claude` process group (the CLI and every stdio
MCP server it spawned) rather than abandoning it -- add
[`forcola`](https://hex.pm/packages/forcola) to your deps and select
its runner:

    # mix.exs
    {:forcola, ">= 0.4.0 and < 0.7.0"}

    # config/config.exs
    config :claude_wrapper, runner: ClaudeWrapper.Runner.Forcola

`Runner.Forcola` only compiles when `forcola` is present, so the
dependency stays optional. See the "Error handling" and leak-free
sections of the README, and #185.

`run_observed/5` is optional. It runs to transport completion while calling
a trusted, nonblocking line observer with stdout lines only. The observer
returns `:continue`, `:observed` to stop observing, or
`{:continue, next_observer}` to replace itself with an observer carrying
updated state. The runner must not call it again after `:observed`. It must execute synchronously
in the process calling `run_observed/5`, before that function returns.
This is an internal execution
seam, not an API for caller-supplied callbacks.

Observed output is `{stdout, exit_code, stderr}` with newline-normalized
stdout. Stderr stays separate and must never be passed to the observer.
Forcola captures stderr on nonzero completion; clean completion returns an
empty stderr field. The same whole-run deadline and cleanup as `run/4`
apply. Unsupported runners are rejected by `Query.execute/3` before spawn.

## Contract

`run/4` returns `System.cmd/3`'s `{stdout, exit_code}` on completion
(a non-zero exit is *not* an error -- callers decide what an exit code
means), `{:error, :timeout}` when the timeout elapsed, and other
`{:error, reason}` tuples for spawn/io failures. `stream_lines/4`
returns a lazy `Enumerable` of complete stdout lines (no trailing
newline); the caller parses each line.

# `error`

```elixir
@type error() :: :timeout | {:signal, term()} | {:spawn, term()} | {:io, term()}
```

Runner error reasons. `:timeout` is common to both runners.

# `line_observer`

```elixir
@type line_observer() :: (String.t() -&gt;
                      :continue | :observed | {:continue, line_observer()})
```

A trusted stdout line observer constructed by Query.

# `opts`

```elixir
@type opts() :: keyword()
```

Execution options, a subset of `System.cmd/3`'s:

  * `:cd` -- working directory (string) or `nil`
  * `:env` -- list of `{name, value}` string tuples
  * `:stderr_to_stdout` -- merge stderr into stdout (default `false`)

# `run`

```elixir
@callback run(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil
) ::
  {:ok, {String.t(), non_neg_integer()}} | {:error, error()}
```

# `run_observed`
*optional* 

```elixir
@callback run_observed(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil,
  observer :: line_observer()
) :: {:ok, {String.t(), non_neg_integer(), String.t()}} | {:error, error()}
```

# `stream_lines`

```elixir
@callback stream_lines(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil
) :: Enumerable.t()
```

# `impl`

```elixir
@spec impl() :: module()
```

The configured runner module, `ClaudeWrapper.Runner.Port` by default.

Set with `config :claude_wrapper, runner: ClaudeWrapper.Runner.Forcola`.

---

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