From 60c6b07128744ebbd8ad8c2f24f40081811bef43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=98=8A?= Date: Tue, 28 Apr 2026 17:51:57 +0800 Subject: [PATCH] fix(cron): keep SOUL.md identity when workdir is unset --- cron/scheduler.py | 8 +++++--- run_agent.py | 11 +++++++++-- tests/cron/test_cron_workdir.py | 4 ++++ tests/run_agent/test_run_agent.py | 20 ++++++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cron/scheduler.py b/cron/scheduler.py index d41c7ed86..685b8ae56 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -1033,10 +1033,12 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]: enabled_toolsets=_resolve_cron_enabled_toolsets(job, _cfg), disabled_toolsets=["cronjob", "messaging", "clarify"], quiet_mode=True, - # When a workdir is configured, inject AGENTS.md / CLAUDE.md / - # .cursorrules from that directory; otherwise preserve the old - # behaviour (don't inject SOUL.md/AGENTS.md from the scheduler cwd). + # Cron jobs should always inherit the user's SOUL.md identity from + # HERMES_HOME. When a workdir is configured, also inject project + # context files (AGENTS.md / CLAUDE.md / .cursorrules) from there. + # Without a workdir, keep cwd context discovery disabled. skip_context_files=not bool(_job_workdir), + load_soul_identity=True, skip_memory=True, # Cron system prompts would corrupt user representations platform="cron", session_id=_cron_session_id, diff --git a/run_agent.py b/run_agent.py index 895e68644..dc6c0e13d 100644 --- a/run_agent.py +++ b/run_agent.py @@ -926,6 +926,7 @@ class AIAgent: thread_id: str = None, gateway_session_key: str = None, skip_context_files: bool = False, + load_soul_identity: bool = False, skip_memory: bool = False, session_db=None, parent_session_id: str = None, @@ -977,6 +978,9 @@ class AIAgent: skip_context_files (bool): If True, skip auto-injection of SOUL.md, AGENTS.md, and .cursorrules into the system prompt. Use this for batch processing and data generation to avoid polluting trajectories with user-specific persona or project instructions. + load_soul_identity (bool): If True, still use ~/.hermes/SOUL.md as the primary + identity even when skip_context_files=True. Project context files from the cwd + remain skipped. """ _install_safe_stdio() @@ -1005,6 +1009,7 @@ class AIAgent: self._print_fn = None self.background_review_callback = None # Optional sync callback for gateway delivery self.skip_context_files = skip_context_files + self.load_soul_identity = load_soul_identity self.pass_session_id = pass_session_id self._credential_pool = credential_pool self.log_prefix_chars = log_prefix_chars @@ -4742,9 +4747,11 @@ class AIAgent: # 6. Current date & time (frozen at build time) # 7. Platform-specific formatting hint - # Try SOUL.md as primary identity (unless context files are skipped) + # Try SOUL.md as primary identity unless the caller explicitly skipped it. + # Some execution modes (cron) still want HERMES_HOME persona while keeping + # cwd project instructions disabled. _soul_loaded = False - if not self.skip_context_files: + if self.load_soul_identity or not self.skip_context_files: _soul_content = load_soul_md() if _soul_content: prompt_parts = [_soul_content] diff --git a/tests/cron/test_cron_workdir.py b/tests/cron/test_cron_workdir.py index 03777dd47..5f317c4f4 100644 --- a/tests/cron/test_cron_workdir.py +++ b/tests/cron/test_cron_workdir.py @@ -265,6 +265,7 @@ class TestRunJobTerminalCwd: class FakeAgent: def __init__(self, **kwargs): observed["skip_context_files"] = kwargs.get("skip_context_files") + observed["load_soul_identity"] = kwargs.get("load_soul_identity") observed["terminal_cwd_during_init"] = os.environ.get( "TERMINAL_CWD", "_UNSET_" ) @@ -335,6 +336,7 @@ class TestRunJobTerminalCwd: # AIAgent was built with skip_context_files=False (feature ON). assert observed["skip_context_files"] is False + assert observed["load_soul_identity"] is True # TERMINAL_CWD was pointing at the job workdir while the agent ran. assert observed["terminal_cwd_during_init"] == str(tmp_path.resolve()) assert observed["terminal_cwd_during_run"] == str(tmp_path.resolve()) @@ -373,6 +375,8 @@ class TestRunJobTerminalCwd: # Feature is OFF — skip_context_files stays True. assert observed["skip_context_files"] is True + # Cron still forces SOUL.md identity even when cwd context files stay off. + assert observed["load_soul_identity"] is True # TERMINAL_CWD saw the same value during init as it had before. assert observed["terminal_cwd_during_init"] == before # And after run_job completes, it's still the sentinel (nothing diff --git a/tests/run_agent/test_run_agent.py b/tests/run_agent/test_run_agent.py index 2c13a1569..5585eea48 100644 --- a/tests/run_agent/test_run_agent.py +++ b/tests/run_agent/test_run_agent.py @@ -862,6 +862,26 @@ class TestBuildSystemPrompt: prompt = agent._build_system_prompt() assert DEFAULT_AGENT_IDENTITY in prompt + def test_can_use_soul_identity_even_when_context_files_are_skipped(self): + with ( + patch("run_agent.get_tool_definitions", return_value=_make_tool_defs("terminal")), + patch("run_agent.check_toolset_requirements", return_value={}), + patch("run_agent.OpenAI"), + patch("run_agent.load_soul_md", return_value="SOUL IDENTITY"), + ): + agent = AIAgent( + api_key="test-k...7890", + base_url="https://openrouter.ai/api/v1", + quiet_mode=True, + skip_context_files=True, + load_soul_identity=True, + skip_memory=True, + ) + prompt = agent._build_system_prompt() + + assert "SOUL IDENTITY" in prompt + assert DEFAULT_AGENT_IDENTITY not in prompt + def test_includes_system_message(self, agent): prompt = agent._build_system_prompt(system_message="Custom instruction") assert "Custom instruction" in prompt