fix: save /plan output in workspace (#1381)

This commit is contained in:
Teknium
2026-03-14 21:28:51 -07:00
committed by GitHub
parent ff3473a37c
commit b14a07315b
10 changed files with 44 additions and 31 deletions

View File

@@ -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
View File

@@ -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}"
),
)

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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` |

View File

@@ -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 &lt;prompt&gt;) |
| `/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 &lt;prompt&gt;` | 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. |

View File

@@ -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: