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
38 changes: 38 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Frontend
on:
push:
tags:
- v*
branches:
- master
pull_request:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v5

- name: Set up Node
uses: actions/setup-node@v7
with:
node-version-file: frontend/.nvmrc
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install
run: npm ci

- name: Build
run: npm run build:all

# Runtime deps only: they ship inside the embedded bundle. Dev-server
# advisories (vite, rolldown) don't reach production.
- name: Audit
run: npm audit --omit=dev --audit-level=high
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.12.2
version: v2.13.2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ tsconfig.tsbuildinfo
docs/patches/pgmigrator.toml
frontend/dist/
frontend/dist-vt/
/reviewctl

# local-only planning docs (kept on disk, never committed)
docs/llm/DirectAPIRunner.md
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build frontend
FROM node:20-alpine AS frontend
FROM node:24-alpine AS frontend
WORKDIR /frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ ifeq ($(RACE),1)
GOFLAGS+=-race
endif

LINT_VERSION := v2.12.2
LINT_VERSION := v2.13.2

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)

Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ GitLab CI (merge request)

- Go 1.26+
- PostgreSQL
- Node.js 20+ (for frontend build)
- Node.js 22.12+ (for frontend build; `frontend/.nvmrc` pins 24)

## Quick Start

Expand Down Expand Up @@ -106,6 +106,10 @@ PoolSize = 5
[Sentry]
DSN = ""
Environment = ""

[Debug] # optional; defaults shown
Capacity = 200 # runs kept at /v1/debug/storage/ (metadata)
FilesCapacity = 20 # newest runs that keep their artifact files
```

## API Endpoints
Expand Down Expand Up @@ -152,14 +156,14 @@ reviewctl comment # Post MR comments for an existing review
reviewctl version # Print version
```

Key flags: `--key`, `--url`, `--runner` (`claude` | `opencode` | `codex` | `direct`), `--model`, `--session` (prompt cache reuse), `--continue` (resume last session), `--allow-dangerous-permissions` (opencode `--dangerously-skip-permissions`, default `true` for unattended CI). All flags have env variable equivalents for CI. See `reviewctl --help` for details.
Key flags: `--key`, `--url`, `--runner` (`claude` | `opencode` | `codex` | `direct`), `--model`, `--session` (prompt cache reuse), `--continue` (resume last session), `--allow-dangerous-permissions` (opencode `--auto`; default `true` for unattended CI). All flags have env variable equivalents for CI. See `reviewctl --help` for details.

**Runners:** the runner and its model/effort/provider come from the project's **runner profile** (configured in the admin panel and fetched at run time); CI does not pass them. The `--runner`/`--model`/`--effort`/`--api-*` flags still override the profile for local runs.

