6 phases, 13 plans, 21 requirements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
22 KiB
Phase 27: Hermes Adapter - Research
Researched: 2026-04-01 Domain: Hermes Agent adapter integration into Nexus (hermes-paperclip-adapter v0.2.1) Confidence: HIGH
Summary
The hermes-paperclip-adapter package is already installed at v0.2.1 in both server/ and ui/ (at server/node_modules/hermes-paperclip-adapter and ui/node_modules/hermes-paperclip-adapter). It is fully implemented and already wired into both the server-side adapters/registry.ts and the UI-side adapters/registry.ts. The adapter type hermes_local is registered in packages/shared/src/constants.ts, appears in NewAgentDialog.tsx, NewAgent.tsx, AgentDetail.tsx, and has a HermesIcon component and HermesLocalConfigFields form.
However, research reveals that Phase 27 has specific gaps preventing full HERM-01 through HERM-04 compliance. These are mostly small integration bugs and missing wiring rather than large new features:
hermes_localis absent fromSESSIONED_LOCAL_ADAPTERSinheartbeat.ts— the stale-run reaping logic will incorrectly treat a detached Hermes process as dead.config-fields.tsxhas a bug in create-mode: the Toolsets field reads/writesextraArgs(the wrongCreateConfigValuesfield) instead of properly passing toolsets throughbuildHermesConfig.AGENT_ADAPTER_TYPESinpackages/shared/src/constants.tshas a duplicategemini_localentry — low-severity lint issue.- The
hermes-dual-source.test.tstest file already exists and all 7 tests pass. Ahermes-session-codec.test.tsis absent fromadapter-session-codecs.test.ts— there is no test for the hermes session codec round-trip.
Primary recommendation: Fix the four gaps above. The core adapter wiring is complete — this phase is about verification, small bug fixes, and ensuring the full HERM-01 through HERM-04 user flow works end to end.
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
Claude's Discretion
All implementation choices are at Claude's discretion.
Deferred Ideas (OUT OF SCOPE)
None — discuss phase skipped. </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| HERM-01 | Hermes adapter is installed, enabled, and appears in the "Add Agent" dropdown | Package installed. hermesLocalAdapter registered in server registry. hermes_local in ADVANCED_ADAPTER_OPTIONS array in NewAgentDialog.tsx. HermesIcon exists. Already complete — confirm with smoke test. |
| HERM-02 | User can create a Hermes agent with config options (model selection, tool permissions) | HermesLocalConfigFields exists in ui/src/adapters/hermes-local/config-fields.tsx with model + toolsets + persistSession + timeoutSec fields. Bug in create-mode toolsets binding needs fix. |
| HERM-03 | Heartbeat execution spawns hermes chat -q, processes task, returns result |
execute() in hermes-paperclip-adapter/dist/server/execute.js is fully implemented. hermesLocalAdapter wired in server registry. Needs hermes_local added to SESSIONED_LOCAL_ADAPTERS. |
| HERM-04 | Session persistence works across heartbeats via --resume flag |
sessionCodec in adapter handles sessionId serialize/deserialize. execute() reads ctx.runtime?.sessionParams?.sessionId and passes --resume. Session saved in executionResult.sessionParams. No codec test exists yet. |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| hermes-paperclip-adapter | 0.2.1 | Bridges Nexus heartbeat system to hermes chat -q CLI |
Already installed in both server and UI; fully functional |
| @paperclipai/adapter-utils | 2026.325.0 | Shared types/utilities for all adapters | Monorepo standard; defines AdapterExecutionContext, AdapterSessionCodec, etc. |
Supporting
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| vitest | 3.2.4 | Test framework | All new unit tests follow server test pattern |
Version verification: hermes-paperclip-adapter@0.2.1 confirmed via node_modules/.pnpm/hermes-paperclip-adapter@0.2.1/ directory and package.json. ^0.2.0 is in both server/package.json and ui/package.json.
No installation needed — package is already installed. Run pnpm install only if lock file needs updating.
Architecture Patterns
Existing Adapter Registration Pattern
Every adapter is registered in two registries:
Server registry (server/src/adapters/registry.ts):
// Source: server/src/adapters/registry.ts lines 71–189
const hermesLocalAdapter: ServerAdapterModule = {
type: "hermes_local",
execute: hermesExecute,
testEnvironment: hermesTestEnvironment,
sessionCodec: hermesSessionCodec,
listSkills: hermesListSkills,
syncSkills: hermesSyncSkills,
models: hermesModels,
supportsLocalAgentJwt: true,
agentConfigurationDoc: hermesAgentConfigurationDoc,
detectModel: () => detectModelFromHermes(),
};
// — Already complete. No changes needed.
UI registry (ui/src/adapters/registry.ts):
// Source: ui/src/adapters/registry.ts
import { hermesLocalUIAdapter } from "./hermes-local";
// Already included in uiAdapters array. No changes needed.
UI adapter module (ui/src/adapters/hermes-local/index.ts):
export const hermesLocalUIAdapter: UIAdapterModule = {
type: "hermes_local",
label: "Hermes Agent",
parseStdoutLine: parseHermesStdoutLine, // from hermes-paperclip-adapter/ui
ConfigFields: HermesLocalConfigFields, // local component
buildAdapterConfig: buildHermesConfig, // from hermes-paperclip-adapter/ui
};
Heartbeat Execution Flow
Nexus heartbeat scheduler
→ heartbeat.ts: getServerAdapter("hermes_local")
→ registry.ts: hermesLocalAdapter.execute(ctx)
→ hermes-paperclip-adapter/server/execute.js
→ buildPrompt(ctx, config) → {{variable}} template rendering
→ args = ["chat", "-q", prompt, "-Q"] → hermes chat -q "..." -Q
→ if (prevSessionId) args.push("--resume", prevSessionId)
→ runChildProcess(runId, "hermes", args, ...)
→ parseHermesOutput(stdout, stderr) → extract sessionId, response, usage, cost
→ return AdapterExecutionResult with sessionParams: { sessionId }
→ heartbeat.ts: saves sessionParams to agentTaskSessions
→ next run: ctx.runtime.sessionParams.sessionId = last sessionId
Session Persistence Flow (HERM-04)
Run N:
ctx.runtime.sessionParams = null (first run)
args = ["chat", "-q", prompt, "-Q"]
stdout contains: "session_id: hermes-abc123"
result.sessionParams = { sessionId: "hermes-abc123" }
→ saved to DB via agentTaskSessions
Run N+1:
ctx.runtime.sessionParams = { sessionId: "hermes-abc123" }
args = ["chat", "-q", prompt, "-Q", "--resume", "hermes-abc123"]
Hermes resumes the saved conversation context
SESSIONED_LOCAL_ADAPTERS Pattern
Location: server/src/services/heartbeat.ts lines 72–79.
This set controls whether a run's orphaned PID is checked for liveness. All child-process adapters should be in this set. hermes_local is currently missing:
// Current (MISSING hermes_local):
const SESSIONED_LOCAL_ADAPTERS = new Set([
"claude_local", "codex_local", "cursor",
"gemini_local", "opencode_local", "pi_local",
]);
// Fix (ADD hermes_local):
const SESSIONED_LOCAL_ADAPTERS = new Set([
"claude_local", "codex_local", "cursor",
"gemini_local", "opencode_local", "pi_local",
"hermes_local", // ← add this
]);
Config-Fields Create-Mode Bug
Location: ui/src/adapters/hermes-local/config-fields.tsx lines 75–88.
In create mode (isCreate === true), the Toolsets field reads/writes values!.extraArgs and set!({ extraArgs: v }). This incorrectly stores toolsets as CLI extra-args. The buildHermesConfig function (called on form submit) reads v.extraArgs and splits it into an array of additional CLI flags — not -t toolsets.
The fix in create mode should set values!.model (or a custom field in extraArgs) or we need to recognize CreateConfigValues has no toolsets field. Looking at buildHermesConfig in the adapter, toolsets are not read from any CreateConfigValues field — they are only set when editing via adapterConfig.toolsets directly. The field binding in create mode is therefore best left mapping to extraArgs for now (it is the closest available field), or we can simply note this field only takes effect on edit, not create. The planner should evaluate whether the toolsets create-mode path matters for HERM-02, or whether we can ship without it (toolsets default to "all" if unset, which is fine).
Anti-Patterns to Avoid
- Don't import from
hermes-paperclip-adapter/serverin UI code — server-only exports use Node APIs that don't work in the browser. The UI only imports fromhermes-paperclip-adapter/ui. - Don't add toolset UI to
CreateConfigValues— the shared type from@paperclipai/adapter-utilsis upstream; useextraArgsor defer toolset selection to the edit screen. - Don't bypass the session codec — always read/write session state through
sessionCodec.serialize/deserialize; never store raw strings.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| CLI spawning | Custom child_process wrapper | runChildProcess from @paperclipai/adapter-utils/server-utils |
Handles PID tracking, timeout, SIGTERM/SIGKILL, log streaming |
| Hermes stdout parsing | Custom line-by-line parser | parseHermesStdoutLine from hermes-paperclip-adapter/ui |
Already handles all Hermes output patterns (tool cards, quiet-mode, session_id, etc.) |
| Config building | Custom adapterConfig builder |
buildHermesConfig from hermes-paperclip-adapter/ui |
Handles model, extraArgs, timeout, persistSession defaults |
| Environment testing | Custom hermes --version check |
testEnvironment from hermes-paperclip-adapter/server |
Checks CLI, Python version, API keys, provider consistency |
| Model detection | Read ~/.hermes/config.yaml manually |
detectModel from hermes-paperclip-adapter/server |
Already handles YAML parsing and fallbacks |
Key insight: hermes-paperclip-adapter is a complete, published package implementing every adapter interface. The plan should fix wiring gaps, not reimplement adapter logic.
Common Pitfalls
Pitfall 1: hermes_local Missing from SESSIONED_LOCAL_ADAPTERS
What goes wrong: When a Nexus server restarts mid-heartbeat, the orphaned Hermes child process is incorrectly reaped (marked as failed) because isTrackedLocalChildProcessAdapter("hermes_local") returns false. The liveness check never fires.
Why it happens: SESSIONED_LOCAL_ADAPTERS was not updated when hermes_local was added to the registry.
How to avoid: Add "hermes_local" to the set in heartbeat.ts.
Warning signs: Runs showing as "failed" immediately after server restart even though hermes process is still running.
Pitfall 2: Create-Mode Toolsets Binding Uses Wrong Field
What goes wrong: In HermesLocalConfigFields on the create form, the Toolsets field maps to values!.extraArgs, not a dedicated toolsets key. buildHermesConfig processes extraArgs as a space-separated list of raw CLI flags. Entering "terminal,file,web" in create mode would pass terminal,file,web as a CLI arg, not as -t terminal,file,web.
Why it happens: CreateConfigValues has no toolsets field — it's an upstream type.
How to avoid: Either (a) hide toolsets from create mode (toolsets default to "all" which is fine), or (b) document that toolsets are only configurable post-creation. Do not add a toolsets field to CreateConfigValues.
Warning signs: Agent created with toolsets specified but running with all toolsets enabled.
Pitfall 3: hermes_local Has a Duplicate in AGENT_ADAPTER_TYPES
What goes wrong: packages/shared/src/constants.ts has "gemini_local" twice in AGENT_ADAPTER_TYPES. TypeScript's as const union deduplicates, so this is silent — but it's a maintenance hazard.
Why it happens: Stale edit from when hermes_local was added.
How to avoid: Remove the duplicate "gemini_local" entry.
Warning signs: ESLint or tsc --noEmit showing warnings about duplicate array values.
Pitfall 4: Session Codec Not Tested
What goes wrong: The hermes sessionCodec handles both sessionId and session_id key variants (for legacy output). Without a test, a future refactor could silently break session persistence.
Why it happens: adapter-session-codecs.test.ts tests all other adapters but not hermes.
How to avoid: Add a hermes session codec test to adapter-session-codecs.test.ts.
Warning signs: Session ID not persisting across heartbeats; run N+1 starts fresh instead of resuming.
Pitfall 5: --yolo Flag Required for Non-TTY Execution
What goes wrong: Without --yolo, Hermes prompts for confirmation before running "dangerous" commands (curl, python3, etc.). Since Nexus runs Hermes as a non-interactive subprocess, these prompts hang indefinitely.
Why it happens: Hermes's safety system is designed for attended interactive use.
How to avoid: The adapter already appends --yolo unconditionally. Do not remove this flag.
Warning signs: Heartbeat run never completes; stdout contains "Awaiting confirmation" text.
Code Examples
Session Codec Round-Trip (for test)
// Source: hermes-paperclip-adapter/dist/server/index.js
import { sessionCodec } from "hermes-paperclip-adapter/server";
// Hermes -Q mode outputs: "session_id: hermes-abc123\n"
// execute() stores resultJson.session_id → executionResult.sessionParams = { sessionId: "hermes-abc123" }
const params = sessionCodec.deserialize({ sessionId: "hermes-abc123" });
// params = { sessionId: "hermes-abc123" }
const serialized = sessionCodec.serialize(params);
// serialized = { sessionId: "hermes-abc123" }
sessionCodec.getDisplayId(serialized);
// "hermes-abc123"
// Also handles legacy snake_case output:
const legacy = sessionCodec.deserialize({ session_id: "hermes-legacy-456" });
// legacy = { sessionId: "hermes-legacy-456" }
Execute Context Shape
// Source: hermes-paperclip-adapter/dist/server/execute.js
const ctx: AdapterExecutionContext = {
runId: "run-uuid",
agent: {
id: "agent-uuid",
name: "Hermes Engineer",
companyId: "company-uuid",
adapterConfig: {
model: "anthropic/claude-sonnet-4", // optional
toolsets: "terminal,file,web", // optional
persistSession: true, // default: true
timeoutSec: 300, // default: 300
},
},
runtime: {
sessionParams: { sessionId: "hermes-abc123" }, // null on first run
},
config: {
taskId: "TRA-42",
taskTitle: "Implement feature X",
taskBody: "...",
},
onLog: async (stream, chunk) => { /* stream stdout/stderr to UI */ },
};
SESSIONED_LOCAL_ADAPTERS Fix
// Source: server/src/services/heartbeat.ts line 72
const SESSIONED_LOCAL_ADAPTERS = new Set([
"claude_local",
"codex_local",
"cursor",
"gemini_local",
"opencode_local",
"pi_local",
"hermes_local", // ← add
]);
Runtime State Inventory
This phase does not involve rename/refactor — no runtime state migration required.
Stored data: None — no Hermes-specific records to migrate.
Live service config: None — Hermes config lives in ~/.hermes/config.yaml on the user's machine, not in Nexus state.
OS-registered state: None.
Secrets/env vars: None — API keys are in ~/.hermes/.env, not Nexus-managed.
Build artifacts: None.
Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|---|---|---|---|---|
| hermes-paperclip-adapter npm pkg | HERM-01 through HERM-04 | Yes | 0.2.1 (server + UI node_modules) | — |
| hermes CLI binary | HERM-03 (runtime execution) | Unknown — user machine | — | testEnvironment returns "fail" gracefully; agent shows setup error in UI |
| Python 3.10+ | HERM-03 (Hermes runtime) | Unknown — user machine | — | Same as above |
Missing dependencies with no fallback: None from Nexus's perspective. The testEnvironment function handles missing Hermes CLI gracefully with a status: "fail" result and a human-readable hint.
Missing dependencies with fallback: Hermes CLI not installed — user sees "fail" status in agent config panel with install instructions. This is expected behavior, not a blocking issue for the phase.
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | vitest 3.2.4 |
| Config file | server/vitest.config.ts |
| Quick run command | pnpm --filter server exec vitest run src/__tests__/hermes-dual-source.test.ts |
| Full suite command | pnpm test:run |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| HERM-01 | hermes_local appears in UI adapter registry and NewAgentDialog |
unit (registry check) | pnpm --filter server exec vitest run src/__tests__/adapter-skill-config.test.ts |
Yes |
| HERM-02 | HermesLocalConfigFields renders model + toolsets fields |
unit/smoke | pnpm --filter server exec vitest run src/__tests__/hermes-dual-source.test.ts |
Yes (skill tests) |
| HERM-03 | Heartbeat execution spawns hermes chat -q and returns result |
integration | pnpm --filter server exec vitest run src/__tests__/hermes-dual-source.test.ts |
Partial |
| HERM-04 | Session persistence via --resume across heartbeats |
unit (session codec) | pnpm --filter server exec vitest run src/__tests__/adapter-session-codecs.test.ts |
Needs hermes case added |
Sampling Rate
- Per task commit:
pnpm --filter server exec vitest run src/__tests__/hermes-dual-source.test.ts src/__tests__/adapter-session-codecs.test.ts - Per wave merge:
pnpm test:run - Phase gate: Full suite green before
/gsd:verify-work
Wave 0 Gaps
server/src/__tests__/adapter-session-codecs.test.ts— addhermes sessionCodecdescribe block covering serialize, deserialize, getDisplayId, and legacysession_idkey variant
(All other test infrastructure is already present)
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Custom execute function per adapter | Use runChildProcess from @paperclipai/adapter-utils/server-utils |
adapter-utils 2026.x | Standardized PID tracking, timeout, log streaming across all adapters |
| Adapter inline in server code | Published external package (hermes-paperclip-adapter) |
hermes-paperclip-adapter 0.2.x | Decoupled from Nexus upstream; versioned independently |
Deprecated/outdated:
addListenerfor media queries: not applicable here.
Open Questions
-
Create-mode toolsets field
- What we know:
CreateConfigValueshas notoolsetsfield; the current create-form code incorrectly usesextraArgs. - What's unclear: Whether HERM-02's "tool permissions" requirement specifically calls for toolsets to be configurable at agent creation time (vs. post-creation in the edit form).
- Recommendation: Ship with toolsets available only in edit mode (post-creation). Default is "all toolsets" which is sensible. Document in agent config panel.
- What we know:
-
detectModelcapability inhermesLocalAdapter- What we know:
hermesLocalAdapterhas adetectModelproperty callingdetectModelFromHermes(), which reads~/.hermes/config.yaml. No other adapter currently usesdetectModel. - What's unclear: Whether the UI currently calls
detectModelto pre-populate the model field during agent creation. - Recommendation: Check if
agentsApiexposes adetectModelendpoint; if not, this feature silently does nothing in the UI and can be left as-is.
- What we know:
Sources
Primary (HIGH confidence)
server/src/adapters/registry.ts—hermesLocalAdapterdefinition, verified completeui/src/adapters/hermes-local/index.tsandconfig-fields.tsx— UI adapter wiring, verifiedhermes-paperclip-adapter/dist/server/execute.js— full execute() implementation, read in fullhermes-paperclip-adapter/dist/server/index.js— sessionCodec, read in fullhermes-paperclip-adapter/README.md— canonical usage docs, read in fullserver/src/services/heartbeat.ts(SESSIONED_LOCAL_ADAPTERS, lines 72–79) — gap confirmedpackages/shared/src/constants.ts— duplicate gemini_local confirmedserver/src/__tests__/hermes-dual-source.test.ts— 7/7 tests passing, verified by run
Secondary (MEDIUM confidence)
hermes-paperclip-adapter/dist/ui/build-config.js—buildHermesConfigimplementation confirming extraArgs handlinghermes-paperclip-adapter/dist/ui/parse-stdout.js— full stdout parser implementation
Tertiary (LOW confidence)
- None.
Metadata
Confidence breakdown:
- Standard stack: HIGH — packages installed, imports verified, tests passing
- Architecture: HIGH — full implementation read from source
- Pitfalls: HIGH — gaps confirmed by reading actual source files
- Session persistence: HIGH — codec and execute() both read in full
Research date: 2026-04-01 Valid until: 2026-05-01 (30 days — adapter package is stable)