feat(hermes): modernize hermes agent adapter and skills support

- Add --yolo and --accept-hooks headless auto-approval flags to spawn_spec and resume_spec
- Define Hermes TUI input delimiters (input_prompt='❯', input_rule_pattern='─{10,}')
- Refactor verify_artifact and discover in hermes.py using session started_at timestamps
- Fix HERDR_EPOCH capture timing before spawn in create_session.sh to prevent epoch race
- Update reconcile.sh for hermes drift-C multi-candidate and sibling claimed exclusion
- Add Hermes contract, epoch filtering, and C-ambiguous unit tests (140 passed)
- Add agent evaluation report and final review reports for Hermes support
This commit is contained in:
2026-08-28 20:17:04 +09:00
parent 4a3328d0b7
commit 6208a7fda3
12 changed files with 624 additions and 34 deletions
+157 -3
View File
@@ -72,7 +72,7 @@ def test_adapter_required_properties():
expected = {
'claude': ('Anthropic|Assistant|Chat|Welcome|Claude Code|Opus|Sonnet|Haiku', '/exit', 'claude-code', ('session_id', 'session_jsonl', 'session_size_bytes', 'session_lines')),
'agy': ('Antigravity', 'Exit', 'antigravity-cli', ('conversation_id', 'conversation_db', 'conversation_brain_dir')),
'hermes': ('Hermes', '/exit', 'hermes-agent', ('session_id',)),
'hermes': ('Hermes|Welcome to Hermes Agent|NOUS HERMES', '/exit', 'hermes-agent', ('session_id',)),
'cline': ('Cline|history|Chat|What can I do|slash commands', '/exit', 'cline-agent', ('session_id',)),
'grok': ('Grok|xAI|Assistant||>>>', '/exit', 'grok-build', ('session_id', 'session_jsonl')),
}
@@ -223,8 +223,9 @@ def test_adapter_spawn_and_resume_specs():
assert agy.resume_spec('agy', 'u1', materialized=True) == 'agy --dangerously-skip-permissions --conversation u1'
hermes = get_adapter('hermes')
assert hermes.spawn_spec('hermes', 'u1') == 'hermes'
assert hermes.resume_spec('hermes', 'u1', materialized=True) == 'hermes --resume u1'
assert hermes.spawn_spec('hermes', 'u1') == 'hermes --yolo --accept-hooks'
assert hermes.resume_spec('hermes', 'u1', materialized=True) == 'hermes --resume u1 --no-restore-cwd --yolo --accept-hooks'
assert hermes.resume_spec('hermes', 'u1', materialized=False) == 'hermes --yolo --accept-hooks'
cline = get_adapter('cline')
assert cline.spawn_spec('cline', 'u1') == 'cline -i'
@@ -397,3 +398,156 @@ def test_wait_for_tui_ready_missing_tokens_diagnostic(mam_sandbox):
res = subprocess.run(["bash", "-c", cmd], capture_output=True, text=True)
assert res.returncode != 0
assert "no ready tokens for agent 'bogus-agent'" in res.stderr
def test_hermes_input_region_properties():
adapter = get_adapter('hermes')
assert adapter.input_prompt == ''
assert adapter.input_placeholder == ''
assert adapter.input_rule_pattern == '{10,}'
def test_hermes_verify_artifact_started_at_epoch_filtering(tmp_path):
import sqlite3
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
hdb = f"{home}/.hermes/state.db"
os.makedirs(os.path.dirname(hdb), exist_ok=True)
conn = sqlite3.connect(hdb)
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
conn.execute("INSERT INTO sessions VALUES ('u-old', ?, 1000.0)", (ws,))
conn.execute("INSERT INTO sessions VALUES ('u-new', ?, 2000.0)", (ws,))
conn.commit()
conn.close()
adapter = get_adapter('hermes')
ctx_old = DiscoveryContext(workspace=ws, agent_name='hermes', home_dir=home, epoch=1500)
assert adapter.verify_artifact('u-old', ctx_old) is False
assert adapter.verify_artifact('u-new', ctx_old) is True
def test_hermes_reconcile_dual_direction_epoch_guards(tmp_path):
import sqlite3
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
hdb = f"{home}/.hermes/state.db"
os.makedirs(os.path.dirname(hdb), exist_ok=True)
conn = sqlite3.connect(hdb)
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
# Direction 1: Old historical session (started_at=1000) + 1 new session (started_at=2000)
conn.execute("INSERT INTO sessions VALUES ('u-hist', ?, 1000.0)", (ws,))
conn.execute("INSERT INTO sessions VALUES ('u-fresh1', ?, 2000.0)", (ws,))
conn.commit()
epoch = 1500
s_row = {'name': 'sess1', 'pane': {'cwd': ws}, 'herdr_session_epoch': epoch}
# Query with epoch filter (Rev.2 pattern)
rows = conn.execute(
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
(ws, epoch)
).fetchall()
valid = [u for (u,) in rows if verify_session_uuid(ws, 'hermes', u, s_row, home_dir=home, mode="discover")]
assert valid == ['u-fresh1']
assert len(valid) == 1 # Exactly 1, no false C-ambiguous!
# Direction 2: Add a second concurrent fresh session (started_at=2100) -> must detect C-ambiguous
conn.execute("INSERT INTO sessions VALUES ('u-fresh2', ?, 2100.0)", (ws,))
conn.commit()
conn.close()
rows2 = sqlite3.connect(hdb).execute(
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
(ws, epoch)
).fetchall()
valid2 = [u for (u,) in rows2 if verify_session_uuid(ws, 'hermes', u, s_row, home_dir=home, mode="discover")]
assert len(valid2) == 2 # Ambiguous detected correctly!
def test_hermes_verify_artifact_spawn_epoch_timing_f1(tmp_path):
"""F1 Regression: Ensure spawn-time epoch capture allows freshly started session to be verified."""
import sqlite3, time
from lib_py.agents.base import DiscoveryContext
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
# 1. Pre-spawn epoch capture (T0)
spawn_epoch = int(time.time())
# 2. Process spawns and writes session row (T0 + 0.1s)
session_started_at = spawn_epoch + 0.1
hdb = f"{home}/.hermes/state.db"
os.makedirs(os.path.dirname(hdb), exist_ok=True)
conn = sqlite3.connect(hdb)
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
conn.execute("INSERT INTO sessions VALUES ('u-fresh', ?, ?)", (ws, session_started_at))
conn.execute("INSERT INTO sessions VALUES ('u-old', ?, ?)", (ws, spawn_epoch - 3600.0))
conn.commit()
conn.close()
adapter = get_adapter('hermes')
# Context with pre-spawn epoch
ctx_spawn = DiscoveryContext(workspace=ws, agent_name='hermes', home_dir=home, epoch=spawn_epoch)
assert adapter.verify_artifact('u-fresh', ctx_spawn) is True
assert adapter.verify_artifact('u-old', ctx_spawn) is False
# Discover with pre-spawn epoch returns the fresh candidate only
assert adapter.discover(ctx_spawn) == ['u-fresh']
def test_hermes_reconcile_full_block_integration(tmp_path):
"""F3: Verify hermes drift-C block behavior with sibling_claimed exclusion."""
import sqlite3
from lib_py.agents.base import DiscoveryContext
from lib_py.verify_session import verify_session_uuid
ws = str(tmp_path / "ws")
home = str(tmp_path / "home")
os.makedirs(ws, exist_ok=True)
os.makedirs(home, exist_ok=True)
hdb = f"{home}/.hermes/state.db"
os.makedirs(os.path.dirname(hdb), exist_ok=True)
conn = sqlite3.connect(hdb)
conn.execute("CREATE TABLE sessions (id TEXT, cwd TEXT, started_at REAL)")
conn.execute("INSERT INTO sessions VALUES ('u-claimed', ?, 2000.0)", (ws,))
conn.execute("INSERT INTO sessions VALUES ('u-target', ?, 2001.0)", (ws,))
conn.commit()
conn.close()
epoch = 1900
s_eval = {
'name': 'hermes-target-01',
'pane': {'cwd': ws},
'herdr_session_epoch': epoch,
'_sibling_claimed_uuids': ['u-claimed']
}
# Simulate reconcile.sh candidate gathering
conn = sqlite3.connect(hdb)
rows = conn.execute(
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
(ws, epoch)
).fetchall()
conn.close()
valid_candidates = []
for (uuid,) in rows:
if uuid in s_eval['_sibling_claimed_uuids']:
continue
if verify_session_uuid(ws, 'hermes', uuid, s_eval, home_dir=home, mode="discover"):
valid_candidates.append(uuid)
assert valid_candidates == ['u-target']