diff --git a/agent/skill_commands.py b/agent/skill_commands.py index 8afdfa93b..67315ee8d 100644 --- a/agent/skill_commands.py +++ b/agent/skill_commands.py @@ -7,7 +7,6 @@ can invoke skills via /skill-name commands and prompt-only built-ins like import json import logging -import os import re from datetime import datetime from pathlib import Path @@ -24,15 +23,20 @@ def build_plan_path( *, now: datetime | None = None, ) -> Path: - """Return the default markdown path for a /plan invocation.""" - hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes")) + """Return the default workspace-relative markdown path for a /plan invocation. + + Relative paths are intentional: file tools are task/backend-aware and resolve + them against the active working directory for local, docker, ssh, modal, + daytona, and similar terminal backends. That keeps the plan with the active + workspace instead of the Hermes host's global home directory. + """ slug_source = (user_instruction or "").strip().splitlines()[0] if user_instruction else "" slug = _PLAN_SLUG_RE.sub("-", slug_source.lower()).strip("-") if slug: slug = "-".join(part for part in slug.split("-")[:8] if part)[:48].strip("-") slug = slug or "conversation-plan" timestamp = (now or datetime.now()).strftime("%Y-%m-%d_%H%M%S") - return hermes_home / "plans" / f"{timestamp}-{slug}.md" + return Path(".hermes") / "plans" / f"{timestamp}-{slug}.md" def _load_skill_payload(skill_identifier: str, task_id: str | None = None) -> tuple[dict[str, Any], Path | None, str] | None: diff --git a/cli.py b/cli.py index 654dfb25f..70a202d3a 100755 --- a/cli.py +++ b/cli.py @@ -3318,7 +3318,8 @@ class HermesCLI: user_instruction, task_id=self.session_id, runtime_note=( - f"Save the markdown plan with write_file to this exact path: {plan_path}" + "Save the markdown plan with write_file to this exact relative path " + f"inside the active workspace/backend cwd: {plan_path}" ), ) diff --git a/gateway/run.py b/gateway/run.py index c8c5831e2..67e93d2c5 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -1161,7 +1161,8 @@ class GatewayRunner: user_instruction, task_id=_quick_key, runtime_note=( - f"Save the markdown plan with write_file to this exact path: {plan_path}" + "Save the markdown plan with write_file to this exact relative path " + f"inside the active workspace/backend cwd: {plan_path}" ), ) if not event.text: diff --git a/skills/software-development/plan/SKILL.md b/skills/software-development/plan/SKILL.md index 92f39e8ca..daf6bf792 100644 --- a/skills/software-development/plan/SKILL.md +++ b/skills/software-development/plan/SKILL.md @@ -1,6 +1,6 @@ --- name: plan -description: Plan mode for Hermes — inspect context, write a markdown plan, save it under $HERMES_HOME/plans, and do not execute the work. +description: Plan mode for Hermes — inspect context, write a markdown plan into the active workspace's `.hermes/plans/` directory, and do not execute the work. version: 1.0.0 author: Hermes Agent license: MIT @@ -22,7 +22,7 @@ For this turn, you are planning only. - Do not edit project files except the plan markdown file. - Do not run mutating terminal commands, commit, push, or perform external actions. - You may inspect the repo or other context with read-only commands/tools when needed. -- Your deliverable is a markdown plan saved to `$HERMES_HOME/plans`. +- Your deliverable is a markdown plan saved inside the active workspace under `.hermes/plans/`. ## Output requirements @@ -42,10 +42,12 @@ If the task is code-related, include exact file paths, likely test targets, and ## Save location Save the plan with `write_file` under: -- `$HERMES_HOME/plans/YYYY-MM-DD_HHMMSS-.md` +- `.hermes/plans/YYYY-MM-DD_HHMMSS-.md` + +Treat that as relative to the active working directory / backend workspace. Hermes file tools are backend-aware, so using this relative path keeps the plan with the workspace on local, docker, ssh, modal, and daytona backends. If the runtime provides a specific target path, use that exact path. -If not, create a sensible timestamped filename yourself. +If not, create a sensible timestamped filename yourself under `.hermes/plans/`. ## Interaction style diff --git a/tests/agent/test_skill_commands.py b/tests/agent/test_skill_commands.py index 8daa7b36b..c02446138 100644 --- a/tests/agent/test_skill_commands.py +++ b/tests/agent/test_skill_commands.py @@ -2,6 +2,7 @@ import os from datetime import datetime +from pathlib import Path from unittest.mock import patch import tools.skills_tool as skills_tool_module @@ -277,32 +278,34 @@ Generate some audio. class TestPlanSkillHelpers: - def test_build_plan_path_uses_hermes_home_and_slugifies_request(self, tmp_path, monkeypatch): - monkeypatch.setenv("HERMES_HOME", str(tmp_path)) - + def test_build_plan_path_uses_workspace_relative_dir_and_slugifies_request(self): path = build_plan_path( "Implement OAuth login + refresh tokens!", now=datetime(2026, 3, 15, 9, 30, 45), ) - assert path == tmp_path / "plans" / "2026-03-15_093045-implement-oauth-login-refresh-tokens.md" + assert path == Path(".hermes") / "plans" / "2026-03-15_093045-implement-oauth-login-refresh-tokens.md" def test_plan_skill_message_can_include_runtime_save_path_note(self, tmp_path): with patch("tools.skills_tool.SKILLS_DIR", tmp_path): _make_skill( tmp_path, "plan", - body="Save plans under $HERMES_HOME/plans and do not execute the work.", + body="Save plans under .hermes/plans in the active workspace and do not execute the work.", ) scan_skill_commands() msg = build_skill_invocation_message( "/plan", "Add a /plan command", - runtime_note="Save the markdown plan with write_file to /tmp/plans/plan.md", + runtime_note=( + "Save the markdown plan with write_file to this exact relative path inside " + "the active workspace/backend cwd: .hermes/plans/plan.md" + ), ) assert msg is not None - assert "Save plans under $HERMES_HOME/plans" in msg + assert "Save plans under $HERMES_HOME/plans" not in msg + assert ".hermes/plans" in msg assert "Add a /plan command" in msg - assert "/tmp/plans/plan.md" in msg + assert ".hermes/plans/plan.md" in msg assert "Runtime note:" in msg diff --git a/tests/gateway/test_plan_command.py b/tests/gateway/test_plan_command.py index 2cfea42eb..d43f46cde 100644 --- a/tests/gateway/test_plan_command.py +++ b/tests/gateway/test_plan_command.py @@ -83,7 +83,7 @@ description: Plan mode skill. # Plan Use the current conversation context when no explicit instruction is provided. -Save plans under $HERMES_HOME/plans. +Save plans under the active workspace's .hermes/plans directory. """ ) @@ -96,7 +96,6 @@ class TestGatewayPlanCommand: runner = _make_runner() event = _make_event("/plan Add OAuth login") - monkeypatch.setenv("HERMES_HOME", str(tmp_path)) monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}) monkeypatch.setattr( "agent.model_metadata.get_model_context_length", @@ -112,7 +111,9 @@ class TestGatewayPlanCommand: forwarded = runner._run_agent.call_args.kwargs["message"] assert "Plan mode skill" in forwarded assert "Add OAuth login" in forwarded - assert str(tmp_path / "plans") in forwarded + assert ".hermes/plans" in forwarded + assert str(tmp_path / "plans") not in forwarded + assert "active workspace/backend cwd" in forwarded assert "Runtime note:" in forwarded @pytest.mark.asyncio diff --git a/tests/test_cli_plan_command.py b/tests/test_cli_plan_command.py index 50fa1c5e4..8f8205d75 100644 --- a/tests/test_cli_plan_command.py +++ b/tests/test_cli_plan_command.py @@ -29,14 +29,13 @@ description: Plan mode skill. # Plan Use the current conversation context when no explicit instruction is provided. -Save plans under $HERMES_HOME/plans. +Save plans under the active workspace's .hermes/plans directory. """ ) class TestCLIPlanCommand: def test_plan_command_queues_plan_skill_message(self, tmp_path, monkeypatch): - monkeypatch.setenv("HERMES_HOME", str(tmp_path)) cli_obj = _make_cli() with patch("tools.skills_tool.SKILLS_DIR", tmp_path): @@ -49,11 +48,12 @@ class TestCLIPlanCommand: queued = cli_obj._pending_input.put.call_args[0][0] assert "Plan mode skill" in queued assert "Add OAuth login" in queued - assert str(tmp_path / "plans") in queued + assert ".hermes/plans" in queued + assert str(tmp_path / "plans") not in queued + assert "active workspace/backend cwd" in queued assert "Runtime note:" in queued def test_plan_without_args_uses_skill_context_guidance(self, tmp_path, monkeypatch): - monkeypatch.setenv("HERMES_HOME", str(tmp_path)) cli_obj = _make_cli() with patch("tools.skills_tool.SKILLS_DIR", tmp_path): @@ -63,4 +63,5 @@ class TestCLIPlanCommand: queued = cli_obj._pending_input.put.call_args[0][0] assert "current conversation context" in queued - assert "conversation-plan" in queued + assert ".hermes/plans/" in queued + assert "conversation-plan.md" in queued diff --git a/website/docs/reference/skills-catalog.md b/website/docs/reference/skills-catalog.md index a6eb510ef..7e128f11f 100644 --- a/website/docs/reference/skills-catalog.md +++ b/website/docs/reference/skills-catalog.md @@ -236,7 +236,7 @@ Skills for controlling smart home devices — lights, switches, sensors, and hom | Skill | Description | Path | |-------|-------------|------| | `code-review` | Guidelines for performing thorough code reviews with security and quality focus | `software-development/code-review` | -| `plan` | Plan mode for Hermes — inspect context, write a markdown plan, save it under `$HERMES_HOME/plans`, and do not execute the work. | `software-development/plan` | +| `plan` | Plan mode for Hermes — inspect context, write a markdown plan into `.hermes/plans/` in the active workspace/backend working directory, and do not execute the work. | `software-development/plan` | | `requesting-code-review` | Use when completing tasks, implementing major features, or before merging. Validates work meets requirements through systematic review process. | `software-development/requesting-code-review` | | `subagent-driven-development` | Use when executing implementation plans with independent tasks. Dispatches fresh delegate_task per task with two-stage review (spec compliance then code quality). | `software-development/subagent-driven-development` | | `systematic-debugging` | Use when encountering any bug, test failure, or unexpected behavior. 4-phase root cause investigation — NO fixes without understanding the problem first. | `software-development/systematic-debugging` | diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index a9e9f4205..d69d1c75d 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -11,7 +11,7 @@ Hermes has two slash-command surfaces: - **Interactive CLI slash commands** — handled by `cli.py` / `hermes_cli/commands.py` - **Messaging slash commands** — handled by `gateway/run.py` -Installed skills are also exposed as dynamic slash commands on both surfaces. That includes bundled skills like `/plan`, which opens plan mode and saves markdown plans under `~/.hermes/plans/`. +Installed skills are also exposed as dynamic slash commands on both surfaces. That includes bundled skills like `/plan`, which opens plan mode and saves markdown plans under `.hermes/plans/` relative to the active workspace/backend working directory. ## Interactive CLI slash commands @@ -32,7 +32,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in | `/compress` | Manually compress conversation context (flush memories + summarize) | | `/rollback` | List or restore filesystem checkpoints (usage: /rollback [number]) | | `/background` | Run a prompt in the background (usage: /background <prompt>) | -| `/plan [request]` | Load the bundled `plan` skill to write a markdown plan instead of executing the work. Plans are saved under `~/.hermes/plans/`. | +| `/plan [request]` | Load the bundled `plan` skill to write a markdown plan instead of executing the work. Plans are saved under `.hermes/plans/` relative to the active workspace/backend working directory. | ### Configuration @@ -110,7 +110,7 @@ The messaging gateway supports the following built-in commands inside Telegram, | `/voice [on\|off\|tts\|join\|channel\|leave\|status]` | Control spoken replies in chat. `join`/`channel`/`leave` manage Discord voice-channel mode. | | `/rollback [number]` | List or restore filesystem checkpoints. | | `/background <prompt>` | Run a prompt in a separate background session. | -| `/plan [request]` | Load the bundled `plan` skill to write a markdown plan instead of executing the work. Plans are saved under `~/.hermes/plans/`. | +| `/plan [request]` | Load the bundled `plan` skill to write a markdown plan instead of executing the work. Plans are saved under `.hermes/plans/` relative to the active workspace/backend working directory. | | `/reload-mcp` | Reload MCP servers from config. | | `/update` | Update Hermes Agent to the latest version. | | `/help` | Show messaging help. | diff --git a/website/docs/user-guide/features/skills.md b/website/docs/user-guide/features/skills.md index bf40f5e07..f9073ce74 100644 --- a/website/docs/user-guide/features/skills.md +++ b/website/docs/user-guide/features/skills.md @@ -30,7 +30,7 @@ Every installed skill is automatically available as a slash command: /excalidraw ``` -The bundled `plan` skill is a good example of a skill-backed slash command with custom behavior. Running `/plan [request]` tells Hermes to inspect context if needed, write a markdown implementation plan instead of executing the task, and save the result under `~/.hermes/plans/`. +The bundled `plan` skill is a good example of a skill-backed slash command with custom behavior. Running `/plan [request]` tells Hermes to inspect context if needed, write a markdown implementation plan instead of executing the task, and save the result under `.hermes/plans/` relative to the active workspace/backend working directory. You can also interact with skills through natural conversation: