Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
51be21f
Lock down the API: auth, sandbox, ingest roots, MCP logging
Sep 24, 2026
b1f0580
Add architecture and design docs for the agentic workflows
Sep 24, 2026
bc8a107
docs: add task-level implementation plan for the agentic workflows
Sep 24, 2026
ae12480
docs: record Phase 1 spike results
Sep 24, 2026
c235781
build: split engine and server dependencies, add LangChain agent stack
Sep 24, 2026
2124b9a
refactor: keep server-only imports out of the engine import path
Sep 24, 2026
c4837a3
feat: add Claude 5 model prices to the telemetry table
Sep 24, 2026
39728f0
feat: add unified-diff model with RIGHT-side commentable line ranges
Sep 24, 2026
59d01c4
refactor: extract process-group-bounded subprocess runner
Sep 24, 2026
35b2a1e
feat: add repository workspace with agent tools
Sep 24, 2026
87b0301
feat: add .codesage.yml review config and per-role model settings
Sep 24, 2026
0904f95
feat: add chat-model factory, budget guard and agent middleware
Sep 24, 2026
362dd0e
feat: add agent output schemas and tool-use prompts
Sep 24, 2026
dda6def
fix: refuse .git paths case-insensitively in workspace tools
Sep 24, 2026
4a8941b
feat: rebuild the review engine on tool-using agents
Sep 24, 2026
86550fd
fix: resolve reflection drops before merges; don't show the gate runn…
Sep 24, 2026
2c1717c
docs: record Phase 4 live verification
Sep 24, 2026
1a1e44a
feat: extend GitHub client with PR metadata, reviews, check runs and …
Sep 24, 2026
92df3d5
feat: render and publish reviews to GitHub
Sep 24, 2026
1b5e829
feat: add the GitHub Action runner
Sep 24, 2026
330a62c
feat: add action.yml, example workflow and config, and README usage
Sep 24, 2026
f6493df
feat: add review columns for the approval gate, traces and telemetry
Sep 24, 2026
8c5d6d6
feat: pause reviews at the gate and resume on a human decision
Sep 24, 2026
91d0d29
fix: never leave the CodeSage check in progress; cap the review body
Sep 24, 2026
96f7ebd
feat: console support for the real approval gate
Sep 24, 2026
78a32d6
fix: persist gate reasons for reviews paused at the gate
Sep 24, 2026
cf27953
feat: show the gate as waiting while a review is paused
Sep 24, 2026
c57c211
chore: remove the DigitalOcean deploy path
Sep 24, 2026
1af2944
fix: make the approval decision atomic so a review resumes once
Sep 24, 2026
1691f35
refactor: remove the unused diff sandbox
Sep 25, 2026
9626eac
docs: update CLAUDE.md for the agentic engine, Action and approval gate
Sep 25, 2026
8e683dd
docs: rewrite README around the GitHub Action and the local approval …
Sep 25, 2026
3c3008f
docs: refresh the workflow diagram for the agentic pipeline
Sep 25, 2026
f483a80
docs: record GitHub behaviours verified on the test repo; drop issues…
Sep 25, 2026
b27b1f0
docs: record Phase 6 verification, including PR-mode posting on the t…
Sep 25, 2026
97cc418
refactor: share PR-context building; drop unused settings
Sep 25, 2026
439c6c3
docs: record paid Action runs and mark the design implemented
Sep 25, 2026
5807ad1
docs: note the live revise round as not yet exercised
Sep 25, 2026
6c9194e
fix: wait for the killed process group to exit on timeout
Sep 30, 2026
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
1 change: 0 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ eval/results
data
.DS_Store
docker-compose*.yml
deploy
36 changes: 26 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# ── Claude API ──────────────────────────────────────────────────────────────
ANTHROPIC_API_KEY=sk-ant-your-key-here

# Primary reviewing model. Defaults to the most capable Opus-tier model.
CODESAGE_MODEL=claude-opus-4-8
# Model used by the LLM-as-Judge eval harness.
CODESAGE_JUDGE_MODEL=claude-opus-4-8
# Effort level for Claude calls: low | medium | high | xhigh | max
CODESAGE_EFFORT=high
# Reviewer + reflection agents, and the judge (a different, stronger model).
# A repo's .codesage.yml overrides these for Action runs.
CODESAGE_MODEL=claude-sonnet-5
CODESAGE_JUDGE_MODEL=claude-opus-5
# Effort: low | medium | high | xhigh | max
CODESAGE_REVIEWER_EFFORT=medium
CODESAGE_JUDGE_EFFORT=high
# Hard cap on model spend per review, in USD.
CODESAGE_BUDGET_USD=0.50
# LangGraph checkpointer DSN (psycopg). Empty = DATABASE_URL without "+asyncpg".
CODESAGE_CHECKPOINT_DSN=

