Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c8ea1fa
ci: add PR auto-review workflow on self-hosted runner with codex backend
Yukoval-Dakia Jun 6, 2026
6571f5b
Merge pull request #1 from Yukoval-Dakia/ci/ocr-review-workflow
Yukoval-Dakia Jun 6, 2026
f2b0f40
feat(llm): add codex provider via official Codex CLI (#2)
Yukoval-Dakia Jun 6, 2026
654e11d
ci: switch PR auto-review from Codex to opencode-go API
Yukoval-Dakia Jun 6, 2026
debaaa5
feat: add claude cli provider
Yukoval-Dakia Jun 7, 2026
5271e26
fix: close claude stream json input
Yukoval-Dakia Jun 7, 2026
cd6458d
fix: isolate claude cli provider
Yukoval-Dakia Jun 7, 2026
1791f8b
fix: stabilize claude stream json provider
Yukoval-Dakia Jun 7, 2026
a4188f1
ci: switch fork self-review to BigModel anthropic backend
Yukoval-Dakia Jun 18, 2026
614ab6f
Merge remote-tracking branch 'upstream/main' into sync-upstream
Yukoval-Dakia Jun 18, 2026
da016bb
feat(review): add severity/confidence self-assessment + noise filter
Yukoval-Dakia Jun 18, 2026
dec9a38
docs(spec): cross-reference impact context design
Yukoval-Dakia Jun 18, 2026
d430429
docs(plan): cross-reference impact implementation plan
Yukoval-Dakia Jun 18, 2026
b72cc74
feat(reviewctx): ContextProvider framework for injectable review context
Yukoval-Dakia Jun 18, 2026
7578f70
feat(impact): analyzer types and changed-line extraction
Yukoval-Dakia Jun 18, 2026
0fec89d
feat(impact): Go analyzer via go/parser
Yukoval-Dakia Jun 18, 2026
7b14c81
fix(impact): distinguish var/const in Go analyzer; correct References…
Yukoval-Dakia Jun 18, 2026
07b4759
feat(impact): TypeScript analyzer via embedded Node helper (no CGO)
Yukoval-Dakia Jun 18, 2026
ea23e40
feat(impact): CrossRefProvider — grep + confirm + capped summary
Yukoval-Dakia Jun 18, 2026
5a508a5
fix(impact): silent-skip parse errors; normalize def-file exclusion
Yukoval-Dakia Jun 18, 2026
e0f477c
feat(review): inject cross-reference impact context into MAIN_TASK
Yukoval-Dakia Jun 18, 2026
1fc1cf6
fix(impact): grep references at the reviewed ref; run TS helper in re…
Yukoval-Dakia Jun 18, 2026
6e11f35
fix(impact): drop empty cross-ref wrapper; cap symbols probed
Yukoval-Dakia Jun 19, 2026
a62f8fc
docs(spec): cross-PR learnings design
Yukoval-Dakia Jun 19, 2026
7ff2d06
docs(plan): cross-PR learnings Phase 1 (collect + store)
Yukoval-Dakia Jun 19, 2026
f406214
feat(learn): Learning types + JSON-lines store (dedupe, soft-cap)
Yukoval-Dakia Jun 19, 2026
f56d873
feat(learn): BigModel embeddings client
Yukoval-Dakia Jun 19, 2026
6c2f8a5
feat(learn): ingest feedback.json into the store (idempotent, best-ef…
Yukoval-Dakia Jun 19, 2026
e17c9e3
feat(learn): env config + best-effort ingest wired into review
Yukoval-Dakia Jun 19, 2026
79793e6
style(learn): gofmt import order in review_cmd.go
Yukoval-Dakia Jun 19, 2026
538af1e
fix(learn): roll back on flush error, clean temp file, reject empty i…
Yukoval-Dakia Jun 19, 2026
36f8786
tune(review): relax over-conservative prompt + severity defaults
Yukoval-Dakia Jun 19, 2026
463678e
feat(learn): wire feedback collector into review workflow (Phase 1)
Yukoval-Dakia Jun 19, 2026
4e0502f
feat(learn): add standalone `ocr learn ingest` subcommand
Yukoval-Dakia Jun 19, 2026
d131d0c
feat(learn): Phase 2 reflag suppression + decoupled collector + calib…
Yukoval-Dakia Jun 20, 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
69 changes: 69 additions & 0 deletions .github/workflows/ocr-learn-ingest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# OpenCodeReview - Learnings Ingest (decoupled from review)
#
# Fixes the collector timing flaw: the review-time collector only sees thread
# state that exists *before* the review runs, so manual resolves / disagreements
# that happen afterward are never captured. This workflow runs at the reliable
# capture points — when a PR closes and when a review thread is resolved — to
# record final verdicts via `ocr learn ingest` (no review, cheap).
#
# Self-hosted macOS runner with a prebuilt `ocr` at ~/.local/bin/ocr.

name: OpenCodeReview Learnings Ingest

on:
pull_request:
types: [closed]
pull_request_review_thread:
types: [resolved, unresolved]

permissions:
contents: read
pull-requests: read

# Serialize with the review workflow's runner; never cancel an in-flight ingest.
concurrency:
group: ocr-review
cancel-in-progress: false

jobs:
learn-ingest:
runs-on: self-hosted
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up PATH for ocr
run: |
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"

- name: Collect final thread verdicts (learnings)
id: collect-feedback
uses: actions/github-script@v7
env:
OCR_BOT_LOGIN: ${{ vars.OCR_BOT_LOGIN || 'github-actions[bot]' }}
OCR_FEEDBACK_REJECT_AGE_DAYS: ${{ vars.OCR_FEEDBACK_REJECT_AGE_DAYS || '3' }}
OCR_FEEDBACK_PATH: /tmp/ocr-feedback.json
OCR_PR_NUMBER: ${{ github.event.pull_request.number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { collectFeedback } = require(
`${process.env.GITHUB_WORKSPACE}/scripts/github-actions/collect-feedback.js`);
const { items } = await collectFeedback({ github, context, core, fs, env: process.env });
if (items.length === 0) core.info('No verdicted feedback to ingest.');

- name: Ingest into the learnings store
env:
# Endpoint resolution supplies the token used for embedding calls.
OCR_LLM_PROTOCOL: anthropic
OCR_LLM_URL: ${{ vars.OCR_LLM_URL }}
OCR_LLM_TOKEN: ${{ secrets.OCR_LLM_TOKEN }}
OCR_LLM_MODEL: ${{ vars.OCR_LLM_MODEL }}
OCR_EMBED_URL: ${{ vars.OCR_EMBED_URL }}
OCR_EMBED_MODEL: ${{ vars.OCR_EMBED_MODEL }}
OCR_LEARNINGS: on
run: |
ocr learn ingest --feedback /tmp/ocr-feedback.json || true
219 changes: 130 additions & 89 deletions .github/workflows/ocr-review.yml
Original file line number Diff line number Diff line change
@@ -1,80 +1,117 @@
# OpenCodeReview - GitHub Actions PR Auto-Review Demo
# OpenCodeReview - PR Auto-Review (self-hosted runner + opencode-go API)
#
# This workflow automatically reviews pull requests using OpenCodeReview
# and posts review comments directly on the PR.
# Runs on a self-hosted macOS runner where:
# - a prebuilt `ocr` binary lives at ~/.local/bin/ocr
#
# Triggers:
# - PR opened (uses pull_request_target for fork secret access)
#
# Required secrets:
# OCR_LLM_URL - LLM API endpoint (e.g., https://api.openai.com/v1/chat/completions)
# OCR_LLM_AUTH_TOKEN - Authentication token for the LLM API
#
# Optional secrets:
# OCR_LLM_MODEL - Model name (default: gpt-4o)
# LLM endpoint is opencode-go (OpenAI-compatible): URL/model come from
# repository variables, the API token from the OCR_LLM_TOKEN secret.
#
# Note: GITHUB_TOKEN is automatically provided by GitHub Actions.
# Triggers:
# - PR opened
# - Comment on PR containing '/open-code-review' or '@open-code-review'

name: OpenCodeReview PR Review

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

on:
# Use pull_request_target instead of pull_request so that secrets are
# available even for PRs from forks. This is safe because OCR only reads
# the diff and does not execute any code from the PR.
pull_request_target:
pull_request:
types: [opened]
issue_comment:
types: [created]

permissions:
contents: read
pull-requests: write

# Serialize runs to keep load on the single self-hosted runner predictable.
concurrency:
group: ocr-review
cancel-in-progress: false

jobs:
code-review:
runs-on: self-hosted
container:
image: node:20
if: github.event_name == 'pull_request_target'
timeout-minutes: 30
# Run on PR events, or on comments starting with trigger keywords
if: |
github.event_name == 'pull_request' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/open-code-review')) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@open-code-review'))
steps:
- name: Get PR context
id: pr-context
if: github.event_name != 'pull_request'
uses: actions/github-script@v7
with:
script: |
// For issue_comment events, get PR info
const prNumber = context.issue.number;
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('base_ref', pullRequest.base.ref);
core.setOutput('head_ref', pullRequest.head.ref);
core.setOutput('head_sha', pullRequest.head.sha);

- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for merge-base diff
ref: ${{ github.event.pull_request.head.sha }}

- name: Mark repository as safe directory
run: git config --global --add safe.directory '*'
ref: ${{ github.event_name != 'pull_request' && steps.pr-context.outputs.head_sha || '' }}

- name: Fetch PR head ref (ensures fork commits are available)
run: git fetch origin pull/${{ github.event.pull_request.number }}/head

- name: Install OpenCodeReview
run: npm install -g @alibaba-group/open-code-review

- name: Configure OCR
- name: Set up PATH for ocr
run: |
ocr config set llm.url ${{ secrets.OCR_LLM_URL }}
ocr config set llm.auth_token ${{ secrets.OCR_LLM_AUTH_TOKEN }}
ocr config set llm.model ${{ secrets.OCR_LLM_MODEL }}
ocr config set llm.use_anthropic ${{ secrets.OCR_LLM_USE_ANTHROPIC }}
ocr config set llm.extra_body '{"enable_thinking": false}'
ocr config set language English

- name: Run OpenCodeReview
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"

# Learnings (Phase 1): retroactively collect resolve/reply state of OCR's
# prior inline comments on THIS PR, derive a verdict per comment, and write
# feedback.json. The OCR binary ingests it (best-effort) during review.
- name: Collect prior-review feedback (learnings)
id: collect-feedback
uses: actions/github-script@v7
env:
# Login of the account that posts OCR review comments (the workflow's
# GITHUB_TOKEN → github-actions[bot] by default).
OCR_BOT_LOGIN: ${{ vars.OCR_BOT_LOGIN || 'github-actions[bot]' }}
# An unresolved thread older than this many days counts as rejected (weak).
OCR_FEEDBACK_REJECT_AGE_DAYS: ${{ vars.OCR_FEEDBACK_REJECT_AGE_DAYS || '3' }}
OCR_FEEDBACK_PATH: /tmp/ocr-feedback.json
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const { collectFeedback } = require(
`${process.env.GITHUB_WORKSPACE}/scripts/github-actions/collect-feedback.js`);
await collectFeedback({ github, context, core, fs, env: process.env });

- name: Run OpenCodeReview (zai/bigmodel anthropic backend)
id: review
env:
OCR_LLM_PROTOCOL: anthropic
OCR_LLM_URL: ${{ vars.OCR_LLM_URL }}
OCR_LLM_TOKEN: ${{ secrets.OCR_LLM_TOKEN }}
OCR_LLM_MODEL: ${{ vars.OCR_LLM_MODEL }}
# Learnings ingestion: feed the collector's feedback.json to OCR.
OCR_LEARNINGS: on
OCR_LEARNINGS_FEEDBACK: ${{ steps.collect-feedback.outputs.feedback_path }}
run: |
BASE_REF="${{ github.event.pull_request.base.ref }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
# Get base and head refs from PR context (different for comment triggers)
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="${{ github.event.pull_request.base.ref }}"
HEAD_REF="${{ github.event.pull_request.head.ref }}"
else
BASE_REF="${{ steps.pr-context.outputs.base_ref }}"
HEAD_REF="${{ steps.pr-context.outputs.head_ref }}"
fi

echo "Reviewing PR: ${HEAD_SHA} against origin/${BASE_REF}"
echo "Reviewing PR: ${HEAD_REF} against ${BASE_REF}"

# Run OCR in range mode with JSON output
ocr review \
--from "origin/${BASE_REF}" \
--to "${HEAD_SHA}" \
--to "origin/${HEAD_REF}" \
--format json \
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true

Expand Down Expand Up @@ -128,7 +165,20 @@ jobs:

// Prepare PR review with inline comments
const prNumber = context.issue.number;
let commitSha = context.payload.pull_request.head.sha;
let commitSha;

// Get commit SHA from event context
if (context.eventName === 'pull_request') {
commitSha = context.payload.pull_request.head.sha;
} else {
// For comment events, we need to fetch the PR
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
commitSha = pullRequest.head.sha;
}

// Build review comments array for the PR review API
// Only inline comments with line info can be posted via createReview
Expand Down Expand Up @@ -164,17 +214,27 @@ jobs:
reviewComment.side = 'RIGHT';
}

reviewComments.push({ comment, reviewComment });
reviewComments.push(reviewComment);
}

// Submit as a single PR review with all comments
const totalCount = comments.length;
const inlineCount = reviewComments.length;
const summaryCount = commentsWithoutLine.length;
let summaryBody = buildSummaryBody(totalCount, inlineCount, summaryCount, warnings);
let summaryBody = `🔍 **OpenCodeReview** found **${totalCount}** issue(s) in this PR.`;
if (totalCount > 0) {
summaryBody += `\n- ✅ ${inlineCount} posted as inline comment(s)`;
summaryBody += `\n- 📝 ${summaryCount} posted as summary (missing line info)`;
}
if (warnings.length > 0) {
summaryBody += `\n\n⚠️ ${warnings.length} warning(s) occurred during review.`;
}

// Add comments without line info to summary body
summaryBody += formatSummaryComments(commentsWithoutLine);
for (const { comment, body } of commentsWithoutLine) {
summaryBody += '\n\n---\n\n';
summaryBody += formatCommentMarkdown(comment);
}

// Statistics tracking
let successCount = 0;
Expand All @@ -189,16 +249,16 @@ jobs:
commit_id: commitSha,
body: summaryBody,
event: 'COMMENT',
comments: reviewComments.map(({ reviewComment }) => reviewComment)
comments: reviewComments
});
successCount = reviewComments.length;
console.log(`Successfully posted review with ${successCount} inline comments (${commentsWithoutLine.length} in summary)`);
} catch (e) {
console.log('Failed to post review with inline comments:', e.message);
console.log('Falling back to posting comments individually...');

// Fallback: post comments one by one
for (const { comment, reviewComment } of reviewComments) {
for (const reviewComment of reviewComments) {
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
Expand All @@ -213,29 +273,32 @@ jobs:
console.log(`Successfully posted comment for ${reviewComment.path}`);
} catch (innerE) {
failedCount++;
failedComments.push({ comment, error: innerE.message });
failedComments.push({ comment: reviewComment, error: innerE.message });
console.log(`Failed to post comment for ${reviewComment.path}: ${innerE.message}`);
}
}

// Post summary comment with statistics
let finalBody = buildSummaryBody(totalCount, successCount, commentsWithoutLine.length + failedComments.length, warnings);
finalBody += formatSummaryComments(commentsWithoutLine);
let finalBody = summaryBody;
finalBody += `\n\n---\n\n📊 **Posting Statistics:**`;
finalBody += `\n- ✅ Successfully posted: ${successCount} comment(s)`;
if (failedCount > 0) {
finalBody += `\n- ❌ Failed to post: ${failedCount} comment(s)`;
}

// Add failed comments as summary content so review feedback is not lost.

// Add failed comments details. Include the full comment body so a
// finding whose inline placement failed is never lost — it stays
// visible in the summary instead of being reduced to path+error.
if (failedComments.length > 0) {
finalBody += '\n\n---\n\n### ⚠️ Inline comments shown in summary';
finalBody += '\n\n<details><summary>❌ Failed Comments Details</summary>\n\n';
for (const { comment, error } of failedComments) {
finalBody += '\n\n---\n\n';
finalBody += formatCommentMarkdown(comment, error);
finalBody += `\n#### 📄 \`${comment.path}\`\n`;
finalBody += `_Could not post inline: ${error}_\n\n`;
if (comment.body) finalBody += `${comment.body}\n`;
}
finalBody += '\n</details>';
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
Expand All @@ -256,15 +319,12 @@ jobs:
return body;
}

function formatCommentMarkdown(comment, error) {
function formatCommentMarkdown(comment) {
let md = `### 📄 \`${comment.path}\``;
if (comment.start_line && comment.end_line) {
md += ` (L${comment.start_line}-L${comment.end_line})`;
}
md += '\n\n';
if (error) {
md += `⚠️ GitHub could not post this as an inline comment: ${error}\n\n`;
}
md += comment.content || '';

if (comment.suggestion_code && comment.existing_code) {
Expand All @@ -277,27 +337,8 @@ jobs:
return md;
}

function buildSummaryBody(totalCount, inlineCount, summaryCount, warnings) {
let body = `🔍 **OpenCodeReview** found **${totalCount}** issue(s) in this PR.`;
if (totalCount > 0) {
body += `\n- ✅ ${inlineCount} posted as inline comment(s)`;
body += `\n- 📝 ${summaryCount} posted as summary`;
}
if (warnings.length > 0) {
body += `\n\n⚠️ ${warnings.length} warning(s) occurred during review.`;
}
return body;
}

function formatSummaryComments(summaryComments) {
let body = '';
for (const { comment } of summaryComments) {
body += '\n\n---\n\n';
body += formatCommentMarkdown(comment);
}
return body;
}

// fencedBlock wraps content in a code fence long enough that any
// backtick runs inside it cannot prematurely close the block.
function fencedBlock(content, language = '') {
const text = String(content || '');
const fence = safeFence(text);
Expand Down
Loading
Loading