opencode-hermes-mcp 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/PKG-INFO +22 -1
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/README.md +21 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/__init__.py +1 -1
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/controller.py +23 -2
- opencode_hermes_mcp-0.4.2/opencode_hermes_mcp/journal.py +121 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/PKG-INFO +22 -1
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/SOURCES.txt +3 -1
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/pyproject.toml +1 -1
- opencode_hermes_mcp-0.4.2/tests/test_journal.py +83 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/LICENSE +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/client.py +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/installer.py +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/models.py +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/pin.txt +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/server.py +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp/smoke_client.py +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/dependency_links.txt +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/entry_points.txt +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/requires.txt +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/top_level.txt +0 -0
- {opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-hermes-mcp
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Deterministic MCP controller between Hermes (supervisor LLM) and the permanent OpenCode server. OpenCode pin defined in opencode_hermes_mcp/pin.txt (currently 1.18.21).
|
|
5
5
|
Author-email: Arthur Hottier <arthur.ho2tier@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -195,6 +195,25 @@ The launcher reads the OpenCode server credentials from
|
|
|
195
195
|
`python -m opencode_hermes_mcp.server` in the repo venv — `config.yaml` stays
|
|
196
196
|
secret-free.
|
|
197
197
|
|
|
198
|
+
## Delegation journal
|
|
199
|
+
|
|
200
|
+
Every `opencode_run` is recorded in a durable, append-only JSONL journal so
|
|
201
|
+
the delegation history survives sessions (the per-turn state file
|
|
202
|
+
`turn_<sid>.json` is cleared on completion). One line per record:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{"ts": 1787750000123, "kind": "start", "session_id": "ses_...", "directory": "/abs/repo", "agent": "build", "task": "...(truncated to 500 chars)"}
|
|
206
|
+
{"ts": 1787750100456, "kind": "end", "session_id": "ses_...", "directory": "/abs/repo", "state": "completed|error|aborted|timeout", "elapsed_ms": 100333, "files": 12, "additions": 1209, "deletions": 18, "changed_files": ["a.py", "..."]}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- Path: `~/.local/state/opencode-hermes-mcp/delegations.jsonl` by default,
|
|
210
|
+
overridable with the `OPENCODE_HERMES_MCP_JOURNAL` env var.
|
|
211
|
+
- `ts` is epoch milliseconds; `state` is the run's terminal state;
|
|
212
|
+
`changed_files` is truncated to 50 entries.
|
|
213
|
+
- The journal is best-effort: a write failure (disk full, permissions) is
|
|
214
|
+
logged and swallowed — it never fails a run.
|
|
215
|
+
- Read-only access for consumers: `opencode_hermes_mcp.journal.read_journal()`.
|
|
216
|
+
|
|
198
217
|
## TUI attach helpers (watch OpenCode live)
|
|
199
218
|
|
|
200
219
|
`install.sh` also drops two helpers into `~/.local/bin/` (sources:
|
|
@@ -277,8 +296,10 @@ smoke test and the integration suite, and the contribution conventions.
|
|
|
277
296
|
| `opencode_hermes_mcp/controller.py` | state machine: submit / wait / resume / classify |
|
|
278
297
|
| `opencode_hermes_mcp/client.py` | HTTP + SSE client for the OpenCode server |
|
|
279
298
|
| `opencode_hermes_mcp/models.py` | data helpers for turns / interactions |
|
|
299
|
+
| `opencode_hermes_mcp/journal.py` | durable delegation journal (append-only JSONL) |
|
|
280
300
|
| `opencode_hermes_mcp/smoke_client.py` | no-LLM smoke test (tool surface + basic calls) |
|
|
281
301
|
| `tests/run_tests.py` | full integration suite (live LLM turns) |
|
|
302
|
+
| `tests/test_journal.py` | journal unit tests (pytest) |
|
|
282
303
|
| `opencode_hermes_mcp/installer.py` | setup wizard (Python + rich; self-bootstrapping venv) |
|
|
283
304
|
| `opencode_hermes_mcp/pin.txt` | the OpenCode version pin (single source of truth, one line) |
|
|
284
305
|
| `scripts/install.sh` / `uninstall.sh` / `upgrade.sh` | lifecycle (`install.sh` is a thin wrapper around the wizard) |
|
|
@@ -167,6 +167,25 @@ The launcher reads the OpenCode server credentials from
|
|
|
167
167
|
`python -m opencode_hermes_mcp.server` in the repo venv — `config.yaml` stays
|
|
168
168
|
secret-free.
|
|
169
169
|
|
|
170
|
+
## Delegation journal
|
|
171
|
+
|
|
172
|
+
Every `opencode_run` is recorded in a durable, append-only JSONL journal so
|
|
173
|
+
the delegation history survives sessions (the per-turn state file
|
|
174
|
+
`turn_<sid>.json` is cleared on completion). One line per record:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{"ts": 1787750000123, "kind": "start", "session_id": "ses_...", "directory": "/abs/repo", "agent": "build", "task": "...(truncated to 500 chars)"}
|
|
178
|
+
{"ts": 1787750100456, "kind": "end", "session_id": "ses_...", "directory": "/abs/repo", "state": "completed|error|aborted|timeout", "elapsed_ms": 100333, "files": 12, "additions": 1209, "deletions": 18, "changed_files": ["a.py", "..."]}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- Path: `~/.local/state/opencode-hermes-mcp/delegations.jsonl` by default,
|
|
182
|
+
overridable with the `OPENCODE_HERMES_MCP_JOURNAL` env var.
|
|
183
|
+
- `ts` is epoch milliseconds; `state` is the run's terminal state;
|
|
184
|
+
`changed_files` is truncated to 50 entries.
|
|
185
|
+
- The journal is best-effort: a write failure (disk full, permissions) is
|
|
186
|
+
logged and swallowed — it never fails a run.
|
|
187
|
+
- Read-only access for consumers: `opencode_hermes_mcp.journal.read_journal()`.
|
|
188
|
+
|
|
170
189
|
## TUI attach helpers (watch OpenCode live)
|
|
171
190
|
|
|
172
191
|
`install.sh` also drops two helpers into `~/.local/bin/` (sources:
|
|
@@ -249,8 +268,10 @@ smoke test and the integration suite, and the contribution conventions.
|
|
|
249
268
|
| `opencode_hermes_mcp/controller.py` | state machine: submit / wait / resume / classify |
|
|
250
269
|
| `opencode_hermes_mcp/client.py` | HTTP + SSE client for the OpenCode server |
|
|
251
270
|
| `opencode_hermes_mcp/models.py` | data helpers for turns / interactions |
|
|
271
|
+
| `opencode_hermes_mcp/journal.py` | durable delegation journal (append-only JSONL) |
|
|
252
272
|
| `opencode_hermes_mcp/smoke_client.py` | no-LLM smoke test (tool surface + basic calls) |
|
|
253
273
|
| `tests/run_tests.py` | full integration suite (live LLM turns) |
|
|
274
|
+
| `tests/test_journal.py` | journal unit tests (pytest) |
|
|
254
275
|
| `opencode_hermes_mcp/installer.py` | setup wizard (Python + rich; self-bootstrapping venv) |
|
|
255
276
|
| `opencode_hermes_mcp/pin.txt` | the OpenCode version pin (single source of truth, one line) |
|
|
256
277
|
| `scripts/install.sh` / `uninstall.sh` / `upgrade.sh` | lifecycle (`install.sh` is a thin wrapper around the wizard) |
|
|
@@ -22,6 +22,7 @@ import os
|
|
|
22
22
|
import time
|
|
23
23
|
from typing import Any
|
|
24
24
|
|
|
25
|
+
from . import journal
|
|
25
26
|
from .client import OpenCode, OpenCodeError, event_stream
|
|
26
27
|
from .models import (
|
|
27
28
|
assistant_text,
|
|
@@ -621,7 +622,17 @@ class Controller:
|
|
|
621
622
|
turn["assistant_count_before"] = sum(
|
|
622
623
|
1 for m in pre if (m.get("info") or {}).get("role") == "assistant"
|
|
623
624
|
)
|
|
624
|
-
|
|
625
|
+
journal.append_start(sid, directory, turn.get("agent"), task)
|
|
626
|
+
result = await self._wait(turn, timeout)
|
|
627
|
+
if result.get("state") in ("completed", "error", "aborted", "timeout"):
|
|
628
|
+
diff = result.get("diff") or {}
|
|
629
|
+
journal.append_end(
|
|
630
|
+
sid, directory, result["state"],
|
|
631
|
+
(result.get("timing") or {}).get("elapsed_ms"),
|
|
632
|
+
diff.get("files"), diff.get("additions"), diff.get("deletions"),
|
|
633
|
+
diff.get("changed_files"),
|
|
634
|
+
)
|
|
635
|
+
return result
|
|
625
636
|
else:
|
|
626
637
|
try:
|
|
627
638
|
sess = await self.oc.create_session(
|
|
@@ -665,7 +676,17 @@ class Controller:
|
|
|
665
676
|
"error": f"failed to submit prompt: {exc}",
|
|
666
677
|
"session_id": sid,
|
|
667
678
|
}
|
|
668
|
-
|
|
679
|
+
journal.append_start(sid, directory, turn.get("agent"), task)
|
|
680
|
+
result = await self._wait(turn, timeout)
|
|
681
|
+
if result.get("state") in ("completed", "error", "aborted", "timeout"):
|
|
682
|
+
diff = result.get("diff") or {}
|
|
683
|
+
journal.append_end(
|
|
684
|
+
sid, directory, result["state"],
|
|
685
|
+
(result.get("timing") or {}).get("elapsed_ms"),
|
|
686
|
+
diff.get("files"), diff.get("additions"), diff.get("deletions"),
|
|
687
|
+
diff.get("changed_files"),
|
|
688
|
+
)
|
|
689
|
+
return result
|
|
669
690
|
|
|
670
691
|
# ------------------------------------------------------------------ #
|
|
671
692
|
# Resume primitives (answer / permission) — same turn, no resubmit
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Durable delegation journal (append-only JSONL).
|
|
2
|
+
|
|
3
|
+
Records the start and terminal state of every controller run so the
|
|
4
|
+
delegation history survives sessions (the per-turn state file
|
|
5
|
+
turn_<sid>.json is cleared on completion). The journal is best-effort:
|
|
6
|
+
any write failure is logged and swallowed — it must never fail a run.
|
|
7
|
+
|
|
8
|
+
Path: ~/.local/state/opencode-hermes-mcp/delegations.jsonl by default,
|
|
9
|
+
overridable via the OPENCODE_HERMES_MCP_JOURNAL env var.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
ENV_JOURNAL = "OPENCODE_HERMES_MCP_JOURNAL"
|
|
23
|
+
DEFAULT_JOURNAL = Path(
|
|
24
|
+
os.path.join(
|
|
25
|
+
os.path.expanduser("~"), ".local", "state", "opencode-hermes-mcp", "delegations.jsonl"
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
TASK_MAX_CHARS = 500
|
|
29
|
+
CHANGED_FILES_MAX = 50
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def journal_path() -> Path:
|
|
33
|
+
"""Journal path: env override (OPENCODE_HERMES_MCP_JOURNAL) or default."""
|
|
34
|
+
return Path(os.environ.get(ENV_JOURNAL) or DEFAULT_JOURNAL)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _now_ms() -> int:
|
|
38
|
+
return int(time.time() * 1000)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _append(record: dict[str, Any]) -> None:
|
|
42
|
+
"""Append one JSON line (open 'a', one full write, flush + fsync).
|
|
43
|
+
|
|
44
|
+
Never raises: a journal failure is logged and swallowed.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
path = journal_path()
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
line = json.dumps(record, ensure_ascii=False) + "\n"
|
|
50
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
51
|
+
f.write(line)
|
|
52
|
+
f.flush()
|
|
53
|
+
os.fsync(f.fileno())
|
|
54
|
+
except Exception as exc: # noqa: BLE001 — the journal must never fail a run
|
|
55
|
+
log.warning("journal: append %s failed: %s", record.get("kind"), exc)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def append_start(session_id: str, directory: str, agent: str | None, task: str) -> None:
|
|
59
|
+
"""Record a run start (session resolved, before the wait)."""
|
|
60
|
+
_append(
|
|
61
|
+
{
|
|
62
|
+
"ts": _now_ms(),
|
|
63
|
+
"kind": "start",
|
|
64
|
+
"session_id": session_id,
|
|
65
|
+
"directory": directory,
|
|
66
|
+
"agent": agent,
|
|
67
|
+
"task": (task or "")[:TASK_MAX_CHARS],
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def append_end(
|
|
73
|
+
session_id: str,
|
|
74
|
+
directory: str,
|
|
75
|
+
state: str,
|
|
76
|
+
elapsed_ms: int | None = None,
|
|
77
|
+
files: int | None = None,
|
|
78
|
+
additions: int | None = None,
|
|
79
|
+
deletions: int | None = None,
|
|
80
|
+
changed_files: list[str] | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Record a run's terminal state (completed / error / aborted / timeout)."""
|
|
83
|
+
_append(
|
|
84
|
+
{
|
|
85
|
+
"ts": _now_ms(),
|
|
86
|
+
"kind": "end",
|
|
87
|
+
"session_id": session_id,
|
|
88
|
+
"directory": directory,
|
|
89
|
+
"state": state,
|
|
90
|
+
"elapsed_ms": elapsed_ms,
|
|
91
|
+
"files": files,
|
|
92
|
+
"additions": additions,
|
|
93
|
+
"deletions": deletions,
|
|
94
|
+
"changed_files": (changed_files or [])[:CHANGED_FILES_MAX],
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_journal(path: str | Path | None = None) -> list[dict[str, Any]]:
|
|
100
|
+
"""Read the journal (read-only, for tests and consumers).
|
|
101
|
+
|
|
102
|
+
Missing file -> []. Malformed lines are skipped.
|
|
103
|
+
"""
|
|
104
|
+
p = Path(path) if path is not None else journal_path()
|
|
105
|
+
try:
|
|
106
|
+
with open(p, encoding="utf-8") as f:
|
|
107
|
+
lines = f.readlines()
|
|
108
|
+
except OSError:
|
|
109
|
+
return []
|
|
110
|
+
records: list[dict[str, Any]] = []
|
|
111
|
+
for line in lines:
|
|
112
|
+
line = line.strip()
|
|
113
|
+
if not line:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
rec = json.loads(line)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
continue
|
|
119
|
+
if isinstance(rec, dict):
|
|
120
|
+
records.append(rec)
|
|
121
|
+
return records
|
{opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: opencode-hermes-mcp
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Deterministic MCP controller between Hermes (supervisor LLM) and the permanent OpenCode server. OpenCode pin defined in opencode_hermes_mcp/pin.txt (currently 1.18.21).
|
|
5
5
|
Author-email: Arthur Hottier <arthur.ho2tier@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -195,6 +195,25 @@ The launcher reads the OpenCode server credentials from
|
|
|
195
195
|
`python -m opencode_hermes_mcp.server` in the repo venv — `config.yaml` stays
|
|
196
196
|
secret-free.
|
|
197
197
|
|
|
198
|
+
## Delegation journal
|
|
199
|
+
|
|
200
|
+
Every `opencode_run` is recorded in a durable, append-only JSONL journal so
|
|
201
|
+
the delegation history survives sessions (the per-turn state file
|
|
202
|
+
`turn_<sid>.json` is cleared on completion). One line per record:
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{"ts": 1787750000123, "kind": "start", "session_id": "ses_...", "directory": "/abs/repo", "agent": "build", "task": "...(truncated to 500 chars)"}
|
|
206
|
+
{"ts": 1787750100456, "kind": "end", "session_id": "ses_...", "directory": "/abs/repo", "state": "completed|error|aborted|timeout", "elapsed_ms": 100333, "files": 12, "additions": 1209, "deletions": 18, "changed_files": ["a.py", "..."]}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- Path: `~/.local/state/opencode-hermes-mcp/delegations.jsonl` by default,
|
|
210
|
+
overridable with the `OPENCODE_HERMES_MCP_JOURNAL` env var.
|
|
211
|
+
- `ts` is epoch milliseconds; `state` is the run's terminal state;
|
|
212
|
+
`changed_files` is truncated to 50 entries.
|
|
213
|
+
- The journal is best-effort: a write failure (disk full, permissions) is
|
|
214
|
+
logged and swallowed — it never fails a run.
|
|
215
|
+
- Read-only access for consumers: `opencode_hermes_mcp.journal.read_journal()`.
|
|
216
|
+
|
|
198
217
|
## TUI attach helpers (watch OpenCode live)
|
|
199
218
|
|
|
200
219
|
`install.sh` also drops two helpers into `~/.local/bin/` (sources:
|
|
@@ -277,8 +296,10 @@ smoke test and the integration suite, and the contribution conventions.
|
|
|
277
296
|
| `opencode_hermes_mcp/controller.py` | state machine: submit / wait / resume / classify |
|
|
278
297
|
| `opencode_hermes_mcp/client.py` | HTTP + SSE client for the OpenCode server |
|
|
279
298
|
| `opencode_hermes_mcp/models.py` | data helpers for turns / interactions |
|
|
299
|
+
| `opencode_hermes_mcp/journal.py` | durable delegation journal (append-only JSONL) |
|
|
280
300
|
| `opencode_hermes_mcp/smoke_client.py` | no-LLM smoke test (tool surface + basic calls) |
|
|
281
301
|
| `tests/run_tests.py` | full integration suite (live LLM turns) |
|
|
302
|
+
| `tests/test_journal.py` | journal unit tests (pytest) |
|
|
282
303
|
| `opencode_hermes_mcp/installer.py` | setup wizard (Python + rich; self-bootstrapping venv) |
|
|
283
304
|
| `opencode_hermes_mcp/pin.txt` | the OpenCode version pin (single source of truth, one line) |
|
|
284
305
|
| `scripts/install.sh` / `uninstall.sh` / `upgrade.sh` | lifecycle (`install.sh` is a thin wrapper around the wizard) |
|
{opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/SOURCES.txt
RENAMED
|
@@ -5,6 +5,7 @@ opencode_hermes_mcp/__init__.py
|
|
|
5
5
|
opencode_hermes_mcp/client.py
|
|
6
6
|
opencode_hermes_mcp/controller.py
|
|
7
7
|
opencode_hermes_mcp/installer.py
|
|
8
|
+
opencode_hermes_mcp/journal.py
|
|
8
9
|
opencode_hermes_mcp/models.py
|
|
9
10
|
opencode_hermes_mcp/pin.txt
|
|
10
11
|
opencode_hermes_mcp/server.py
|
|
@@ -14,4 +15,5 @@ opencode_hermes_mcp.egg-info/SOURCES.txt
|
|
|
14
15
|
opencode_hermes_mcp.egg-info/dependency_links.txt
|
|
15
16
|
opencode_hermes_mcp.egg-info/entry_points.txt
|
|
16
17
|
opencode_hermes_mcp.egg-info/requires.txt
|
|
17
|
-
opencode_hermes_mcp.egg-info/top_level.txt
|
|
18
|
+
opencode_hermes_mcp.egg-info/top_level.txt
|
|
19
|
+
tests/test_journal.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "opencode-hermes-mcp"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "Deterministic MCP controller between Hermes (supervisor LLM) and the permanent OpenCode server. OpenCode pin defined in opencode_hermes_mcp/pin.txt (currently 1.18.21)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Unit tests for the durable delegation journal (opencode_hermes_mcp.journal).
|
|
2
|
+
|
|
3
|
+
Pure unit tests: no LLM, no server. Run with:
|
|
4
|
+
.venv/bin/python -m pytest tests/test_journal.py
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
13
|
+
|
|
14
|
+
from opencode_hermes_mcp import journal # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_journal_path_default(monkeypatch):
|
|
18
|
+
monkeypatch.delenv(journal.ENV_JOURNAL, raising=False)
|
|
19
|
+
p = journal.journal_path()
|
|
20
|
+
assert p.name == "delegations.jsonl"
|
|
21
|
+
assert p.parent.name == "opencode-hermes-mcp"
|
|
22
|
+
assert ".local" in p.parts and "state" in p.parts
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_journal_path_env_override(tmp_path, monkeypatch):
|
|
26
|
+
custom = tmp_path / "custom" / "delegations.jsonl"
|
|
27
|
+
monkeypatch.setenv(journal.ENV_JOURNAL, str(custom))
|
|
28
|
+
assert journal.journal_path() == custom
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_append_start_end_jsonl_format(tmp_path, monkeypatch):
|
|
32
|
+
custom = tmp_path / "sub" / "delegations.jsonl"
|
|
33
|
+
monkeypatch.setenv(journal.ENV_JOURNAL, str(custom))
|
|
34
|
+
journal.append_start("ses_1", "/repo", "build", "t" * 600)
|
|
35
|
+
journal.append_end(
|
|
36
|
+
"ses_1", "/repo", "completed",
|
|
37
|
+
elapsed_ms=1234, files=2, additions=10, deletions=1,
|
|
38
|
+
changed_files=[f"f{i}.py" for i in range(60)],
|
|
39
|
+
)
|
|
40
|
+
lines = custom.read_text(encoding="utf-8").splitlines()
|
|
41
|
+
assert len(lines) == 2
|
|
42
|
+
start, end = (json.loads(line) for line in lines)
|
|
43
|
+
assert start["kind"] == "start"
|
|
44
|
+
assert start["session_id"] == "ses_1"
|
|
45
|
+
assert start["directory"] == "/repo"
|
|
46
|
+
assert start["agent"] == "build"
|
|
47
|
+
assert start["task"] == "t" * 500
|
|
48
|
+
assert isinstance(start["ts"], int) and start["ts"] > 0
|
|
49
|
+
assert end["kind"] == "end"
|
|
50
|
+
assert end["session_id"] == "ses_1"
|
|
51
|
+
assert end["state"] == "completed"
|
|
52
|
+
assert end["elapsed_ms"] == 1234
|
|
53
|
+
assert end["files"] == 2
|
|
54
|
+
assert end["additions"] == 10
|
|
55
|
+
assert end["deletions"] == 1
|
|
56
|
+
assert len(end["changed_files"]) == 50
|
|
57
|
+
assert end["ts"] >= start["ts"]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_read_journal(tmp_path, monkeypatch):
|
|
61
|
+
custom = tmp_path / "d.jsonl"
|
|
62
|
+
monkeypatch.setenv(journal.ENV_JOURNAL, str(custom))
|
|
63
|
+
assert journal.read_journal() == []
|
|
64
|
+
journal.append_start("ses_2", "/r", "plan", "hello")
|
|
65
|
+
journal.append_end("ses_2", "/r", "error")
|
|
66
|
+
recs = journal.read_journal()
|
|
67
|
+
assert [r["kind"] for r in recs] == ["start", "end"]
|
|
68
|
+
assert recs[1]["state"] == "error"
|
|
69
|
+
assert journal.read_journal(custom) == recs
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_read_journal_skips_bad_lines(tmp_path):
|
|
73
|
+
p = tmp_path / "bad.jsonl"
|
|
74
|
+
p.write_text('{"kind": "start", "session_id": "s"}\nnot-json\n\n', encoding="utf-8")
|
|
75
|
+
recs = journal.read_journal(p)
|
|
76
|
+
assert len(recs) == 1
|
|
77
|
+
assert recs[0]["kind"] == "start"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_append_never_raises_on_unwritable_path(tmp_path, monkeypatch):
|
|
81
|
+
monkeypatch.setenv(journal.ENV_JOURNAL, "/proc/no-such-dir-ocmcp/delegations.jsonl")
|
|
82
|
+
journal.append_start("ses_x", "/r", "build", "task")
|
|
83
|
+
journal.append_end("ses_x", "/r", "completed", elapsed_ms=1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/requires.txt
RENAMED
|
File without changes
|
{opencode_hermes_mcp-0.4.1 → opencode_hermes_mcp-0.4.2}/opencode_hermes_mcp.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|