# ── Embeddings (pgvector RAG) ────────────────────────────────────────────────
# If VOYAGE_API_KEY is set, Voyage embeddings are used; otherwise CodeSage falls
Expand All @@ -27,10 +32,21 @@ GITHUB_TOKEN=
# ── Behavior ─────────────────────────────────────────────────────────────────
# Judge score (0-1) below which a review requires human approval.
CODESAGE_HITL_THRESHOLD=0.6
# Max RAG context chunks injected per reviewer.
CODESAGE_RAG_TOP_K=6
# Allow the sandbox to execute tests found in the diff (true/false).
CODESAGE_SANDBOX_ENABLED=true

# ── Security ─────────────────────────────────────────────────────────────────
# API keys accepted on /api/v1 (comma-separated). REQUIRED: with none set the
# API refuses every request. Generate one with:
# python -c "import secrets; print(secrets.token_urlsafe(32))"
CODESAGE_API_KEYS=
# Local development only: skip API-key checks entirely. Never on a reachable host.
CODESAGE_AUTH_DISABLED=false
# Directories POST /ingest may read (comma-separated). Empty disables API ingest;
# the CLI (python -m scripts.ingest_repo) is unaffected. Docker compose defaults
# this to /app (the API's own source) when unset.
CODESAGE_INGEST_ROOTS=
# Cross-origin callers allowed (comma-separated). Empty = none; the web console
# is same-origin through its proxy and doesn't need CORS.
CODESAGE_CORS_ORIGINS=

# ── Server ───────────────────────────────────────────────────────────────────
CODESAGE_HOST=0.0.0.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
pip install -e ".[server,dev]"

- name: Lint
run: ruff check app tests scripts
Expand Down
134 changes: 134 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

CodeSage is a Claude-powered PR reviewer. It has two parts:
- **The main product:** a reusable composite **GitHub Action** (`action.yml` → the `codesage-action` console script).
- **A local companion:** a FastAPI backend (`app/`), a React operator console (`web/`), an MCP server and a pgvector RAG store, with a real human approval gate.

Both run the same LangGraph review engine. The design docs are in `docs/`: `architecture.md`, `design/01-03`, `decisions.md` (the decision log plus verified facts) and `roadmap.md`.

## Commands

Backend (use the local venv at `.venv`). Running the API needs `CODESAGE_API_KEYS=<key>` (or `CODESAGE_AUTH_DISABLED=true` for local-only dev); otherwise every `/api/v1` route returns 503:

```bash
pip install -e ".[server,dev]" # or: make dev (engine only, as the Action installs it: pip install .)
.venv/bin/pytest -q # full suite (make test)
.venv/bin/pytest tests/test_review_graph.py::test_name -q # single test
ruff check app tests scripts # lint (make lint) — CI fails on this
ruff check --fix app tests scripts && ruff format app tests scripts # make fmt
mypy app # advisory only in CI (|| true)
uvicorn app.main:app --reload # API on :8000, OpenAPI at /docs
python -m scripts.migrate # apply migrations/*.sql in order (idempotent)
python -m scripts.init_db # alternative: pgvector extension + ORM create_all (fresh DB)
python -m scripts.ingest_repo --path /path/to/repo --repo owner/name # RAG ingest
python -m app.eval.harness run --dataset eval/datasets/sample.jsonl
python -m app.mcp.server # stdio MCP server
codesage-action review --event e.json --event-name pull_request --repository o/r \
--workspace /path/to/checkout --dry-run --local-diff # reproduce an Action run, post nothing
```

Frontend (`web/`): `npm run dev` (:5173; Vite proxies `/api` to `VITE_API_TARGET`, default :8000), `npm run build` (`tsc -b && vite build`), `npm run lint` (`tsc --noEmit`).

