Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ your agentic loop while context is fresh.

```bash
roborev init # layer 1: per-commit reviews
roborev skills install
roborev agent-hook install # layer 2: auto-detect and wire installed agents
roborev agent-hook install # layer 2: wire agents and bundled skills
roborev agent-hook install --agent all # or wire every supported profile
```

Expand Down Expand Up @@ -103,11 +102,13 @@ closing the loop.
`roborev agent-hook install` auto-detects installed Claude Code, Codex, Copilot
CLI, Cursor, Factory Droid, Gemini CLI, Hermes, Qwen, and Grok Build harnesses
and adds optional hooks after configured turn, commit, or failed-review
thresholds are met. Reminders include a complete CLI fallback when no roborev
skill is installed. Hermes delivers queued post-tool reminders at `Stop`; Cursor
records the same events but emits no control response.
thresholds are met. Reminders name exact review IDs, invoke the bundled
`roborev-fix` skill, and include a complete CLI fallback when no roborev skill is
installed; they do not run `roborev fix --open`. Supported profiles get current
bundled skills during hook installation. Hermes delivers queued post-tool
reminders at `Stop`; Cursor records the same events but emits no control response.
Installed hooks post events to the regular roborev daemon. That daemon evaluates
the reminders and persists session counters in
the reminders and persists session counters and delivered review IDs in
`${ROBOREV_DATA_DIR:-~/.roborev}/agent-hook/state.json`. Hook callbacks fail open
when the daemon is unavailable, so they do not block the coding agent.

Expand Down
5 changes: 2 additions & 3 deletions cmd/roborev/agent_hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ func TestAgentHookRunSupportsLegacyProfilelessRegistration(t *testing.T) {

require.NoError(t, cmd.Execute())
assert.Equal(t, "legacy-1", got.Event.SessionID)
assert.JSONEq(t, `{"decision":"block","reason":"resolve reviews If Roborev issues are found, fix them, then continue the task you were doing before this hook interrupted you."}`, stdout.String())
assert.JSONEq(t, `{"decision":"block","reason":"resolve reviews"}`, stdout.String())
}

// If a legacy or Grok encoder bypasses policy-aware output, users get different
Expand Down Expand Up @@ -235,8 +235,7 @@ func TestRunAgentHookEncodesKitStopResponse(t *testing.T) {
var output map[string]any
require.NoError(t, json.Unmarshal(stdout.Bytes(), &output))
assert.Equal(t, "block", output["decision"])
assert.Contains(t, output["reason"], "resolve reviews")
assert.Contains(t, output["reason"], "continue the task")
assert.Equal(t, "resolve reviews", output["reason"])
}

// If kit-backed profiles omit policy composition, most supported hooks keep
Expand Down
29 changes: 22 additions & 7 deletions docs/agent-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,20 @@ Roborev scopes commit and failed-review accounting to repository lineage, so
activity in one worktree does not consume another worktree's reminder. Outside a
tracked git repository the hook returns an empty native response.

The default instruction is self-contained. It uses the richer `roborev-fix`
skill when available and otherwise tells the agent how to discover, inspect,
fix, comment on, and close each review with the CLI. Installing skills remains
recommended for Claude Code, Codex, and Factory Droid, but it is not required
for the other profiles to receive an actionable reminder.
The default instruction names the exact review job IDs and invokes the
`roborev-fix` skill for only those jobs. It never runs `roborev fix --open` or
discovers additional reviews. The skill treats every finding as an unverified
claim: invalid findings are documented and closed without code changes, valid
in-scope findings are fixed and verified, and valid out-of-scope or unclear
findings remain open until the user gives direction.

Delivered review IDs are acknowledged in the Agent Hook daemon's session state,
scoped to the repository lineage. They do not trigger another reminder in that
session, while newly created review IDs still do. Deferred reminders acknowledge
their IDs only when delivered.

`instruction` is a complete override. Custom instructions are emitted without
the built-in scope or continuation guidance.

## Install

Expand Down Expand Up @@ -78,6 +87,11 @@ Automatic and `all` installs attempt every selected profile and report all
errors after preserving successful installs. `--dry-run` plans the same changes
without writing.

For Claude Code, Codex, Factory Droid, and Grok Build, installation also creates
or updates that profile's bundled roborev skills before activating the hook.
Other hook profiles do not currently have bundled skill variants and receive no
CLI fallback.

Factory Droid remains user-scoped. Roborev rejects project `.factory/hooks.json`
paths because they are executable repository-local configuration.

Expand Down Expand Up @@ -154,7 +168,8 @@ roborev agent-hook run --agent <profile>
passes it through kit's typed dispatcher, posts a normalized request to the
regular roborev daemon, and lets kit encode the native response.

The regular daemon loads and persists session accounting at:
The regular daemon loads and persists session accounting and delivered review
IDs at:

```text
${ROBOREV_DATA_DIR:-~/.roborev}/agent-hook/state.json
Expand Down Expand Up @@ -182,7 +197,7 @@ trigger type. The next Hermes `Stop` delivers one reminder, ordered by failed
reviews before commits and then creation time.

Queued reminders retain the absolute triggering worktree and tell the agent to
change to it before running fallback commands, even if the session changed
change to it before running review commands, even if the session changed
directories or used `git -C`. Delivery waits until that worktree is back on the
triggering branch, or the exact triggering commit for a detached checkout, so
the fallback commands query the intended lineage. Repeated triggers coalesce
Expand Down
10 changes: 5 additions & 5 deletions docs/automation/post-commit-reviews.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ Then act on the reviews in whichever way fits how you work:
re-reviews until every review passes.

The `roborev-fix` and `roborev-refine` skills come from `roborev skills install`
(see [Agent Skills](../guides/agent-skills.md)).
(see [Agent Skills](../guides/agent-skills.md)). Agent Hook installation updates
the bundled skills automatically for supported profiles.

## Layer 2 - Agent hook

The agent hook watches supported coding-agent sessions and, once review work
piles up, supplies either the roborev-fix skill or a complete CLI fallback
before the session ends - closing the write -> review -> fix loop automatically.
piles up, supplies exact review IDs to the `roborev-fix` skill before the
session ends. It never runs the separate `roborev fix --open` agent workflow.

```bash
roborev skills install # optional richer workflow for bundled agents
roborev agent-hook install # auto-detect and wire installed agent harnesses
roborev agent-hook install # wire harnesses and update supported bundled skills
```

See [Agent Hook](../agent-hook.md) for thresholds and configuration.
Expand Down
14 changes: 14 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ All notable changes to roborev, grouped by minor release.
started. Before upgrading from a release with the auxiliary daemon, stop it
with that release's `roborev agent-hook daemon stop` command. See
[Agent Hook](/agent-hook/#upgrading-existing-hooks).
- Default Agent Hook autofix reminders now keep the user's current task as an
immutable scope boundary, name exact review job IDs, and invoke only the
bundled `roborev-fix` skill. They never run `roborev fix --open` or discover
additional reviews. Custom instructions remain complete overrides.
- The bundled `roborev-fix` skills now require agents to prove every finding
against current code before editing. Invalid reviews are documented and
closed without code changes; valid out-of-scope findings remain open for
user direction.
- `roborev agent-hook install` now installs or updates bundled skills
automatically for Claude Code, Codex, Factory Droid, and Grok Build.
- `roborev status` now lists active Agent Hook snoozes with their exact
repository, worktree, branch, and expiry, while the TUI shows a contextual
snooze badge for an exactly filtered checkout. See
Expand Down Expand Up @@ -97,6 +107,10 @@ All notable changes to roborev, grouped by minor release.
- Fresh agent sessions now receive a short, bounded agentsview usage-indexing
retry before Roborev falls back to job-log token data, reducing permanently
missing cost estimates. See [Token Usage](/commands/#token-usage).
- Agent Hook remembers delivered review IDs per agent session and repository
lineage, preventing repeated reminders for the same reviews while allowing
newly created reviews to trigger. Deferred reminders acknowledge IDs only
when they are delivered.
- The Codex `maximum` preset now requests literal `max` for explicit GPT-5.6
`sol`, `terra`, and `luna` models. Older, default, and unknown models retain
the compatible `xhigh` mapping, while exact `xhigh` remains distinct.
Expand Down
21 changes: 14 additions & 7 deletions docs/guides/agent-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ before roborev receives the path. Custom destinations are not tracked by
an invocation; Claude Code, Codex, and Factory Droid must handle the surrounding
request with their native agent behavior.

An Agent Hook invocation names exact job IDs and never broadens the user's
active task. The skill does not discover other reviews in that mode. It first
proves or disproves each finding against the current code, fixes only valid
in-scope findings, closes invalid reviews with evidence and no code change, and
leaves valid out-of-scope findings open for user direction.

**Claude Code** enforces this in skill metadata: the bundled skills set
`disable-model-invocation: true`, so the model never selects a roborev skill on
its own. Invoke a skill by typing its slash command (`/roborev-review-branch`)
Expand Down Expand Up @@ -212,11 +218,11 @@ The agent:

1. Discovers open reviews (or uses provided job IDs)
1. Fetches all reviews and collects findings
1. Groups findings by file and prioritizes by severity
1. Fixes all issues across all reviews
1. Runs tests to verify
1. Records a comment on each closed review
1. Offers to commit
1. Proves each finding against the current code and repository constraints
1. Fixes and verifies valid findings within the current task
1. Documents and closes invalid reviews without changing code
1. Leaves valid out-of-scope reviews open and asks the user
1. Audits the original review IDs before reporting completion

This is the interactive equivalent of `roborev fix --batch` -- the agent sees
all findings at once and can make coordinated fixes across related issues.
Expand All @@ -229,8 +235,9 @@ Target a specific job ID with `/roborev-fix`:
/roborev-fix 1019
```

The agent fetches the review, fixes issues by priority, runs tests, and offers
to commit.
The agent fetches the review, validates every finding, fixes and verifies only
valid in-scope issues, and records evidence before closing the review. Valid
out-of-scope findings remain open.

!!! note

Expand Down
1 change: 0 additions & 1 deletion internal/agenthook/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,4 @@ func TestResolveOptionsForAgentGrokUsesSelfContainedInstruction(t *testing.T) {

require.NoError(t, err)
assert.Equal(t, DefaultInstruction, opts.Instruction)
assert.Contains(t, opts.Instruction, "roborev fix --open --list")
}
11 changes: 0 additions & 11 deletions internal/agenthook/grok_install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,6 @@ func DefaultGrokHooksPath() string {
return filepath.Join(home, "hooks", "roborev.json")
}

func runGrokInstall(opts InstallOptions) (kitagenthook.Result, error) {
result, err := planGrokInstall(opts)
if err != nil || opts.DryRun || !result.Changed {
return result, err
}
if err := commitAgentHookConfig(result.ConfigPath, result.Data); err != nil {
return kitagenthook.Result{}, err
}
return result, nil
}

func planGrokInstall(opts InstallOptions) (kitagenthook.Result, error) {
path := strings.TrimSpace(opts.ConfigPath)
if path == "" {
Expand Down
53 changes: 43 additions & 10 deletions internal/agenthook/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"errors"
"fmt"
"io"
"path/filepath"
"strings"
"time"
"unicode"

kitagenthook "go.kenn.io/kit/agenthook"

"go.kenn.io/roborev/internal/skills"
)

const (
Expand Down Expand Up @@ -110,24 +113,29 @@ func RunDump(opts DumpOptions, stdout io.Writer) error {
}

func runInstall(agent kitagenthook.Agent, opts InstallOptions) (kitagenthook.Result, error) {
var planned kitagenthook.Result
var err error
if agent == AgentGrok {
return runGrokInstall(opts)
}
kitOpts, err := validatedKitInstallOptions(agent, opts)
if err != nil {
return kitagenthook.Result{}, err
}
planned, err := kitagenthook.PlanInstall(agent, kitOpts)
if err != nil {
return kitagenthook.Result{}, err
planned, err = planGrokInstall(opts)
} else {
var kitOpts kitagenthook.InstallOptions
kitOpts, err = validatedKitInstallOptions(agent, opts)
if err == nil {
planned, err = kitagenthook.PlanInstall(agent, kitOpts)
}
if err == nil {
planned, err = planLegacyHookMigration(agent, planned)
}
}
planned, err = planLegacyHookMigration(agent, planned)
if err != nil {
return kitagenthook.Result{}, err
}
if opts.DryRun {
return planned, nil
}
if err := installAgentHookSkills(agent, planned.ConfigPath); err != nil {
return kitagenthook.Result{}, err
}
if !planned.Changed {
return planned, nil
}
Expand All @@ -137,6 +145,31 @@ func runInstall(agent kitagenthook.Agent, opts InstallOptions) (kitagenthook.Res
return planned, nil
}

func installAgentHookSkills(agent kitagenthook.Agent, configPath string) error {
var skillAgent skills.Agent
switch agent {
case kitagenthook.AgentClaude:
skillAgent = skills.AgentClaude
case kitagenthook.AgentCodex:
skillAgent = skills.AgentCodex
case kitagenthook.AgentDroid:
skillAgent = skills.AgentDroid
case AgentGrok:
skillAgent = skills.AgentGrok
default:
return nil
}

configDir := filepath.Dir(configPath)
if agent == AgentGrok && strings.EqualFold(filepath.Base(configDir), "hooks") {
configDir = filepath.Dir(configDir)
}
if _, err := skills.InstallToPath(skillAgent, filepath.Join(configDir, "skills")); err != nil {
return fmt.Errorf("install bundled %s skills: %w", skillAgent, err)
}
return nil
}

func validatedKitInstallOptions(
agent kitagenthook.Agent,
opts InstallOptions,
Expand Down
35 changes: 35 additions & 0 deletions internal/agenthook/kit_install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,41 @@ func TestRunInstallUsesKitForQwen(t *testing.T) {
assert.Contains(t, stdout.String(), "installed Qwen Code agent hooks")
}

func TestRunInstallInstallsAndUpdatesBundledSkillsForSupportedProfiles(t *testing.T) {
tests := []struct {
agent string
configName string
}{
{agent: "claude", configName: "settings.json"},
{agent: "codex", configName: "hooks.json"},
{agent: "droid", configName: "hooks.json"},
{agent: "grok", configName: filepath.Join("hooks", "roborev.json")},
}

for _, tt := range tests {
t.Run(tt.agent, func(t *testing.T) {
root := t.TempDir()
configPath := filepath.Join(root, tt.configName)
opts := InstallOptions{
Agent: tt.agent, Executable: "/opt/bin/roborev",
ConfigPath: configPath, Timeout: 10 * time.Second,
}

require.NoError(t, RunInstall(opts, &bytes.Buffer{}))
skillPath := filepath.Join(root, "skills", "roborev-fix", "SKILL.md")
installed, err := os.ReadFile(skillPath)
require.NoError(t, err)
assert.NotEmpty(t, installed)

require.NoError(t, os.WriteFile(skillPath, []byte("stale"), 0o644))
require.NoError(t, RunInstall(opts, &bytes.Buffer{}))
updated, err := os.ReadFile(skillPath)
require.NoError(t, err)
assert.NotEqual(t, []byte("stale"), updated)
})
}
}

func TestRunInstallMigratesLegacyProfileHooks(t *testing.T) {
tests := []struct {
agent string
Expand Down
15 changes: 5 additions & 10 deletions internal/agenthook/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,12 @@ import (
"go.kenn.io/roborev/internal/autofix"
)

const continuationInstruction = "If Roborev issues are found, fix them, " +
"then continue the task you were doing before this hook interrupted you."

const postToolUseContinuationInstruction = continuationInstruction

func PostToolUseAdditionalContext(reason string) string {
return withContinuationInstruction(reason)
return resolvedInstruction(reason)
}

func StopReason(reason string) string {
return withContinuationInstruction(reason)
return resolvedInstruction(reason)
}

func PostToolUseAdditionalContextWithFixGuidelines(reason, guidelines string) string {
Expand Down Expand Up @@ -60,10 +55,10 @@ func BuildOutputWithFixGuidelines(input Input, resp Response, guidelines string)
}
}

func withContinuationInstruction(reason string) string {
func resolvedInstruction(reason string) string {
reason = strings.TrimSpace(reason)
if reason == "" {
return continuationInstruction
return DefaultInstruction
}
return reason + " " + continuationInstruction
return reason
}
Loading
Loading