Port from earendil-works/pi#7681: support AGENTS.override.md context override

AGENTS.override.md now takes priority over AGENTS.md in both startup
project-context loading (prompt_builder) and progressive subdirectory
hint discovery (subdirectory_hints). Lets developers keep a personal,
typically-gitignored override next to committed project instructions
without editing the tracked file.
This commit is contained in:
Teknium
2026-08-06 17:52:54 -07:00
parent 08d9828503
commit a8d5e16ccf
5 changed files with 51 additions and 8 deletions

View File

@@ -2274,18 +2274,21 @@ def _load_agents_md(cwd_path: Path, context_length: Optional[int] = None) -> str
"""AGENTS.md — merged directory chain from git root down to cwd.
Each directory on the chain (see ``_agents_md_directory_chain``)
contributes its ``AGENTS.md`` / ``agents.md`` (first name wins per
directory) as its own provenance-labelled section. Identical content
encountered again further down the chain (copied or symlinked files) is
deduplicated. With a single match — the common case, and always the
case outside a git repo — output is identical to the historical
single-file behavior.
contributes its ``AGENTS.override.md`` / ``AGENTS.md`` / ``agents.md``
(first name wins per directory) as its own provenance-labelled section.
``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). Identical content encountered again further
down the chain (copied or symlinked files) is deduplicated. With a
single match — the common case, and always the case outside a git repo —
output is identical to the historical single-file behavior.
"""
cwd_resolved = cwd_path.resolve()
sections: List[str] = []
seen_content: set = set()
for directory in _agents_md_directory_chain(cwd_resolved):
for name in ["AGENTS.md", "agents.md"]:
for name in ["AGENTS.override.md", "AGENTS.md", "agents.md"]:
candidate = directory / name
if not candidate.exists():
continue

View File

@@ -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",

View File

@@ -515,6 +515,29 @@ class TestBuildContextFilesPrompt:
assert _load_agents_md(sub) == ""
# --- AGENTS.override.md personal override (port of pi#7681) ---
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

View File

@@ -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

View File

@@ -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