diff --git a/agent/system_prompt.py b/agent/system_prompt.py index 4a61a2ee0..559b66f48 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -24,7 +24,6 @@ Pure helpers that read the agent's state. AIAgent keeps thin forwarders. from __future__ import annotations import json -import os from typing import Any, Dict, List, Optional from agent.prompt_builder import ( @@ -41,6 +40,7 @@ from agent.prompt_builder import ( TOOL_USE_ENFORCEMENT_GUIDANCE, TOOL_USE_ENFORCEMENT_MODELS, ) +from agent.runtime_cwd import resolve_context_cwd def _ra(): @@ -288,13 +288,12 @@ def build_system_prompt_parts(agent: Any, system_message: Optional[str] = None) context_parts.append(system_message) if not agent.skip_context_files: - # Use TERMINAL_CWD for context file discovery when set (gateway - # mode). The gateway process runs from the hermes-agent install - # dir, so os.getcwd() would pick up the repo's AGENTS.md and - # other dev files — inflating token usage by ~10k for no benefit. - _context_cwd = os.getenv("TERMINAL_CWD") or None + # Prefer the configured TERMINAL_CWD (gateway mode). When unset (local + # CLI), None lets build_context_files_prompt fall back to the launch + # dir — the user's real cwd there, but the install dir for the gateway + # daemon, which is why the gateway sets TERMINAL_CWD. context_files_prompt = _r.build_context_files_prompt( - cwd=_context_cwd, skip_soul=_soul_loaded) + cwd=resolve_context_cwd(), skip_soul=_soul_loaded) if context_files_prompt: context_parts.append(context_files_prompt) diff --git a/tests/agent/test_system_prompt.py b/tests/agent/test_system_prompt.py new file mode 100644 index 000000000..75bf28b54 --- /dev/null +++ b/tests/agent/test_system_prompt.py @@ -0,0 +1,57 @@ +"""Tests for agent/system_prompt.py — context-file cwd wiring.""" + +from types import SimpleNamespace +from unittest.mock import patch + +from agent.system_prompt import build_system_prompt_parts + + +def _make_agent(**overrides): + base = dict( + load_soul_identity=False, + skip_context_files=False, + valid_tool_names=[], + _task_completion_guidance=False, + _tool_use_enforcement=False, + _environment_probe=False, + _kanban_worker_guidance="", + _memory_store=None, + _memory_manager=None, + model="", + provider="", + platform="", + pass_session_id=False, + session_id="", + ) + base.update(overrides) + return SimpleNamespace(**base) + + +def _captured_context_cwd(agent): + """The cwd build_system_prompt_parts hands to build_context_files_prompt.""" + captured = {} + + def fake_context_files(cwd=None, skip_soul=False): + captured["cwd"] = cwd + return "" + + with ( + patch("run_agent.load_soul_md", return_value=""), + patch("run_agent.build_nous_subscription_prompt", return_value=""), + patch("run_agent.build_environment_hints", return_value=""), + patch("run_agent.build_context_files_prompt", side_effect=fake_context_files), + ): + build_system_prompt_parts(agent) + return captured["cwd"] + + +class TestContextFileCwd: + def test_none_when_terminal_cwd_unset(self, monkeypatch): + # Unset → None, so discovery falls back to the launch dir inside + # build_context_files_prompt (the local-CLI #19242 contract). + monkeypatch.delenv("TERMINAL_CWD", raising=False) + assert _captured_context_cwd(_make_agent()) is None + + def test_configured_dir_when_terminal_cwd_set(self, monkeypatch, tmp_path): + monkeypatch.setenv("TERMINAL_CWD", str(tmp_path)) + assert _captured_context_cwd(_make_agent()) == tmp_path