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.mdBefore 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
| Variable | What it is |
|---|---|
RELAYFILE_LOCAL_DIR | The mount root, when you have a mirror. Every path below is relative to it. |
RELAYFILE_WORKSPACE | The rw_… workspace id. Use it verbatim; never substitute another id. |
RELAYFILE_TOKEN | Your scoped workspace token, already narrowed to the paths you may touch. |
RELAYFILE_BASE_URL | The 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 contractThe 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:
- GitHub record directories are
<number>__<slug>, number first. Linear uses<name>__<uuid>, id last. There is no single rule — read the provider'sLAYOUT.md. - Reads need the full directory name.
pulls/<pull-number>/meta.jsonreturns 404; onlypulls/<pr-dir>/meta.jsonresolves. Take<pr-dir>from the index or a listing; never assemble it. - Records are envelopes. A record is
{ provider, objectType, objectId, deleted, payload }. The provider's object is underpayload—jq .payload.state, notjq .state. - 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.jsonorclose.jsonunder 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.mdfirst. 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": [] }
JSONThe 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" --jsonIt 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
| Symptom | What it means | Do this |
|---|---|---|
404 not_found on a record | You assembled a path instead of reading it | Take the exact name from _index.json or a listing |
jq returns null on a field | You read the envelope, not the record | Go through .payload |
429 workspace_busy | The workspace is busy | Retry with backoff; it is not an error in your input |
| A path from this brief doesn't exist | Adapter layout differs by version | Re-read LAYOUT.md and .adapter.md; use what the workspace says |
| A write is rejected | Outside your ACL grant | Stop, report the path, check .relay/state.json → deniedPaths |
| Write succeeded, no receipt appears | Writeback queued, failed, or dead-lettered | relayfile writeback status, then the dead-letter record |
| The tree is empty or missing a provider | Mount raced the first sync, or the provider isn't connected | Report it; don't review partial data |
Rules
- Orient from
LAYOUT.mdand_index.jsonbefore touching anything. - Never invent a path, a filename, or a payload shape — discover all three.
- Read record fields through
.payload. - Write only your own findings file; never edit another agent's.
- Never write under
.relay/,merge.json, orclose.json. - Read
recon.mdinstead of re-deriving the PR map. - Record the commit you reviewed in your output.
- Confirm the receipt before reporting success.
- Report blockers instead of working around them.