open-swe/agent/utils/agents_md.py
Johannes du Plessis ccea80b887
feat: inline AGENTS.md into reviewer system prompt (#1328)
* feat(reviewer): inline AGENTS.md into reviewer system prompt

Fetches AGENTS.md from the PR's head_sha via the GitHub contents API
during reviewer setup and inlines it as a "Repository conventions"
block in the system prompt. Mirrors how the per-repo review style
prompt is already wired.

The main agent has long had a mandatory step to read AGENTS.md after
cloning, but the reviewer often skips cloning entirely (it can
`gh pr diff` directly), so it never saw the file. Loading it
deterministically means the reviewer judges findings against the
project's own conventions instead of relying on the model to fetch
the file itself.

* fix(reviewer): fetch AGENTS.md from base_sha, not head_sha

The reviewer inlines AGENTS.md into its system prompt. Reading from
head_sha means a PR author can edit AGENTS.md in the same PR being
reviewed and smuggle instructions like "ignore all bugs" / "publish
no findings" into the reviewer's prompt. Switch to base_sha (the
target branch's pre-PR state, which is trusted) and update the prompt
text to reflect that the contents come from the base, not the head.

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-22 14:58:27 -07:00

73 lines
2.1 KiB
Python

"""Fetch ``AGENTS.md`` from a GitHub repo so it can be inlined into prompts.
Used by the reviewer to deterministically load repo conventions into context
without the model having to clone the repo and read the file itself.
"""
from __future__ import annotations
import logging
import httpx
logger = logging.getLogger(__name__)
# Cap the inlined content. AGENTS.md is meant to be a short conventions doc;
# anything larger is probably accidental and would bloat every reviewer prompt.
_MAX_AGENTS_MD_BYTES = 64 * 1024
async def fetch_agents_md(
owner: str,
repo: str,
ref: str,
*,
token: str | None,
timeout: float = 10.0,
) -> str | None:
"""Fetch ``AGENTS.md`` at ``ref`` from ``owner/repo``.
Returns the raw file contents, or ``None`` if the file is missing, the
request fails, or the file exceeds the size cap.
"""
if not owner or not repo or not ref:
return None
url = f"https://api.github.com/repos/{owner}/{repo}/contents/AGENTS.md"
headers = {
"Accept": "application/vnd.github.raw",
"X-GitHub-Api-Version": "2022-11-28",
}
if token:
headers["Authorization"] = f"Bearer {token}"
try:
async with httpx.AsyncClient(timeout=timeout) as client:
response = await client.get(url, headers=headers, params={"ref": ref})
except httpx.HTTPError:
logger.exception("Failed to fetch AGENTS.md from %s/%s@%s", owner, repo, ref)
return None
if response.status_code == 404:
return None
if response.status_code != 200:
logger.warning(
"Unexpected status %s fetching AGENTS.md from %s/%s@%s",
response.status_code,
owner,
repo,
ref,
)
return None
content = response.text
if len(content.encode("utf-8")) > _MAX_AGENTS_MD_BYTES:
logger.info(
"AGENTS.md in %s/%s@%s exceeds %d bytes; skipping inline",
owner,
repo,
ref,
_MAX_AGENTS_MD_BYTES,
)
return None
return content