nexus/.planning/phases/27-hermes-adapter/27-RESEARCH.md
Nexus Dev 285bf585be 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-04 03:55:49 +00:00

406 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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