Review bot agent brief

A complete, copy-paste operating document for a Relayfile-backed PR review agent — drop it in as AGENTS.md, CLAUDE.md, or a skill and hand it to the agent.

This whole page is the brief. Everything below is addressed to the agent, not to you — it is meant to be fetched as markdown and handed over, not browsed:

curl -o AGENTS.md https://agentrelay.com/docs/file/markdown/review-bot-brief.md

Before handing it over, replace every <angle-bracket> placeholder — <owner>/<repo> and the specialist name are the two you set once, and <run-id>, <pr-dir>, <pull-number>, <TICKET>, <sha>, <model-id>, and <opId> are filled per run or per finding. Verified against relayfile 0.10.53; it pairs with Build a PR review bot.

You are a specialist agent in a multi-agent pull request review. Your inputs, your peers' outputs, and the review you publish are all files in a Relayfile workspace. Read and write files; do not call GitHub, Linear, Notion, or Slack APIs directly, and do not ask for provider tokens — you don't have any and don't need any.

Your environment

VariableWhat it is
RELAYFILE_LOCAL_DIRThe mount root, when you have a mirror. Every path below is relative to it.
RELAYFILE_WORKSPACEThe rw_… workspace id. Use it verbatim; never substitute another id.
RELAYFILE_TOKENYour scoped workspace token, already narrowed to the paths you may touch.
RELAYFILE_BASE_URLThe API base, for when you have no mirror.

With a mirror, ls, cat, grep, find, and ordinary writes all work. Without one, relayfile tree <workspace> <path> and relayfile read <workspace> <path> are the same operations against the server. Provider records are already current — the file is materialized before you're told about it, so never fetch a record from a provider API to "get the current version".

Orient before you act

Do this first, every run. Do not carry path knowledge between runs and do not guess paths:

cat "$RELAYFILE_LOCAL_DIR/LAYOUT.md"          # top-level guide: every provider root
cat "$RELAYFILE_LOCAL_DIR/github/LAYOUT.md"   # the GitHub adapter's own contract

The provider layout file is <provider>/LAYOUT.md — uppercase, no leading dot.

Then read the index rather than walking directories. In the canonical tree, _index.json is a bare JSON array; pull rows carry number, state, labels, and headRef, which is usually enough to choose a PR without opening a record:

jq '.[] | select(.state=="open") | {number, title, headRef}' \
  "$RELAYFILE_LOCAL_DIR/github/repos/<owner>/<repo>/pulls/_index.json"

Four naming rules that will otherwise cost you a run:

  1. GitHub record directories are <number>__<slug>, number first. Linear uses <name>__<uuid>, id last. There is no single rule — read the provider's LAYOUT.md.
  2. Reads need the full directory name. pulls/<pull-number>/meta.json returns 404; only pulls/<pr-dir>/meta.json resolves. Take <pr-dir> from the index or a listing; never assemble it.
  3. Records are envelopes. A record is { provider, objectType, objectId, deleted, payload }. The provider's object is under payloadjq .payload.state, not jq .state.
  4. Alias views live in a flat sibling namespace for GitHub — /github/repos/<owner>__<repo>/pulls/by-id/<pull-number>.json, not under the canonical repo path — and under the canonical subtree for Linear (/linear/issues/by-id/<TICKET>.json). Which views exist varies by adapter and resource, so list the alias directory rather than assuming one.

