mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 23:11:37 +08:00
Compare commits
1 Commits
skill/gith
...
hermes/pla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
459b00254c |
@@ -7,7 +7,6 @@ can invoke skills via /skill-name commands and prompt-only built-ins like
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -24,15 +23,20 @@ def build_plan_path(
|
|||||||
*,
|
*,
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
) -> Path:
|
) -> Path:
|
||||||
"""Return the default markdown path for a /plan invocation."""
|
"""Return the default workspace-relative markdown path for a /plan invocation.
|
||||||
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
|
||||||
|
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_source = (user_instruction or "").strip().splitlines()[0] if user_instruction else ""
|
||||||
slug = _PLAN_SLUG_RE.sub("-", slug_source.lower()).strip("-")
|
slug = _PLAN_SLUG_RE.sub("-", slug_source.lower()).strip("-")
|
||||||
if slug:
|
if slug:
|
||||||
slug = "-".join(part for part in slug.split("-")[:8] if part)[:48].strip("-")
|
slug = "-".join(part for part in slug.split("-")[:8] if part)[:48].strip("-")
|
||||||
slug = slug or "conversation-plan"
|
slug = slug or "conversation-plan"
|
||||||
timestamp = (now or datetime.now()).strftime("%Y-%m-%d_%H%M%S")
|
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:
|
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,
|
user_instruction,
|
||||||
task_id=self.session_id,
|
task_id=self.session_id,
|
||||||
runtime_note=(
|
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,
|
user_instruction,
|
||||||
task_id=_quick_key,
|
task_id=_quick_key,
|
||||||
runtime_note=(
|
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:
|
if not event.text:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: plan
|
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
|
version: 1.0.0
|
||||||
author: Hermes Agent
|
author: Hermes Agent
|
||||||
license: MIT
|
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 edit project files except the plan markdown file.
|
||||||
- Do not run mutating terminal commands, commit, push, or perform external actions.
|
- 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.
|
- 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
|
## Output requirements
|
||||||
|
|
||||||
@@ -42,10 +42,12 @@ If the task is code-related, include exact file paths, likely test targets, and
|
|||||||
## Save location
|
## Save location
|
||||||
|
|
||||||
Save the plan with `write_file` under:
|
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 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
|
## Interaction style
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import tools.skills_tool as skills_tool_module
|
import tools.skills_tool as skills_tool_module
|
||||||
@@ -277,32 +278,34 @@ Generate some audio.
|
|||||||
|
|
||||||
|
|
||||||
class TestPlanSkillHelpers:
|
class TestPlanSkillHelpers:
|
||||||
def test_build_plan_path_uses_hermes_home_and_slugifies_request(self, tmp_path, monkeypatch):
|
def test_build_plan_path_uses_workspace_relative_dir_and_slugifies_request(self):
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
||||||
|
|
||||||
path = build_plan_path(
|
path = build_plan_path(
|
||||||
"Implement OAuth login + refresh tokens!",
|
"Implement OAuth login + refresh tokens!",
|
||||||
now=datetime(2026, 3, 15, 9, 30, 45),
|
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):
|
def test_plan_skill_message_can_include_runtime_save_path_note(self, tmp_path):
|
||||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||||
_make_skill(
|
_make_skill(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
"plan",
|
"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()
|
scan_skill_commands()
|
||||||
msg = build_skill_invocation_message(
|
msg = build_skill_invocation_message(
|
||||||
"/plan",
|
"/plan",
|
||||||
"Add a /plan command",
|
"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 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 "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
|
assert "Runtime note:" in msg
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ description: Plan mode skill.
|
|||||||
# Plan
|
# Plan
|
||||||
|
|
||||||
Use the current conversation context when no explicit instruction is provided.
|
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()
|
runner = _make_runner()
|
||||||
event = _make_event("/plan Add OAuth login")
|
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(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"})
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"agent.model_metadata.get_model_context_length",
|
"agent.model_metadata.get_model_context_length",
|
||||||
@@ -112,7 +111,9 @@ class TestGatewayPlanCommand:
|
|||||||
forwarded = runner._run_agent.call_args.kwargs["message"]
|
forwarded = runner._run_agent.call_args.kwargs["message"]
|
||||||
assert "Plan mode skill" in forwarded
|
assert "Plan mode skill" in forwarded
|
||||||
assert "Add OAuth login" 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
|
assert "Runtime note:" in forwarded
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -29,14 +29,13 @@ description: Plan mode skill.
|
|||||||
# Plan
|
# Plan
|
||||||
|
|
||||||
Use the current conversation context when no explicit instruction is provided.
|
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:
|
class TestCLIPlanCommand:
|
||||||
def test_plan_command_queues_plan_skill_message(self, tmp_path, monkeypatch):
|
def test_plan_command_queues_plan_skill_message(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
||||||
cli_obj = _make_cli()
|
cli_obj = _make_cli()
|
||||||
|
|
||||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
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]
|
queued = cli_obj._pending_input.put.call_args[0][0]
|
||||||
assert "Plan mode skill" in queued
|
assert "Plan mode skill" in queued
|
||||||
assert "Add OAuth login" 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
|
assert "Runtime note:" in queued
|
||||||
|
|
||||||
def test_plan_without_args_uses_skill_context_guidance(self, tmp_path, monkeypatch):
|
def test_plan_without_args_uses_skill_context_guidance(self, tmp_path, monkeypatch):
|
||||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
||||||
cli_obj = _make_cli()
|
cli_obj = _make_cli()
|
||||||
|
|
||||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
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]
|
queued = cli_obj._pending_input.put.call_args[0][0]
|
||||||
assert "current conversation context" in queued
|
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 |
|
| Skill | Description | Path |
|
||||||
|-------|-------------|------|
|
|-------|-------------|------|
|
||||||
| `code-review` | Guidelines for performing thorough code reviews with security and quality focus | `software-development/code-review` |
|
| `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` |
|
| `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` |
|
| `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` |
|
| `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`
|
- **Interactive CLI slash commands** — handled by `cli.py` / `hermes_cli/commands.py`
|
||||||
- **Messaging slash commands** — handled by `gateway/run.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
|
## 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) |
|
| `/compress` | Manually compress conversation context (flush memories + summarize) |
|
||||||
| `/rollback` | List or restore filesystem checkpoints (usage: /rollback [number]) |
|
| `/rollback` | List or restore filesystem checkpoints (usage: /rollback [number]) |
|
||||||
| `/background` | Run a prompt in the background (usage: /background <prompt>) |
|
| `/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
|
### 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. |
|
| `/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. |
|
| `/rollback [number]` | List or restore filesystem checkpoints. |
|
||||||
| `/background <prompt>` | Run a prompt in a separate background session. |
|
| `/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. |
|
| `/reload-mcp` | Reload MCP servers from config. |
|
||||||
| `/update` | Update Hermes Agent to the latest version. |
|
| `/update` | Update Hermes Agent to the latest version. |
|
||||||
| `/help` | Show messaging help. |
|
| `/help` | Show messaging help. |
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Every installed skill is automatically available as a slash command:
|
|||||||
/excalidraw
|
/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:
|
You can also interact with skills through natural conversation:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user