- `claude` (default) — Claude Code CLI, full agentic exploration.
- `opencode` — opencode CLI (any provider configured in opencode, incl. OpenRouter), `--model provider/model`.
- `codex` — `codex exec` CLI (OpenAI Codex), `--model gpt-5.1-codex`.
- `direct` — calls the LLM API itself (no CLI) with a narrow review tool set (read/grep/glob/git_diff/ast, plus `http_fetch` scoped to the project's task tracker). Prompt caching + diff preload make it the cheapest and fastest path. Adds `--api-provider` (`deepseek` | `openai-compat` | `anthropic`), `--api-base-url`, `--effort` (`low`..`max`); the API key comes from `REVIEW_API_KEY` (or `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`). The `ast_*` navigation tools shell out to the optional [`ast-index`](https://github.com/defendend/Claude-ast-index-search) binary — auto-detected on `PATH` (the generated CI image installs it), with the index rebuilt per run; when absent the tools are simply not offered.
- `opencode` — opencode CLI (any provider configured in opencode, incl. OpenRouter), `--model provider/model`. Runs on the operator's own opencode config only (project config, formatters and LSP off) and refuses a checkout that ships `.opencode/` or `opencode.json(c)`: opencode would execute their plugins with the job's secrets, and no opencode switch prevents it.
- `codex` — `codex exec` CLI (OpenAI Codex), `--model gpt-6-sol` by default; `--effort` maps to `model_reasoning_effort`.
- `direct` — calls the LLM API itself (no CLI) with a narrow review tool set (read/grep/glob/git_diff/ast, plus `http_fetch` scoped to the project's task tracker). Prompt caching + diff preload make it the cheapest and fastest path. Adds `--api-provider` (`deepseek` | `openai` (Responses API) | `openai-compat` | `anthropic`), `--api-base-url`, `--effort` (`low`..`max`); the API key comes from `REVIEW_API_KEY` (or `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENAI_API_KEY`). The `ast_*` navigation tools shell out to the optional [`ast-index`](https://github.com/defendend/Claude-ast-index-search) binary — auto-detected on `PATH` (the generated CI image installs it), with the index rebuilt per run; when absent the tools are simply not offered.

**Task tracker access:** the tracker token never appears in the prompt. reviewctl fetches it with the review config and hands it to runners out-of-band: CLI runners get it as the `REVIEW_TRACKER_TOKEN` env var (prompts reference `$REVIEW_TRACKER_TOKEN` in curl instructions), the `direct` runner offers the model an `http_fetch` tool locked to the tracker origin that injects the `Authorization` header itself. A `REVIEW_TRACKER_TOKEN` CI variable overrides the server-stored token. Backward compatible in both directions: a legacy reviewctl (which doesn't export the env var) receives the old-style prompt with the token substituted in, and a new reviewctl against an older server falls back to the same legacy prompt.

Expand Down Expand Up @@ -219,6 +223,7 @@ When deploying behind a reverse proxy, URLs should be split by access level:
| `/v1/upload/` | Deprecated upload aliases + debug bundle upload (older CI images) |
| `/v1/debug/` | Debug bundle viewer — raw runner transcripts and artifacts |
| `/debug/` | pprof and service metadata |
| `/metrics` | Prometheus metrics (incl. per-project run outcomes and spend) |
| `/status` | Healthcheck (DB ping) |

Example nginx configuration:
Expand All @@ -237,9 +242,35 @@ location /v1/reviewctl/ { deny all; }
location /v1/upload/ { deny all; }
location /v1/debug/ { deny all; }
location /debug/ { deny all; }
location /metrics { deny all; }
location /status { deny all; }
```

### Monitoring

Besides HTTP and DB-pool metrics, `/metrics` (Prometheus) exposes the outcome of every reviewctl run:

| Metric | Labels | Description |
|--------|--------|-------------|
| `reviewer_runs_total` | `project`, `runner`, `status`, `reason` | Runs by outcome: `ok` is counted when the review is uploaded, any other status when reviewctl uploads the run's debug bundle |
| `reviewer_run_cost_usd_total` | `project`, `runner`, `status` | Dollars spent by runs, failed ones included |

- `status`: `ok` \| `failed` \| `cancelled` (the CI job was cancelled — not a failure) \| `timeout`.
- `reason` (for `failed`): `billing` (provider credit balance exhausted), `auth`, `rate_limit`, `api_error` (provider 5xx / overload / dropped stream, after retries), `max_rounds`, `truncated`, `not_submitted` (the last three from the `direct` runner), `other`.
- `project` is the project title (`unknown` for an unknown key); a runner outside `claude`/`opencode`/`codex`/`direct` is `other`.

Alerting examples:

```promql
# Credit balance exhausted — every review fails until it is topped up
increase(reviewer_runs_total{reason="billing"}[15m]) > 0

# A project keeps failing (cancelled jobs excluded)
sum by (project) (increase(reviewer_runs_total{status=~"failed|timeout"}[1h])) >= 3
```

Counters live in memory and reset on restart (`increase()` handles that). For the details of a run, failed runs upload their artifacts (runner transcript, `review.json`, `R*.md`) to `/v1/debug/storage/`: the list shows status, reason, cost and error of the last 200 runs, the newest 20 keep their files (see `[Debug]`); the buffer is dropped on restart. reviewsrv logs each such upload as `debug bundle stored` — at WARN for `failed`/`timeout`, at INFO for a cancelled job.

## Development

```bash
Expand Down
5 changes: 5 additions & 0 deletions cfg/local.toml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ ApplicationName = "reviewsrv"
[Sentry]
DSN = ""
Environment = ""

# In-memory ring of recent reviewctl runs at /v1/debug/storage/ (defaults shown).
[Debug]
Capacity = 200 # runs kept (metadata)
FilesCapacity = 20 # newest runs that keep their artifact files
15 changes: 12 additions & 3 deletions cmd/reviewctl/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ passes only the project key, server URL and credentials, so it stays thin. The
runner flags below are **local overrides**: an explicit flag always wins over the
profile. Multi-review is profile-driven too (see below).

The reviewed checkout is untrusted, so CLI runners ignore the agent config it
ships: `claude` loads user settings only, no MCP servers and no auto-memory;
`opencode` runs with project config off (`OPENCODE_DISABLE_PROJECT_CONFIG`) and
formatters and LSP off (merged into your `OPENCODE_CONFIG_CONTENT`, if set), and
refuses to start when the checkout contains `.opencode/` or `opencode.json(c)`,
whose plugins it would load regardless.

## Flags & Environment Variables

Most flags have an environment-variable default for CI.
Expand All @@ -43,10 +50,12 @@ Most flags have an environment-variable default for CI.
|------|-------------|---------|-------------|
| `--runner` | `$REVIEW_RUNNER` | `claude` | Runner: `claude` \| `opencode` \| `codex` \| `direct` |
| `--model` | `$REVIEW_MODEL` | *runner default* | Model name |
| `--effort` | `$REVIEW_EFFORT` | — | Reasoning effort `low`..`max` (honored by the `claude` and `direct` Anthropic runners; others ignore it) |
| `--api-provider` | `$REVIEW_API_PROVIDER` | `deepseek` | `direct` runner provider: `deepseek` \| `openai-compat` \| `anthropic` |
| `--effort` | `$REVIEW_EFFORT` | — | Reasoning effort `low`..`max` (honored by the `claude`, `codex` and `direct` runners; opencode and `direct` with `openai-compat` ignore it) |
| `--api-provider` | `$REVIEW_API_PROVIDER` | `deepseek` | `direct` runner provider: `deepseek` \| `openai` (Responses API) \| `openai-compat` \| `anthropic` |
| `--api-base-url` | `$REVIEW_API_BASE_URL` | *provider default* | `direct` runner API base URL |
| `--allow-dangerous-permissions` | `$REVIEW_ALLOW_DANGEROUS_PERMISSIONS` | `true` | Pass `--dangerously-skip-permissions` to opencode (needed for unattended CI) |
| `--max-rounds` | `$REVIEW_MAX_ROUNDS` | `60` | `direct` runner round budget (10–500); the flag or env var overrides the profile's `maxRounds`. The model is warned 10 and 3 rounds before it, then gets up to 5 review-only rounds to deliver |
| `--allow-dangerous-permissions` | `$REVIEW_ALLOW_DANGEROUS_PERMISSIONS` | `true` | Pass `--auto` to opencode (needed for unattended CI) |
| `--codex-sandbox` | `$REVIEW_CODEX_SANDBOX` | `workspace-write` | codex `--sandbox` mode; `danger-full-access` inside containers, where codex's bubblewrap sandbox can't start |

### MR & CI metadata (from GitLab CI)

Expand Down
Loading
Loading