# `CodexWrapper.Runner`
[🔗](https://github.com/genagent/codex_wrapper_ex/blob/v0.5.3/lib/codex_wrapper/runner.ex#L1)

How one-shot `codex` subprocesses are executed.

`Command.run/3` routes every synchronous command (`Exec`, `ExecResume`,
`Review`, `CodexWrapper.exec/2`) through the configured runner. The
default is `CodexWrapper.Runner.Port`, the `/bin/sh`-with-closed-stdin
`Port` the library has always used.

For leak-free execution -- where a timeout or BEAM death kills the whole
`codex` 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.3.5"}

    # config/config.exs
    config :codex_wrapper, runner: CodexWrapper.Runner.Forcola

`Runner.Forcola` only compiles when `forcola` is present, so the
dependency stays optional. See #48.

## Contract

`run/4` returns `{:ok, {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/signal/io failures.

`stream_lines/4` returns a lazy `Enumerable` of stdout lines for the
NDJSON paths (`Exec.stream/2`, `ExecResume.stream/2`, `Review.stream/2`,
and `Session.stream/3` through the first two). Lines arrive without
their trailing newline, stderr is never merged into them, and the
stream ends when the process exits.

A non-zero exit ends the stream *without* raising, on either runner --
the same silent-halt the streaming paths have always had. Callers that
need the exit code should use `execute/2` rather than `stream/2`.

# `error`

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

Runner error reasons. `:timeout` is common to every runner.

# `opts`

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

Execution options, as produced by `CodexWrapper.Config.cmd_opts/1`:

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

# `effective_timeout`
*optional* 

```elixir
@callback effective_timeout(timeout :: timeout() | nil) :: timeout() | nil
```

The timeout the runner will actually enforce for a caller's `timeout`.

Callers pass `nil` for "no timeout", but a runner may substitute a bound
of its own (`Runner.Forcola` does, since forcola requires a finite one).
`Command.run/3` uses this to report the timeout that actually elapsed.
Optional; defaults to the caller's timeout unchanged.

# `run`

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

# `stream_lines`
*optional* 

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

Run `binary` with `args` and stream its stdout as lines.

Returns a lazy `Enumerable` of lines without their trailing newlines.
Opening the process is deferred until the stream is consumed, and the
process is terminated when the stream halts (including an early
`Enum.take/2`).

`timeout` is enforced as each runner can: `Runner.Port` applies it as
an idle bound between output frames (what the streaming paths have
always done), `Runner.Forcola` as forcola's whole-run bound with a
process-group kill. `nil` means each runner's own default.

Optional. `CodexWrapper.Runner.stream_lines/4` falls back to
`Runner.Port` for runners that do not implement it.

# `effective_timeout`

```elixir
@spec effective_timeout(module(), timeout() | nil) :: timeout() | nil
```

`effective_timeout/1` on `runner`, falling back to `timeout` unchanged
when the runner does not implement the optional callback.

# `impl`

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

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

Set with a module:

    config :codex_wrapper, runner: CodexWrapper.Runner.Forcola

or with the `:task` / `:forcola` shorthand for the two built-in runners:

    config :codex_wrapper, runner: :forcola

`:forcola` resolves to `Runner.Port` when the `forcola` dependency is
absent, so the shorthand is safe to set unconditionally.

# `stream_lines`

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

`stream_lines/4` on the configured runner.

Falls back to `CodexWrapper.Runner.Port` when the configured runner
does not implement the optional callback, so a runner written against
the one-shot `run/4` contract alone keeps working.

---

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