Status: implemented and verified on a test repo (designed 2026-09-23). Facts marked (verified) were checked against docs.github.com, with sources in ../decisions.md. Items marked (verified on a test repo) are undocumented by GitHub and were confirmed on
Badri-Rajendran/codesage-sandboxon 2026-09-25.
- Any of the owner's repos can add CodeSage with a ~15-line workflow and one secret.
- It reviews automatically when a PR is opened, and afterwards only when asked.
- Results live in GitHub: a PR review with inline comments, a
CodeSagecheck run, and a job summary. - It is cheap: $0.50 cap per review, a lean install, and no always-on server.
- Composite action at the repo root (
action.yml), used asuses: Badri-Rajendran/CodeSage@v1. Composite actions run on any OS and avoid the container build of Docker actions, which are Linux-only and slower (verified). - Versioning: semver tags plus a moving major tag (
v1). During development, repos can point at@feat/agentic-workflows. - Lean install:
pyproject.tomlis split so the action installs only what the engine needs:dependencies(engine):anthropic,langchain,langchain-anthropic,langgraph,pydantic,pydantic-settings,httpx,pyyaml,tenacity.[server]extra (local mode only):fastapi,uvicorn,sqlalchemy[asyncio],asyncpg,pgvector,numpy,mcp,langgraph-checkpoint-postgres,psycopg[binary],psycopg-pool.- Engine modules must not import server-only modules at import time.
semantic_searchis injected, not imported.
name: CodeSage review
description: Agentic PR review with Claude
inputs:
anthropic-api-key: { required: true }
github-token: { required: false, default: "${{ github.token }}" }
config-path: { required: false, default: ".codesage.yml" }
outputs:
score: { value: "${{ steps.review.outputs.score }}" }
gate: { value: "${{ steps.review.outputs.gate }}" } # pass | tripped | skipped
cost-usd: { value: "${{ steps.review.outputs.cost_usd }}" }
review-url: { value: "${{ steps.review.outputs.review_url }}" }
runs:
using: composite
steps:
- uses: actions/setup-python@v7 # current major (verified 2026-09-24)
with: { python-version: "3.12" }
- shell: bash
run: python -m venv "$RUNNER_TEMP/codesage" && "$RUNNER_TEMP/codesage/bin/pip" install -q "${{ github.action_path }}"
- id: resolve # decide whether to run; find head SHA
shell: bash
env: { GITHUB_TOKEN: "${{ inputs.github-token }}" }
run: '"$RUNNER_TEMP/codesage/bin/codesage-action" resolve'
- if: steps.resolve.outputs.run == 'true'
uses: actions/checkout@v7 # current major (verified); pin in Phase 5
with: { ref: "${{ steps.resolve.outputs.head_sha }}", fetch-depth: 0, persist-credentials: false }
- id: review
if: steps.resolve.outputs.run == 'true'
shell: bash
env:
ANTHROPIC_API_KEY: "${{ inputs.anthropic-api-key }}"
GITHUB_TOKEN: "${{ inputs.github-token }}"
CODESAGE_CONFIG: "${{ inputs.config-path }}"
run: '"$RUNNER_TEMP/codesage/bin/codesage-action" review'Composite steps may use uses: (such as setup-python and checkout) and must set shell:
on run: steps. github.action_path points at the action's own files (verified).
Inputs are read from the inputs context rather than INPUT_* environment variables
(verified). The current majors of actions/checkout and actions/setup-python are both
v7 (verified 2026-09-24).
- Console script, not
python -m. The steps call thecodesage-actionconsole script.python -m app.actionwould put the working directory (the reviewed repo) first onsys.path, so a repo with its ownapp/package would have its code imported. This was found during the Phase 5 dry run. - No persisted credentials. The checkout uses
persist-credentials: false, so the token isn't written to.git/config, whererun_testscode could read it. - No
.envfile. Settings are built without reading a.envfile, because the working directory is the reviewed repo.
name: CodeSage
on:
pull_request:
types: [opened, ready_for_review, labeled]
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
checks: write # pull-requests: write also covers removing the label (verified on a test repo)
concurrency:
group: codesage-${{ github.event.pull_request.number || github.event.issue.number }}
cancel-in-progress: true
jobs:
review:
runs-on: ubuntu-latest
if: >-
(github.event_name == 'pull_request' && (
(github.event.action == 'opened' && !github.event.pull_request.draft) ||
github.event.action == 'ready_for_review' ||
(github.event.action == 'labeled' && github.event.label.name == 'codesage:review'))) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request &&
startsWith(github.event.comment.body, '/codesage review') &&
contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))
steps:
- uses: Badri-Rajendran/CodeSage@v1
with:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}The job-level if avoids starting a runner for events that won't be reviewed. The
resolve step applies the same rules again, so a mistake in a copied workflow can't
cause unintended runs.
| Event | Runs when | Head SHA source |
|---|---|---|
pull_request opened |
not a draft | pull_request.head.sha |
pull_request ready_for_review |
always | pull_request.head.sha |
pull_request labeled |
label.name == "codesage:review"; the label is removed after the review |
pull_request.head.sha |
issue_comment created |
issue.pull_request is set, body starts with /codesage review, and author_association is OWNER, MEMBER or COLLABORATOR |
GET /repos/{r}/pulls/{n}, then head.sha, because the payload doesn't include it (verified) |
| anything else | never |
- Fork PRs are skipped: if
head.repo.full_name != github.repository, the step exits 0 with a job-summary note. Fork PRs get no secrets and a read-only token anyway (verified). issue_commentruns the workflow from the default branch (verified), so the consumer workflow must exist on the default branch before comments trigger it. The README must say this.
A Pydantic model. Every key is optional; defaults are shown.
models:
reviewer: claude-sonnet-5
judge: claude-opus-5
effort:
reviewer: medium
judge: high
budget_usd: 0.50
gate:
threshold: 0.6 # judge score below this trips the gate
fail_on: [critical, high] # severities that trip the gate
ignore_paths: # glob patterns, merged with built-in defaults
- "**/*.lock"
- "**/package-lock.json"
- "dist/**"
- "build/**"
max_files: 50 # files beyond this are listed in the body, not reviewed
tests:
setup: null # e.g. "pip install -e .[dev]" or "npm ci"
command: null # e.g. "pytest -q {target}" or "npm test -- {target}"
timeout_s: 300Unknown keys or bad values fail fast, with a clear error in the check run and the job summary.
- Created at the start of
reviewwithstatus: in_progress, namedCodeSage, on the head SHA, so the PR shows a pending check while the review runs. - Completed at the end:
| Outcome | conclusion |
Title |
|---|---|---|
| Gate passes | success |
"No blocking issues · score 0.82" |
| Gate trips | failure |
"Needs attention · 1 critical, 2 high" or "score 0.52 < 0.60" |
| Budget-limited before the judge ran, and it's the only gate reason | neutral |
"Budget reached before judging" |
| Engine error | failure |
"CodeSage error: " |
Precedence: engine error > any critical/high finding or low score (failure) >
budget-limited-only (neutral) > pass (success).
output.summaryholds the same markdown as the review body, truncated to stay under 65,535 characters (verified limit).- Up to 50 findings with a line become annotations (
failurefor critical and high,warningfor medium,noticefor the rest), within the 50-per-request limit (verified). GITHUB_TOKENcan create check runs whenchecks: writeis granted (verified).
- One review per run:
POST /repos/{r}/pulls/{n}/reviews, withevent: COMMENTandcommit_idset to the head SHA. - Inline comments: only findings where
Diff.is_commentable(path, line)is true, usingpath,line,side: RIGHT(plusstart_lineandstart_sidefor ranges). The deprecatedpositionfield isn't used (verified).- At most 30 inline comments; the rest go in the body. GitHub documents no maximum, so this is our own conservative choice.
- Comment body: a severity badge, the title, the rationale, the suggestion,
evidencein a collapsed<details>block, and the reviewer name.
- Review body:
- a header with the judge score and gate result
- counts by severity
- findings that couldn't be anchored inline
- reflection's summary
- a per-agent cost table, and a "budget-limited" note when relevant
- a footer with the CodeSage version
- Fallback: if the create-review call returns 422, it's retried once with no inline comments and every finding in the body. One comment on a line outside the diff fails the whole review with 422 "Line could not be resolved", and nothing is posted (verified on a test repo).
APPROVEandREQUEST_CHANGESare never used.APPROVEbyGITHUB_TOKENis off by default in new repos (verified), and the gate's blocking signal is the check run.
$GITHUB_STEP_SUMMARYgets the review body plus the full agent traces, in collapsed sections, capped at 1 MiB (verified limit).- The step outputs are
score,gate,cost_usdandreview_url.
After a label-triggered run, codesage:review is removed
(DELETE /repos/{r}/issues/{n}/labels/codesage:review), so adding it again
triggers a new review. pull-requests: write is enough; issues: write isn't
needed (verified on a test repo): a run whose token had only checks, contents,
metadata and pull-requests removed the label.
| Situation | Exit code | Why |
|---|---|---|
| Skipped (fork, rules not met) | 0 | Nothing to do |
| Review published, gate passes or trips | 0 | The pass/fail signal is the CodeSage check run, which branch protection can require |
| Engine or publishing error | 1 | Shows as a failed job; the check run says CodeSage error |
- The Anthropic key comes only from the
anthropic-api-keyinput (a repo secret). It is never passed torun_tests(see 01-agent-engine.md). - The workflow grants only
contents: read,pull-requests: writeandchecks: write. - Only same-repo PRs from trusted authors are reviewed.
pull_request_targetis not used, because GitHub warns against running PR code under it (verified). - Repo names are checked with
validate_repo(app/github/client.py) before any API call that carries the token.
codesage-action review --event path/to/event.json --event-name pull_request --repository owner/name --workspace /path/to/repo --dry-run --local-diff
runs the same code outside Actions, using a saved event payload. It's used for
Phase 5 verification and for debugging without pushing.