106 lines
3.8 KiB
Go
106 lines
3.8 KiB
Go
// Package claudecode is the claude-code reviewer adapter. claude-code is a
|
|
// prompt-driven agent, so this reviewer feeds AO's review prompt (authored
|
|
// centrally and passed in ReviewInvocation.Prompt) to the worker claude-code
|
|
// adapter's launch-command construction (binary resolution, flags). The reviewer
|
|
// contract stays prompt-agnostic, so a one-shot CLI reviewer (e.g. greptile) can
|
|
// ignore the prompt entirely.
|
|
package claudecode
|
|
|
|
import (
|
|
"context"
|
|
|
|
workeragent "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/claudecode"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/domain"
|
|
"github.com/aoagents/agent-orchestrator/backend/internal/ports"
|
|
)
|
|
|
|
// Reviewer is the claude-code code-review adapter.
|
|
type Reviewer struct {
|
|
agent ports.Agent
|
|
}
|
|
|
|
// New builds the claude-code reviewer adapter.
|
|
func New() *Reviewer {
|
|
return &Reviewer{agent: workeragent.New()}
|
|
}
|
|
|
|
// Harness identifies this reviewer in the reviewer registry.
|
|
func (r *Reviewer) Harness() domain.ReviewerHarness {
|
|
return domain.ReviewerClaudeCode
|
|
}
|
|
|
|
var _ ports.Reviewer = (*Reviewer)(nil)
|
|
|
|
// reviewerAllowedTools is the read-only tool allowlist the reviewer launches
|
|
// with. The reviewer runs headless (no human to approve prompts) but must stay
|
|
// read-only, so instead of bypassPermissions — which skips the permission
|
|
// system entirely and ignores allow/deny rules — it launches in the default
|
|
// mode where these rules are honored: allow rules auto-approve without
|
|
// prompting, so the reviewer can read the checkout and run the few commands it
|
|
// needs (git diff/log/show to inspect the PR, gh to post the review, and
|
|
// `ao review submit` to record the verdict) without stalling.
|
|
var reviewerAllowedTools = []string{
|
|
"Read",
|
|
"Grep",
|
|
"Glob",
|
|
"Bash(gh:*)",
|
|
"Bash(git diff:*)",
|
|
"Bash(git log:*)",
|
|
"Bash(git show:*)",
|
|
"Bash(git status:*)",
|
|
"Bash(ao review submit:*)",
|
|
}
|
|
|
|
// reviewerDisallowedTools hard-denies the write paths as defense in depth, so a
|
|
// misbehaving model cannot edit files or move the branch even if a future
|
|
// allowlist entry would otherwise admit it.
|
|
var reviewerDisallowedTools = []string{
|
|
"Edit",
|
|
"Write",
|
|
"NotebookEdit",
|
|
"Bash(git push:*)",
|
|
"Bash(git commit:*)",
|
|
}
|
|
|
|
// ReviewCommand builds a claude-code invocation that reviews the worker's
|
|
// checkout for the PR, with the review prompt baked in.
|
|
func (r *Reviewer) ReviewCommand(ctx context.Context, inv ports.ReviewInvocation) (ports.ReviewCommandSpec, error) {
|
|
argv, err := r.agent.GetLaunchCommand(ctx, ports.LaunchConfig{
|
|
SessionID: inv.ReviewerID,
|
|
WorkspacePath: inv.WorkspacePath,
|
|
Prompt: inv.Prompt,
|
|
SystemPrompt: inv.SystemPrompt,
|
|
// Launch off bypassPermissions so the allow/deny lists are enforced.
|
|
// Set an explicit non-bypass mode instead of deferring to the user's
|
|
// Claude defaultMode, which may itself be bypassPermissions.
|
|
Permissions: ports.PermissionModeAuto,
|
|
AllowedTools: reviewerAllowedTools,
|
|
DisallowedTools: reviewerDisallowedTools,
|
|
})
|
|
if err != nil {
|
|
return ports.ReviewCommandSpec{}, err
|
|
}
|
|
return ports.ReviewCommandSpec{Argv: argv}, nil
|
|
}
|
|
|
|
// PreLaunch runs any reviewer-specific preflight. For Claude Code this records
|
|
// the worker checkout as trusted before the headless reviewer pane starts.
|
|
func (r *Reviewer) PreLaunch(ctx context.Context, inv ports.ReviewInvocation) error {
|
|
pl, ok := r.agent.(interface {
|
|
PreLaunch(context.Context, ports.LaunchConfig) error
|
|
})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return pl.PreLaunch(ctx, ports.LaunchConfig{
|
|
SessionID: inv.ReviewerID,
|
|
WorkspacePath: inv.WorkspacePath,
|
|
})
|
|
}
|
|
|
|
// ReviewMessage is the text injected into an already-running reviewer pane to
|
|
// review a new commit — AO's central review prompt.
|
|
func (r *Reviewer) ReviewMessage(_ context.Context, inv ports.ReviewInvocation) (string, error) {
|
|
return inv.Prompt, nil
|
|
}
|