The comment contract
Every line the comment can emit, verbatim. Parse against this.
Agentic reviewers
CodeRabbit, Copilot, Bugbot, Qodo, Greptile, Devin Review. One record, one gate, one shared paragraph.
Coding agents and the MCP
Devin, Claude Code, Cursor, Codex. One hosted URL, no auth.
The comment contract
One sticky comment per pull request, updated in place, pinned to the head commit it records. These are the bytes of a comparison comment at contract v6.10.0 — the renderer’s own locked render, one job changed and four unchanged. Parse against them. The same comment rendered: Rendered from the renderer’s v6.10.0 golden fixture (private testbed). Links inside the fixture are illustrative; the live comment is on garnet-runtime-review-reference#29.Historical example
The logged-out comparison example, recorded with an earlier released action and sensor (contract v6.10.0), is:- Pull request: garnet-runtime-review-reference#29
- Head:
23bbd8859904af464185d0bb4d1f93ff75ef8864 - App comment: 5416650121
- Execution Profile: public report
- JSON: public API response
- Run: 32909555254
23bbd88 with previously recorded 5a6561f — one job, unchanged. It shows the unchanged shape, while the fixture above shows the changed shape.
Every line the comment emits
Markers. Match the comment authorgarnet-runtime-review[bot] and the first marker. The second is namespaced by the posting frontend and carries the Execution Profile’s state. The third carries the full SHA the Execution Profile covers. The fourth is the machine register — the counts and the contract version, as JSON:
garnet-control-plane-pending-pr-comment:v1:app.garnet.ai.
Read counts from garnet:summary, not from the prose. It is the only place chain counts and runner-background movement are carried, and contract tells you which version of this page you are parsing.
Headline. Bold body text, never a heading:
Both shapes above are the v6.13 contract, which ships with a coming App update. It is not deployed yet: action v2.3.0 against the production control plane renders v6.10. Today’s live pending headline is v6.10:v6.10 has no terminal state. Match the pending state on the marker and on
recording in the headline, not on the full sentence.· segment, each count noun pluralized against its own number. Zero-delta comparisons state the verdict and name what they compared against:
+A −R counts destinations attributed to the workload; a zero side drops.
Job fold row. One row per job, always. On a changed job the delta leads, so the left edge is the scan column; an unchanged job carries its destination count and the word unchanged:
unchanged sits directly after the destination count, so it claims the destination projection only — ancestry, steps, and other recorded context may differ. A job row counts destinations, not chains; read chains from garnet:summary.
Changed job. The tree renders as a diff fence instead of a <pre> block — + only in the current record, − only in the previous one, unmarked in both:
+/− mark, the root line appends the boundary label — the count sits beside the marks it counts, and nothing is hidden:
backgroundAdded / backgroundRemoved in garnet:summary.
Observed action. A shaped terminal in the leaf position: ○ is a network destination — the first recorded name defanged at the final dot, or a bare IP when no name was recorded. □ (file) and ▷ (execution) are reserved and never render today. Read observation kinds from garnet:summary’s kinds, never from the shapes. PIDs stay in the Actions job summary; ports and protocol stay in the Execution Profile and the JSON:
(…) annotation is recorded or deterministically derived, and never a verdict: (step: "<name>"), (ran from /tmp/…), (dns resolver) — loopback on port 53, (cloud metadata) — 169.254.169.254, (github infra) — a name under .githubapp.com or .actions.githubusercontent.com, (github infra · rotated from <previous identity>) — a provable address rotation inside GitHub’s published ranges, counted in neither +A nor −R, and (garnet sensor) — Garnet’s own upload.
Attribution. Structural, never by name: a root whose edges descend through Runner.Worker or bear a recorded step is the workload; the rest is runner background, labelled at the boundary when it moved. Typography carries no meaning of its own — <strong> marks the process that acted, <em> marks bracket context.
No egress, and truncation. Both are stated, never silent:
profile parameter; ignore the campaign parameters after it:
(run, profile) pair and nothing else. A bare run URL, an unpublished or expired profile, and a profile that belongs to another run all return the same 404 — indistinguishable by design, so a 404 tells you nothing about whether a profile exists. The page’s canonical and og:url are the selector URL, its JSON alternate sits on the same host, and the page’s own copy-link button hands back the same URL without the utm_ parameters. The job summary’s permalink carries utm_medium=step_summary instead of pr_comment; the pair is what resolves, the campaign parameters never are.
Display rules that change what a name looks like
Display rules that change what a name looks like
Three transforms apply to display only — the Execution Profile, the Actions job summary, and the JSON keep the recorded value:
- Hostnames are defanged at the final dot:
registry.npmjs.orgrenders asregistry.npmjs[.]org. - Step names lose the runner’s ordinal prefix and any unexpanded workflow expression:
4. Run workload ${{ matrix.name }}renders asRun workload. The comment never shows an ordinal. - Process names lose a trailing run of four or more digits:
provjobd1326539233renders asprovjobd.
Agentic reviewers
Garnet decides, reviewers read. Each layer below has one job, and a reviewer integration uses all of them. Timing, staleness and fail-closed belong to the gate, never to instruction wording.
The gate and the push run as workflows in your repository. On the pnpm fork, the mirror writes the description block for each head-bound comment. The gate is paused and runs by hand only: it cannot pass until a record declares its capture complete, and no comment does that yet.
The shared paragraph
Paste it unchanged into each reviewer’s instruction file:Where each reviewer reads it
Vendor docs checked 2026-10-08. Recheck them before you rely on a plan or setting name.How to tell it worked
Delivered (gate green, description block present) < pulled (CodeRabbit lists the tool under Additional context used) < consumed (the review repeats a fact bound to the current head, or correctly says there is no Execution Profile for this head) < judged (the review ties a recorded change to the diff). Only consumed and judged count. So far, head-bound citations have come from Devin Review (pnpm#102) and Greptile (posthog#107), both reading the pull request. None has come through an MCP call yet. Two demo pull requests carry the same manifest-only diff and different recorded egress. Use them to check that a reviewer reads the Execution Profile rather than the diff: registry-only egress and an install script reaching named hosts.Coding agents and the MCP
The Runtime Review MCP gives coding agents, MCP-capable review bots and custom harnesses the same Execution Profile as structured data. It takes the pull request’s current head, finds the Runtime Review comment pinned to it, and separates workload changes from runner background and GitHub platform changes. Nothing is cached, so every answer is about the head that exists when you ask.- Endpoint:
https://garnet-runtime-review-mcp.vercel.app/mcp - Authentication: none. Public repositories only.
Connect
- Claude Code
- Cursor
- Devin
- Copilot code review
- CodeRabbit
- Any client
changed / workload-change, with registry.npmjs.org removed under Garnet Bootstrap Routing / bootstrap-routing.
Read the result
Agent instructions
Paste into the agent’s skills, rules orAGENTS.md:
Limits
- Zero changed jobs read as
undeterminabletoday. The comment does not yet declare a complete capture, and a missed connection looks exactly like no change. A recorded change stands either way, sono-changedoes not appear yet. - Public repositories only. The hosted endpoint refuses private repositories.
- Shared rate limit. Hosted calls share GitHub’s anonymous API limit.
GitHub returned 403 ... (rate limited)means retry later. - The comment is the index. The MCP finds a pull request’s Execution Profiles through the Runtime Review comment. No comment, no evidence.
Custom harnesses and gates
For a reproducible decision instead of a judgment, parse the comment directly. StampHog, the reference repository’s vendored PostHog merge-gate agent, does exactly that in the policy example: every static gate green, held solely on recorded egress the diff never mentions. Without a parser, callget_runtime_review and gate on assessment: start with workload-change only, because undeterminable covers every zero-change record today.
The harness on a pull request
The harness consuming the Execution Profile end to end, posting its own decision next to Garnet’s. The same harness on a replayed upstream change.
If your harness already locks its own output shape and gates, keep it. Carry the primitive into it instead: (1) fail closed on the four evidence states above, (2) gate on the comment’s own
+A −R delta against the previous profiled commit, (3) make every finding cite its recorded chain and destination.Structured data — the JSON twin (preview)
Structured data — the JSON twin (preview)
Every fold’s permalink has a JSON twin at the same exact selector. The response carries its own The selector is required. Wrong or missing
schema_version (currently runtime-review-public/v3); read it before parsing, and treat the shape as preview alongside the comment contract above:?profile=, or a bare /public/runs/<run-id> URL, returns a 404 by design. There is never a silent fallback. See The public run report.