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:
@@ -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']
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user