Packages

fnord

0.9.37

AI code archaeology

Current section

Files

Jump to
fnord lib ai tools reviewer.ex
Raw

lib/ai/tools/reviewer.ex

defmodule AI.Tools.Reviewer do
@moduledoc """
Tool entry point for the review agent pipeline. Delegates to
`AI.Agent.Review.Decomposer`, which triages changes by complexity, partitions
large diffs into focused review units, and fans out scoped Reviewers - each
running five specialists (pedantic, acceptance, state flow, no-slop,
breadcrumbs) - before synthesizing a deduplicated final report.
Always available - reviews are read-only operations.
"""
@behaviour AI.Tools
@impl AI.Tools
def async?, do: true
@impl AI.Tools
def is_available?, do: true
@impl AI.Tools
def read_args(args), do: {:ok, args}
@impl AI.Tools
def ui_note_on_request(%{"scope" => scope}) do
{"Starting code review", scope}
end
@impl AI.Tools
def ui_note_on_result(_args, result) do
{"Review complete", result}
end
@impl AI.Tools
def tool_call_failure_message(_args, reason), do: reason
@impl AI.Tools
def spec do
%{
type: "function",
name: "reviewer_tool",
description: """
AI-powered, multi-agent code review.
Triages change complexity, decomposes large diffs into focused review
units, and fans out scoped multi-specialist reviewers in parallel.
Produces a unified, deduplicated, severity-grouped report.
The reviewer reads the target via git directly - it does NOT need the
target checked out in your working tree. Always name an explicit
target via `branch`, `pr`, or `range`; the reviewer will fetch refs
as needed.
Use for:
- Post-implementation review of a branch or commit range
- Pre-merge quality checks of a PR
- Comprehensive audit of changes spanning multiple files
NOT for quick, single-file checks - just read the file yourself.
Not safe to run concurrently. Only one active review per fnord
process at a time.
""",
parameters: %{
type: "object",
required: ["scope"],
additionalProperties: false,
properties: %{
scope: %{
type: "string",
description: """
Design context and specific concerns for the review. Free text.
Describe intent, risky areas, what "done" looks like. Do NOT
use this field to specify the target branch or PR - use the
dedicated `branch`, `pr`, or `range` parameters for that.
"""
},
branch: %{
type: "string",
description: """
Branch to review (e.g. "feature-x"). The reviewer fetches the
branch from origin if it is not locally reachable, then reviews
the range `merge-base(branch, base)..branch`. Mutually
exclusive with `pr` and `range` - **omit the other two
entirely**; do not pass them as empty strings, 0, or null.
"""
},
pr: %{
type: "integer",
description: """
GitHub pull request number. Requires the `gh` CLI to be
installed and authenticated. The reviewer resolves the PR's
head and base via `gh pr view`, fetches both refs, and
reviews the range `merge-base(head, base)..head`. Mutually
exclusive with `branch` and `range` - **omit the other two
entirely**; do not pass them as empty strings, 0, or null.
"""
},
range: %{
type: "string",
description: """
Explicit git range in `A..B` or `A...B` form (e.g.
`HEAD~3..HEAD`, `abc123..def456`). Use for commit-scoped
reviews that are not tied to a branch or PR. The reviewer
fetches endpoints from origin if they are not locally
reachable. Mutually exclusive with `branch` and `pr` -
**omit the other two entirely**; do not pass them as empty
strings, 0, or null.
"""
},
base: %{
type: "string",
description: """
Override the base branch used to compute merge-base. Default
is the repo's default branch (`main` or `master`). Useful for
stacked branches - set to the parent branch, not `main`.
Ignored when `range` is used.
"""
}
}
}
}
end
@impl AI.Tools
def call(args) do
with {:ok, scope} <- AI.Tools.get_arg(args, "scope"),
{:ok, target_args} <- normalize_target_args(args) do
AI.Agent.Review.Decomposer
|> AI.Agent.new()
|> AI.Agent.get_response(Map.put(target_args, :scope, scope))
end
end
# Extract and validate the target-selection params. The LLM may pass any
# subset (or none) of branch / pr / range; at most one is allowed. `base`
# is free-standing and applies only when resolving branch: or falling back
# to the current checkout.
#
# `present?/1` rather than Elixir truthiness: LLMs often emit type-default
# placeholders (`""` for omitted strings, `0` for omitted integers) instead
# of leaving the key out of the tool call. Those are semantically "not
# provided" - treating them as set produces a mutex-failure-loop where every
# retry passes the same shape and gets rejected the same way.
defp normalize_target_args(args) do
branch = Map.get(args, "branch")
pr = Map.get(args, "pr")
range = Map.get(args, "range")
base = Map.get(args, "base")
given =
[{:branch, branch}, {:pr, pr}, {:range, range}]
|> Enum.filter(fn {_, v} -> present?(v) end)
case given do
[] ->
{:ok, %{branch: nil, pr: nil, range: nil, base: present_or_nil(base)}}
[{:branch, _}] ->
{:ok, %{branch: branch, pr: nil, range: nil, base: present_or_nil(base)}}
[{:pr, _}] ->
{:ok, %{branch: nil, pr: pr, range: nil, base: present_or_nil(base)}}
[{:range, _}] ->
{:ok, %{branch: nil, pr: nil, range: range, base: present_or_nil(base)}}
multiple ->
details =
multiple
|> Enum.map_join(", ", fn {k, v} -> "#{k}=#{inspect(v)}" end)
{:error,
"reviewer_tool: only one of branch, pr, range may be set; got: #{details}. " <>
"Pass ONLY the target you want and OMIT the others entirely - do not " <>
"send them as empty strings, 0, or null. Use `range` for explicit commit " <>
"ranges, `pr` for a GitHub PR number, `branch` for a branch name."}
end
end
# Treat type-default placeholders ("", 0, []) as semantically absent, matching
# how OpenAPI / JSON Schema callers typically intend "field not provided."
defp present?(nil), do: false
defp present?(""), do: false
defp present?(0), do: false
defp present?([]), do: false
defp present?(_), do: true
defp present_or_nil(value), do: if(present?(value), do: value, else: nil)
end