Full stack: `docker compose up --build` gives web :3000 (nginx, which proxies `/api` with SSE buffering off), api :8000 and db :5432. The compose file mounts `migrations/` into `docker-entrypoint-initdb.d`, so a fresh volume runs every migration; for an existing volume run `python -m scripts.migrate`.

## Stub mode (important for tests)

With no `ANTHROPIC_API_KEY`, `LLMClient.stubbed` (`app/llm/client.py`) is true. Reviewer, reflection and judge nodes then call `app/llm/stub.py` (deterministic regex heuristics keyed by `security`, `correctness`, `style`, `reflection` and `judge`) instead of Claude, and they emit a scripted trace.

`tests/conftest.py` forces stub mode and a dummy DB DSN before any app import, so **the suite must pass offline with no API key and no database**. Consequences:
- Any new LLM-calling node needs a stub branch.
- Tests only exercise the stub path. The live path isn't covered by tests: LangChain `create_agent` with `ChatAnthropic` from `app/llm/models.py`, `ProviderStrategy` structured output, and the middleware.
- Without Voyage (`VOYAGE_API_KEY`), embeddings fall back to a hash embedding (`app/rag/embeddings.py`).
- `Settings` is `lru_cache`d (`app/config.py`), so env must be set before the first `get_settings()` call. The Action builds `Settings(_env_file=None)` and passes it explicitly, because its working directory is the reviewed repo.

## Architecture

**Review graph** (`app/agents/graph.py`): a LangGraph `StateGraph` over `ReviewState` (`app/agents/state.py`):
`START → {security, correctness, style} (parallel) → reflection → judge → [revise → judge, at most once] → human_gate → publish → END`.

- **Reviewers** (`app/agents/reviewers/base.py`):
- Each is a `create_agent` loop over workspace tools, with `response_format=ProviderStrategy(Findings)`.
- Code, not the model, assigns each finding's `id` and `reviewer` (`app/agents/findings.py`).
- Middleware (`app/agents/middleware.py`):
- `FinishMiddleware` meters cost into `BudgetGuard` (`app/llm/budget.py`). Near the budget or at the last allowed call, it removes the tools so the model must return structured findings; jumping to `end` would return none.
- `TraceMiddleware` records the real tool calls as a `ReactTrace`.
- **Reflection** (`app/agents/reflection.py`):
- Returns keep/drop/merge *decisions*; `apply_decisions()` applies them in code, so attribution survives.
- Line numbers are cleared unless they fall on commentable diff lines.
- The same node, given the judge's feedback, is `revise`.
- **Judge:** `app/eval/judge.py`, native `json_schema` structured output.
- **`human_gate`:** computes `gate_reasons`. Its behaviour depends on the graph mode:
- `report` (default: tests, MCP, eval) and `action`: never pause.
- `local`: `interrupt()`, which needs a checkpointer.
- **`publish`:** calls `deps.publisher` if one is set.
- **State reducers:** the parallel reviewers write `draft_findings` and `traces` (`operator.add`) and `budget_limited` (`operator.or_`). `ReviewService._merge_delta` mirrors these using `ADDITIVE_KEYS`/`OR_KEYS` from `state.py`. Only the three reviewers may append to `traces`; tests assert this, and reflection's trace goes in `reflection_trace`.
- **Dependencies:** `make_deps(client, workspace, settings, cfg=, publisher=)` builds `EngineDeps` (`app/agents/deps.py`). `ReviewConfig` (`app/review_config.py`) merges a repo's `.codesage.yml` over env settings; `prepare_diff()` applies the ignore globs and `max_files`.
- **Where graphs are built:** `app/action/__main__.py`, `ReviewService`, `app/mcp/server.py` and `app/eval/harness.py`.

**Workspace and diff** (`app/workspace/`, `app/diff/`):
- **Tools:** `read_file`, `search_code` (`git grep`), `list_dir`, `git_log`, `git_blame`, `get_diff`, `run_tests` and `semantic_search`. They are LangChain tools, and errors come back as `"error: …"` strings, not exceptions.
- Paths are resolved, symlinks included, and must stay inside the root.
- `.git` is refused, case-insensitively.
- Output is capped at 12,000 chars.
- **`run_tests`:** runs only when `.codesage.yml` sets `tests.command`, and is offered only to the correctness reviewer. It gets a scrubbed environment (no tokens or keys) and a process-group timeout (`app/workspace/proc.py`).
- **Three workspace shapes:**
- `Workspace(root, diff)`: the Action's checkout.
- `Workspace.clone_at(...)`: local PR mode, a blobless temporary clone; the token is passed via `GIT_CONFIG_*` env, never written to `.git/config`.
- `Workspace.diff_only(...)`: local diff mode.
- **`Diff.is_commentable(path, line)`:** true for added or context lines on the RIGHT side. These are the only lines GitHub accepts for inline comments.

**GitHub Action** (`action.yml`, `app/action/`, `app/publish/`):
- **Steps:** setup-python@v7 → `pip install` the engine into a venv → `codesage-action resolve` → checkout@v7 at the head SHA (full history, `persist-credentials: false`) → `codesage-action review`.
- It is a console script, **not** `python -m app.action`: `-m` would put the reviewed repo first on `sys.path`, so a repo with its own `app/` package would be imported.
- **Triggers** (`app/action/rules.py`, mirrored in `examples/codesage-workflow.yml`):
- PR opened (not a draft) or marked ready for review
- the `codesage:review` label, which is removed afterwards
- a `/codesage review` comment from OWNER/MEMBER/COLLABORATOR

Forks are skipped.
- **Publishing:** `GitHubPublisher` posts one `COMMENT` review with up to 30 inline comments (retrying body-only on a 422) and the `CodeSage` check run. The check conclusion is error > failure > neutral (budget-limited only) > success.
- **Exit codes:** 0 when the review is published, whether the gate passed or tripped; 1 on an engine or publishing error. The check run is always completed.

**Local mode** (`app/services/`, `app/api/`, `app/main.py`):
- **Lifespan:** opens LangGraph's `AsyncPostgresSaver` (psycopg v3, using `settings.checkpoint_dsn`, which is `DATABASE_URL` without `+asyncpg`) and marks rows stuck in `running` as `failed`. Without a checkpointer, reviews run in `report` mode and the gate can't pause.
- **`POST /reviews/async`:** `JobManager` runs `ReviewService.run_review_streamed` with `thread_id` = review id. A tripped gate interrupts the run and sets status `awaiting_approval`. PR mode means a PR number, no diff, and `GITHUB_TOKEN` set: the agents review a clone at the head, and the review can be posted.
- **`POST /reviews/{id}/decision`:** the `awaiting_approval → running` transition is atomic (409 otherwise). The review then resumes with `Command(resume=…)`.
- Approve posts to the PR. Posting is idempotent: a marker `<!-- codesage-review:{id} -->` is checked via `list_reviews`.
- Reject posts nothing.
- A passing gate posts automatically.
- **Statuses:** `running`, `awaiting_approval`, `completed`, `rejected`, `failed`.
- **SSE** (`GET /reviews/{id}/stream`, backed by the in-process `ReviewBroker`, so a single Uvicorn worker only):
- Unknown or malformed ids return 404.
- Streams end on completed, failed or awaiting.
- A finished or paused review that's no longer buffered gets its final event rebuilt from the DB.
- `broker.reopen()` lets a resumed review continue the same stream, and buffers are evicted 10 minutes after the end.
- **Concurrency:** the reviewers run in parallel, so `semantic_search` (`app/rag/search.py`) opens its own session per call. Never share one `AsyncSession` across graph nodes.

**Stage names live in five places.** If you add, rename or reorder graph nodes, update all of them:
- `STAGES` in `app/agents/graph.py`; `ReviewService.STAGE_ORDER` derives from it
- `STAGES` in `web/src/lib/format.ts`
- `STAGE_DEPS`/`STAGE_LABEL`/`OPTIONAL_STAGES` in `web/src/lib/useReviewStream.ts`
- `StageName` in `web/src/lib/types.ts`
- `ICONS` in `web/src/components/StageTimeline.tsx`

The stream only reports completions, so the console derives running, pending, skipped and awaiting from the dependency map.

**Security model:**
- **Auth:** `/api/v1` routes sit on `router` (`app/api/routes.py`) with `require_api_key` (`app/api/auth.py`, keys from `CODESAGE_API_KEYS`; it fails closed with 503). Only `/meta` is on `public_router`. New routes belong on `router`, and `tests/test_security.py` lists every protected route.
- **Console:** sends the key from localStorage and reads SSE via `fetch`.
- **Other limits:** API ingest is limited to `CODESAGE_INGEST_ROOTS`, and API eval datasets to `eval/datasets/`. Repo names go through `validate_repo` before any token-bearing call.
- **Logging:** goes to **stderr**, because stdout belongs to the MCP stdio transport.

