A simple AI review tool, designed to be run in spindle CI/CD
0

Configure Feed

Select the types of activity you want to include in your feed.

Python 96.9%
Nix 3.1%
9 4 0

Clone this repository

https://tangled.org/xenolandscapes.com/ci-review-tools https://tangled.org/did:plc:to45bc2ww36ezdzoegiawx76
git@knot.xenolandscapes.com:xenolandscapes.com/ci-review-tools git@knot.xenolandscapes.com:did:plc:to45bc2ww36ezdzoegiawx76

For self-hosted knots, clone URLs may differ based on your setup.



README.md

ci-review-tools#

Agentic code review for Tangled Spindle CI — packaged as a Nix flake.

Setup#

CI (Spindle pipeline)#

Add this to .tangled/workflows/review.yml:

when:
  - event: ["push"]
    branch: ["main"]

clone:
  depth: 40

engine: microvm
image: nixos

dependencies:
  - git+https://tangled.org/<you>/ci-review-tools?ref=main#default

steps:
  - name: "agentic code review"
    environment:
      LLM_API_KEY: "${{ secrets.LLM_API_KEY }}"
      LLM_MODEL: "${{ secrets.LLM_MODEL }}"
      LLM_CONTEXT_LIMIT: "${{ secrets.LLM_CONTEXT_LIMIT }}"
      ATP_APP_PASSWORD: "${{ secrets.ATP_APP_PASSWORD }}"
    command: ci-review

Secrets (repo Settings → Secrets):

Secret Required Default Description
LLM_API_KEY Yes API key for OpenAI-compatible LLM
LLM_MODEL Yes Model name (e.g. gpt-4o, deepseek-v4-flash)
ATP_APP_PASSWORD Yes AT Protocol app password
LLM_BASE_URL No https://api.openai.com/v1 API base URL
LLM_CONTEXT_LIMIT No 120000 Max tokens before degradation
LLM_MAX_TOKENS No 16384 Max output tokens per LLM call
DETECTIVE_RUNS No 2 Detective passes per review (more = more consistent)

CI automatically receives TANGLED_REPO_DID, TANGLED_REPO_URL, TANGLED_REPO_NAME, and TANGLED_PIPELINE_ID from Spindle — no manual config.

Local development#

Create a .env file in the repo root:

LLM_API_KEY=sk-...
LLM_MODEL=deepseek-v4-flash
ATP_IDENTIFIER=xenolandscapes.com
ATP_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx

Run from any repo:

uv run --with tree-sitter-language-pack --with tree-sitter --with tiktoken --with openai \
  python3 /path/to/ci-review-tools/ci-review.py

PDS is auto-resolved from the handle's DID document. Repo DID is auto-resolved from the repo record on your PDS. No manual PDS or TANGLED_REPO_URL needed.

Set DETECTIVE_RUNS=1 for faster (but noisier) local testing.

Review flow#

The review runs in two passes. The detective runs multiple times for deterministic results — each run finds a different subset of issues.

Pass 1: Detective (runs N times, default 2)#

Finds everything — exhaustive, no judgment, no filtering.

Context:

  • Inline diffs (git diff -U999999) for every changed file
  • Full commit messages with per-commit file lists
  • Dependency modules (imported by changed files, sorted by uniqueness)
  • AGENTS.md conventions

Output per run: JSON array of raw findings — title, summary, problem, fix, commit references. No severity or impact.

Pass 2: Triage (runs once)#

Deduplicates across detective runs, filters, and assigns severity.

Context:

  • Full file contents of every changed file (not diffs)
  • Dependency modules (same as pass 1)
  • AGENTS.md conventions
  • All raw findings from ALL detective runs (tagged with _run number)
  • No commit messages — judges purely on code + conventions

Output: Severity (high/medium/low), impact map, and drop flag for each finding. Duplicate findings (same issue, different wording) are dropped.

Merged result: Detective's title/summary/problem/fix + triage's severity/impact. Dropped findings excluded.

Context degradation#

If the detective pass exceeds LLM_CONTEXT_LIMIT tokens, context is degraded across 5 levels. Inline diffs are the most valuable context and survive longest:

Level Changed files Commits Deps AGENTS.md
0 Inline diffs Full (message + body + files) All Yes
1 Inline diffs Short (subject + files only) All Yes
2 Inline diffs None All Yes
3 Inline diffs None None Yes
4 Compact diff (3 lines) None None Yes

Dependencies in levels 0-2 are sorted by uniqueness — files imported by fewer changed files rank first (e.g. 1/37 files before 35/37 files). Framework code everyone imports is deprioritized.

Dependency detection#

Tree-sitter parses changed files and resolves imports 1 level deep:

  • Built-in detection (process() from tree-sitter-language-pack): Python, JavaScript, TypeScript, Go, Rust, Java
  • Custom tree-sitter queries (QueryCursor): Lua (require()), C (#include)
  • Resolution via git ls-files '*/module/path.ext' — works for any project structure, no path guessing

Push range detection#

  1. Authenticates with AT Protocol via app password
  2. Fetches the pipeline record at TANGLED_PIPELINE_ID via getRecord
  3. Extracts triggerMetadata.push.oldSha and .newSha — the exact push range
  4. Fallback: if the fetch fails (manual trigger, network error), reviews HEAD~20

Issue format#

Each finding becomes a Tangled issue with:

Title: Emoji severity + one-line summary (🔴 Null pointer in buffer.lua)

Body:

## Summary
Brief description referencing specific files/functions

## The Problem
Context and mechanism — what triggers it, why it happens

## Impact
- **correctness:** description
- **safety:** description

## Recommended Fix
Concrete fix with code pointers

---
**Commit range:** `abc1234`..`def5678`
**Code Review:** CR-2026-07-25-3

Issues are created on the repo's own DID (resolved from sh.tangled.repo record), not the user DID. The repo field MUST be a bare DID per the lexicon.

Identity resolution#

On startup, the tool resolves the full AT Protocol identity chain:

handle (xenolandscapes.com)
  → DID (did:web:... or did:plc:...)
  → DID document (.well-known/did.json or plc.directory)
  → PDS endpoint (AtprotoPersonalDataServer service)
  → repo record (sh.tangled.repo/<name>)
  → repo's own DID (repoDid field)

All derived from a single handle. Handles did:web and did:plc identities. In CI, TANGLED_REPO_DID (owner DID) is used for auth — no handle needed.