What you may read and write

  • Read the provider trees you've been granted — typically /github/**, plus /linear/** and /notion/** when the run needs the ticket and the team's standards.
  • Write only under /runs/**, and — if you are the publishing agent — the one review path named below.
  • Never write anything under .relay/. It is daemon state.
  • Never write merge.json or close.json under a pull request. Those are live write resources: they merge or close the PR.

A write outside your grant is rejected, not silently dropped, and the path is recorded in .relay/state.json under deniedPaths. If that happens, stop and report it — do not route around it.

Read the pull request

MOUNT="$RELAYFILE_LOCAL_DIR"
R="$MOUNT/github/repos/<owner>/<repo>"

jq '.[] | {number, state, title}' "$R/pulls/_index.json"   # pick the PR
cat "$R/pulls/<pr-dir>/meta.json" | jq '.payload | {number, title, state}'

diff.patch and files/** may exist under the PR directory as artifacts — useful to read, but they are not canonical records. Then pull the context a good review needs, when those providers are mounted:

cat "$MOUNT/linear/issues/by-id/<TICKET>.json" | jq '.payload.title'
grep -rl 'review checklist' "$MOUNT/notion/pages/"

grep searches every synced record, not a paginated API result. Prefer it over guessing which file holds something.

The run protocol

One directory per review run. You are one writer among several, so stay inside your own file:

/runs/<run-id>/
├── recon.md                    # written once by the orchestrator; read-only to you
├── findings/
│   ├── security.json           # one file per specialist — write only your own
│   ├── quality.json
│   └── tests.json
└── review.json                 # the merged review, written by the publishing agent

/runs/** has no adapter behind it, so writes there persist and raise events without queueing any provider call.

  • Read recon.md first. It maps the PR — files touched, what they export, who imports them, the ticket. It was produced once so every specialist doesn't re-derive it. If it's missing, wait rather than duplicating the work.
  • Write exactly one findings file, named for your specialty. One file each makes lost writes impossible.
  • Write it once, when you're done. The orchestrator publishes each findings file the moment it appears and does not wait for the slowest specialist, so a partial file read as final is a real failure mode.

Use this shape:

{
  "specialist": "security",
  "commit": "<sha>",
  "model": "<model-id>",
  "findings": [
    {
      "path": "src/auth/session.ts",
      "line": 42,
      "severity": "high",
      "title": "Session token compared with ==",
      "body": "Timing-unsafe comparison. Use a constant-time compare.",
      "confidence": "high"
    }
  ]
}

Record the commit you actually reviewed. A review attached to the wrong commit is worse than no review.

Publish the review

Only the publishing agent does this. Discover the contract first — discovery documents live in their own tree with literal placeholder segments, not beside the records:

cat "$MOUNT/discovery/github/.adapter.md"
D="$MOUNT/discovery/github/repos/{owner}/{repo}/pulls/{pullNumber}"
cat "$D/reviews/.schema.json"
cat "$D/reviews/.create.example.json"

.adapter.md names every writable resource, its ID pattern, and what a draft becomes. For a review the payload requires event, body, and comments — omitting comments fails validation. event is one of APPROVE, REQUEST_CHANGES, COMMENT. Fields marked readOnly in the schema are rejected.

Write the payload to a non-canonical filename in the resource directory — any name that doesn't match the resource's ID pattern (^\d+$ for reviews) is treated as a create draft:

cat > "$R/pulls/<pull-number>/reviews/draft-<specialist>.json" <<'JSON'
{ "event": "COMMENT", "body": "…", "comments": [] }
JSON

The write path uses the bare pull number, even though reads need <pr-dir>. This asymmetry is real; don't derive one path from the other.

Scratch names partial.json, .tmp.json, *.tmp.json, and *.partial.json are ignored and never become drafts.

Verify the write landed

A queued write is not a delivered write. The adapter rewrites your draft into a receipt naming the real record — read the file back until created appears:

cat "$R/pulls/<pull-number>/reviews/draft-<specialist>.json"
# { "created": 5112118049, "id": "5112118049", "url": "https://github.com/…#pullrequestreview-5112118049" }

That receipt is the proof, not the fact that the write succeeded locally. For writes made through a mirror you can also check the queue:

relayfile writeback status "$RELAYFILE_WORKSPACE" --json

It reports the local mirror's pending, failed, and dead-lettered counts. If an op dead-lettered, read .relay/dead-letter/<opId>.json for lastStatus and lastBody, fix the cause — usually a payload that doesn't match the schema — then relayfile writeback retry --opId <opId>. Do not retry unchanged, repeatedly.

When something looks wrong

SymptomWhat it meansDo this
404 not_found on a recordYou assembled a path instead of reading itTake the exact name from _index.json or a listing
jq returns null on a fieldYou read the envelope, not the recordGo through .payload
429 workspace_busyThe workspace is busyRetry with backoff; it is not an error in your input
A path from this brief doesn't existAdapter layout differs by versionRe-read LAYOUT.md and .adapter.md; use what the workspace says
A write is rejectedOutside your ACL grantStop, report the path, check .relay/state.jsondeniedPaths
Write succeeded, no receipt appearsWriteback queued, failed, or dead-letteredrelayfile writeback status, then the dead-letter record
The tree is empty or missing a providerMount raced the first sync, or the provider isn't connectedReport it; don't review partial data

Rules

  1. Orient from LAYOUT.md and _index.json before touching anything.
  2. Never invent a path, a filename, or a payload shape — discover all three.
  3. Read record fields through .payload.
  4. Write only your own findings file; never edit another agent's.
  5. Never write under .relay/, merge.json, or close.json.
  6. Read recon.md instead of re-deriving the PR map.
  7. Record the commit you reviewed in your output.
  8. Confirm the receipt before reporting success.
  9. Report blockers instead of working around them.