# `MingaAgent.ToolRouter`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_agent/tool_router.ex#L1)

Routes file tool operations through buffer forks and changesets.

Tools call this module instead of directly doing filesystem I/O.
The routing decision tree for each file operation:

1. Is there an open buffer for this path AND a fork store is active?
   YES: route through Buffer.Fork (in-memory, instant, undo integration)
2. Is there an active changeset?
   YES: route through Changeset overlay (filesystem-level isolation)
3. Neither: fall through to direct I/O (returns `:passthrough`)

Buffer.Fork handles files the user has open in a buffer. Changeset
handles files that aren't open, or external tools that need a coherent
filesystem view. A session can use both simultaneously.

This module is stateless. The fork store pid and changeset pid come
from the caller (stored in session state, captured in tool closures).

# `changeset`

```elixir
@type changeset() :: pid() | nil
```

Changeset reference (nil when changeset is disabled).

# `context`

```elixir
@type context() :: %{fork_store: fork_store(), changeset: changeset()}
```

Routing context passed by tool callbacks.

# `fork_store`

```elixir
@type fork_store() :: pid() | nil
```

Fork store reference (nil when fork routing is disabled).

# `active?`

```elixir
@spec active?(context()) :: boolean()
```

Returns true if any routing is active (fork store or changeset).

# `context`

```elixir
@spec context(fork_store(), changeset()) :: context()
```

Builds a routing context from the fork store and changeset pids.

# `delete_file`

```elixir
@spec delete_file(context(), String.t()) :: :ok | :passthrough | {:error, term()}
```

Deletes a file, routing through changeset if active.

Buffer forks don't support deletion (you can't delete an open buffer
through a fork). Falls through to changeset or passthrough.

# `edit_file`

```elixir
@spec edit_file(context(), String.t(), String.t(), String.t()) ::
  :ok | :passthrough | {:error, term()}
```

Edits a file by find-and-replace, routing through fork or changeset.

# `has_forks?`

```elixir
@spec has_forks?(context()) :: boolean()
```

Returns true if a fork store is active and has forks.

# `read_file`

```elixir
@spec read_file(context(), String.t()) :: {:ok, binary()} | {:error, term()}
```

Reads a file, routing through fork or changeset if active.

Forks take priority: if there's a fork for this path, read from it.
Otherwise try the changeset. If neither, fall through to buffer/filesystem.

# `working_dir`

```elixir
@spec working_dir(context()) :: String.t() | nil
```

Returns the working directory for shell commands.

Returns the changeset overlay directory if active, nil otherwise.
(Buffer forks don't affect the filesystem view for shell commands.)

# `write_file`

```elixir
@spec write_file(context(), String.t(), binary()) ::
  :ok | :passthrough | {:error, term()}
```

Writes a file, routing through fork or changeset if active.

If a buffer is open for this path and a fork store exists, creates
a fork (lazily) and writes to it. Otherwise tries changeset.
Returns `:passthrough` if neither is active.

---

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