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
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ CLI contract read `README.md` and `llms.txt`; for usage read `crq help`.
## Package layout

Dependency rule (Go-enforced, no cycles): `dialect ← engine ← crq`, `state ← crq`,
`gh ← {state, crq}`. The engine does no I/O by construction.
`gh ← {state, crq}`, `workspace ← crq`. The engine does no I/O by construction.

- `internal/dialect/` — ALL bot-text knowledge, zero deps. CodeRabbit/Codex
completion, rate-limit, paused, in-progress, failed, summary-only-plan,
Expand All @@ -26,6 +26,10 @@ Dependency rule (Go-enforced, no cycles): `dialect ← engine ← crq`, `state
The only package (besides dialect) allowed to say "rate limit".
`ListCheckRuns` fetches a ref's check runs (envelope-paged, ETag'd); matching
them to a bot is dialect's `ClassifyCheckRun`, never gh's.
- `internal/workspace/` — reusable repository mirrors, detached PR worktrees,
credential-safe Git execution, stale-worktree pruning, and mirror migration.
Owns persistent filesystem and process I/O for checkouts; `crq` supplies only
configured roots and a current-token resolver.
- `internal/state/` — persisted schema v3: one `Round` per PR, one global
`FireSlot`, the CodeRabbit `AccountQuota`, an `Archive` ring. Round transition
methods, the CAS store, and dashboard rendering. `Round.CoBots` holds per-
Expand Down
12 changes: 11 additions & 1 deletion internal/crq/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,16 @@ type Config struct {
// Reviewers is the single description of who reviews and what they cost.
// Bot / RequiredBots / FeedbackBots / CoBots above are DERIVED from it and
// kept only so existing consumers keep compiling; new code should read this.
Reviewers []Reviewer
Reviewers []Reviewer
// WorkspaceRoot holds crq's own mirrors and worktrees. Read here rather than
// from the process environment, so a value in ~/.config/crq/env — the
// documented place for crq settings — is actually used.
WorkspaceRoot string
// WorkDir is the checkout the local-work probe inspects. Empty means the
// process's own directory, which is what an agent running crq from its
// working copy means. Set programmatically by a caller working in a
// worktree it made, since that caller is not standing in one.
WorkDir string
RateLimitCommand string
RateLimitMarker string
CalibrationMarker string
Expand Down Expand Up @@ -172,6 +181,7 @@ func LoadConfig() (Config, error) {
AutoReviewMaxScan: intEnv(env, "CRQ_AUTOREVIEW_MAX_SCAN", 400),
LeaderTTL: durationEnv(env, "CRQ_LEADER_TTL", 3*time.Minute),
FiredMax: intEnv(env, "CRQ_FIRED_MAX", 500),
WorkspaceRoot: env["CRQ_WORKSPACE"],
NoOpen: env["CRQ_NO_OPEN"] != "",
DryRun: env["CRQ_DRY_RUN"] == "1",
FeedbackWaitTimeout: durationEnv(env, "CRQ_FEEDBACK_WAIT_TIMEOUT", 20*time.Minute),
Expand Down
11 changes: 6 additions & 5 deletions internal/crq/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package crq

import (
"context"
"os/exec"
"strings"
"time"

Expand Down Expand Up @@ -293,7 +292,7 @@ func (s *Service) checkLocalWork(ctx context.Context, repos []string, head, head
if s.localWorkFn != nil {
return s.localWorkFn(ctx, head)
}
return localWork(ctx, repos, head, headRef)
return localWork(ctx, s.cfg.WorkDir, repos, head, headRef)
}

// localWork reports whether the working copy holds changes the PR head does not
Expand All @@ -313,13 +312,15 @@ func (s *Service) checkLocalWork(ctx context.Context, repos []string, head, head
// `done` rather than `push`. That is the safe direction: `push` is only ever
// emitted once the head is already released, so a missed one costs one extra
// call, while a spurious `hold` would stall the loop.
func localWork(ctx context.Context, repos []string, head, headRef string) (bool, string) {
// dir is the checkout to inspect; "" means the process's own directory,
// which is what an agent running crq from its working copy means.
func localWork(ctx context.Context, dir string, repos []string, head, headRef string) (bool, string) {
git := func(args ...string) (string, bool) {
out, err := exec.CommandContext(ctx, "git", args...).Output()
out, err := gitDir(ctx, dir, args...)
if err != nil {
return "", false
}
return strings.TrimSpace(string(out)), true
return out, true
}
if _, ok := git("rev-parse", "--is-inside-work-tree"); !ok {
return false, "not run inside a git checkout"
Expand Down
9 changes: 3 additions & 6 deletions internal/crq/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/url"
"os/exec"
"strings"
)

Expand Down Expand Up @@ -117,10 +116,8 @@ func remoteSlugs(remotes string) (repos, owners []string) {
return repos, owners
}

// gitIn runs git in the process's own working directory, which is what target
// inference is about: the checkout the caller is standing in.
func gitIn(ctx context.Context, args ...string) (string, error) {
out, err := exec.CommandContext(ctx, "git", args...).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
return gitDir(ctx, "", args...)
}
23 changes: 23 additions & 0 deletions internal/crq/workspace_wiring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package crq

import (
"context"

ghapi "github.com/kristofferR/coderabbit-queue/internal/gh"
"github.com/kristofferR/coderabbit-queue/internal/workspace"
)

// workspace wires crq's configured cache root and GitHub credential resolution
// into the reusable workspace implementation.
func (s *Service) workspace(context.Context) workspace.Workspace {
return workspace.Workspace{
Root: s.cfg.WorkspaceRoot,
TokenSource: ghapi.LookupToken,
}
}

// gitDir keeps command-local Git probes in crq while the process and error
// handling live with the rest of the Git implementation.
func gitDir(ctx context.Context, dir string, args ...string) (string, error) {
return workspace.Git(ctx, dir, args...)
}
21 changes: 21 additions & 0 deletions internal/crq/workspace_wiring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package crq

import (
"context"
"testing"
)

// The daemon's workspace has to come from the config file, not the process
// environment, and credentials must be resolved when Git runs so token rotation
// does not leave a long-lived checkout with a stale value.
func TestServiceWorkspaceUsesTheConfiguredRootAndCurrentToken(t *testing.T) {
t.Setenv("GITHUB_TOKEN", "ghp_from_the_environment")
s := &Service{cfg: Config{WorkspaceRoot: "/tmp/crq-configured-root"}}
ws := s.workspace(context.Background())
if ws.Root != "/tmp/crq-configured-root" {
t.Errorf("workspace root = %q, want the configured one", ws.Root)
}
if got := ws.TokenSource(context.Background()); got != "ghp_from_the_environment" {
t.Errorf("workspace token = %q, want the one the API client resolves", got)
}
}
5 changes: 5 additions & 0 deletions internal/gh/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ func (g *GitHub) cacheGET(url string, resp *http.Response) (*http.Response, erro
return resp, nil
}

// LookupToken resolves a GitHub token from the environment or the gh CLI, for
// callers outside this package that need the same credential — git, which does
// not read GITHUB_TOKEN or gh's store by itself.
func LookupToken(ctx context.Context) string { return lookupToken(ctx) }

// lookupToken resolves a GitHub token from the environment or the gh CLI. gh can
// hand back a freshly-rotated OAuth token, which is why send re-runs this on a 401.
func lookupToken(ctx context.Context) string {
Expand Down
Loading