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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,11 @@ Run a live review:
cr review https://github.com/OWNER/REPO/pull/123
```

The PR URL and selected profile are the repository inputs. `cr review` may be
run outside a Git checkout or from an unrelated repository; it fetches the
PR's exact base and head commits into the run-owned workbench using the
profile's repository credential.

Run a live review and fail the command when a major or blocking finding exists:

```bash
Expand Down
21 changes: 17 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,19 @@ This keeps markers, retries, reconciliation, idempotency, and resume behavior in
one place. New commands such as `cr respond` should produce planned thread

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File-level note: docs/architecture.md

This file is the repo-local architecture source of truth, but after adding the new Git transport guidance it still says review runtime assembly belongs in internal/reviewruntime and that cr review/cr respond should construct reviewruntime.OpenRequest values. In the current tree the production composition root is internal/app/app.OpenRequest, and internal/architecture/git_transport_test.go now codifies internal/app as the only place allowed to construct gitexec. Leaving the old package names here gives contributors the wrong layering rule for future runtime work. Update the guardrail text to point at internal/app for production runtime assembly, while keeping internal/appruntime scoped to small command-independent helpers like retention and repo-root resolution.

Reply inline to this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - the architecture guardrail now names internal/app and app.OpenRequest as the production composition boundary.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That update aligns the architecture guardrail with the current production composition boundary in internal/app and app.OpenRequest.

Summary:
Updated docs/architecture.md to name internal/app and app.OpenRequest as the production runtime composition boundary, replacing the outdated internal/reviewruntime guidance.

actions and let the reviewplan/ledger/outbox flow perform provider writes.

## Authenticated Git Reads

`internal/gitexec` is the narrow transport adapter for non-interactive Git
commands. The application composition roots construct it from the configured
repository/read provider's refreshable token source and inject its command
function into review and selection pipelines. Authentication is host-scoped
and process-only; posting/reviewer credentials are not checkout credentials.

The pipeline validates the requested, fetched, base, and head repository
identities before invoking Git. `internal/workbench.RunPreparer` then owns only
run-workbench creation, exact-commit fetching, validation, reuse, and metadata.
It does not inspect the caller's current directory or local Git origin.

## Command And Review Harness Guardrails

Command packages should stay thin: parse args and flags, load runtime
Expand All @@ -85,10 +98,10 @@ packages; shared command infrastructure belongs in packages such as
`internal/cmd/root`. Command-independent helpers used to compose review and
response application runtimes, such as retention-policy conversion and
repository-root resolution, belong in `internal/appruntime` rather than
`internal/cmd/cmdruntime`. Review lifecycle runtime assembly belongs in
`internal/reviewruntime`; `cr review` and `cr respond` command packages should
construct `reviewruntime.OpenRequest` values and keep CLI-only validation,
rendering, config-path selection, and error mapping at the command boundary.
`internal/cmd/cmdruntime`. Production review lifecycle assembly belongs in
`internal/app`; `cr review` and `cr respond` command packages should construct
`app.OpenRequest` values and keep CLI-only validation, rendering, config-path
selection, and error mapping at the command boundary.

Application packages outside `internal/cmd` and `internal/view` should not
depend on Cobra, command packages, or view packages. Those packages should
Expand Down
15 changes: 10 additions & 5 deletions docs/checkout-native-review-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ Notes:
The workbench is run-owned, not cache-owned. Shared clone or fetch caches are a
possible future optimization but are not part of the correctness contract.

`workbench/metadata.json` is a versioned durable artifact. Schema version `1`
`workbench/metadata.json` is a versioned durable artifact. Schema version `2`
records:

- `schema_version`
- `source_repo_root`
- `checkout_mode`
- normalized PR identity under `pr`
- pinned base and head refs
Expand All @@ -103,13 +102,19 @@ records:
downstream tasks can fingerprint workbench semantics without re-deriving them
from the checkout tree ad hoc.

Schema version `1` remains readable for exact-byte workbench reuse. Its legacy
`source_repo_root` field is ignored because the invocation checkout is not a
workbench input; a valid reused v1 metadata file is not rewritten, preserving
durable task fingerprints.

`SelectionOnly` and benchmark callers still own their artifact directory choice.
Checkout-native additions must work with caller-owned artifact roots instead of
assuming that every selector run is ledger-backed.

Pinned review entrypoints require base and head refs that can be fetched from
the same repository remote. Fork-backed heads need explicit fetch and
authentication handling before pinned review can accept them.
Normal review entrypoints derive credential-free HTTPS remotes from the
validated PR identity and fetch the exact base and head commits. Same-host fork
heads are supported; cross-host heads are rejected before authenticated Git is
invoked. Pinned dry-run entrypoints retain their stricter same-repository rule.

## Reviewer-Facing Context

Expand Down
27 changes: 17 additions & 10 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ data lifecycle commands.

The current Go code is a Cobra command tree in `internal/cmd/*` with a thin
`cmd/cr` entrypoint, shared exit-code mapping in `internal/cmd/exitcode`, and
version plumbing in `internal/version`. Command-independent helpers used to
compose review and response application runtimes live in `internal/appruntime`;
command packages keep CLI composition, rendering, config-path selection, and
error mapping. Review orchestration is split across `internal/reviewruntime`,
version plumbing in `internal/version`. Production review and response runtime
assembly lives in `internal/app`; `internal/appruntime` contains only small
command-independent helpers such as retention conversion and invocation-root
resolution. Command packages keep CLI composition, rendering, config-path
selection, and error mapping. Review orchestration is split across
`internal/pipeline`, `internal/reviewrun`,
`internal/threadrespond`, `internal/reviewplan`, `internal/outbox`,
`internal/gate`, and `internal/gateio`. Command packages construct typed
runtime requests, while `internal/reviewruntime` owns the reusable provider,
ledger, adapter, pipeline, live-run, and response-run assembly.
runtime requests, while `internal/app` owns the reusable provider, ledger,
adapter, pipeline, live-run, and response-run assembly.

Architecture guardrails for LLM execution, model resolution, Git provider
writes, command and review harness boundaries, inline thread lifecycle, and
Expand All @@ -43,6 +44,12 @@ Within `internal/pipeline`, the public entry points are `DryRun`, `Live`, and
and no ledger or posting side effects so benchmark tooling can reuse the real

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File-level note: docs/development.md

The repo-local development map should point contributors at the current runtime composition boundary, but this section still says review/response runtime assembly lives in internal/appruntime and internal/reviewruntime even though this PR’s new guardrail and the settled architecture guidance move that responsibility to internal/app/app.OpenRequest. Leaving the old package map in the same source-of-truth doc makes future boundary work harder to discover and encourages new composition code to land in the wrong place. Update the overview (and any repeated package-shape summary in this file) to name the current internal/app boundary explicitly and demote the older packages to whatever narrower role they still have, if any.

Reply inline to this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - development.md now identifies internal/app as the production composition root and limits internal/appruntime to small runtime helpers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Updating docs/development.md to name internal/app as the production composition root and narrowing internal/appruntime to helper duties addresses the stale boundary guidance.

Summary:
docs/development.md now reflects the current runtime composition boundary by identifying internal/app as the production composition root and limiting internal/appruntime to helper responsibilities.

selector implementation.

Review workbench preparation is location-independent. Runtime composition
constructs the authenticated Git command adapter from the repository/read
credential and injects it into the pipeline; workbench code derives remotes
from the validated PR identity and must not resolve or inspect the invocation
checkout.

