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
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ ANTHROPIC_API_KEY=your_anthropic_api_key_here
# GitHub
GITHUB_TOKEN=your_github_personal_access_token_here
TARGET_GITHUB_USERNAME=your_github_username_here
# PR review: absolute path to a local clone of the repo whose PRs you review
REPO_PATH=/absolute/path/to/local/clone/of/the/repo
# Your personal oh-my-workers DB
DATABASE_URL=postgresql://user:password@localhost:5432/work_coordinator
# Company DB (update these when you change jobs)
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"news": "pnpm run dev --job=news",
"jobs": "pnpm run dev --list-jobs",
"seed-mock": "node --loader ts-node/esm src/scripts/seed-mock-users.ts",
"review:dev": "node --loader ts-node/esm src/scripts/run-pr-review.ts",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\""
},
Expand Down
9 changes: 9 additions & 0 deletions packages/pr-review/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Anthropic API key (the review model runs on Claude)
ANTHROPIC_API_KEY=your_anthropic_api_key_here

# GitHub token — fine-grained, read-only: Contents + Pull requests on the repo you review
GITHUB_TOKEN=your_github_personal_access_token_here

# Absolute path to a local clone of the repo whose PRs you review.
# Check out the PR's branch there so the surrounding-code context matches the PR.
REPO_PATH=/absolute/path/to/local/clone/of/the/repo
167 changes: 167 additions & 0 deletions packages/pr-review/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# @oh-my-worker/pr-review

AI code-review CLI. Point it at a GitHub pull request and it reviews the changes for **bugs, security, and performance** issues — reading the surrounding code from a local clone so its judgement is grounded in real context, not just the diff.

```bash
omw-review https://github.com/acme/widgets/pull/42
```

---

## What you need before you start

1. **Node.js 18+** — check with `node --version`.
2. **ripgrep** (`rg`) on your PATH — the tool uses it to search code. macOS: `brew install ripgrep`. Check with `rg --version`.
3. **An Anthropic API key** — the review runs on Claude. Get one at <https://console.anthropic.com>.
4. **A GitHub token** (fine-grained, read-only) — so the tool can fetch the PR. See [Get a GitHub token](#get-a-github-token) below.
5. **A local clone** of the repo whose PRs you review — the tool reads files from it for context.

---

## Install

```bash
npm install -g @oh-my-worker/pr-review
```

This gives you the `omw-review` command everywhere on your machine.

---

## Configure (the important part)

The tool needs **three** settings. Because you installed it **globally** (you run `omw-review` from anywhere), the cleanest place for these is your **shell environment** — not a `.env` file. (A `.env` file is only read from the folder you happen to run the command in, which is fragile for a global tool.)

| Variable | What it is | Where to put it |
| --- | --- | --- |
| `ANTHROPIC_API_KEY` | Your Anthropic key (rarely changes) | Shell profile (`~/.zshrc`) |
| `GITHUB_TOKEN` | Read-only GitHub token (rarely changes) | Shell profile (`~/.zshrc`) |
| `REPO_PATH` | Absolute path to the local clone you're reviewing (changes per repo) | Per command, at run time |

### Step 1 — put the two secrets in your shell profile

Open `~/.zshrc` (the default on macOS; use `~/.bashrc` on bash) and add:

```bash
export ANTHROPIC_API_KEY="sk-ant-..."
export GITHUB_TOKEN="github_pat_..."
```

Then reload it (or just open a new terminal):

```bash
source ~/.zshrc
```

Now those two are set in every terminal session — you only do this once.

### Step 2 — give `REPO_PATH` when you run a review

`REPO_PATH` points at the local clone of the repo the PR belongs to. The easiest pattern is to `cd` into that clone and use the current directory:

```bash
cd ~/work/the-company-repo # the clone
git fetch origin # make sure you have the PR's branch
git checkout the-pr-branch # so the context files match the PR
REPO_PATH="$(pwd)" omw-review https://github.com/acme/widgets/pull/42
```

> If you **only ever review one repo**, you can instead add `export REPO_PATH="/abs/path/to/that/repo"` to `~/.zshrc` and skip typing it each time.

---

## Get a GitHub token

Use a **fine-grained, read-only** token — minimal access, so it's safe even if it leaks.

1. GitHub → **Settings → Developer settings → Fine-grained tokens → Generate new token**.
2. **Repository access** → *Only select repositories* → pick the repo you review.
3. **Permissions** (read-only):
- Contents → **Read**
- Pull requests → **Read**
4. Generate, copy the token, and paste it into `~/.zshrc` as `GITHUB_TOKEN` (Step 1 above).

---

## Usage

```bash
REPO_PATH="$(pwd)" omw-review <github-pr-url>
```

The tool will:
1. Fetch the PR's changed files and diffs from GitHub.
2. Read related files from `REPO_PATH` (callers, definitions, types) to understand the change in context.
3. Print a ranked list of findings — each with **severity · category · `file:line` · explanation · suggested fix**.

It reviews for **bugs, security, and performance** only — no style nits.

---

## Troubleshooting

| Message | What it means / fix |
| --- | --- |
| `Missing required environment variable(s): ...` | One of the three isn't set. Re-check Step 1, and that you passed `REPO_PATH`. Run `echo $ANTHROPIC_API_KEY` to confirm it's exported. |
| `Invalid GitHub PR URL: ...` | The URL isn't a PR link. It must look like `https://github.com/<owner>/<repo>/pull/<number>`. |
| `... 404` while fetching the PR | Your `GITHUB_TOKEN` can't see that repo. Make sure the token's *repository access* includes it, with Contents + Pull requests read. |
| `Refusing to read "..." — it resolves outside REPO_PATH` | A safety guard — the tool only reads files inside `REPO_PATH`. Usually harmless. |
| `rg: command not found` / search failures | Install ripgrep (`brew install ripgrep`). |

---

## Programmatic use

You can also call it from your own code instead of the CLI:

```ts
import { reviewPullRequest, formatReview } from '@oh-my-worker/pr-review'

const result = await reviewPullRequest('https://github.com/acme/widgets/pull/42')
console.log(formatReview(result))
```

The same environment variables apply.

---

## Releasing a new version (for maintainers)

> This section is for whoever **publishes** the package — if you're just using the tool, you can ignore it.

Versions follow [semver](https://semver.org): `MAJOR.MINOR.PATCH` — **patch** = bugfix, **minor** = new backward-compatible feature, **major** = breaking change.

**First time only** — log in to npm and make sure you own the `@oh-my-worker` scope (create a free npm org named `oh-my-worker`, or rename the package to your own `@username` scope):

```bash
npm whoami # logged in? if not, run: npm login
```

**Each release — four commands:**

```bash
cd packages/pr-review
npm version patch # 1. bumps package.json (e.g. 0.1.0 → 0.1.1) AND creates a git commit + tag "v0.1.1"
git push --follow-tags # 2. pushes the commit and the new tag to GitHub
npm publish # 3. builds (via prepublishOnly) and publishes to npm
gh release create v0.1.1 --title "v0.1.1" --notes "What changed in this release" # 4. GitHub Release
```

- **Step 1 is the key one:** `npm version` keeps the npm version and the git tag identical (`v0.1.1`). That tag is what a GitHub Release attaches to — it's the "Latest" entry on the repo's **Releases** page.
- Use `npm version minor` or `npm version major` instead of `patch` depending on the change.
- **Step 4** can also be done in the GitHub UI: **repo → Releases → Draft a new release → pick the tag → write notes**. The number of releases shown there is just your number of version tags over time.

**Monorepo note:** since `pr-review` is the only publishable package here, plain `vX.Y.Z` tags are fine. If you add a second publishable package later, switch to prefixed tags like `pr-review-v0.1.1` to avoid tag collisions.

**Automating it (optional):** add a `.github/workflows/release.yml` that triggers on tag push (`on: push: tags: ['v*']`), runs the build, then `npm publish` (using an `NPM_TOKEN` repo secret) and `gh release create`. Then every release is just `npm version patch && git push --follow-tags` — CI does the publish and the GitHub Release for you.

---

## Notes

- The review is an **assistant, not an oracle** — treat findings as leads to verify, not gospel. It can miss issues and occasionally flag non-issues.
- Cost and time scale with PR size (each review uses Anthropic API tokens).

## License

ISC
53 changes: 53 additions & 0 deletions packages/pr-review/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@oh-my-worker/pr-review",
"version": "0.1.0",
"description": "AI code-review CLI: reviews a GitHub pull request for bugs, security and performance, using a local clone for surrounding-code context.",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"omw-review": "dist/cli.js"
},
"files": [
"dist",
"README.md",
".env.example"
],
"engines": {
"node": ">=18"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "node --loader ts-node/esm src/cli.ts",
"prepublishOnly": "pnpm run build"
},
"keywords": [
"code-review",
"pull-request",
"github",
"ai",
"llm",
"claude",
"langchain",
"cli",
"typescript"
],
"author": "damengrandom",
"license": "ISC",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@langchain/anthropic": "^1.3.25",
"@langchain/core": "^1.1.36",
"@octokit/rest": "^22.0.1",
"dotenv": "^17.3.1",
"langchain": "^1.2.37",
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^25.5.0",
"ts-node": "^10.9.2",
"typescript": "^6.0.2"
}
}
43 changes: 43 additions & 0 deletions packages/pr-review/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node
import 'dotenv/config'
import { reviewPullRequest } from './pr-review.agent.js'
import { formatReview } from './review-formatter.js'

// Deterministic guardrail #1: fail fast (with a clear message) if the environment is not
// configured, instead of crashing deep inside a tool call later. This is plain code, not an agent.
function assertEnv(): void {
const required = ['ANTHROPIC_API_KEY', 'GITHUB_TOKEN', 'REPO_PATH']
const missing = required.filter((name) => !process.env[name])

if (missing.length > 0) {
console.error(`❌ Missing required environment variable(s): ${missing.join(', ')}`)
console.error(' Set them in your environment or a .env file (see .env.example).')
process.exit(1)
}
}

async function main(): Promise<void> {
// process.argv = ['node', '/path/to/cli.js', '<pr-url>', ...]. The first real argument is index 2.
const prUrl = process.argv[2]

// Deterministic guardrail #2: no URL provided → print usage and exit.
if (!prUrl) {
console.error('Usage: omw-review <github-pr-url>')
console.error('Example: omw-review https://github.com/acme/widgets/pull/42')
process.exit(1)
}

assertEnv()

// reviewPullRequest parses/validates the URL (throws on a bad one), runs the agent, and
// returns the structured findings. The formatter turns them into the coloured terminal report.
const result = await reviewPullRequest(prUrl)

console.log('\n' + formatReview(result) + '\n')
}

main().catch((err) => {
console.error('❌ PR review failed:', err instanceof Error ? err.message : err)

process.exit(1)
})
5 changes: 5 additions & 0 deletions packages/pr-review/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Model used for the review reasoning. Sonnet balances code-reasoning quality and cost;
export const REVIEW_LLM = 'claude-haiku-4-5'

// Caps how many times the agent may read/search context before it must produce a verdict.
export const MAX_REVIEW_TOOL_CALLS = 30
6 changes: 6 additions & 0 deletions packages/pr-review/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Public library API — lets other code `import { reviewPullRequest } from '@oh-my-worker/pr-review'`.
// The CLI (src/cli.ts) is a separate entry exposed via the package `bin`.
export { reviewPullRequest, prReviewAgent } from './pr-review.agent.js'
export { formatReview } from './review-formatter.js'
export { parsePrUrl } from './pr-url.js'
export { ReviewResultSchema, ReviewFindingSchema, type ReviewResult, type ReviewFinding } from './schemas.js'
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { createAgent, toolCallLimitMiddleware } from 'langchain'
import { ChatAnthropic } from '@langchain/anthropic'
import { AIMessage } from '@langchain/core/messages'
import { getPrDiffTool, readFileTool, searchCodeTool } from '../tools/pr-review.tool.js'
import { getPrDiffTool, readFileTool, searchCodeTool } from './pr-review.tool.js'
import { PR_REVIEW_PROMPT } from './prompt.js'
// import { REVIEW_LLM, MAX_REVIEW_TOOL_CALLS } from '../constants/index.js'
import { DEFAULT_LLM, MAX_REVIEW_TOOL_CALLS } from '../constants/index.js'
import { ReviewResultSchema, type ReviewResult } from '../schemas/index.js'
import { parsePrUrl } from '../utils/pr-url.js'
import { REVIEW_LLM, MAX_REVIEW_TOOL_CALLS } from './constants.js'
import { ReviewResultSchema, type ReviewResult } from './schemas.js'
import { parsePrUrl } from './pr-url.js'

const llm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 })
const llm = new ChatAnthropic({ model: REVIEW_LLM, temperature: 0 })

// Unlike the single-call fetch agents, the reviewer loops: read the diff, pull in
// context with read_file/search_code, then judge. The tool-call limit caps that loop.
// The reviewer loops: read the diff, pull in context with read_file/search_code, then judge.
// The tool-call limit caps that loop.
//
// NOTE: we deliberately do NOT use createAgent's `responseFormat` here. In this langchain
// build, combining structured-output machinery with an afterModel middleware produces
Expand All @@ -27,7 +26,7 @@ export const prReviewAgent = createAgent({

// A separate, single-shot model call that turns the agent's free-text review into the
// typed ReviewResult. Runs outside the agent graph, so it avoids the jumpTo conflict.
const structuredLlm = new ChatAnthropic({ model: DEFAULT_LLM, temperature: 0 }).withStructuredOutput(ReviewResultSchema)
const structuredLlm = new ChatAnthropic({ model: REVIEW_LLM, temperature: 0 }).withStructuredOutput(ReviewResultSchema)

// Extract the text of the agent's final assistant message (content may be a string or
// an array of content blocks).
Expand All @@ -43,7 +42,7 @@ function finalMessageText(messages: Array<{ content: unknown }>): string {
return ''
}

// Orchestrator shared by the Task 1 dev script and the future Task 2 CLI.
// Orchestrator: parse the URL, run the review agent, then structure its findings.
export async function reviewPullRequest(prUrl: string): Promise<ReviewResult> {
const { owner, repo, number } = parsePrUrl(prUrl)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@ function resolveInsideRepo(relativePath: string): string {
const root = realpathSync(repoRoot())
const resolved = path.resolve(root, relativePath)

// Follow symlinks for paths that exist. If the path doesn't exist there is nothing to read,
// so the lexical check on `resolved` is sufficient (path.resolve already normalises any '..').
let real = resolved
try {
real = realpathSync(resolved)
} catch {
// non-existent path — fall through to the lexical check below
throw new Error(`Refusing to read "${relativePath}" — it does not exist.`)
}

Expand Down
1 change: 0 additions & 1 deletion src/utils/pr-url.ts → packages/pr-review/src/pr-url.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Parses a GitHub pull request URL into its owner / repo / number parts.
// Basic validation only — full guardrails (size limits, richer input handling) are deferred to a later task.

export type ParsedPr = {
owner: string
Expand Down
25 changes: 25 additions & 0 deletions packages/pr-review/src/prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const PR_REVIEW_PROMPT = `You are a senior code reviewer with deep expertise in TypeScript, JavaScript, Node.js, Vue.js and NestJS. You review a single pull request and report only genuine BUGS, SECURITY issues, and PERFORMANCE problems. Do NOT comment on style, formatting, or naming.

You have three tools:
- get_pr_diff — fetch the PR's changed files and diffs. Call this FIRST.
- read_file — read any file from the local checkout (callers, definitions, types, config).
- search_code — find where a symbol is defined or used across the codebase.

How to review — investigate before judging:
1. Call get_pr_diff to see exactly what changed.
2. For each meaningful change, DO NOT judge from the diff alone. Pull in context first:
- read_file the file being changed (the diff only shows a slice) and the files/functions/types it touches.
- search_code for callers and definitions to understand how the changed code is actually used.
3. Only after you understand the surrounding code, decide whether something is a real issue. Examples worth reporting:
- Bugs: null/undefined dereferences, wrong/edge-case logic, broken contracts with callers, unhandled promise rejections, incorrect async/await, off-by-one, type mismatches that survive at runtime.
- Security: injection (SQL/command/XSS), missing authz/authn, secret/PII leakage, unsafe deserialization, SSRF, path traversal.
- Performance: N+1 queries, unbounded loops/allocations, blocking the event loop, missing pagination, redundant network/db calls.

Rules:
- Report a finding ONLY if you are confident it is a real problem after inspecting the surrounding code. When in doubt, leave it out — false positives waste the reviewer's time.
- Every finding must cite the specific file and line in the changed code.
- Explain WHY it is a problem, referencing the context you read (e.g. "parseUser() returns null when the row is missing — see src/db/user.ts:42").
- Give a concrete suggested fix.
- If you find no genuine issues, say so clearly.

When done investigating, write your final answer as plain text: a one-paragraph overall summary, then a numbered list of findings. For EACH finding include severity (critical/high/medium/low), category (bug/security/performance), the file path and line number, a short title, an explanation of why it is a real problem, and a concrete suggested fix.`
Loading
Loading