Skip to content
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ mcp_servers:
memory:
provider: agentmemory

Verify with `curl http://localhost:3111/agentmemory/health`. Open http://localhost:3113 for the real-time viewer. For deeper 6-hook memory provider integration (pre-LLM context injection, turn capture, archived local memory removals, system prompt block), copy integrations/hermes from the agentmemory repo to ~/.hermes/plugins/agentmemory.
Verify with `curl http://localhost:3111/agentmemory/health`. Open http://localhost:3113 for the real-time viewer. For deeper 7-hook memory provider integration (pre-LLM context injection, turn and tool-result capture, archived local memory removals, task completion, system prompt block), copy integrations/hermes from the agentmemory repo to ~/.hermes/plugins/agentmemory.
```

Full guide: [`integrations/hermes/`](integrations/hermes/)
Expand Down
326 changes: 326 additions & 0 deletions docs/todos/2026-06-19-issue-331-hermes-python-plugin-hooks/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
# Hermes Python Plugin Hooks Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Expose Hermes Python plugin tool-level `post_tool_use` observations and a task-completion lifecycle hook without changing core REST, MCP, schema, auth, or persistence behavior.

**Architecture:** Keep the change inside the Hermes adapter boundary. `sync_turn()` will prefer explicit completed tool-use/tool-result pairs from `messages`, otherwise preserve the existing conversation fallback; a shared observation helper will keep session/project/cwd/timestamp/agent-id behavior consistent. `on_task_completed()` will emit the existing `task_completed` observe payload shape and be declared as a Hermes Python method-name hook.

**Tech Stack:** Python Hermes plugin, TypeScript/Vitest subprocess tests, YAML manifest contract.

---

## Files

- Modify: `integrations/hermes/__init__.py`
- Add helper functions for truncation, message content block walking, tool pair extraction, and shared observe emission.
- Extend `sync_turn()` to emit structured tool observations when explicit pairs exist.
- Add `on_task_completed(..., **kwargs)` and fallback `MemoryProvider` stub method.
- Modify: `integrations/hermes/plugin.yaml`
- Add `on_task_completed` to the declared Hermes lifecycle hooks.
- Modify: `integrations/hermes/README.md`
- Update the Hermes hook badge/count and behavior list from 6 to 7 lifecycle hooks.
- Modify: `README.md`
- Update only the Hermes-specific deeper integration text from 6-hook to 7-hook; do not touch Codex/Factory hook counts.
- Modify: `test/hermes-plugin.test.ts`
- Update expected hook list and manifest/implementation parity coverage.
- Modify: `test/integration-plaintext-http.test.ts`
- Add Python subprocess tests for sync-turn extraction, fallback preservation, malformed input tolerance, agent-id propagation, and task completion payload shape.
- Modify: `docs/todos/2026-06-19-issue-331-hermes-python-plugin-hooks/todo.md`
- Keep progress, review notes, and verification evidence current.

## Task 1: Add Red Tests For Hermes Hook Behavior

**Files:**
- Modify: `test/integration-plaintext-http.test.ts`
- Modify: `test/hermes-plugin.test.ts`

- [x] **Step 1: Add a manifest contract expectation**

In `test/hermes-plugin.test.ts`, add `on_task_completed` to `expectedHermesHooks`.

```ts
const expectedHermesHooks = [
"prefetch",
"sync_turn",
"on_session_end",
"on_pre_compress",
"on_memory_write",
"on_task_completed",
"system_prompt_block",
];
```

- [x] **Step 2: Add Python behavior tests**

Append focused tests under `describe("Hermes plaintext bearer guard", ...)` or split into a nearby `describe("Hermes hook payloads", ...)` in `test/integration-plaintext-http.test.ts`.

Test cases:
- `sync_turn extracts Anthropic-style tool_use/tool_result messages`
- `sync_turn preserves conversation fallback when no completed tool pairs exist`
- `sync_turn ignores malformed message history without throwing`
- `sync_turn does not re-emit the same tool_use_id for cumulative histories`
- `sync_turn truncates long string tool output with the Node-hook sentinel`
- `sync_turn truncates large object tool output as serialized JSON with the Node-hook sentinel`
- `on_task_completed emits task_completed observations`

Use Python subprocess snippets that:
- import `integrations/hermes/__init__.py` with `importlib.util`
- initialize `AgentMemoryProvider`
- monkeypatch `mod._api` before `provider.initialize(...)` so tests never make real startup network calls
- monkeypatch `mod._api_bg` to capture observe payloads synchronously
- assert request body fields instead of making network calls

Expected Anthropic-style message fixture:

```python
messages = [
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_1",
"name": "Bash",
"input": {"command": "pwd"},
}
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "repo\n",
}
],
},
]
```

Expected observation body:

```python
{
"hookType": "post_tool_use",
"sessionId": "session-331",
"project": "/tmp/project",
"cwd": "/tmp/project",
"data": {
"tool_name": "Bash",
"tool_input": {"command": "pwd"},
"tool_output": "repo\n",
},
}
```

Expected task completion body:

```python
{
"hookType": "task_completed",
"sessionId": "session-331",
"data": {
"task_id": "task-1",
"task_subject": "Hermes hooks",
"task_description": "x" * 2000,
"teammate_name": "worker",
"team_name": "team",
},
}
```

Expected long string truncation:

```python
assert calls[-1]["body"]["data"]["tool_output"] == "x" * 8000 + "\n[...truncated]"
```

Expected large object truncation:

```python
assert calls[-1]["body"]["data"]["tool_output"].endswith("...[truncated]")
assert len(calls[-1]["body"]["data"]["tool_output"]) <= 8014
```

- [x] **Step 3: Run red tests**

Run:

```bash
corepack pnpm exec vitest run test/hermes-plugin.test.ts test/integration-plaintext-http.test.ts --exclude test/integration.test.ts
```

Expected: FAIL because `on_task_completed` is not implemented/declared and `sync_turn()` still emits only `tool_name: "conversation"`.

## Task 2: Implement Minimal Hermes Hook Support

**Files:**
- Modify: `integrations/hermes/__init__.py`
- Modify: `integrations/hermes/plugin.yaml`
- Modify: `integrations/hermes/README.md` if exact hook list text is stale

- [x] **Step 1: Add helper methods in `AgentMemoryProvider`**

Add private helpers near `_with_agent_id` / `sync_turn`:

```python
def _timestamp(self) -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())

