Skip to content

feat: claude-code-relay provider + hermes-tools MCP server - #20

Merged
jarvis-stark-ops merged 16 commits into
mainfrom
claude-code-relay-2026-06-15
Jun 16, 2026
Merged

feat: claude-code-relay provider + hermes-tools MCP server#20
jarvis-stark-ops merged 16 commits into
mainfrom
claude-code-relay-2026-06-15

Conversation

@jarvis-stark-ops

Copy link
Copy Markdown
Collaborator

Summary

Implements Phase 0a + 0b of Milestone 1 (1Team-Engineering/hermes-jarvis#106) — the foundation, MCP server, DB state, provider plugin, transport, lifecycle hooks, and boot checks for the claude-code-relay provider.

This unlocks Hermes profiles to bill against the Max plan (via interactive claude CLI in tmux) instead of API extra-usage, fixing the HTTP 400 wall hit on 2026-06-15 during Phase 2.6 build.

Spec: ~/.hermes/plans/claude-code-relay-provider-2026-06-15.md (v3 — per-(profile, project) sessions)
Implementation plan: ~/.hermes/plans/claude-code-relay-milestone-1-implementation-plan-2026-06-15.md

What this PR ships

Layer What Tests
MCP server (hermes_cli/mcp_servers/) stdio JSON-RPC server exposing filtered Hermes tool registry to claude --mcp-config 5
DB state (hermes_cli/kanban_db.py) task_sessions table + 6 helpers (upsert/get/touch/mark_compacted/evict/list_idle) 5
Helpers (agent/claude_code_relay_helpers.py) derive_project (git root basename or scratch, no silent default — B11), build_task_header 7
Provider (plugins/model-providers/claude-code-relay/) ProviderProfile registration with api_mode='claude_code_relay', auth_type='none' (claude binary handles OAuth) 1
Transport (agent/claude_code_relay_transport.py) ScopeContext, ensure_scope (idempotent spawn), send_turn, chat_completion OpenAI-compat 4
Lifecycle hooks (hermes_cli/kanban_db.py, hermes_cli/relay_eviction.py) /compact on kanban_complete/block (B10 non-blocking), idle-eviction at 24h default 5
Boot check _check_stop_hook validates ~/.claude/settings.json has the Hermes Stop hook (B3 enforcement) 3

Sibling work on ~/.hermes branch spike/tmux-relay-slack-routing-2026-06-13 (4 commits, ships separately):

  • _lib.sh encode_cwd realpath fix (macOS /tmp symlink bug)
  • relay-spawn-scope.sh — per-(profile, project) tmux+claude spawner
  • relay-kill-scope.sh + state_clear helper
  • relay-send.sh slash-command extension (no Stop-hook wait for /usage, /compact, etc.)

Bypass discipline (spec §3) enforced

  • B2 No silent fallback to allowedTools='*' — missing MCP config fails loud
  • B3 Stop-hook is mandatory — boot-check raises ProviderError if missing
  • B8 Scope-name collision guard — [a-zA-Z0-9_-]+ validation in spawn script
  • B10 /compact failures don't block next task
  • B11 No silent project-derivation default — raises clear ProviderError

Test plan

  • All 30 unit + integration tests pass locally (pytest tests/test_hermes_tools_mcp.py tests/test_claude_code_relay_helpers.py tests/test_task_sessions_db.py tests/test_relay_compact_hook.py tests/test_relay_eviction.py)
  • S1 smoke confirmed inline: hermes-tools-mcp boots and responds to initialize
  • E1 acceptance (1Team-Engineering/hermes-jarvis#118) — Friday opt-in + Phase 2.6 build retry (post-merge)
  • E2 acceptance (1Team-Engineering/hermes-jarvis#119) — Multi-task continuity proof
  • Fleet rollout (1Team-Engineering/hermes-jarvis#120) — Cap, Elon, Tony, Tchalla, JARVIS opt-in + 48h observation

Notable adaptations from spec

The subagents implementing this PR adapted to the actual codebase shape:

  • ProviderProfile uses auth_type (not auth_kind), fallback_models (not models)
  • Tool registry API is get_all_tool_names(), get_entry(), dispatch() (used for the MCP tools/list/tools/call handlers)
  • Provider lives at plugins/model-providers/claude-code-relay/ (the actual discovery path)
  • Substituted real tool names where the plan listed nonexistent ones (memory, patch, kanban_list in place of kanban_log, edit_file, gbrain_memory_*)
  • init_db extended to accept sqlite3.Connection for in-memory test fixtures
  • Both SCHEMA_SQL AND additive ALTER add the tasks.provider column (fresh in-memory DBs need it without migration)

These improvements should be reviewed as part of the merge gate.

Empirical foundation

S0 pre-flight + 2 spikes (all 2026-06-15) established:

  1. Interactive claude bills Max plan (rate_limit_event: isUsingOverage=false, overageDisabledReason=org_level_disabled)
  2. claude --resume <session-id> preserves multi-turn context across tmux kill+respawn
  3. Empty scope cwd auto-discovers NO project context (clean profile SOUL injection via --append-system-prompt-file)
  4. /compact has minimum-turn threshold ("Not enough messages to compact") — handled gracefully via B10

Linked issues

  • Closes 1Team-Engineering/hermes-jarvis#107 (_lib.sh realpath fix — separate branch but same milestone)
  • Closes 1Team-Engineering/hermes-jarvis#108 (relay-spawn-scope.sh + relay-kill-scope.sh — separate branch)
  • Closes 1Team-Engineering/hermes-jarvis#109 (hermes-tools-mcp server)
  • Closes 1Team-Engineering/hermes-jarvis#111 (relay-send.sh slash extension — separate branch)
  • Closes 1Team-Engineering/hermes-jarvis#112 (claude_code_relay provider + transport)
  • Closes 1Team-Engineering/hermes-jarvis#113 (task_sessions DB table)
  • Closes 1Team-Engineering/hermes-jarvis#114 (project derivation + task header)
  • Closes 1Team-Engineering/hermes-jarvis#115 (/compact between-task hook)
  • Closes 1Team-Engineering/hermes-jarvis#116 (idle-eviction cron)
  • Closes 1Team-Engineering/hermes-jarvis#117 (unit + integration tests)

🤖 Generated with Claude Code

Jarvis and others added 16 commits June 15, 2026 15:02
NousResearch#109

Minimal stdio JSON-RPC server that responds to MCP 'initialize' with
the expected protocol version + capabilities. Tools surface comes in
follow-up commits. Env-bound to (profile, project) scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109

Filters the live Hermes tool registry to the v1 surface defined in
spec §4.1. Includes special hermes_set_task tool for per-turn task
context updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109

hermes_set_task updates per-turn task context (task_id, workspace).
All other tools/call requests filter through ALLOWED_TOOLS, then
dispatch via registry.dispatch() which handles both sync and async
tools. Denied tools (kanban_dispatch etc.) return isError=true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#113

Per spec §4.7 — tracks (profile, project) → tmux session, claude
session_id, FIFO path, MCP config path, project root, scope cwd,
task count, timestamps. Helpers: upsert_session, get_session,
touch_session, mark_compacted, evict_session, list_idle_sessions.
NousResearch#114

derive_project maps a kanban workspace to its 'project' key:
  - scratch workspaces -> 'scratch'
  - git workspaces -> basename of git root
  - else raises ProviderError (B11: no silent default)

build_task_header prepends '[task_id=... workspace=...]' to each
user prompt so claude knows the current task without an MCP roundtrip.
NousResearch#112

Registers the provider via the existing bundled-plugin pattern under
plugins/model-providers/claude-code-relay/__init__.py. api_mode=
'claude_code_relay' is a new tag for the transport defined in the
next commit. auth_type='none' because the claude binary handles its
own OAuth — the relay just shells out.
NousResearch#112

ScopeContext bundles (profile, project, workspace). ensure_scope
either finds a live tmux or invokes relay-spawn-scope.sh (writing
the MCP config first). send_turn prepends the task header and
relays through relay-send.sh, returning the captured response.
Also added monkeypatch for derive_project_root in spawn test since
tmp_path is not a git repo.
NousResearch#112

chat_completion() returns the standard OpenAI ChatCompletion object.
Only the latest user message is forwarded — prior conversation rides
on claude's in-session memory. Usage fields left null; /compact hook
in next task captures /usage and backfills.
NousResearch#115

After a task with provider=claude-code-relay completes or blocks,
send /compact to its (profile, project) scope. Per B10, failures
are logged but never block the completion. Records last_compacted_at
on task_sessions.

Also: add tasks.provider column to SCHEMA_SQL + additive ALTER migration;
set conn.row_factory=sqlite3.Row in init_db test-fixture shortcut so
complete_task works correctly in :memory: tests.

Spike 2 (2026-06-15) confirmed /compact returns 'Not enough messages
to compact' on short sessions; that's handled cleanly via the timeout
in relay-send.sh slash-command extension.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#116

evict_idle_scopes deletes task_sessions rows whose last_used_at is
older than HERMES_RELAY_IDLE_EVICT_SECS (default 86400 = 24h),
and tears down the tmux session via relay-kill-scope.sh.

Wire into nightly-hermes-maintenance follows separately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NousResearch#109

B3: Stop hook is mandatory for the relay to detect turn-end via FIFO.
Boot-check raises ProviderError with installer path if the hook is
missing — fail loud instead of silently hanging 180s per turn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per code review on PR #20 — the shell relay-spawn-scope.sh validates
^[a-zA-Z0-9_-]+$ but Python-side _write_mcp_config writes /tmp/hermes-mcp-{slug}.json
BEFORE invoking the shell. Slugs containing / or .. would have allowed
path traversal out of /tmp.

Add same regex check in ScopeContext.__post_init__. Closes B8 (spec §3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Defense-in-depth: prevent workspace paths in the MCP config from leaking
to other users sharing /tmp. Per code review on PR #20.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Transport reads HERMES_RELAY_BIN; the kanban_db /compact hook should
too. Per code review on PR #20.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per code review on PR #20 — previously evicted the DB row even on kill
failure, orphaning the tmux session with no record to retry. Now check
rc; only evict DB row on rc=0. Failed scopes get retried on next cron tick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per code review on PR #20 — exit 4 means partial response, but callers
had no signal. Add a logger.warning so operators see the partial in logs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@jarvis-stark-ops
jarvis-stark-ops merged commit a0f0076 into main Jun 16, 2026
@jarvis-stark-ops
jarvis-stark-ops deleted the claude-code-relay-2026-06-15 branch June 16, 2026 02:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant