Org-wide GitHub defaults and shared reusable workflows.
.github/workflows/biggiepockets-review.yml is a reusable workflow that runs a
two-stage AI code review on a pull request:
- Codex first pass — reviews the diff against the PR's JIRA ticket and writes findings.
- pi verify & synthesize — validates Codex's findings, reviews the diff independently (grepping for callers/tests, factoring in the existing PR discussion), checks the change against the ticket's acceptance criteria, and decides a single verdict.
The BiggiePockets service account then submits the resulting approve /
request_changes review on the PR. If the PR has no BIG-XXXXX key in its title (or the
ticket can't be fetched), the review degrades gracefully to a diff-based review instead of
failing.
Codex and pi run as separate GitHub Actions jobs. Codex uploads the reviewed commit's diff, ticket/discussion context, and findings as a short-lived artifact; pi downloads that immutable handoff. If the pi pass is rate-limited, use Re-run failed jobs on the workflow run. GitHub reruns only the pi job, reusing the completed Codex pass instead of invoking Codex again.
The review logic lives centrally in this repo. Each consuming repo only adds a thin caller workflow that owns the triggers and gating and delegates to this one.
Do this once per repo you want BiggiePockets to review.
The Stage-2 verification stage runs @earendil-works/pi-coding-agent,
a plain npm CLI that the workflow installs onto the runner itself (npm install -g).
There is no GitHub app, no OIDC, and no per-repo installation. pi authenticates to
OpenRouter with the shared OPENROUTER_API_KEY described below — no OAuth login or
personal token is required.
Create .github/workflows/biggiepockets-review.yml in the target repo:
name: BiggiePockets Code Review
# Thin caller for the org-wide reusable review workflow in BiggerPockets/.github.
# This file owns the triggers and gating; the review logic lives centrally.
on:
pull_request:
types: [review_requested]
workflow_dispatch:
inputs:
pr:
description: 'PR number to review'
required: true
type: string
# The reusable workflow's jobs need read scopes. Declare them explicitly so the
# caller works regardless of the repo's default token permissions.
permissions:
contents: read
pull-requests: read
jobs:
review:
# React to a manual dispatch, or to BiggiePockets specifically being requested.
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.requested_reviewer.login == 'BiggiePockets'
uses: BiggerPockets/.github/.github/workflows/biggiepockets-review.yml@main
with:
pr: ${{ github.event.pull_request.number || inputs.pr }}
# The BiggerPockets/.github ref to resolve review prompts from. Defaults to `main`,
# so callers tracking `@main` can omit it. If you pin the `uses:` ref above to a tag
# or SHA, pass the matching ref here too — otherwise prompts silently track main.
registry_ref: main
secrets: inheritThe reusable workflow consumes several secrets via secrets: inherit: credentials for the
the AI review provider (OPENROUTER_API_KEY, shared by both the Codex and pi stages),
an Atlassian email + API token to fetch the PR's JIRA ticket for intent, and a personal access
token for the BiggiePockets service account that submits the review. Configure them as
organization secrets (recommended — set once, available to every repo) or as per-repo
secrets if you prefer to scope them.
It also reports per-review traces to the biggiepockets-review app in Datadog LLM
Observability via secrets.DATADOG_API_KEY: verdict, timing, prompt template and version
(tracked as prompts, see below), the model each
stage ran (CODEX_MODEL/PI_MODEL env vars in the workflow — both are OpenRouter
model slugs and must be set), and the actual findings text from Codex and the summary pi wrote,
so review quality is inspectable, not just counted. This secret is optional — reviews still
run and post normally without it, but no metrics are reported.
The exact secret names each step expects are visible in the env: and with: blocks of
.github/workflows/biggiepockets-review.yml.
secrets.DATADOG_API_KEY also turns on cost tracking. Every LLM span in the review
trace carries its token usage — input, output, total, and cached-read counts — under
metrics, and Datadog prices the span from its own model pricing catalog. Cost is
therefore attributed per pass and per model on the same trace as the quality metrics,
and shows up in LLM Observability's spend views without a separate report.
For the catalog to recognise a model, a span has to name it the way Datadog does: the
bare model and the provider that originated it. The workflow runs everything through
OpenRouter, whose slugs look like openai/gpt-5.6-sol, so scripts/llm-usage.py
splits the slug into model_name: gpt-5.6-sol / model_provider: openai and records
the routing as a gateway:openrouter tag.
Cost is never computed in the reporting script: scripts/llm-usage.py holds no rate
table. Instead each span carries whichever of the two things Datadog needs. For a model
in the catalog, token counts are enough. For one it does not carry, the span reports a
total_cost metric taken at face value.
The two passes record their usage differently. pi runs in --mode json and writes a
JSON event stream; every assistant message carries a usage object with token counts and
cost.total — a price computed by pi from the model's OpenRouter list rates, the same
rates OpenRouter bills against, so it is the amount the pass is charged. The Stage-2
model and its rates are pinned in scripts/pi/models.json (the catalog pi ships
predates the model, and the live catalog refresh is a background fetch, not a startup
step — a committed pin is what makes a fresh runner deterministic); if you roll the
Stage-2 model, update that file in the same commit. Codex writes running token counters
to a session rollout on its own runner, and since its action exposes no usage output,
the workflow reads that rollout in the Codex job and hands the totals to the reporting
job. In both cases only usage objects are read — never message content, transcripts,
prompts, or diffs. (The Claude Code harness that previously ran Stage 2 translated
usage into Anthropic's schema, so OpenRouter's reported cost regularly did not survive
to the span — the pi pass records usage itself and is what this repo now trusts.)
Two organization-level variables (vars, not secrets — Settings → Secrets and
variables → Actions → Variables at the org level) configure where the trace lands.
Both are optional, and nothing here is committed to the repository:
DD_SITE— the Datadog site to report to (e.g.datadoghq.eu,us5.datadoghq.com). Defaults to the publicdatadoghq.com. Set this to the organization's actual site; a private or internal Datadog hostname belongs in this variable and nowhere else.DD_LLMOBS_ML_APP— the LLM Obsml_appthe review trace is grouped under. Defaults tobiggiepockets-review.
Cost tracking is best-effort and never fails a review. With no DATADOG_API_KEY the
whole reporting step is skipped, and a missing event stream, an absent rollout, or
malformed usage data degrades to fewer metrics on the span.
With the move off claude-code-action, the review no longer needs OIDC (id-token),
so the default "Read repository contents" setting is fine — repos that previously
had to loosen their workflow permissions for the review can leave them restrictive.
The reusable workflow's jobs declare contents: read and pull-requests: read only;
the review itself is submitted with the BIGGIEPOCKETS_PAT secret, not the repo's
GITHUB_TOKEN. Callers that already declare id-token: write in their caller file can
remove it, but leaving it is harmless.
The BiggiePockets service account must have access to the repo so it can be requested as a reviewer and post the review. Add it as a collaborator (or via a team) with at least write access.
Once installed, trigger a review either way:
- Request a review — add BiggiePockets as a reviewer on the PR. The workflow fires
on
review_requestedand only runs when BiggiePockets specifically is the requested reviewer. - On demand — run the
BiggiePockets Code Reviewworkflow via Actions → workflow_dispatch and pass the PR number. (Available once the caller file is on the repo's default branch.)
The review-stage prompts are not inline in the workflow. They live in this repo under
prompts/ and are resolved at runtime by scripts/resolve-prompts.sh:
prompts/
registry.json # arms + control arm + split + codex prompt
codex-first-pass.md # Stage 1 prompt (template)
claude-synthesize.md # Stage 2 control arm (template)
claude-synthesize-thesis-first.md # Stage 2 thesis-first arm (template)
_shared/{completeness,privacy,migration-data,perf,parsing,navigation,rename-compatibility,spec-value}-rules.md # shared rule blocks
-
Templates + shared blocks. Each prompt references the shared rule blocks via
{{@prompts/_shared/<name>.md}}, so the Codex and Stage-2 prompts can never drift out of sync. Prompts resolve{{PR}},{{PROMPT_NAME}},{{PROMPT_VERSION}}too. -
Content-derived versions.
prompt_versionis a content hash of the template plus the shared blocks it includes — it changes only when that prompt's text changes, not per PR or per arm, so Datadog LLM Obs can attribute quality to the exact prompt text that ran. -
One arm per pull request. Two Stage-2 prompts sit in the registry —
controlandthesis-first— and each review runs exactly one of them.scripts/resolve-prompts.shhashes<repo>:<pr>into a bucket 0-99 and assigns the PR to the experiment arm when that bucket falls underexperiment_split_percent(50 today), so the arms split traffic evenly and every review costs a single Stage-2 pass. Assignment is a pure function of repo and PR number: re-running a review reuses the same arm, and one PR never sees two review styles. -
The assigned arm decides. Whichever arm a PR draws writes the posted summary and the approve / request-changes verdict. No separate control gate holds the decision back, which is the tradeoff for one pass per review: an experiment prompt affects real review outcomes on its share of PRs. Set
experiment_split_percentto0to route every review tocontrol_armwithout removing the arm. The arm is never named in the review comment — a reviewer who knows which prompt wrote a summary can't judge it blind. -
Comparison is between PRs, not within one. No PR is reviewed twice, so there is no paired A/B to diff on a single PR. Compare the arms by grouping Datadog on
armacross many reviews — verdict rate, latency, tokens. That needs volume before it means anything; a gap in approve rate over a dozen PRs is noise. -
Prompt Tracking. Every LLM span carries the prompt that produced it under
meta.input.prompt— the registry template with its{{PR}}-style placeholders intact, plus the values that filled them asvariables, plusid/name/version. Keeping the placeholders is what makes each prompt one tracked prompt in Datadog rather than a new template per PR, so the Prompts view shows call volume, latency, tokens, and a version diff per prompt, and any span can be replayed in the Playground with its exact template and variables. A version starts when the prompt text changes (a Roll), sinceversionis the same content hash reported as a tag. -
Datadog. Each review is one trace, tagged with the arm that ran it:
biggiepockets.review → codex.review, pi.synthesizeA tag key resolves to one value per submitted payload, so an
armtag is only trustworthy while a payload carries a single arm — which it does by construction now that one arm runs per review. Tags includearm,arm_role(control/experiment),prompt_name,prompt_version,verdict,assignment_bucket, andexperiment_split_percent. Recording the bucket and the split keeps an assignment auditable: changing the split later can't rewrite what an already-recorded review ran under. A stablerun_id(repo-pr-runid) joins offline evals and panel ratings to the exact review.Spans carrying an
arm_agreement,experiment_verdict, orlabel_assignmenttag came from an earlier setup and are not comparable to these; exclude them when grouping by arm.
Registry operations (kept distinct so a formatting experiment can't silently change the production prompt):
- Roll — edit a prompt or shared-rule file; its content-derived
prompt_versionbumps. - Apply — point
control_armor an arm at a different stored prompt inregistry.json, or changeexperiment_split_percent(no version change). Callers tracking@mainpick the change up on their next run. A caller that pinsuses:to a tag or SHA needs BOTH that@refand itsregistry_refinput bumped in lockstep — Apply owns that ref-bump explicitly. - Split — add a new arm entry in
registry.json+ its prompt file, and give it a share of traffic viaexperiment_split_percent. - Merge — fold a variant's content into another prompt and remove the arm.
A validate-prompts.yml workflow guards the registry: it fails a PR if a template has
dangling includes, registry.json references a missing prompt, two arms point at the
same prompt, an arm's prompt fails to resolve, arm assignment isn't stable for a fixed PR,
the resolver isn't deterministic, or a shared-rule edit doesn't bump versions.