diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index aee2670be6234..11333b492be45 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -2059,8 +2059,14 @@ def _load_hermes_md(cwd_path: Path, context_length: Optional[int] = None) -> str def _load_agents_md(cwd_path: Path, context_length: Optional[int] = None) -> str: - """AGENTS.md — top-level only (no recursive walk).""" - for name in ["AGENTS.md", "agents.md"]: + """AGENTS.override.md / AGENTS.md — top-level only (no recursive walk). + + ``AGENTS.override.md`` wins over ``AGENTS.md`` so a developer can keep a + personal, typically-gitignored override next to the committed project + instructions without editing the tracked file (same convention as + earendil-works/pi#7681). + """ + for name in ["AGENTS.override.md", "AGENTS.md", "agents.md"]: candidate = cwd_path / name if candidate.exists(): try: diff --git a/agent/subdirectory_hints.py b/agent/subdirectory_hints.py index 4e9f7f5ed335e..4fb268bd4ec8e 100644 --- a/agent/subdirectory_hints.py +++ b/agent/subdirectory_hints.py @@ -28,6 +28,7 @@ logger = logging.getLogger(__name__) # Same filenames as prompt_builder.py but we load ALL found (not first-wins) # since different subdirectories may use different conventions. _HINT_FILENAMES = [ + "AGENTS.override.md", "AGENTS.md", "agents.md", "CLAUDE.md", "claude.md", ".cursorrules", diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 28def42c05095..1fcb91be84c01 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -449,6 +449,27 @@ class TestBuildContextFilesPrompt: assert "Ruff for linting" in result assert "Project Context" in result + def test_agents_override_md_wins_over_agents_md(self, tmp_path): + (tmp_path / "AGENTS.md").write_text("Use Ruff for linting.") + (tmp_path / "AGENTS.override.md").write_text("Use Black instead.") + result = build_context_files_prompt(cwd=str(tmp_path)) + assert "Use Black instead" in result + assert "Ruff for linting" not in result + assert "AGENTS.override.md" in result + + def test_agents_override_md_loads_alone(self, tmp_path): + (tmp_path / "AGENTS.override.md").write_text("Override-only context.") + result = build_context_files_prompt(cwd=str(tmp_path)) + assert "Override-only context" in result + assert "Project Context" in result + + def test_hermes_md_still_wins_over_agents_override(self, tmp_path): + (tmp_path / ".hermes.md").write_text("Hermes-first context.") + (tmp_path / "AGENTS.override.md").write_text("Override context.") + result = build_context_files_prompt(cwd=str(tmp_path)) + assert "Hermes-first context" in result + assert "Override context" not in result + def test_skips_agents_md_in_install_tree_on_fallback(self, monkeypatch, tmp_path): # A backend that FALLS BACK into the install tree (cwd=None → getcwd, # the desktop default) must not load that tree's contributor AGENTS.md diff --git a/tests/agent/test_subdirectory_hints.py b/tests/agent/test_subdirectory_hints.py index 01de13d4eada8..85b89f647ef5a 100644 --- a/tests/agent/test_subdirectory_hints.py +++ b/tests/agent/test_subdirectory_hints.py @@ -284,3 +284,16 @@ class TestExcludedDirectories: tracker = SubdirectoryHintTracker(working_dir=str(tmp_path)) result = tracker.check_tool_call("read_file", {"path": str(normal / "f.py")}) assert result is not None and "Backend rules" in result + + def test_agents_override_md_wins_in_subdirectory(self, tmp_path): + """AGENTS.override.md takes priority over AGENTS.md per directory.""" + sub = tmp_path / "backend" + sub.mkdir() + (sub / "AGENTS.md").write_text("Committed backend rules") + (sub / "AGENTS.override.md").write_text("Personal backend override") + + tracker = SubdirectoryHintTracker(working_dir=str(tmp_path)) + result = tracker.check_tool_call("read_file", {"path": str(sub / "f.py")}) + assert result is not None + assert "Personal backend override" in result + assert "Committed backend rules" not in result diff --git a/website/docs/user-guide/features/context-files.md b/website/docs/user-guide/features/context-files.md index c3e3f61edfbce..bd57a7ef6a4ca 100644 --- a/website/docs/user-guide/features/context-files.md +++ b/website/docs/user-guide/features/context-files.md @@ -13,6 +13,7 @@ Hermes Agent automatically discovers and loads context files that shape how it b | File | Purpose | Discovery | |------|---------|-----------| | **.hermes.md** / **HERMES.md** | Project instructions (highest priority) | Walks to git root | +| **AGENTS.override.md** | Personal, per-directory override of AGENTS.md (typically gitignored) | CWD at startup + subdirectories progressively | | **AGENTS.md** | Project instructions, conventions, architecture | CWD at startup + subdirectories progressively | | **CLAUDE.md** | Claude Code context files (also detected) | CWD at startup + subdirectories progressively | | **SOUL.md** | Global personality and tone customization for this Hermes instance | `HERMES_HOME/SOUL.md` only | @@ -20,7 +21,9 @@ Hermes Agent automatically discovers and loads context files that shape how it b | **.cursor/rules/*.mdc** | Cursor IDE rule modules | CWD only | :::info Priority system -Only **one** project context type is loaded per session (first match wins): `.hermes.md` → `AGENTS.md` → `CLAUDE.md` → `.cursorrules`. **SOUL.md** is always loaded independently as the agent identity (slot #1). +Only **one** project context type is loaded per session (first match wins): `.hermes.md` → `AGENTS.override.md` → `AGENTS.md` → `CLAUDE.md` → `.cursorrules`. **SOUL.md** is always loaded independently as the agent identity (slot #1). + +If an `AGENTS.override.md` exists next to an `AGENTS.md`, the override is loaded **instead of** the committed file — keep a personal (usually gitignored) `AGENTS.override.md` when you want different instructions than the ones checked into the repo, without editing the tracked `AGENTS.md`. ::: ## AGENTS.md