drep-ai 2.3.0

A local commit gate: runs the linters your repo configures, and sends changed code to an LLM for review
Documentation
<div align="center">

# drep

<img src="docs/images/drep.png" alt="drep logo" width="200" />

**A local commit gate.** It runs the linters your repository already
configures, and sends the code you changed to an LLM for review.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust 1.88+](https://img.shields.io/badge/rust-1.88+-orange.svg)](https://www.rust-lang.org)

```sh
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/slb350/drep/releases/latest/download/drep-ai-installer.sh | sh
```

</div>

## What it does

On `git commit` and `git push`, drep checks your changes twice.

| Layer | Source | Blocks? |
|---|---|---|
| Deterministic | ruff, eslint, tsc, gofmt, go vet, clippy | Yes |
| Semantic | an LLM you point it at | No, unless you ask |

Your linters are precise, so their findings block. A model's opinion about
naming is not precise at any severity, so it informs instead. Splitting by
source rather than by severity is what makes the gate calibratable; opt the
LLM into blocking with `--fail-on error` when you want it.

drep is a single binary. It talks to no source-control platform, runs no
server, and needs no drep account. The deterministic half needs no model and
no API key at all. Semantic review can use an HTTP API or a separately
installed Codex CLI authenticated through a ChatGPT subscription.

## Install

```sh
# Shell installer (macOS and Linux, x86_64 and arm64)
curl --proto '=https' --tlsv1.2 -LsSf \
  https://github.com/slb350/drep/releases/latest/download/drep-ai-installer.sh | sh

# Homebrew
brew install slb350/tap/drep

# crates.io
cargo install drep-ai

# Current main branch
cargo install --git https://github.com/slb350/drep drep-ai
```

The crate is `drep-ai` because `drep` was taken on crates.io. The binary is
`drep`.

## Set up a repository

```sh
cd your-repo
drep init                              # interactive: pick a provider, paste a key,
                                       # choose from the models it actually serves
drep init --provider openrouter        # HTTP API
drep init --provider codex             # ChatGPT/Codex subscription
# Other presets: local, zai, minimax, kimi, openai, custom
export OPENROUTER_API_KEY='...'
```

`drep init` writes two things: a `drep.toml` naming your model, and a git hook.
The default is a pre-push hook; `--hooks pre-commit` or `--hooks both` if you
want the gate earlier, `--hooks none` for the config alone.

The interactive path fills in `temperature` and `max_tokens` for the model you
picked rather than for its provider, using a weekly-refreshed copy of
[models.dev](https://models.dev). It only ever removes a parameter or lowers a
required ceiling, never the reverse, so a model that refuses `temperature` gets
no `temperature` line and `k3` gets its own 131,072 instead of a number chosen
for the endpoint. Offline, or for a model too new to be listed, the provider's
defaults are written and setup carries on. `DREP_QUIRKS_PATH` points drep at a
different cache.

It writes native git hooks rather than a pre-commit entry, and it handles
`core.hooksPath`: if you have a global hooks directory, a repository-local hook
would otherwise never fire, silently.

To adopt drep through [pre-commit](https://pre-commit.com) instead:

```yaml
repos:
  - repo: https://github.com/slb350/drep
    rev: v2.3.0
    hooks:
      - id: drep-check-push   # pre-push: what the push touches
      # - id: drep-check      # pre-commit: staged files
      # - id: drep-lint-docs  # markdown, rule-based
```

## Commands

```sh
drep check                      # this directory
drep check src/ main.go         # named files or directories
drep check --staged             # what is staged, for a pre-commit hook
drep check --diff origin/main   # what changed since a ref, for pre-push
drep check --fail-on error      # also block on LLM findings
drep check --format json        # machine-readable

drep lint-docs                  # markdown in this tree
drep lint-docs --staged --fail-on error
drep doctor                     # what will actually run here
```

Exit codes, shared by `check` and `lint-docs`:

| Code | Meaning |
|---|---|
| 0 | Everything that should have run, ran, and found nothing blocking |
| 1 | Blocking findings |
| 2 | Something that should have run did not |

Exit 2 is the one that matters. An unreachable endpoint, a file too large for
the model, a configured tool that is not installed: none of those are a pass,
and a gate that reports them as one is worse than no gate.

## What will run here

```console
$ drep doctor
drep in /Users/you/your-repo
============================================================

Languages found:
  Go: 12 file(s)         Python: 48 file(s)      TypeScript: 31 file(s)

Deterministic checks (these gate):
  ruff: ready
  gofmt: ready
  go vet: ready
  eslint: not configured (add one of: eslint.config.js, ...)
  tsc: configured but NOT INSTALLED - these checks will not run

LLM analysis (required):
  1. deepseek/deepseek-v4-pro-0813 at https://openrouter.ai/api/v1
```

Three rules decide what runs:

- Repository-local before PATH, so a project is checked by the version its CI
  runs.
- Only where the project configured it. No eslint config means no eslint
  opinion.
- A configured tool that is missing makes the run exit 2. A check that did not
  run is never reported as a pass.

## Languages

| Language | Extensions | Tools |
|---|---|---|
| Python | `.py` | ruff |
| JavaScript | `.js` `.jsx` `.mjs` `.cjs` | eslint |
| TypeScript | `.ts` `.tsx` `.mts` `.cts` | eslint, tsc |
| Go | `.go` | gofmt, go vet |
| Rust | `.rs` | clippy |

The LLM half reads any of them. It parses nothing, so it needs no grammar per
language; it is told which language it is reading and which conventions that
language's ecosystem expects.

## Markdown

`drep lint-docs` is rule-based only: no LLM, no network, no config file. Ten
checks, and their severity answers one question, which is whether the finding
changes how the document renders.

| Severity | Checks |
|---|---|
| error | `unclosed_code_fence` |
| warning | `empty_heading`, `missing_space_after_heading`, `link_syntax_invalid` |
| info | `bare_url`, `long_line`, `tab_character`, `trailing_whitespace` |
| info | `trailing_blank_lines`, `multiple_blank_lines` |

An unclosed fence turns every line below it into code, so it alone blocks by
default. Whitespace renders identically, so it does not. `--strict` is the
shorthand for `--fail-on info`, which blocks on everything; over a real
repository that is dominated by line length, and a hook that blocks a commit
over a long line is a hook that gets deleted.

## Configuration

`drep.toml`, written by `drep init`:

```toml
[[llm]]
endpoint = "https://openrouter.ai/api/v1"
model = "deepseek/deepseek-v4-pro-0813"
api_key = "${OPENROUTER_API_KEY}"
timeout_secs = 1800
```

HTTP is the default backend, so existing configurations remain valid without a
`backend` field. OpenAI API usage is the `openai` preset and uses per-token API
billing:

```sh
drep init --provider openai
export OPENAI_API_KEY='...'
```

ChatGPT/Codex subscription usage is a separate backend. Install the official
[Codex CLI](https://learn.chatgpt.com/docs/codex/cli), follow the
[Codex authentication](https://learn.chatgpt.com/docs/auth) guide to run
`codex login`, then select the `codex` preset:

```sh
codex login
drep init --provider codex
```

```toml
[[llm]]
backend = "codex"
model = "gpt-5.6-sol"
reasoning_effort = "high"
timeout_secs = 1800
max_concurrent = 1
```

This mode consumes the ChatGPT/Codex plan allowance, not OpenAI API credits.
drep never reads or stores the subscription tokens: Codex owns login and token
refresh. Every review is an ephemeral, non-interactive, read-only Codex run in
an empty directory with tools, apps, MCP, hooks, memories, user configuration,
and project instructions disabled. `drep doctor` verifies the installed CLI
and ChatGPT-managed authentication without printing account details.

`drep init` does not write your key into this file. Keys go to a per-machine
store (`~/.config/drep/auth.toml`, mode 0600, keyed by endpoint), so `drep.toml`
carries only the provider choice:

```sh
drep auth list                                   # endpoints held; never the keys
drep auth login --provider kimi                  # paste a key, no echo
drep auth logout --endpoint https://api.kimi.com/coding/v1
```

`api_key = "${VAR}"` still works and takes precedence over a stored key, which
is what CI wants — there is nobody to paste anything there. `DREP_AUTH_PATH`
points drep at a different store.

By default `drep init` also adds `drep.toml` to `.gitignore`; pass
`--no-gitignore` to commit it instead and share the provider choice with the
repository.

`[[llm]]` is an array of tables, and the order is a failover chain. Each entry
is tried in turn: a timeout, a refused connection, a 429, a 5xx or an empty
answer falls through to the next one. A 401 or 403 does not, because that is a
broken key and falling back would hide it. Set `enabled = false` on an entry to
park it without deleting it.

```toml
# Local model first, cloud when it is not running.
[[llm]]
endpoint = "http://localhost:1234/v1"
model = "qwen3-30b-a3b"

[[llm]]
endpoint = "https://openrouter.ai/api/v1"
model = "deepseek/deepseek-v4-pro-0813"
api_key = "${OPENROUTER_API_KEY}"
```

### Subscription coding plans

`drep init` has presets for three of them, so a plan you already pay for can run
the gate instead of per-token API billing:

```sh
drep init --provider zai       # GLM 5.3, OpenAI-compatible
drep init --provider minimax   # MiniMax M3, Anthropic protocol
drep init --provider kimi      # Kimi k3, Anthropic protocol
```

Two of those endpoints expose the Anthropic messages API rather than chat
completions, which is what `protocol` selects:

```toml
[[llm]]
endpoint = "https://api.minimax.io/anthropic/v1"
model = "MiniMax-M3"
api_key = "${MINIMAX_API_KEY}"
protocol = "anthropic"
```

`protocol` defaults to `openai`, so an existing file needs no change. `doctor`
tags a non-default protocol in its listing.

Check the plan's own terms before pointing a commit gate at it. They are not
uniform, and some restrict a subscription to a named list of client tools or to
interactive use.

### Other keys

`temperature` (unset means the parameter is not sent at all, which is what some
models require — `k3` and `gpt-5.6-sol` reject any value), `max_tokens` (unset
by default, so a reasoning model is never truncated mid-thought; a few endpoints
refuse a request without it), `max_retries`, `max_concurrent`.

Both are properties of the model rather than the endpoint, which is why the
wizard resolves them per model. Editing either by hand always wins: drep reads
the file as written and never revisits it.

## Development

```sh
cargo test --all-targets --all-features
cargo clippy --all-targets --all-features     # levels come from [lints] in Cargo.toml
cargo mutants                                 # a green suite is not a discriminating one
```

`docs/technical-design.md` is the architecture. `CLAUDE.md` carries the
invariants, each with the defect that produced it.

## History

Versions up to 1.3.0 were a Python package (`drep-ai` on PyPI) that also ran a
webhook server, posted reviews to Gitea, GitHub and GitLab, generated
docstrings and kept a SQLite cache. 2.0 is a rewrite in Rust and keeps only the
local gate. The PyPI releases stay up; nothing new is published there.

## License

MIT. See [LICENSE](LICENSE).