evex
itemcode-reviewersource: ok · hit
Code Reviewer
Eve agent for GitHub PR review. Mention it on a PR for inline comments on bugs, not style.
npx shadcn add "https://evex.sh/r/code-reviewer.json"free
Where it goes
testimonialcomment
Visual style
terminal-mono
Use cases
community
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@upstash/ratelimit@^2.0.8@upstash/redis@^1.38.0ai@^7.0.38eve@^0.47.5zod@4.3.6
Files14 · source cached 10/8/2026, 9:12:32 PM
agent/agent.ts
import { defineAgent } from "eve";
export default defineAgent({
model: "zai/glm-5.2",
});
agent/channels/github.ts
import {
defaultGitHubAuth,
type GitHubJsonObject,
githubChannel,
type GitHubChannelState,
type GitHubEventContext,
type GitHubInboundContext,
type GitHubThread,
} from "eve/channels/github";
import { toolResultFrom } from "eve/tools";
import {
checkCodeReviewRateLimit,
claimReviewPublication,
type RateLimitDecision,
shouldPostCooldownReply,
} from "../lib/review-rate-limit";
import submitPrReviewTool, {
type SubmitPrReviewComment,
type SubmitPrReviewOutput,
} from "../tools/submit_pr_review";
const BOT_NAME = process.env.GITHUB_APP_SLUG || "code-reviewer";
const BOT_MENTION_PATTERN = new RegExp(
`@${escapeRegExp(BOT_NAME)}(?=$|[^A-Za-z0-9_-])`,
"i",
);
const GITHUB_COMMENT_CHUNK_SIZE = 60_000;
type CodeReviewerGitHubState = GitHubChannelState & {
codeReviewerReviewSubmitted?: boolean;
};
export default githubChannel({
botName: BOT_NAME,
pullRequestContext: {
excludedFiles: [
"**/pnpm-lock.yaml",
"**/package-lock.json",
"**/yarn.lock",
"**/bun.lockb",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/coverage/**",
],
},
async onComment(ctx, comment) {
if (!BOT_MENTION_PATTERN.test(comment.body)) {
return null;
}
if (!isPullRequestConversation(ctx)) {
return null;
}
const pullRequestNumber = ctx.conversation.pullRequestNumber;
if (pullRequestNumber === null) {
return null;
}
const decision = await checkCodeReviewRateLimit({
installationId: ctx.github.installationId,
isPrivateRepository: ctx.repository.private,
pullRequestNumber,
repositoryId: ctx.repository.id,
senderId: ctx.sender.id,
senderLogin: ctx.sender.login,
});
if (decision.allowed) {
return { auth: defaultGitHubAuth(ctx) };
}
await maybePostCooldownReply(ctx, decision);
return null;
},
events: {
async "action.result"(data, channel) {
const match = toolResultFrom(data.result, submitPrReviewTool);
if (!match) {
return;
}
const state = channel.state as CodeReviewerGitHubState;
if (state.codeReviewerReviewSubmitted) {
return;
}
const claimed = await claimReviewPublication({
headSha: state.headSha,
installationId: channel.github.installationId,
pullRequestNumber: state.pullRequestNumber ?? 0,
repositoryId: channel.repository.id,
toolCallId: match.callId,
});
if (!claimed) {
state.codeReviewerReviewSubmitted = true;
return;
}
try {
await publishReview(channel, match.output);
state.codeReviewerReviewSubmitted = true;
} catch {
state.codeReviewerReviewSubmitted = false;
}
},
async "message.completed"(data, channel) {
if (data.finishReason === "tool-calls" || !data.message) {
return;
}
const state = channel.state as CodeReviewerGitHubState;
if (state.codeReviewerReviewSubmitted) {
return;
}
await postCommentChunks(channel.thread, data.message);
},
},
});
function isPullRequestConversation(ctx: GitHubInboundContext) {
return (
ctx.conversation.kind === "pull_request" ||
ctx.conversation.kind === "review_thread"
);
}
async function maybePostCooldownReply(
ctx: GitHubInboundContext,
decision: Extract<RateLimitDecision, { allowed: false }>,
) {
const pullRequestNumber = ctx.conversation.pullRequestNumber;
if (pullRequestNumber === null) {
return;
}
const canReply = await shouldPostCooldownReply({
installationId: ctx.github.installationId,
pullRequestNumber,
repositoryId: ctx.repository.id,
});
if (!canReply) {
return;
}
try {
await ctx.thread.post(formatCooldownReply(decision));
} catch {
// If the cooldown notice cannot be posted, still suppress the model run.
}
}
function formatCooldownReply(decision: Extract<RateLimitDecision, { allowed: false }>) {
if (decision.reason === "rate_limit_unavailable") {
return `\`${BOT_NAME}\` cannot run because rate limiting is unavailable for this repository.`;
}
const retryAfter = formatRetryAfter(decision.retryAfterSeconds);
return `\`${BOT_NAME}\` is cooling down for this pull request. Try again ${retryAfter}.`;
}
function formatRetryAfter(retryAfterSeconds: number | undefined) {
if (!retryAfterSeconds) {
return "later";
}
if (retryAfterSeconds <= 90) {
return "in about 1 minute";
}
return `in about ${Math.ceil(retryAfterSeconds / 60)} minutes`;
}
async function publishReview(
channel: GitHubEventContext,
review: SubmitPrReviewOutput,
) {
const pullRequestNumber = channel.state.pullRequestNumber;
if (pullRequestNumber === null || review.comments.length === 0) {
await postCommentChunks(channel.thread, review.summary);
return;
}
try {
await createBatchReview(channel, review, pullRequestNumber);
return;
} catch {
await publishFallbackReview(channel, review, pullRequestNumber);
}
}
async function createBatchReview(
channel: GitHubEventContext,
review: SubmitPrReviewOutput,
pullRequestNumber: number,
) {
const body: GitHubJsonObject = {
body: review.summary,
comments: review.comments.map(toGitHubReviewComment),
event: "COMMENT",
...(channel.state.headSha ? { commit_id: channel.state.headSha } : {}),
};
await channel.github.request({
body,
method: "POST",
path: githubPullRequestReviewPath(channel.state, pullRequestNumber),
});
}
async function publishFallbackReview(
channel: GitHubEventContext,
review: SubmitPrReviewOutput,
pullRequestNumber: number,
) {
const failedComments: SubmitPrReviewComment[] = [];
let publishedCount = 0;
if (channel.state.headSha) {
for (const comment of review.comments) {
try {
await channel.github.request({
body: {
...toGitHubReviewComment(comment),
commit_id: channel.state.headSha,
},
method: "POST",
path: githubPullRequestCommentPath(channel.state, pullRequestNumber),
});
publishedCount += 1;
} catch {
failedComments.push(comment);
}
}
} else {
failedComments.push(...review.comments);
}
await postCommentChunks(
channel.thread,
formatFallbackSummary(review.summary, failedComments, publishedCount),
);
}
function toGitHubReviewComment(comment: SubmitPrReviewComment): GitHubJsonObject {
return {
body: formatInlineCommentBody(comment),
line: comment.line,
path: comment.path,
side: comment.side,
...(comment.startLine
? {
start_line: comment.startLine,
start_side: comment.startSide ?? comment.side,
}
: {}),
};
}
function formatInlineCommentBody(comment: SubmitPrReviewComment) {
const parts = [`**${comment.severity}:** ${comment.body}`];
if (comment.suggestion) {
parts.push("", "```suggestion", comment.suggestion, "```");
}
return parts.join("\n");
}
function formatFallbackSummary(
summary: string,
failedComments: readonly SubmitPrReviewComment[],
publishedCount: number,
) {
if (failedComments.length === 0) {
return publishedCount > 0
? `${summary}\n\nPosted ${publishedCount} inline comments.`
: summary;
}
const failedList = failedComments.map(
(comment) =>
`- ${comment.path}:${comment.line} **${comment.severity}:** ${comment.body}`,
);
const prefix =
publishedCount > 0
? `Posted ${publishedCount} inline comments. These findings could not be anchored inline:`
: "These findings could not be anchored inline:";
return [summary, "", prefix, ...failedList].join("\n");
}
async function postCommentChunks(thread: GitHubThread, message: string) {
for (const chunk of splitCommentBody(message)) {
await thread.post(chunk);
}
}
function splitCommentBody(message: string) {
if (message.length <= GITHUB_COMMENT_CHUNK_SIZE) {
return [message];
}
const chunks: string[] = [];
for (
let startIndex = 0;
startIndex < message.length;
startIndex += GITHUB_COMMENT_CHUNK_SIZE
) {
chunks.push(message.slice(startIndex, startIndex + GITHUB_COMMENT_CHUNK_SIZE));
}
return chunks;
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function githubPullRequestReviewPath(
state: GitHubChannelState,
pullRequestNumber: number,
) {
return `/repos/${encodeURIComponent(state.owner)}/${encodeURIComponent(
state.repo,
)}/pulls/${pullRequestNumber}/reviews`;
}
function githubPullRequestCommentPath(
state: GitHubChannelState,
pullRequestNumber: number,
) {
return `/repos/${encodeURIComponent(state.owner)}/${encodeURIComponent(
state.repo,
)}/pulls/${pullRequestNumber}/comments`;
}
agent/instructions.md
# Mission
You review GitHub pull request diffs to find concrete bugs, regressions,
security risks, rollout risk, and materially missing tests.
# Default stance
Prefer changed behavior over style commentary. Do not spend time on naming nits,
formatting, or speculative rewrites unless they hide a concrete failure mode.
You are a reviewer, not a patch author: you may suggest fixes, but you do not
claim to have changed the pull request.
# Workflow
1. Start from the GitHub pull request context injected by the channel.
2. Identify risky surfaces in the diff: auth, permissions, user data, external
side effects, schemas, cache invalidation, concurrency, billing, and runtime
behavior.
3. Use read_file, grep, and glob to inspect only the context needed to validate
or reject a finding.
4. Use bash for targeted tests or read-only verification when it materially
increases confidence.
5. Use write_file only inside the sandbox when trying a small local patch helps
validate a suggestion. Never state that the pull request itself was changed.
6. Load the review-calibration skill when severity is ambiguous or multiple
issues compete.
7. Put only concrete, actionable, diff-anchored findings in inline comments.
8. Put non-anchorable concerns, residual risk, and no-finding summaries in the
review summary.
9. Call submit_pr_review exactly once when the review is ready.
10. After submit_pr_review, do not produce a second substantive final answer.
# Inline comment rules
Each inline comment must include:
- user or system impact
- what triggers the issue
- the smallest evidence needed to justify it
Use at most 10 inline comments. Prefer fewer, higher-confidence comments over a
long review.
# Severity
- blocking: probable bug, security issue, data loss, broken runtime behavior, or
user-visible regression.
- warning: real risk that depends on runtime context, rollout conditions, or a
materially missing test.
- nit: small correctness improvement. Do not use this for style.
# Suggestions
Use a suggestion only when the fix is short, local, and almost certainly
correct. Do not suggest broad refactors, migrations, API redesigns, or large
test-suite changes. A suggestion must contain only the replacement code for the
GitHub line or range.
# No findings
If you find no actionable issues, still call submit_pr_review with comments: []
and a summary that says no actionable findings were found, plus any residual
risk that deserves manual verification before merge.
agent/lib/review-rate-limit.ts
import { createHash } from "node:crypto";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const DEFAULT_RATE_LIMIT_PREFIX = "evex:code-reviewer";
const DEFAULT_PR_COOLDOWN_SECONDS = 900;
const DEFAULT_USER_PR_COOLDOWN_SECONDS = 1800;
const DEFAULT_PRIVATE_REPO_DAILY_LIMIT = 25;
const DEFAULT_PUBLIC_REPO_DAILY_LIMIT = 10;
const DEFAULT_COOLDOWN_REPLY_SECONDS = 900;
const REVIEW_PUBLICATION_TTL_SECONDS = 86_400;
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
type FailureMode = "public_closed" | "closed" | "open";
type RateLimitReason =
| "user_pr_cooldown"
| "pr_cooldown"
| "repo_daily_limit"
| "rate_limit_unavailable";
type RateLimitConfig = {
cooldownReply: boolean;
cooldownReplySeconds: number;
enabled: boolean;
failureMode: FailureMode;
prefix: string;
privateRepoDailyLimit: number;
prCooldownSeconds: number;
publicRepoDailyLimit: number;
userPrCooldownSeconds: number;
};
type RateLimiter = ReturnType<typeof createRateLimiter>;
type RateLimiterBundle = ReturnType<typeof createRateLimiterBundle>;
export type CodeReviewRateLimitInput = {
installationId: number | null | undefined;
isPrivateRepository: boolean;
pullRequestNumber: number;
repositoryId: number;
senderId: number | null | undefined;
senderLogin: string | null | undefined;
};
export type CooldownReplyRateLimitInput = {
installationId: number | null | undefined;
pullRequestNumber: number;
repositoryId: number;
};
export type ReviewPublicationClaimInput = {
headSha: string | null | undefined;
installationId: number | null | undefined;
pullRequestNumber: number;
repositoryId: number;
toolCallId: string;
};
export type RateLimitDecision =
| { allowed: true }
| {
allowed: false;
reason: RateLimitReason;
resetAt?: number;
retryAfterSeconds?: number;
};
let cachedRedis: Redis | null = null;
let cachedLimiters: RateLimiterBundle | null = null;
export async function checkCodeReviewRateLimit(
input: CodeReviewRateLimitInput,
): Promise<RateLimitDecision> {
const config = readRateLimitConfig();
if (!config.enabled) {
return { allowed: true };
}
if (!hasUpstashEnvironment()) {
return unavailableDecision(config, input.isPrivateRepository);
}
try {
if (!input.isPrivateRepository && config.publicRepoDailyLimit <= 0) {
return { allowed: false, reason: "repo_daily_limit" };
}
if (input.isPrivateRepository && config.privateRepoDailyLimit <= 0) {
return { allowed: false, reason: "repo_daily_limit" };
}
const limiters = getRateLimiters(config);
const checks = [
await checkLimiter(
limiters.userPr,
identifierForUserPr(input),
"user_pr_cooldown",
),
await checkLimiter(limiters.pr, identifierForPr(input), "pr_cooldown"),
await checkLimiter(
input.isPrivateRepository ? limiters.privateRepoDaily : limiters.publicRepoDaily,
identifierForRepoDaily(input),
"repo_daily_limit",
),
];
return checks.find((decision) => !decision.allowed) ?? { allowed: true };
} catch {
return unavailableDecision(config, input.isPrivateRepository);
}
}
export async function shouldPostCooldownReply(
input: CooldownReplyRateLimitInput,
): Promise<boolean> {
const config = readRateLimitConfig();
if (!(config.enabled && config.cooldownReply && hasUpstashEnvironment())) {
return false;
}
try {
const { cooldownReply } = getRateLimiters(config);
const decision = await checkLimiter(
cooldownReply,
identifierForCooldownReply(input),
"pr_cooldown",
);
return decision.allowed;
} catch {
return false;
}
}
export async function claimReviewPublication(
input: ReviewPublicationClaimInput,
): Promise<boolean> {
if (!hasUpstashEnvironment()) {
return true;
}
try {
const config = readRateLimitConfig();
const redis = getRedis();
const key = `${config.prefix}:review-publish:${hashParts([
"review-publish",
input.installationId ?? "unknown-installation",
input.repositoryId,
input.pullRequestNumber,
input.headSha ?? "unknown-head",
input.toolCallId,
])}`;
const result = await redis.set(key, "1", {
ex: REVIEW_PUBLICATION_TTL_SECONDS,
nx: true,
});
return result === "OK";
} catch {
return true;
}
}
function checkLimiter(
limiter: RateLimiter,
identifier: string,
reason: RateLimitReason,
): Promise<RateLimitDecision> {
return limiter.limit(identifier).then((result) => {
void result.pending.catch(() => undefined);
if (result.success) {
return { allowed: true };
}
return {
allowed: false,
reason,
resetAt: result.reset,
retryAfterSeconds: retryAfterSeconds(result.reset),
};
});
}
function createRateLimiter(redis: Redis, limit: number, windowSeconds: number, prefix: string) {
return new Ratelimit({
analytics: false,
limiter: Ratelimit.slidingWindow(limit, `${windowSeconds} s`),
prefix,
redis,
});
}
function createRateLimiterBundle(config: RateLimitConfig) {
const redis = getRedis();
return {
configKey: JSON.stringify(config),
cooldownReply: createRateLimiter(
redis,
1,
config.cooldownReplySeconds,
`${config.prefix}:cooldown-reply`,
),
pr: createRateLimiter(redis, 1, config.prCooldownSeconds, `${config.prefix}:pr`),
privateRepoDaily: createRateLimiter(
redis,
Math.max(1, config.privateRepoDailyLimit),
86_400,
`${config.prefix}:repo-private-day`,
),
publicRepoDaily: createRateLimiter(
redis,
Math.max(1, config.publicRepoDailyLimit),
86_400,
`${config.prefix}:repo-public-day`,
),
userPr: createRateLimiter(
redis,
1,
config.userPrCooldownSeconds,
`${config.prefix}:user-pr`,
),
};
}
function getRateLimiters(config: RateLimitConfig) {
const configKey = JSON.stringify(config);
if (cachedLimiters?.configKey === configKey) {
return cachedLimiters;
}
cachedLimiters = createRateLimiterBundle(config);
return cachedLimiters;
}
function getRedis() {
if (cachedRedis) {
return cachedRedis;
}
cachedRedis = Redis.fromEnv();
return cachedRedis;
}
function hasUpstashEnvironment() {
return Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
}
function unavailableDecision(
config: RateLimitConfig,
isPrivateRepository: boolean,
): RateLimitDecision {
if (config.failureMode === "open") {
return { allowed: true };
}
if (config.failureMode === "closed") {
return { allowed: false, reason: "rate_limit_unavailable" };
}
return isPrivateRepository
? { allowed: true }
: { allowed: false, reason: "rate_limit_unavailable" };
}
function identifierForUserPr(input: CodeReviewRateLimitInput) {
return hashParts([
"user-pr",
input.installationId ?? "unknown-installation",
input.repositoryId,
input.pullRequestNumber,
input.senderId ?? input.senderLogin ?? "unknown-sender",
]);
}
function identifierForPr(input: CodeReviewRateLimitInput) {
return hashParts([
"pr",
input.installationId ?? "unknown-installation",
input.repositoryId,
input.pullRequestNumber,
]);
}
function identifierForRepoDaily(input: CodeReviewRateLimitInput) {
return hashParts([
"repo-day",
input.installationId ?? "unknown-installation",
input.repositoryId,
input.isPrivateRepository ? "private" : "public",
]);
}
function identifierForCooldownReply(input: CooldownReplyRateLimitInput) {
return hashParts([
"cooldown-reply",
input.installationId ?? "unknown-installation",
input.repositoryId,
input.pullRequestNumber,
]);
}
function hashParts(parts: readonly (number | string)[]) {
return createHash("sha256").update(parts.map(String).join(":")).digest("hex");
}
function retryAfterSeconds(resetAt: number) {
return Math.max(1, Math.ceil((resetAt - Date.now()) / 1000));
}
function readRateLimitConfig(): RateLimitConfig {
return {
cooldownReply: readBoolean(process.env.CODE_REVIEWER_COOLDOWN_REPLY, true),
cooldownReplySeconds: readPositiveInteger(
process.env.CODE_REVIEWER_COOLDOWN_REPLY_SECONDS,
DEFAULT_COOLDOWN_REPLY_SECONDS,
),
enabled: readBoolean(process.env.CODE_REVIEWER_RATE_LIMIT_ENABLED, true),
failureMode: readFailureMode(
process.env.CODE_REVIEWER_RATE_LIMIT_FAILURE_MODE,
),
prefix:
process.env.CODE_REVIEWER_RATE_LIMIT_PREFIX?.trim() ||
DEFAULT_RATE_LIMIT_PREFIX,
privateRepoDailyLimit: readNonNegativeInteger(
process.env.CODE_REVIEWER_PRIVATE_REPO_DAILY_LIMIT,
DEFAULT_PRIVATE_REPO_DAILY_LIMIT,
),
prCooldownSeconds: readPositiveInteger(
process.env.CODE_REVIEWER_PR_COOLDOWN_SECONDS,
DEFAULT_PR_COOLDOWN_SECONDS,
),
publicRepoDailyLimit: readNonNegativeInteger(
process.env.CODE_REVIEWER_PUBLIC_REPO_DAILY_LIMIT,
DEFAULT_PUBLIC_REPO_DAILY_LIMIT,
),
userPrCooldownSeconds: readPositiveInteger(
process.env.CODE_REVIEWER_USER_PR_COOLDOWN_SECONDS,
DEFAULT_USER_PR_COOLDOWN_SECONDS,
),
};
}
function readBoolean(value: string | undefined, fallback: boolean) {
if (!value) {
return fallback;
}
const normalizedValue = value.trim().toLowerCase();
if (TRUE_VALUES.has(normalizedValue)) {
return true;
}
if (FALSE_VALUES.has(normalizedValue)) {
return false;
}
return fallback;
}
function readFailureMode(value: string | undefined): FailureMode {
if (value === "closed" || value === "open" || value === "public_closed") {
return value;
}
return "public_closed";
}
function readPositiveInteger(value: string | undefined, fallback: number) {
const parsedValue = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsedValue) && parsedValue > 0
? parsedValue
: fallback;
}
function readNonNegativeInteger(value: string | undefined, fallback: number) {
const parsedValue = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsedValue) && parsedValue >= 0
? parsedValue
: fallback;
}
agent/skills/review-calibration/references/review-checklist.md
# Review lenses
- Can a valid request now fail because of ordering, retries, or stale cache state?
- Does any new branch bypass authz, rate limiting, or ownership checks?
- Can the new code create partial writes or inconsistent state on failure?
- Does the patch need a migration, rollback, or a feature flag to be safe?
- What is the smallest test that would catch the highest-risk regression?
- Does a changed external side effect need idempotency, approval, or throttling?
- Does any user-controlled input reach file, network, shell, or markup surfaces?
- Can the finding be anchored to a changed diff line, or should it stay in the summary?
agent/skills/review-calibration/references/severity-scale.md
# Severity lenses
- blocking: data loss, privilege bypass, high-probability incident trigger, or user-facing regression without an easy workaround
- warning: correctness issue with limited blast radius, missing validation, fragile recovery, or materially missing tests
- nit: small correctness improvement that is safe and local; never style-only
agent/skills/review-calibration/SKILL.md
---
name: review-calibration
description: Calibrate review severity when impact is ambiguous and the review needs a consistent bar.
---
# Review calibration
**Calibrate** each finding against user harm, exploitability, reversibility, and
detection speed. Escalate issues that can corrupt state, leak data, bypass
authorization, or silently ship broken behavior. De-escalate issues that are
recoverable, obvious, and tightly scoped.
Use the review tool severity labels:
- **blocking** — probable bugs, security issues, data loss, broken runtime
behavior, or user-visible regressions
- **warning** — real risks that depend on runtime context, rollout conditions,
or materially missing tests
- **nit** — small correctness improvements only; never style
For severity lenses and review checklist detail, see
[severity-scale](./references/severity-scale.md) and
[review-checklist](./references/review-checklist.md).
agent/tools/submit_pr_review.ts
import { defineTool } from "eve/tools";
import { z } from "zod";
const reviewSide = z.enum(["RIGHT", "LEFT"]);
const reviewSeverity = z.enum(["blocking", "warning", "nit"]);
const reviewComment = z.object({
body: z.string().min(1).max(4000),
line: z.number().int().positive(),
path: z.string().min(1),
severity: reviewSeverity,
side: reviewSide.default("RIGHT"),
startLine: z.number().int().positive().optional(),
startSide: reviewSide.optional(),
suggestion: z.string().min(1).max(8000).optional(),
});
const submitPrReviewInput = z.object({
comments: z.array(reviewComment).max(10),
summary: z.string().min(1).max(4000),
});
export type SubmitPrReviewComment = z.infer<typeof reviewComment>;
export type SubmitPrReviewOutput = z.infer<typeof submitPrReviewInput>;
export default defineTool({
description:
"Submit the final pull request review as a structured summary plus inline comments. Use this exactly once when the review is ready to publish.",
inputSchema: submitPrReviewInput,
execute(input) {
return input;
},
toModelOutput(output) {
return {
type: "json",
value: {
hasSummary: output.summary.length > 0,
inlineCommentCount: output.comments.length,
readyToPublish: true,
},
};
},
});
evals/evals.config.ts
import { defineEvalConfig } from "eve/evals";
export default defineEvalConfig({
timeoutMs: 120_000,
});
evals/pr-review-input-contract.eval.ts
import { defineEval } from "eve/evals";
import { equals } from "eve/evals/expect";
const HUNK_NEW_START = 10;
const HUNK_NEW_END = 13;
type ReviewComment = {
line?: number;
path?: string;
severity?: string;
side?: string;
};
export default defineEval({
description:
"Submits exactly one review whose inline comments anchor to the changed hunk with correct path, in-range lines, and a blocking or warning severity for an authorization bypass.",
async test(t) {
const turn = await t.send(`
<github_context>
repository: example/widget
pull_request_number: 44
sender: maintainer
head_sha: fed789
</github_context>
Pull request diff:
diff --git a/src/auth.ts b/src/auth.ts
@@ -10,4 +10,4 @@ export function getUserForRequest(session: Session, requestedUserId: string) {
- if (session.userId !== requestedUserId) {
- throw new Error("forbidden");
- }
- return getUser(requestedUserId);
+ if (session.userId) {
+ return getUser(requestedUserId);
+ }
+ return null;
Review this diff and publish the PR review with submit_pr_review.
`);
t.succeeded();
t.calledTool("submit_pr_review", { count: 1 }).gate();
const call = turn.requireToolCall("submit_pr_review");
const comments = (call.input.comments ?? []) as readonly ReviewComment[];
t.check(comments.length >= 1, equals(true).gate());
let allAnchoredToHunk = true;
let hasActionableSeverity = false;
for (const comment of comments) {
if (comment.path !== "src/auth.ts") {
allAnchoredToHunk = false;
}
const line = comment.line;
if (
typeof line !== "number" ||
!Number.isInteger(line) ||
line < HUNK_NEW_START ||
line > HUNK_NEW_END
) {
allAnchoredToHunk = false;
}
if (comment.severity === "blocking" || comment.severity === "warning") {
hasActionableSeverity = true;
}
}
t.check(allAnchoredToHunk, equals(true).gate());
t.check(hasActionableSeverity, equals(true).gate());
t.check(
comments.some((comment) => comment.severity === "blocking"),
equals(true).soft(),
);
t.check(
typeof call.input.summary === "string" && call.input.summary.length > 0,
equals(true).gate(),
);
},
});
evals/pr-review-no-findings.eval.ts
import { defineEval } from "eve/evals";
import { equals, includes } from "eve/evals/expect";
export default defineEval({
description:
"Submits a no-findings review with an empty comments list for an innocuous PR diff.",
async test(t) {
const turn = await t.send(`
<github_context>
repository: example/widget
pull_request_number: 43
sender: maintainer
head_sha: def456
</github_context>
Pull request diff:
diff --git a/src/copy.ts b/src/copy.ts
@@
-export const emptyState = "No items";
+export const emptyState = "No matching items";
Review this diff and publish the PR review with submit_pr_review.
`);
t.succeeded();
t.calledTool("submit_pr_review");
const call = turn.requireToolCall("submit_pr_review");
const comments = (call.input.comments ?? []) as readonly unknown[];
t.check(comments.length === 0, equals(true).gate());
t.check(t.reply, includes("No actionable").soft());
},
});
evals/pr-review-with-inline-finding.eval.ts
import { defineEval } from "eve/evals";
import { includes } from "eve/evals/expect";
export default defineEval({
description: "Finds an actionable PR issue and submits an inline review.",
async test(t) {
await t.send(`
<github_context>
repository: example/widget
pull_request_number: 42
sender: maintainer
head_sha: abc123
</github_context>
Pull request diff:
diff --git a/src/auth.ts b/src/auth.ts
@@
- if (session.userId !== requestedUserId) {
- throw new Error("forbidden");
- }
+ if (session.userId) {
+ return getUser(requestedUserId);
+ }
Review this diff and publish the PR review with submit_pr_review.
`);
t.succeeded();
t.calledTool("submit_pr_review");
t.check(t.reply, includes("submit_pr_review").soft());
},
});
README.md
# Code Reviewer
Review GitHub pull requests from a native GitHub App channel. Mention
`@code-reviewer` on a pull request and it publishes a GitHub review with inline
comments for concrete findings. Small, local fixes may include GitHub suggestion
blocks that the PR author can apply manually.
The agent reviews changed behavior rather than style. It looks for bugs,
regressions, security issues, rollout risk, and materially missing tests.
## How it works
1. Install this agent into an existing Eve app.
2. Deploy the Eve app so GitHub can reach it over HTTPS.
3. Create and install a GitHub App for the repositories you want reviewed.
4. Point the GitHub App webhook to `/eve/v1/github`.
5. Subscribe the GitHub App to PR comment events.
6. Comment on a pull request:
```md
@code-reviewer review this
```
The GitHub channel injects PR metadata and diff context, checks out the
repository into the Eve sandbox, and lets the agent inspect relevant files. The
agent does not push commits, open branches, or modify the pull request. It only
publishes review comments and optional suggestion blocks.
## GitHub App setup
Create the GitHub App from **GitHub Settings -> Developer settings ->
GitHub Apps -> New GitHub App**.
Use these settings:
- **GitHub App name**: `code-reviewer`, or another name that matches
`GITHUB_APP_SLUG`.
- **Homepage URL**: your deployed Eve app URL.
- **Callback URL**: leave blank.
- **Request user authorization (OAuth) during installation**: disabled.
- **Webhook**: active.
- **Webhook URL**: `https://<your-eve-deployment>/eve/v1/github`.
- **Webhook secret**: a long random value. Save the same value as
`GITHUB_WEBHOOK_SECRET`.
The webhook URL must be publicly reachable by GitHub. Localhost URLs do not work
unless you expose them through a tunnel.
After creating the app:
1. Copy the **App ID** into `GITHUB_APP_ID`.
2. Generate a private key from the app settings.
3. Copy the private key PEM into `GITHUB_APP_PRIVATE_KEY`.
4. Install the app on the target repositories from **Install App**.
When storing the private key as a single-line environment variable, replace
literal newlines with `\n`. Eve normalizes that form at runtime.
## GitHub permissions
Use the narrowest permissions that support review comments:
- Metadata: read
- Contents: read
- Pull requests: read/write
- Issues: read/write
The issues permission is needed for PR timeline comments and fallback replies.
## GitHub events
Subscribe only to the events this agent consumes:
- Issue comments
- Pull request review comments
The first event lets users mention `@code-reviewer` from the PR conversation
timeline. The second lets users mention it from a code review thread in the
Files changed view.
You do not need the broader Issues or Pull requests events for the default
mention-driven workflow.
## Environment
The registry installs a `.env.example` template. Put real secret values in your
deployment environment.
Set the GitHub App credentials:
```bash
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_SLUG=code-reviewer
```
`GITHUB_APP_SLUG` must match the mention users type in GitHub. With the default
value above, the trigger is:
```md
@code-reviewer review this
```
Set Vercel Redis/Upstash Marketplace REST credentials for rate limiting:
```bash
KV_REST_API_URL=
KV_REST_API_TOKEN=
```
Do not use the read-only token for this agent. Rate limiting writes cooldown and
review publication keys. `KV_URL` and `REDIS_URL` are Redis protocol URLs and
are not used by this REST client.
The default rate limits are intentionally stricter on public repositories:
- one review every 15 minutes per PR
- one review every 30 minutes per user per PR
- 25 reviews per private repository per day
- 10 reviews per public repository per day
- one cooldown reply every 15 minutes per PR
For local development only, you can disable rate limiting:
```bash
CODE_REVIEWER_RATE_LIMIT_ENABLED=false
```
Production public deployments should keep rate limiting enabled. If Upstash is
unavailable, the default failure mode blocks public repositories and allows
private repositories.
## Deployment checklist
Before testing from GitHub, confirm:
- The Eve app is deployed and has a POST webhook route at
`https://<your-eve-deployment>/eve/v1/github`.
- The deployment has GitHub App credentials, Upstash Redis REST credentials,
and a model credential such as Vercel AI Gateway OIDC or
`AI_GATEWAY_API_KEY`.
- The GitHub App is installed on the repository that contains the pull request.
- The app has **Contents: read**, **Issues: read/write**, and **Pull requests:
read/write** on that repository.
- The webhook is active and subscribed to **Issue comments** and **Pull request
review comments**.
GitHub sends a `ping` webhook when you create or update the app. A successful
delivery should return HTTP 200. Then open a pull request and comment:
```md
@code-reviewer review this
```
Expected behavior:
- GitHub accepts the webhook delivery.
- The agent adds an `eyes` reaction unless progress reactions are disabled.
- The agent posts a GitHub pull request review with inline comments when it
finds concrete issues.
- If no inline finding is warranted, it posts a short PR timeline summary.
Common setup failures:
- HTTP 401 from `/eve/v1/github`: `GITHUB_WEBHOOK_SECRET` does not match the
GitHub App webhook secret.
- No response to a mention: the app is not subscribed to the comment event, the
mention does not match `GITHUB_APP_SLUG`, or the app is not installed on that
repository.
- GitHub API 403: the app installation is missing repository access or one of
the required write permissions.
- Public repository reviews are blocked: Upstash is unavailable and the default
failure mode is `public_closed`.
## Development
```bash
pnpm install
pnpm dev
```
Run `pnpm info` to inspect the Eve surface and `pnpm build` before opening a PR.
Use `pnpm eval -- --skip-report` for lightweight agent behavior checks.
.env.example
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
GITHUB_WEBHOOK_SECRET=
GITHUB_APP_SLUG=code-reviewer
# Vercel Redis/Upstash Marketplace REST credentials.
KV_REST_API_URL=
KV_REST_API_TOKEN=
CODE_REVIEWER_RATE_LIMIT_ENABLED=true
CODE_REVIEWER_RATE_LIMIT_PREFIX=evex:code-reviewer
CODE_REVIEWER_PR_COOLDOWN_SECONDS=900
CODE_REVIEWER_USER_PR_COOLDOWN_SECONDS=1800
CODE_REVIEWER_PRIVATE_REPO_DAILY_LIMIT=25
CODE_REVIEWER_PUBLIC_REPO_DAILY_LIMIT=10
CODE_REVIEWER_COOLDOWN_REPLY=true
CODE_REVIEWER_COOLDOWN_REPLY_SECONDS=900
CODE_REVIEWER_RATE_LIMIT_FAILURE_MODE=public_closed