Skip to content
Merged
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
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ Run `cr config show` to inspect the active profile and credential status.
```yaml
profiles:
default:
fast: false
git:
host: github.com
auth_mode: pat
Expand Down Expand Up @@ -1111,7 +1112,8 @@ Modes:
| `--rerun` | Bypass existing local approval, approval-override, resume, and marker gates and start a new live review while retaining provider-session reuse. Mutually exclusive with `--retry-posts`. |
| `--retry-posts` | Retry missing or failed required posts for an existing run without rerunning LLM planning or checking approval overrides. Mutually exclusive with `--rerun` and incompatible with `--session`. |
| `--fresh-session` | Start a fresh provider conversation for this invocation without changing local review gates. Incompatible with `--retry-posts`, which does not run LLM planning. |
| `--fast` | Request fast execution for reviewer agents. Incompatible with `--retry-posts`; unsupported runtime/model combinations fail before LLM planning. |
| `--fast` | Enable fast execution for reviewer agents, overriding the profile default. Incompatible with `--retry-posts`. |
| `--no-fast` | Disable fast execution for reviewer agents, overriding the profile default. Mutually exclusive with `--fast`. |

Review selection and execution flags:

Expand Down Expand Up @@ -1144,18 +1146,24 @@ Policy and output flags:
| `--no-resolve-threads` | Do not plan thread-resolution actions. Also implied by profile `resolve_threads: never`. |
| `--json` | Emit JSON. |

Fast mode currently supports `claude_cli` and `anthropic_api` with
`claude-opus-4-8` or `claude-opus-4-7`; Anthropic has deprecated Opus 4.7 fast
mode and plans to remove it on July 24, 2026. Claude CLI receives a per-session
`fastMode` setting, while Anthropic API requests use its fast-mode beta. Fast
mode has premium pricing and applies only to reviewer agents, not selection,
synthesis, approval-override classification, or `cr respond` thread analysis.
Fast mode defaults off; set `fast: true` on a profile to enable it by default.
`--fast` and `--no-fast` override the profile. Unsupported runtime/model
combinations warn and continue at normal speed. Fast mode supports `claude_cli`
and `anthropic_api` with `claude-opus-4-8` or `claude-opus-4-7`, and `codex_cli`
with `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`.
Anthropic has deprecated Opus 4.7 fast mode and plans to remove it on July 24,
2026. Claude CLI receives a per-session `fastMode` setting, Anthropic API
requests use its fast-mode beta, and Codex CLI receives `service_tier="fast"`.
Fast mode has premium pricing and applies only to reviewer agents, not
selection, synthesis, approval-override classification, or `cr respond` thread
analysis.
Anthropic API fast mode is a research preview that requires account access;
requests without access fail as upstream errors during the run. Subscription
fast mode bills usage credits, requires owner enablement on Team and Enterprise,
and can silently degrade to standard speed when credits or fast capacity are
unavailable. Reviewer runtime artifacts therefore record both the fast request
and the speed actually reported by the provider, or `unknown` when unavailable.
unavailable. Reviewer runtime artifacts therefore record the fast request,
whether it was ignored as unsupported, and the speed actually reported by the
provider, or `unknown` when unavailable.

Local run state and provider session state are independent. By default, each
PR/profile/posting-identity tuple gets one durable provider session shared by
Expand Down
1 change: 1 addition & 0 deletions docs/init-config-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ intentionally unsupported.
| config.profiles.<name>.reviewer.github_app_installation.mode | Review profile composition chooses how a GitHub App reviewer entity resolves its installation. | Interactive review-profile flow only for now. | Required when `reviewer.entity` uses GitHub App auth. New GitHub App reviewer selections default to `discover_from_repository`. | `discover_from_repository` resolves from PR repository context; `pinned` requires `installation_id`. Must be empty for PAT and Git identity reviewers. | GitHub App reviewer installation routing tests. |
| config.profiles.<name>.reviewer.github_app_installation.installation_id | Review profile composition stores an optional pinned GitHub App installation id for this profile. | Interactive review-profile flow only for now. | Empty unless installation mode is `pinned`. | Must be a decimal GitHub App installation id when present. This is profile routing data, not secret material. | GitHub App reviewer installation routing tests. |
| config.profiles.<name>.llm_runtime | Review profile composition selects one configured LLM runtime by name. | Interactive review-profile flow only for now. Future config commands may own scripted profile composition. | New profiles require an explicit runtime choice. Existing runtime reference is pre-populated. | Must reference `config.llm_runtimes`. API-key runtime credentials must differ from Git and selected reviewer entity credentials when the store also matches. | First-class LLM-runtime reference validation tests. |
| config.profiles.<name>.fast | Not shown in interactive init; fast mode is a power-user profile default. | Direct config-file management; `cr review --fast` and `--no-fast` override it per invocation. | Omitted defaults to `false`. | Unsupported runtime/model combinations warn and continue at normal speed. | Profile config and review command precedence tests. |
| config.profiles.<name>.agent_sources[] | Direct trusted-directory editor shows existing profile agent-source paths in one multiline field with explanatory notes about trust and separate repo-local `.codereview/agents` discovery. | Existing `cr init --agent-source`; existing `cr config agent-source add/remove`; #186 docs sequence. | Omitted means no additional profile-specific trusted directories. Existing values are pre-populated line-by-line. | Preserve on skip. Clearing the field removes all profile-specific agent sources. Entered paths are normalized and deduped through the shared config-edit helper. | #177 extracts helper. #183 tests add/remove/reset/preserve. #289 prompt tests cover explanatory copy, multiline entry, and Back. |
| config.profiles.<name>.hooks[].event | Not shown in interactive init; lifecycle hooks are power-user config. | Direct config-file management. | No hooks by default. | Must be one of the documented review or respond lifecycle events; the benchmark namespace is reserved. | Hook validation and runtime dispatch tests. |
| config.profiles.<name>.hooks[].argv[] | Not shown in interactive init. | Direct config-file management. | Required for each hook. | Executed directly without a shell; the command must be non-empty and no argument may contain NUL. | Hook validation and argv execution tests. |
Expand Down
1 change: 1 addition & 0 deletions internal/app/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,7 @@ func TestAdapterConstructorsCoverLLMRuntimeSpecs(t *testing.T) {
wantFastModels := map[config.LLMAdapter][]string{
config.LLMAdapterClaudeCLI: {"claude-opus-4-8", "claude-opus-4-7"},
config.LLMAdapterAnthropicAPI: {"claude-opus-4-8", "claude-opus-4-7"},
config.LLMAdapterCodexCLI: {"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"},
}
for _, spec := range config.LLMRuntimeSpecs() {
if _, duplicate := want[spec.Adapter]; duplicate {
Expand Down
24 changes: 18 additions & 6 deletions internal/cmd/reviewcmd/reviewcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ including after pushes and across dry-run/live invocations. --session selects
a named live-review session instead; --fresh-session starts a fresh provider
conversation for this invocation.

--fast requests fast execution for reviewer agents only. The configured
adapter and every possible reviewer model must support it.
--fast requests fast execution for reviewer agents only; --no-fast disables it.
CLI flags override the profile default. Unsupported runtimes or reviewer models
emit a warning and continue at normal speed.

If no existing approval is present and a prior codereview marker exists from
the posting identity, cr can fast-path approval when the PR author posts an
Expand All @@ -66,6 +67,7 @@ type commandFlags struct {
retryPosts bool
freshSession bool
fast bool
noFast bool
agentsDirs []string
failOn string
sessionName string
Expand Down Expand Up @@ -108,6 +110,7 @@ func RegisterWithFactory(rootCmd *cobra.Command, opts *root.Options, factory Run
cmd.Flags().BoolVar(&flags.retryPosts, "retry-posts", false, "Retry missing or failed required posts without rerunning review or checking approval overrides")
cmd.Flags().BoolVar(&flags.freshSession, "fresh-session", false, "Start a fresh provider conversation without changing local review gates")
cmd.Flags().BoolVar(&flags.fast, "fast", false, "Request fast execution for supported reviewer runtimes and models")
cmd.Flags().BoolVar(&flags.noFast, "no-fast", false, "Disable fast execution for reviewer agents")
cmd.Flags().StringArrayVar(&flags.agentsDirs, "agents-dir", nil, "Additional trusted agents directory")
cmd.Flags().StringVar(&flags.failOn, "fail-on", "", "Exit 1 when a finding at or above severity exists")
cmd.Flags().StringVar(&flags.sessionName, "session", "", "Override the PR's default LLM session with a named live-review session")
Expand All @@ -129,6 +132,9 @@ func RegisterWithFactory(rootCmd *cobra.Command, opts *root.Options, factory Run
}

func runReview(ctx context.Context, cmd *cobra.Command, opts *root.Options, factory RuntimeFactory, flags commandFlags, prArg string) error {
if cmd.Flags().Changed("fast") && cmd.Flags().Changed("no-fast") {
return exitcode.Usage(fmt.Errorf("--fast and --no-fast are mutually exclusive"))
}
if flags.noPost {
flags.dryRun = true
}
Expand Down Expand Up @@ -207,9 +213,6 @@ func runReview(ctx context.Context, cmd *cobra.Command, opts *root.Options, fact
if flags.freshSession && flags.retryPosts {
return exitcode.Usage(fmt.Errorf("--fresh-session cannot be used with --retry-posts"))
}
if flags.fast && flags.retryPosts {
return exitcode.Usage(fmt.Errorf("--fast cannot be used with --retry-posts"))
}
if flags.maxAgents < 0 {
return exitcode.Usage(fmt.Errorf("--max-agents must be non-negative"))
}
Expand Down Expand Up @@ -266,6 +269,15 @@ func runReview(ctx context.Context, cmd *cobra.Command, opts *root.Options, fact
return exitcode.Usage(profileSpan.End(err))
}
_ = profileSpan.End(nil)
reviewerFast := profile.Fast
if cmd.Flags().Changed("fast") {
reviewerFast = flags.fast
} else if cmd.Flags().Changed("no-fast") {
reviewerFast = !flags.noFast
}
if reviewerFast && flags.retryPosts {
return exitcode.Usage(fmt.Errorf("fast mode cannot be used with --retry-posts"))
}

runtimeReq := app.OpenRequest{
Config: cfg,
Expand Down Expand Up @@ -316,7 +328,7 @@ func runReview(ctx context.Context, cmd *cobra.Command, opts *root.Options, fact
ReviewerModelOverride: reviewerModel,
ReviewerModelTierOverride: reviewerModelTier,
ReviewerEffortOverride: reviewerEffort,
ReviewerFast: flags.fast,
ReviewerFast: reviewerFast,
ReviewBaseSHA: reviewBaseSHA,
ReviewHeadSHA: reviewHeadSHA,
Rerun: flags.rerun,
Expand Down
64 changes: 64 additions & 0 deletions internal/cmd/reviewcmd/reviewcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,69 @@ func TestReviewFastPropagatesToPipelineRequest(t *testing.T) {
}
}

func TestReviewFastPreferencePrecedence(t *testing.T) {
for _, tt := range []struct {
name string
profileFast bool
flags []string
want bool
}{
{name: "default off"},
{name: "profile default", profileFast: true, want: true},
{name: "flag enables", flags: []string{"--fast"}, want: true},
{name: "explicit fast false overrides profile", profileFast: true, flags: []string{"--fast=false"}},
{name: "no-fast overrides profile", profileFast: true, flags: []string{"--no-fast"}},
{name: "explicit no-fast false enables", flags: []string{"--no-fast=false"}, want: true},
} {
t.Run(tt.name, func(t *testing.T) {
cfg := testConfig()
profile := cfg.Profiles["home"]
profile.Fast = tt.profileFast
cfg.Profiles["home"] = profile
runner := &fakeRunner{result: testPipelineResult(false)}
cmd, _ := newTestCommand(t, cfg, fakeFactory(runner))
args := []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--dry-run"}
args = append(args, tt.flags...)
if err := root.Execute(cmd, args); err != nil {
t.Fatalf("Execute: %v", err)
}
if len(runner.requests) != 1 || runner.requests[0].ReviewerFast != tt.want {
t.Fatalf("requests = %#v, want ReviewerFast=%t", runner.requests, tt.want)
}
})
}
}

func TestReviewRetryPostsUsesEffectiveFastPreference(t *testing.T) {
cfg := testConfig()
profile := cfg.Profiles["home"]
profile.Fast = true
cfg.Profiles["home"] = profile

t.Run("profile fast rejects before runtime construction", func(t *testing.T) {
factoryCalled := false
cmd, _ := newTestCommand(t, cfg, func(context.Context, app.OpenRequest) (app.Runtime, error) {
factoryCalled = true
return app.Runtime{}, nil
})
err := root.Execute(cmd, []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--retry-posts"})
if exitcode.FromError(err) != exitcode.UsageError || factoryCalled {
t.Fatalf("Execute error = %v, factory called = %t, want usage before runtime", err, factoryCalled)
}
})

t.Run("no-fast permits retry", func(t *testing.T) {
runner := &fakeRunner{liveResult: testLiveResult(false)}
cmd, _ := newTestCommand(t, cfg, fakeFactory(runner))
if err := root.Execute(cmd, []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--retry-posts", "--no-fast"}); err != nil {
t.Fatalf("Execute: %v", err)
}
if len(runner.liveRequests) != 1 || runner.liveRequests[0].ReviewerFast {
t.Fatalf("live requests = %#v, want standard-speed retry", runner.liveRequests)
}
})
}

func TestReviewLiveSessionPassesNamedSession(t *testing.T) {
runner := &fakeRunner{liveResult: testLiveResult(false)}
cmd, _ := newTestCommand(t, testConfig(), fakeFactory(runner))
Expand Down Expand Up @@ -973,6 +1036,7 @@ func TestReviewRejectsInvalidInputs(t *testing.T) {
{name: "session retry posts", args: []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--retry-posts", "--session", "daily"}},
{name: "fresh session retry posts", args: []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--retry-posts", "--fresh-session"}},
{name: "fast retry posts", args: []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--retry-posts", "--fast"}},
{name: "fast flag conflict", args: []string{"review", "https://github.com/open-cli-collective/codereview-cli/pull/29", "--dry-run", "--fast", "--no-fast"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ type Profile struct {
Git GitConfig `yaml:"-" json:"-"`
Reviewer ProfileReviewer `yaml:"reviewer" json:"reviewer"`
LLMRuntime string `yaml:"llm_runtime" json:"llm_runtime"`
Fast bool `yaml:"fast,omitempty" json:"fast,omitempty"`
AgentSources []string `yaml:"agent_sources,omitempty" json:"agent_sources,omitempty"`
ReviewPolicy ReviewPolicy `yaml:"review_policy,omitempty" json:"review_policy"`
Hooks []Hook `yaml:"hooks,omitempty" json:"hooks,omitempty"`
Expand All @@ -163,6 +164,7 @@ type profileYAML struct {
Git GitConfig `yaml:"git,omitempty" json:"git,omitempty"`
Reviewer ProfileReviewer `yaml:"reviewer" json:"reviewer"`
LLMRuntime string `yaml:"llm_runtime" json:"llm_runtime"`
Fast bool `yaml:"fast,omitempty" json:"fast,omitempty"`
AgentSources []string `yaml:"agent_sources,omitempty" json:"agent_sources,omitempty"`
ReviewPolicy ReviewPolicy `yaml:"review_policy,omitempty" json:"review_policy"`
Hooks []Hook `yaml:"hooks,omitempty" json:"hooks,omitempty"`
Expand Down Expand Up @@ -210,6 +212,7 @@ func (p *Profile) UnmarshalYAML(value *yaml.Node) error {
Git: raw.Git,
Reviewer: raw.Reviewer,
LLMRuntime: raw.LLMRuntime,
Fast: raw.Fast,
AgentSources: raw.AgentSources,
ReviewPolicy: raw.ReviewPolicy,
Hooks: raw.Hooks,
Expand All @@ -224,6 +227,7 @@ func (p Profile) MarshalYAML() (any, error) {
RepositoryAccess: p.RepositoryAccess,
Reviewer: p.Reviewer,
LLMRuntime: p.LLMRuntime,
Fast: p.Fast,
AgentSources: p.AgentSources,
ReviewPolicy: p.ReviewPolicy,
Hooks: p.Hooks,
Expand Down Expand Up @@ -561,6 +565,7 @@ var llmRuntimeSpecs = []LLMRuntimeSpec{
string(ModelTierMedium): "gpt-5.4",
string(ModelTierLarge): "gpt-5.5",
},
FastModeModels: []string{"gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"},
},
{
Provider: LLMProviderOpenAI,
Expand Down
15 changes: 15 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func TestLoadAcceptsCodexCLISubscriptionProfile(t *testing.T) {
adapter: codex_cli
profiles:
codex:
fast: true
git:
host: github.com
auth_mode: pat
Expand All @@ -232,6 +233,20 @@ profiles:
t.Fatalf("Load: %v", err)
}
profile := cfg.Profiles["codex"]
if !profile.Fast {
t.Fatal("Fast = false, want true")
}
savedPath := filepath.Join(t.TempDir(), "saved.yml")
if err := Save(savedPath, cfg); err != nil {
t.Fatalf("Save: %v", err)
}
saved, err := Load(savedPath)
if err != nil {
t.Fatalf("reload saved config: %v", err)
}
if !saved.Profiles["codex"].Fast {
t.Fatal("saved Fast = false, want true")
}
if profile.LLM.Provider != LLMProviderOpenAI || profile.LLM.Adapter != LLMAdapterCodexCLI {
t.Fatalf("LLM = %#v, want openai/codex_cli", profile.LLM)
}
Expand Down
6 changes: 6 additions & 0 deletions internal/llmadapters/subprocess.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,9 @@ func (a *SubprocessAdapter) buildArgsForSession(req Request, scratch string, res
if req.Effort != "" {
args = append(args, "-c", "model_reasoning_effort="+req.Effort)
}
if req.Fast {
args = append(args, "-c", `service_tier="fast"`)
}
if resumeSessionID != "" {
args = subprocessCodexResumeArgs()
if req.Model != "" {
Expand All @@ -667,6 +670,9 @@ func (a *SubprocessAdapter) buildArgsForSession(req Request, scratch string, res
if req.Effort != "" {
args = append(args, "-c", "model_reasoning_effort="+req.Effort)
}
if req.Fast {
args = append(args, "-c", `service_tier="fast"`)
}
args = append(args, resumeSessionID)
}
return append(args, "--", req.Prompt), nil
Expand Down
Loading
Loading