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:
@@ -14,7 +14,7 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
||||
|
||||
@property
|
||||
def ready_tokens(self) -> str:
|
||||
return 'Hermes'
|
||||
return 'Hermes|Welcome to Hermes Agent|NOUS HERMES'
|
||||
|
||||
@property
|
||||
def exit_key(self) -> str:
|
||||
@@ -28,6 +28,18 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
||||
def identity_cache_fields(self) -> tuple:
|
||||
return ('session_id',)
|
||||
|
||||
@property
|
||||
def input_prompt(self) -> str:
|
||||
return '❯'
|
||||
|
||||
@property
|
||||
def input_placeholder(self) -> str:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def input_rule_pattern(self) -> str:
|
||||
return '─{10,}'
|
||||
|
||||
def artifact_path(self, uuid: str, ctx: DiscoveryContext) -> str:
|
||||
return f"{ctx.home_dir}/.hermes/sessions/session_{uuid}.json"
|
||||
|
||||
@@ -35,15 +47,15 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
||||
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
return False
|
||||
if ctx.epoch and os.path.getmtime(hdb) < ctx.epoch:
|
||||
return False
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT cwd FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||
r = conn.execute("SELECT cwd, started_at FROM sessions WHERE id=?", (uuid,)).fetchone()
|
||||
conn.close()
|
||||
if not r:
|
||||
return False
|
||||
found_cwd = r[0]
|
||||
found_cwd, started_at = r[0], r[1]
|
||||
if ctx.epoch and started_at and float(started_at) < float(ctx.epoch):
|
||||
return False
|
||||
if found_cwd and workspace_key(found_cwd) != workspace_key(ctx.cwd):
|
||||
return False
|
||||
except Exception:
|
||||
@@ -70,27 +82,37 @@ class HermesAgentAdapter(BaseAgentAdapter):
|
||||
return purged
|
||||
|
||||
def spawn_spec(self, binary: str, session_uuid: str = "", use_wrapper: bool = False) -> str:
|
||||
return binary
|
||||
return f"{binary} --yolo --accept-hooks"
|
||||
|
||||
def resume_spec(self, binary: str, session_uuid: str, materialized: bool = False) -> str:
|
||||
if materialized and session_uuid:
|
||||
return f"{binary} --resume {session_uuid}"
|
||||
return binary
|
||||
return f"{binary} --resume {session_uuid} --no-restore-cwd --yolo --accept-hooks"
|
||||
return f"{binary} --yolo --accept-hooks"
|
||||
|
||||
def auth_ok(self, run_cmd: Optional[Any] = None) -> bool:
|
||||
return True
|
||||
|
||||
def discover(self, ctx: DiscoveryContext) -> list:
|
||||
hdb = f"{ctx.home_dir}/.hermes/state.db"
|
||||
if os.path.exists(hdb):
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (ctx.workspace,)).fetchone()
|
||||
conn.close()
|
||||
if r:
|
||||
cand = r[0]
|
||||
if cand and self.verify_artifact(cand, ctx):
|
||||
return [cand]
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
if not os.path.exists(hdb):
|
||||
return []
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
if ctx.epoch:
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||
(ctx.workspace, ctx.epoch)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 20",
|
||||
(ctx.workspace,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
candidates = []
|
||||
for (cand,) in rows:
|
||||
if cand and self.verify_artifact(cand, ctx):
|
||||
candidates.append(cand)
|
||||
return candidates
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
@@ -154,6 +154,9 @@ case "$AGENT" in
|
||||
agy)
|
||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "agy --dangerously-skip-permissions"
|
||||
;;
|
||||
hermes)
|
||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "hermes --yolo --accept-hooks"
|
||||
;;
|
||||
grok)
|
||||
herdr new-session -d -s "$SESSION_NAME" -x 140 -y 40 -c "$WORKSPACE" "grok --permission-mode bypassPermissions"
|
||||
;;
|
||||
|
||||
@@ -185,7 +185,7 @@ if [ -z "$CMD_FULL" ]; then
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --session-id ${SESSION_UUID}" ;;
|
||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN}" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --yolo --accept-hooks" ;;
|
||||
cline) CMD_FULL="${RESOLVED_BIN} -i" ;;
|
||||
esac
|
||||
fi
|
||||
@@ -219,6 +219,9 @@ if [ "$DRY_RUN" = "1" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
HERDR_EPOCH=$(date +%s)
|
||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
spawn
|
||||
|
||||
# Trap for rolling back/cleaning up herdr session if script exits due to error
|
||||
@@ -287,8 +290,6 @@ fi
|
||||
PANE_PID=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_pid}' 2>/dev/null || echo "")
|
||||
PANE_CWD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_path}' 2>/dev/null || echo "$WORKSPACE")
|
||||
PANE_CMD=$(_herdr list-panes -t "$SESSION_NAME" -F '#{pane_current_command}' 2>/dev/null || echo "$AGENT")
|
||||
HERDR_EPOCH=$(date +%s)
|
||||
NOW_ISO=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# 시작 명령
|
||||
# NOTE: this must match what `spawn()` actually ran above — env-var-driven
|
||||
|
||||
@@ -736,19 +736,36 @@ for s in d.get('herdr_sessions', []):
|
||||
hdb = f"{home}/.hermes/state.db"
|
||||
if not os.path.exists(hdb):
|
||||
continue
|
||||
|
||||
|
||||
epoch_threshold = s.get('herdr_session_epoch', 0)
|
||||
|
||||
sibling_claimed = [
|
||||
other.get('hermes_conversation_id_own')
|
||||
for other in d.get('herdr_sessions', [])
|
||||
if other is not s
|
||||
and (other.get('pane') or {}).get('cwd') == cwd
|
||||
and other.get('status') not in ('stopped', 'terminated')
|
||||
and other.get('hermes_conversation_id_own')
|
||||
]
|
||||
s_eval = dict(s)
|
||||
s_eval['_sibling_claimed_uuids'] = sibling_claimed
|
||||
|
||||
valid_candidates = []
|
||||
try:
|
||||
conn = sqlite3.connect(hdb)
|
||||
r = conn.execute("SELECT id FROM sessions WHERE cwd=? ORDER BY started_at DESC LIMIT 1", (cwd,)).fetchone()
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM sessions WHERE cwd=? AND started_at >= ? ORDER BY started_at DESC LIMIT 20",
|
||||
(cwd, epoch_threshold)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
if r:
|
||||
uuid = r[0]
|
||||
if verify_session_uuid(cwd, 'hermes', uuid, s, mode="discover"):
|
||||
for (uuid,) in rows:
|
||||
if uuid in (sibling_claimed or []):
|
||||
continue
|
||||
if verify_session_uuid(cwd, 'hermes', uuid, s_eval, mode="discover"):
|
||||
valid_candidates.append(uuid)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if len(valid_candidates) > 1:
|
||||
drifts.append({'class': 'C-ambiguous', 'name': s['name'],
|
||||
'msg': f"{s['name']}: {len(valid_candidates)} candidate transcripts newer than session epoch; not pinning"})
|
||||
|
||||
@@ -87,7 +87,7 @@ fi
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="claude --dangerously-skip-permissions $CLAUDE_ID_FLAG $UUID" ;;
|
||||
agy) CMD_FULL="agy --dangerously-skip-permissions --conversation $UUID" ;;
|
||||
hermes) CMD_FULL="hermes --resume $UUID" ;;
|
||||
hermes) CMD_FULL="hermes --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||
cline) CMD_FULL="cline -i --id $UUID" ;;
|
||||
grok) CMD_FULL="grok --resume $UUID --permission-mode bypassPermissions" ;;
|
||||
esac
|
||||
|
||||
@@ -107,7 +107,7 @@ if [ -z "$CMD_FULL" ]; then
|
||||
case "$AGENT" in
|
||||
claude) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions -r $UUID" ;;
|
||||
agy) CMD_FULL="${RESOLVED_BIN} --dangerously-skip-permissions --conversation $UUID" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID" ;;
|
||||
hermes) CMD_FULL="${RESOLVED_BIN} --resume $UUID --no-restore-cwd --yolo --accept-hooks" ;;
|
||||
cline) CMD_FULL="${RESOLVED_BIN} -i --id $UUID" ;;
|
||||
grok) CMD_FULL="${RESOLVED_BIN} --resume $UUID --permission-mode bypassPermissions" ;;
|
||||
*) echo "ERROR: unsupported agent: $AGENT" >&2; exit 2 ;;
|
||||
|
||||
Reference in New Issue
Block a user