fix: save /plan output in workspace (#1381)
This commit is contained in:
@@ -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:
|
||||
|
||||
3
cli.py
3
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}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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-<slug>.md`
|
||||
- `.hermes/plans/YYYY-MM-DD_HHMMSS-<slug>.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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user