Structured LLM calls in the review pipeline are durable per-task units. See
`docs/llm-task-artifacts.md` for the artifact schema, status taxonomy, and
resume invariants.
Expand Down Expand Up @@ -98,12 +105,12 @@ make clean # remove build artifacts
- Current distribution status: GitHub release archives plus standard package
channels declared in `packaging/identity.yml`.
- Application package layering follows the active codereview implementation:
command glue in `internal/cmd/*`, review/response app runtime helpers in
`internal/appruntime`, presentation in `internal/view`,
command glue in `internal/cmd/*`, production review/response composition in
`internal/app`, small runtime helpers in `internal/appruntime`, presentation in `internal/view`,
state/config adapters in `internal/config`, `internal/ledger`, and
`internal/statepaths`, provider/LLM adapters in their owning packages, and
review runtime assembly in `internal/reviewruntime`, review posting/gating in
`internal/outbox`, `internal/gate`, and `internal/gateio`, and response-only
review posting/gating in `internal/outbox`, `internal/gate`, and
`internal/gateio`, and response-only
inline discussion handling in
`internal/threadrespond`.

Expand Down
9 changes: 6 additions & 3 deletions docs/llm-task-artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ reviewer failure must not approve; the final event is clamped to at least
`comment`.

`failed_blocking` means the task prevents dependent phases from safely running.
Selection and rollup failures are blocking. Once a run exists, blocking LLM task
failures leave the run `incomplete` so the normal resume gate can rerun only the
failed task and downstream work.
Selection and rollup failures are blocking. `reviewrun` is the sole owner of
live planner-error outcomes: blocking LLM failures leave fresh and resumed runs
`incomplete`; transient failures leave resumed runs `incomplete` but fail fresh
runs; terminal failures fail either kind; cancellation leaves the current
outcome untouched. The normal resume gate can therefore rerun only the failed
task and downstream work after an interruption or durable blocking failure.

Provider start/wait failures may have empty `attempts` because no structured
output existed. When a provider session ID is known, retry should seed the next
Expand Down
42 changes: 34 additions & 8 deletions internal/app/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
"github.com/open-cli-collective/cli-common/credstore"

"github.com/open-cli-collective/codereview-cli/internal/approvaloverride"
"github.com/open-cli-collective/codereview-cli/internal/appruntime"
"github.com/open-cli-collective/codereview-cli/internal/config"
"github.com/open-cli-collective/codereview-cli/internal/credentials"
"github.com/open-cli-collective/codereview-cli/internal/datalifecycle"
"github.com/open-cli-collective/codereview-cli/internal/gitexec"
"github.com/open-cli-collective/codereview-cli/internal/gitprovider"
githubprovider "github.com/open-cli-collective/codereview-cli/internal/gitprovider/github"
"github.com/open-cli-collective/codereview-cli/internal/hooks"
Expand Down Expand Up @@ -73,8 +75,6 @@ type OpenRequest struct {
MaxConcurrency int
Retention datalifecycle.RetentionPolicy
RetentionManualOnly bool
ResolveRepoRoot func(context.Context) (string, error)
GitCommand func(context.Context, string, ...string) ([]byte, error)
Dependencies Dependencies
}

