nexus/.planning/phases/27-hermes-adapter/27-RESEARCH.md
Nexus Dev 51eb2edf0b chore: complete v1.5 Smart Onboarding + Personal AI Assistant milestone
6 phases, 13 plans, 21 requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 23:03:46 +00:00

22 KiB
Raw Blame History

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:

  1. hermes_local is absent from SESSIONED_LOCAL_ADAPTERS in heartbeat.ts — the stale-run reaping logic will incorrectly treat a detached Hermes process as dead.
  2. config-fields.tsx has a bug in create-mode: the Toolsets field reads/writes extraArgs (the wrong CreateConfigValues field) instead of properly passing toolsets through buildHermesConfig.
  3. AGENT_ADAPTER_TYPES in packages/shared/src/constants.ts has a duplicate gemini_local entry — low-severity lint issue.
  4. The hermes-dual-source.test.ts test file already exists and all 7 tests pass. A hermes-session-codec.test.ts is absent from adapter-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 71189
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 7279.

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 7588.

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/server in UI code — server-only exports use Node APIs that don't work in the browser. The UI only imports from hermes-paperclip-adapter/ui.
  • Don't add toolset UI to CreateConfigValues — the shared type from @paperclipai/adapter-utils is upstream; use extraArgs or 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 — add hermes sessionCodec describe block covering serialize, deserialize, getDisplayId, and legacy session_id key 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:

  • addListener for media queries: not applicable here.

Open Questions

  1. Create-mode toolsets field

    • What we know: CreateConfigValues has no toolsets field; the current create-form code incorrectly uses extraArgs.
    • 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.
  2. detectModel capability in hermesLocalAdapter

    • What we know: hermesLocalAdapter has a detectModel property calling detectModelFromHermes(), which reads ~/.hermes/config.yaml. No other adapter currently uses detectModel.
    • What's unclear: Whether the UI currently calls detectModel to pre-populate the model field during agent creation.
    • Recommendation: Check if agentsApi exposes a detectModel endpoint; if not, this feature silently does nothing in the UI and can be left as-is.

Sources

Primary (HIGH confidence)

  • server/src/adapters/registry.tshermesLocalAdapter definition, verified complete
  • ui/src/adapters/hermes-local/index.ts and config-fields.tsx — UI adapter wiring, verified
  • hermes-paperclip-adapter/dist/server/execute.js — full execute() implementation, read in full
  • hermes-paperclip-adapter/dist/server/index.js — sessionCodec, read in full
  • hermes-paperclip-adapter/README.md — canonical usage docs, read in full
  • server/src/services/heartbeat.ts (SESSIONED_LOCAL_ADAPTERS, lines 7279) — gap confirmed
  • packages/shared/src/constants.ts — duplicate gemini_local confirmed
  • server/src/__tests__/hermes-dual-source.test.ts — 7/7 tests passing, verified by run

Secondary (MEDIUM confidence)

  • hermes-paperclip-adapter/dist/ui/build-config.jsbuildHermesConfig implementation confirming extraArgs handling
  • hermes-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)