# 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`):
```typescript
// 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`):
```typescript
// 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`):
```typescript
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:

```typescript
// 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/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)
```typescript
// 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
```typescript
// 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
```typescript
// 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.ts` — `hermesLocalAdapter` 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 72–79) — 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.js` — `buildHermesConfig` 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)