**Telemetry:** `BudgetGuard.record` prices each call from LangChain `usage_metadata`. Here `input_tokens` includes cached tokens, and cache writes appear either as `cache_creation` or as the `ephemeral_5m`/`ephemeral_1h` keys. It records into the review's `CostTracker` (`app/llm/telemetry.py`), which is persisted as `usage_events` rows plus the review's `telemetry` column.

**Database schema has two sources of truth:** `migrations/*.sql` (idempotent, applied by `scripts/migrate.py` or on first boot) and the SQLAlchemy models in `app/db/models.py`. Change both together.

## Conventions

- Python ≥3.11, ruff line length 100, rules `E,F,I,UP,B` (B008 ignored for FastAPI `Depends`).
- `pytest` uses `asyncio_mode = "auto"`, so write async tests as plain `async def`.
- Adding a reviewer: see "Adding a reviewer" in `CONTRIBUTING.md`.
- Engine modules (everything the Action imports) must not import server-only packages (fastapi, sqlalchemy, pgvector, mcp, psycopg) at module level; import them lazily or under `TYPE_CHECKING`.
- Commit messages are conventional and present tense (`add`, `fix`, `refactor`). **Never add `Co-Authored-By` or other Claude/session attribution trailers.**
28 changes: 18 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Thanks for your interest! This project follows standard Python practices.
```bash
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pip install -e ".[server,dev]"
cp .env.example .env # add ANTHROPIC_API_KEY (optional — stub mode works without)
```

Expand All @@ -29,15 +29,23 @@ make typecheck # mypy

| Path | What |
| --- | --- |
| `app/agents/` | LangGraph orchestration, ReAct reviewers, reflection, sandbox |
| `app/rag/`, `app/db/vector_store.py` | pgvector RAG pipeline |
| `app/eval/` | LLM-as-Judge harness + regression detection |
| `app/llm/` | Claude client, telemetry, prompts, offline stub |
| `app/api/`, `app/mcp/` | REST and MCP surfaces |
| `deploy/` | DigitalOcean provisioning + production compose |
| `app/agents/` | Review graph, tool-using reviewer agents, reflection, middleware, schemas |
| `app/workspace/`, `app/diff/` | Agent tools over a checkout (confined, capped), diff model |
| `app/llm/` | Model factory, budget guard, telemetry, prompts, offline stub |
| `app/eval/` | LLM-as-Judge (also the graph's judge), eval harness, regression detection |
| `app/action/`, `app/publish/`, `action.yml` | GitHub Action runner and GitHub publishing |
| `app/api/`, `app/services/`, `app/mcp/` | Local REST/SSE API, review jobs + approval gate, MCP |
| `app/rag/`, `app/db/` | pgvector RAG store, ORM models (mirror `migrations/*.sql`) |
| `web/` | React operator console |

## Adding a reviewer

Subclass `app.agents.reviewers.base.ReviewerAgent`, set `name`/`component`/
`system`/`focus_query_template`, register it in `app/agents/graph.py`, and add a
stub branch in `app/llm/stub.py` so it works offline.
1. Add the role to `REVIEWER_ROLES` (`app/agents/reviewers/base.py`) and to the
`ReviewerName` literal (`app/agents/schemas.py`); give it an id prefix in
`app/agents/findings.py`.
2. Write its focus prompt in `REVIEWER_FOCUS` (`app/llm/prompts.py`).
3. Add a stub branch in `app/llm/stub.py` so it works offline.
4. Add the stage to the console: `web/src/lib/format.ts` (`STAGES`),
`web/src/lib/useReviewStream.ts` (`STAGE_DEPS`, `STAGE_LABEL`),
`web/src/lib/types.ts` (`StageName`) and `web/src/components/StageTimeline.tsx`
(`ICONS`). The backend's stage list derives from `REVIEWER_ROLES`.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'

install: ## Install runtime deps
pip install -e .
install: ## Install engine + server deps
pip install -e ".[server]"

dev: ## Install dev deps
pip install -e ".[dev]"
pip install -e ".[server,dev]"

up: ## Start the full stack (api + db) in Docker
docker compose up --build
Expand Down
Loading
Loading