Expand Down Expand Up @@ -103,6 +103,8 @@ type AdapterFactory func(config.LLMConfig, credentials.Reader) (llm.Adapter, err
// defaults.
type Dependencies struct {
NewGitProvider GitProviderFactory
NewGitCommand func(string, gitexec.TokenSource) (func(context.Context, string, ...string) ([]byte, error), error)
ResolveRepoRoot func(context.Context) (string, error)
ResolvePostingIdentity PostingIdentityResolver
NewAdapter AdapterFactory
RuntimeLayout func() (statepaths.Layout, error)
Expand All @@ -119,6 +121,18 @@ func (d Dependencies) withDefaults() Dependencies {
if d.ResolvePostingIdentity == nil {
d.ResolvePostingIdentity = resolvePostingIdentity
}
if d.NewGitCommand == nil {
d.NewGitCommand = func(host string, tokens gitexec.TokenSource) (func(context.Context, string, ...string) ([]byte, error), error) {
client, err := gitexec.New(host, tokens)
if err != nil {
return nil, err
}
return client.Run, nil
}
}
if d.ResolveRepoRoot == nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/architecture.md now says command-independent runtime helpers such as repository-root resolution belong in internal/appruntime, but this default seam now calls reporoot.Resolve directly from internal/app. That duplicates the helper boundary the doc just defined and makes the layering guidance harder to trust. Prefer defaulting to appruntime.ResolveRepoRoot here, or update the architecture doc if the intended convention has changed.

Reply inline to this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - app now defaults the seam through appruntime.ResolveRepoRoot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. Routing the default seam through appruntime.ResolveRepoRoot restores the architecture boundary described in docs/architecture.md.

Summary:
Resolved after switching the default repo-root seam in internal/app to appruntime.ResolveRepoRoot, keeping command-independent runtime helpers behind the documented internal/appruntime boundary.

d.ResolveRepoRoot = appruntime.ResolveRepoRoot
}
if d.NewAdapter == nil {
d.NewAdapter = newAdapter
}
Expand Down Expand Up @@ -150,7 +164,12 @@ func Open(ctx context.Context, req OpenRequest) (Runtime, error) {
cleanup()
return Runtime{}, err
}
repoProvider, _, err := deps.NewGitProvider(profile.Git, repoProviderStore, gitProviderOptions(profile, profile.Git, req.PRRef))
repoProvider, repoCredential, err := deps.NewGitProvider(profile.Git, repoProviderStore, gitProviderOptions(profile, profile.Git, req.PRRef))
if err != nil {
cleanup()
return Runtime{}, err
}
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(repoProvider, repoCredential))
if err != nil {
cleanup()
return Runtime{}, err
Expand Down Expand Up @@ -215,7 +234,7 @@ func Open(ctx context.Context, req OpenRequest) (Runtime, error) {
}
return withProgressAdapter(req.Progress, command, adapter, string(profile.LLM.Provider), string(profile.LLM.Adapter)), nil
})
runner := buildReviewRunner(ledgerStore, repoProvider, postingProvider, adapter, profile, limiter, layout, req.Warnings, req.Progress, dispatcher, req, command)
runner := buildReviewRunner(ledgerStore, repoProvider, postingProvider, adapter, profile, limiter, layout, req.Warnings, req.Progress, dispatcher, req, command, gitCommand, deps.ResolveRepoRoot)
return Runtime{
Runner: runner,
Responder: runner,
Expand Down Expand Up @@ -372,7 +391,7 @@ func runtimeLayout() (statepaths.Layout, error) {
return layout, nil
}

func buildReviewRunner(ledgerStore *ledger.Store, repoProvider gitprovider.GitProvider, postingProvider gitprovider.GitProvider, adapter llm.Adapter, profile config.Profile, limiter outbox.Limiter, layout statepaths.Layout, warnings io.Writer, logger *progress.Logger, dispatcher *hookDispatcher, req OpenRequest, command string) reviewRunner {
func buildReviewRunner(ledgerStore *ledger.Store, repoProvider gitprovider.GitProvider, postingProvider gitprovider.GitProvider, adapter llm.Adapter, profile config.Profile, limiter outbox.Limiter, layout statepaths.Layout, warnings io.Writer, logger *progress.Logger, dispatcher *hookDispatcher, req OpenRequest, command string, gitCommand func(context.Context, string, ...string) ([]byte, error), resolveRepoRoot func(context.Context) (string, error)) reviewRunner {
taskProgress := newPipelineTaskProgress(logger, command, dispatcher)
liveProvider := runtimeProvider{read: repoProvider, write: postingProvider}
pipelineOpts := pipeline.Options{
Expand All @@ -387,8 +406,8 @@ func buildReviewRunner(ledgerStore *ledger.Store, repoProvider gitprovider.GitPr
MaxConcurrency: req.MaxConcurrency,
Retention: req.Retention,
RetentionManualOnly: req.RetentionManualOnly,
ResolveRepoRoot: req.ResolveRepoRoot,
GitCommand: req.GitCommand,
ResolveRepoRoot: resolveRepoRoot,
GitCommand: gitCommand,
}
return reviewRunner{
pipeline: pipelineOpts,
Expand All @@ -403,7 +422,7 @@ func buildReviewRunner(ledgerStore *ledger.Store, repoProvider gitprovider.GitPr
ApprovalOverride: withProgressApprovalOverrideClassifier(logger, buildApprovalOverrideClassifier(profile, adapter, warnings)),
Retention: req.Retention,
RetentionManualOnly: req.RetentionManualOnly,
ResolveRepoRoot: req.ResolveRepoRoot,
ResolveRepoRoot: resolveRepoRoot,
},
respond: threadrespond.Options{
Store: ledgerStore,
Expand Down Expand Up @@ -645,6 +664,13 @@ func resolvePostingIdentity(ctx context.Context, provider gitprovider.GitProvide
return provider.WhoAmI(ctx, credential)
}

func repositoryTokenSource(provider gitprovider.GitProvider, credential gitprovider.Credential) gitexec.TokenSource {
if source, ok := provider.(gitexec.TokenSource); ok {
return source
}
return gitexec.TokenSourceFunc(func(context.Context) (string, error) { return credential.Token, nil })
}

type adapterConstructor func(config.LLMConfig, credentials.Reader, config.LLMRuntimeSpec) (llm.Adapter, error)

var adapterConstructors = map[config.LLMAdapter]adapterConstructor{
Expand Down
28 changes: 19 additions & 9 deletions internal/app/runtime_selection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import (
"context"

"github.com/open-cli-collective/codereview-cli/internal/config"
"github.com/open-cli-collective/codereview-cli/internal/gitprovider"
"github.com/open-cli-collective/codereview-cli/internal/llm"
"github.com/open-cli-collective/codereview-cli/internal/pipeline"
)

// SelectionRuntime contains the dependencies needed for selection-only
// execution paths that must match review-command runtime semantics.
type SelectionRuntime struct {
Provider gitprovider.GitProvider
Adapter llm.Adapter
Cleanup func()
Select func(context.Context, pipeline.SelectionRequest) (pipeline.SelectionResult, error)
Cleanup func()
}

// OpenSelection resolves provider and adapter setup using the same
Expand All @@ -35,7 +33,12 @@ func OpenSelection(ctx context.Context, req SelectionOpenRequest) (SelectionRunt
}
// Selection-only paths read with the profile git credential while keeping
// PR-scoped GitHub App installation lookup aligned with review runs.
provider, _, err := deps.NewGitProvider(profile.Git, gitStore, gitProviderOptions(profile, profile.Git, req.PRRef))
provider, credential, err := deps.NewGitProvider(profile.Git, gitStore, gitProviderOptions(profile, profile.Git, req.PRRef))
if err != nil {
cleanup()
return SelectionRuntime{}, err
}
gitCommand, err := deps.NewGitCommand(profile.Git.Host, repositoryTokenSource(provider, credential))
if err != nil {
cleanup()
return SelectionRuntime{}, err
Expand All @@ -54,8 +57,15 @@ func OpenSelection(ctx context.Context, req SelectionOpenRequest) (SelectionRunt
return SelectionRuntime{}, err
}
return SelectionRuntime{
Provider: provider,
Adapter: adapter,
Cleanup: cleanup,
Select: func(ctx context.Context, selection pipeline.SelectionRequest) (pipeline.SelectionResult, error) {
return pipeline.SelectionOnly(ctx, pipeline.Options{
Provider: provider,
Adapter: adapter,
GitCommand: gitCommand,
ResolveRepoRoot: deps.ResolveRepoRoot,
MaxAgents: selection.MaxAgents,
}, selection)
},
Cleanup: cleanup,
}, nil
}
Loading
Loading