docs(27): research phase hermes-adapter domain

This commit is contained in:
Nexus Dev 2026-04-02 16:16:23 +00:00
parent 78669bfce1
commit 4db0b08acb

View file

@ -0,0 +1,406 @@
# 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)