def _observe_bg(self, hook_type: str, data: dict, session_id: str | None = None) -> None:
_api_bg(self._base, "observe", self._with_agent_id({
"hookType": hook_type,
"sessionId": session_id or self._session_id,
"project": self._project,
"cwd": self._project,
"timestamp": self._timestamp(),
"data": data,
}), secret=self._secret)
```

Use real final names that match local style and keep comments minimal.

- [x] **Step 2: Add conservative extraction helpers**

Implement helpers that:
- accept only a list of message dicts
- walk `message["content"]` lists and single dicts
- record calls where `type == "tool_use"` and `id`, `name` exist
- record results where `type == "tool_result"` and `tool_use_id` exists
- emit only completed pairs
- keep a private per-provider set of emitted `(session_id, tool_use_id)` pairs so cumulative `messages` histories do not re-emit old tool observations
- ignore malformed/unsupported content
- truncate string/object outputs to 8000 characters using the same sentinel style as the Node hook

- [x] **Step 3: Extend `sync_turn()`**

Change `sync_turn()` so:
- it reads `kwargs.get("messages", [])`
- if completed tool observations exist, emits those observations and skips the conversation fallback
- if all completed pairs were already emitted for that session, preserves the existing conversation fallback instead of duplicating old tool observations
- otherwise emits the current conversation fallback data shape
- it continues honoring `kwargs.get("session_id", self._session_id)`

- [x] **Step 4: Add `on_task_completed()`**

Add fallback stub to the local `MemoryProvider` class:

```python
def on_task_completed(self, **kwargs: Any) -> None: pass
```

Add provider method:

```python
def on_task_completed(self, **kwargs: Any) -> None:
self._observe_bg("task_completed", {
"task_id": kwargs.get("task_id"),
"task_subject": kwargs.get("task_subject"),
"task_description": kwargs.get("task_description")[:2000] if isinstance(kwargs.get("task_description"), str) else "",
"teammate_name": kwargs.get("teammate_name"),
"team_name": kwargs.get("team_name"),
}, session_id=kwargs.get("session_id", self._session_id))
```

Keep the signature permissive so Hermes can pass extra context without breaking the plugin.

- [x] **Step 5: Update manifest/docs**

Add `on_task_completed` to `integrations/hermes/plugin.yaml`.

Update exact Hermes hook-count surfaces:
- `integrations/hermes/README.md` badge text/alt from `Hooks-6_lifecycle` / `6 lifecycle hooks` to 7.
- `integrations/hermes/README.md` prompt text from `6-hook memory provider plugin` to 7.
- `integrations/hermes/README.md` lifecycle bullet list for structured `sync_turn()` tool capture and new `on_task_completed()`.
- `README.md` Hermes-specific deeper integration sentence from `6-hook` to 7.

Do not update unrelated Codex/Factory hook counts.

## Task 3: Green Tests, Cleanup, And Verification

**Files:**
- Verify all changed files.
- Update `docs/todos/2026-06-19-issue-331-hermes-python-plugin-hooks/todo.md`.

- [x] **Step 1: Run targeted green tests**

Run:

```bash
corepack pnpm exec vitest run test/hermes-plugin.test.ts test/integration-plaintext-http.test.ts --exclude test/integration.test.ts
```

Expected: PASS.

- [x] **Step 2: Run Python syntax check without source-tree pycache**

Run:

```bash
PYTHONPYCACHEPREFIX=/tmp/agentmemory-pycache python3 -m py_compile integrations/hermes/__init__.py
```

Expected: exit 0.

- [x] **Step 3: Run focused simplification pass**

Use `$simple-code` on the touched Hermes/plugin/test surface only:
- remove duplicate helper branches
- keep explicit ID-pairing logic readable
- preserve API, manifest, auth, URL, persistence, and observe contracts

- [x] **Step 4: Run final verification**

Run:

```bash
git diff --check
corepack pnpm test
semgrep scan --config p/default --error --metrics=off .
```

Expected:
- no whitespace errors
- non-integration suite passes
- Semgrep exits 0

Before any commit, stage only task-owned files and run:

```bash
gitleaks protect --staged --redact
```

Expected: exit 0.

## Task 4: Local GitHub PR Preparation

**Files:**
- Task-owned changed files only.

- [x] **Step 1: Run `$github-push-prepare` local branch-prep**

Allowed by this `$github-feature-loop` invocation:
- local task-owned cleanup
- local staging/commit
- narrow `git fetch origin main`
- merging captured `origin/main` base into this branch if needed
- post-base verification

Not allowed without separate explicit current-turn approval:
- `git push`
- PR creation
- PR merge
- force push
- destructive cleanup

- [x] **Step 2: Handoff**

Report:
- task state path and this plan path
- changed behavior and files
- commits created
- verification/security gate results
- PR base SHA
- whether push/PR creation did not run due to approval gate
Loading
Loading