Skip to content

Commit f5cf54d

Browse files
NagyViktNagyVikt
andauthored
Add local PR review runner (#560)
GitGuardex needs a review path that can reuse local Codex or Claude CLI authentication without requiring model API tokens. This adds gx pr-review as a self-hosted-runner-friendly command that reads a PR diff with gh, asks the selected local provider for structured findings, posts one GitHub review when auth exists, and writes a markdown artifact when it does not. Constraint: Local codex/claude CLI auth must avoid OpenAI or Anthropic API keys Constraint: GitHub comments still require GITHUB_TOKEN or local gh auth Rejected: Hosted bot service | self-hosted CLI auth is the MVP requirement Confidence: medium Scope-risk: moderate Directive: Keep provider output structured and preserve artifact fallback for unauthenticated local runs Tested: rtk test node --test test/pr-review.test.js test/cli-args-dispatch.test.js Tested: openspec validate agent-codex-codex-task-2026-05-11-15-20 --type change --strict Tested: openspec validate --specs Not-tested: Full npm test is not green on current baseline; unrelated branch/setup/README release-note assertions failed while pr-review tests passed Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 4fca9c5 commit f5cf54d

9 files changed

Lines changed: 495 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-11
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Why
2+
3+
- GitGuardex needs a local PR review runner that can reuse already-authenticated Codex or Claude CLI sessions without requiring OpenAI or Anthropic API keys.
4+
- GitHub posting should work with GitHub Actions `GITHUB_TOKEN` or local `gh` auth, with an artifact fallback when GitHub auth is unavailable.
5+
6+
## What Changes
7+
8+
- Add `gx pr-review --provider codex|claude --pr <num> [--post]`.
9+
- Fetch the PR diff through `gh pr diff <num>`, prompt the selected local CLI for structured findings, and post one GitHub review with inline comments through `gh api`.
10+
- Write a markdown review artifact instead of posting when `--post` is omitted or GitHub auth is unavailable.
11+
12+
## Impact
13+
14+
- Scope is CLI-only and self-hosted-runner friendly. It does not introduce model API dependencies or hosted bot state.
15+
- Inline comment accuracy depends on provider output using changed-file paths and changed-line numbers.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Local PR Review Runner
4+
The system SHALL provide a `gx pr-review` command that reviews a GitHub pull request using an authenticated local agent CLI without requiring OpenAI or Anthropic API tokens.
5+
6+
#### Scenario: Review with GitHub posting
7+
- **WHEN** `gx pr-review --provider codex --pr <number> --post` runs in a repository with GitHub auth
8+
- **THEN** the command reads the pull request diff through `gh pr diff <number>`
9+
- **AND** sends a compact structured-review prompt to the selected local provider
10+
- **AND** posts one GitHub review containing inline comments for returned findings.
11+
12+
#### Scenario: Review without GitHub auth
13+
- **WHEN** `gx pr-review --provider claude --pr <number> --post` runs without `GITHUB_TOKEN`, `GH_TOKEN`, or usable `gh auth`
14+
- **THEN** the command does not require model API credentials
15+
- **AND** writes a markdown review artifact containing the structured findings instead of posting.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Definition of Done
2+
3+
This change is complete only when **all** of the following are true:
4+
5+
- Every checkbox below is checked.
6+
- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff.
7+
- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline.
8+
9+
## Handoff
10+
11+
- Handoff: change=`agent-codex-codex-task-2026-05-11-15-20`; branch=`agent/codex/codex-task-2026-05-11-15-20`; scope=`gx pr-review local PR review runner`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`.
12+
- Copy prompt: Continue `agent-codex-codex-task-2026-05-11-15-20` on branch `agent/codex/codex-task-2026-05-11-15-20`. Work inside the existing sandbox, review `openspec/changes/agent-codex-codex-task-2026-05-11-15-20/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/codex-task-2026-05-11-15-20 --base main --via-pr --wait-for-merge --cleanup`.
13+
14+
## 1. Specification
15+
16+
- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-codex-codex-task-2026-05-11-15-20`.
17+
- [x] 1.2 Define normative requirements in `specs/codex-task/spec.md`.
18+
19+
## 2. Implementation
20+
21+
- [x] 2.1 Implement scoped behavior changes.
22+
- [x] 2.2 Add/update focused regression coverage.
23+
24+
## 3. Verification
25+
26+
- [x] 3.1 Run targeted project verification commands.
27+
- [x] 3.2 Run `openspec validate agent-codex-codex-task-2026-05-11-15-20 --type change --strict`.
28+
- [x] 3.3 Run `openspec validate --specs`.
29+
30+
## 4. Cleanup (mandatory; run before claiming completion)
31+
32+
- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent/<your-name>/<branch-slug> --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation.
33+
- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff.
34+
- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch).

src/cli/args.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,69 @@ function parseReviewArgs(rawArgs) {
261261
};
262262
}
263263

264+
function parsePrReviewArgs(rawArgs) {
265+
const parsed = parseTargetFlag(rawArgs, process.cwd());
266+
const options = {
267+
target: parsed.target,
268+
provider: 'codex',
269+
pr: '',
270+
post: false,
271+
artifact: '',
272+
timeoutMs: 10 * 60 * 1000,
273+
};
274+
275+
for (let index = 0; index < parsed.args.length; index += 1) {
276+
const arg = parsed.args[index];
277+
if (arg === '--provider') {
278+
const next = requireValue(parsed.args, index, '--provider');
279+
if (!['codex', 'claude'].includes(next)) {
280+
throw new Error(`Invalid --provider value: ${next} (expected codex|claude)`);
281+
}
282+
options.provider = next;
283+
index += 1;
284+
continue;
285+
}
286+
if (arg === '--pr') {
287+
options.pr = requireValue(parsed.args, index, '--pr');
288+
index += 1;
289+
continue;
290+
}
291+
if (arg === '--post') {
292+
options.post = true;
293+
continue;
294+
}
295+
if (arg === '--no-post') {
296+
options.post = false;
297+
continue;
298+
}
299+
if (arg === '--artifact' || arg === '--output') {
300+
options.artifact = requireValue(parsed.args, index, arg);
301+
index += 1;
302+
continue;
303+
}
304+
if (arg === '--timeout-ms') {
305+
const raw = requireValue(parsed.args, index, '--timeout-ms');
306+
const parsedTimeout = Number.parseInt(raw, 10);
307+
if (!Number.isFinite(parsedTimeout) || parsedTimeout <= 0) {
308+
throw new Error('--timeout-ms requires a positive integer');
309+
}
310+
options.timeoutMs = parsedTimeout;
311+
index += 1;
312+
continue;
313+
}
314+
throw new Error(`Unknown option: ${arg}`);
315+
}
316+
317+
if (!options.pr) {
318+
throw new Error('--pr requires a pull request number');
319+
}
320+
if (!/^\d+$/.test(String(options.pr))) {
321+
throw new Error(`--pr must be a pull request number (received: ${options.pr})`);
322+
}
323+
324+
return options;
325+
}
326+
264327
function parseAgentsArgs(rawArgs) {
265328
const parsed = parseTargetFlag(rawArgs, process.cwd());
266329
const [subcommandRaw = '', ...rest] = parsed.args;
@@ -1138,6 +1201,7 @@ module.exports = {
11381201
parseDoctorArgs,
11391202
parseTargetFlag,
11401203
parseReviewArgs,
1204+
parsePrReviewArgs,
11411205
parseAgentsArgs,
11421206
parseReportArgs,
11431207
parseSyncArgs,

src/cli/main.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { finishAgentSession } = require('../agents/finish');
1313
const sessionSeverityReport = require('../report/session-severity');
1414
const cockpitModule = require('../cockpit');
1515
const agentsStart = require('../agents/start');
16+
const prReviewModule = require('../pr-review');
1617
const {
1718
fs,
1819
path,
@@ -135,6 +136,7 @@ const {
135136
parseDoctorArgs,
136137
parseTargetFlag,
137138
parseReviewArgs,
139+
parsePrReviewArgs,
138140
parseAgentsArgs,
139141
parseReportArgs,
140142
parseSyncArgs,
@@ -2541,6 +2543,13 @@ function review(rawArgs) {
25412543
process.exitCode = typeof result.status === 'number' ? result.status : 1;
25422544
}
25432545

2546+
function prReview(rawArgs) {
2547+
const options = parsePrReviewArgs(rawArgs);
2548+
const result = prReviewModule.runPrReview(options);
2549+
prReviewModule.printPrReviewResult(result);
2550+
process.exitCode = 0;
2551+
}
2552+
25442553
function agentsStatePathForRepo(repoRoot) {
25452554
return path.join(repoRoot, AGENTS_BOTS_STATE_RELATIVE);
25462555
}
@@ -3938,6 +3947,7 @@ async function main() {
39383947
}
39393948

39403949
if (command === 'prompt') return prompt(rest);
3950+
if (command === 'pr-review') return prReview(rest);
39413951
if (command === 'doctor') return doctor(rest);
39423952
if (command === 'branch') return branch(rest);
39433953
if (command === 'pivot') return pivot(rest);

src/context.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ const CLI_COMMAND_GROUPS = [
458458
description: 'Review / cleanup bots, AI setup prompts, and safety reports.',
459459
commands: [
460460
['agents', 'Start/stop repo-scoped review + cleanup bots'],
461+
['pr-review', 'Run local Codex/Claude PR review and post inline GitHub comments or write an artifact'],
461462
['cockpit', 'Create or attach to a repo tmux cockpit session'],
462463
['install-agent-skills', 'Install Guardex Codex/Claude skills into the user home'],
463464
['prompt', 'Print AI setup checklist or named slices (--exec, --part, --list-parts, --snippet)'],

0 commit comments

Comments
 (0)