Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,14 +565,15 @@ def _load_cursorrules(cwd_path: Path) -> str:
def build_context_files_prompt(cwd: Optional[str] = None, skip_soul: bool = False) -> str:
"""Discover and load context files for the system prompt.

Priority (first found wins — only ONE project context type is loaded):
All present context files are composed (each capped independently at
20,000 chars):
1. .hermes.md / HERMES.md (walk to git root)
2. AGENTS.md / agents.md (recursive directory walk)
3. CLAUDE.md / claude.md (cwd only)
4. .cursorrules / .cursor/rules/*.mdc (cwd only)
4. .cursorrules / .cursor/rules/*.mdc (fallback — only if none of the
above are present)

SOUL.md from HERMES_HOME is independent and always included when present.
Each context source is capped at 20,000 chars.

When *skip_soul* is True, SOUL.md is not included here (it was already
loaded via ``load_soul_md()`` for the identity slot).
Expand All @@ -583,15 +584,23 @@ def build_context_files_prompt(cwd: Optional[str] = None, skip_soul: bool = Fals
cwd_path = Path(cwd).resolve()
sections = []

# Priority-based project context: first match wins
project_context = (
_load_hermes_md(cwd_path)
or _load_agents_md(cwd_path)
or _load_claude_md(cwd_path)
or _load_cursorrules(cwd_path)
)
if project_context:
sections.append(project_context)
# Compose all present context files; each loader caps at 20k chars
project_parts = [
p for p in [
_load_hermes_md(cwd_path),
_load_agents_md(cwd_path),
_load_claude_md(cwd_path),
]
if p
]

# .cursorrules is a fallback — only loaded when no other context is present
if not project_parts:
cursorrules = _load_cursorrules(cwd_path)
if cursorrules:
project_parts.append(cursorrules)

sections.extend(project_parts)

# SOUL.md from HERMES_HOME only — skip when already loaded as identity
if not skip_soul:
Expand Down
31 changes: 16 additions & 15 deletions tests/agent/test_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,20 +526,21 @@ def test_hermes_md_blocks_injection(self, tmp_path):
result = build_context_files_prompt(cwd=str(tmp_path))
assert "BLOCKED" in result

def test_hermes_md_beats_agents_md(self, tmp_path):
"""When both exist, .hermes.md wins and AGENTS.md is not loaded."""
def test_hermes_md_and_agents_md_both_loaded(self, tmp_path):
"""When both exist, both are composed into the context."""
(tmp_path / "AGENTS.md").write_text("Agent guidelines here.")
(tmp_path / ".hermes.md").write_text("Hermes project rules.")
result = build_context_files_prompt(cwd=str(tmp_path))
assert "Hermes project rules" in result
assert "Agent guidelines" not in result
assert "Agent guidelines" in result

def test_agents_md_beats_claude_md(self, tmp_path):
def test_agents_md_and_claude_md_both_loaded(self, tmp_path):
"""When both exist, both are composed into the context."""
(tmp_path / "AGENTS.md").write_text("Agent guidelines here.")
(tmp_path / "CLAUDE.md").write_text("Claude guidelines here.")
result = build_context_files_prompt(cwd=str(tmp_path))
assert "Agent guidelines" in result
assert "Claude guidelines" not in result
assert "Claude guidelines" in result

def test_claude_md_beats_cursorrules(self, tmp_path):
(tmp_path / "CLAUDE.md").write_text("Claude guidelines here.")
Expand Down Expand Up @@ -572,17 +573,17 @@ def test_claude_md_blocks_injection(self, tmp_path):
result = build_context_files_prompt(cwd=str(tmp_path))
assert "BLOCKED" in result

def test_hermes_md_beats_all_others(self, tmp_path):
"""When all four types exist, only .hermes.md is loaded."""
(tmp_path / ".hermes.md").write_text("Hermes wins.")
(tmp_path / "AGENTS.md").write_text("Agents lose.")
(tmp_path / "CLAUDE.md").write_text("Claude loses.")
(tmp_path / ".cursorrules").write_text("Cursor loses.")
def test_all_context_files_composed_cursorrules_excluded(self, tmp_path):
"""When all four types exist, hermes/agents/claude are composed; cursorrules is excluded."""
(tmp_path / ".hermes.md").write_text("Hermes rules.")
(tmp_path / "AGENTS.md").write_text("Agents rules.")
(tmp_path / "CLAUDE.md").write_text("Claude rules.")
(tmp_path / ".cursorrules").write_text("Cursor rules.")
result = build_context_files_prompt(cwd=str(tmp_path))
assert "Hermes wins" in result
assert "Agents lose" not in result
assert "Claude loses" not in result
assert "Cursor loses" not in result
assert "Hermes rules" in result
assert "Agents rules" in result
assert "Claude rules" in result
assert "Cursor rules" not in result

def test_cursorrules_loads_when_only_option(self, tmp_path):
"""Cursorrules still loads when no higher-priority files exist."""
Expand Down
Loading