From 6328c4a5f980fda9525635022546f72e86f4ebd0 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Thu, 2 Apr 2026 16:16:23 +0000 Subject: [PATCH] docs(27): research phase hermes-adapter domain --- .../phases/27-hermes-adapter/27-RESEARCH.md | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 .planning/phases/27-hermes-adapter/27-RESEARCH.md diff --git a/.planning/phases/27-hermes-adapter/27-RESEARCH.md b/.planning/phases/27-hermes-adapter/27-RESEARCH.md new file mode 100644 index 00000000..cf03be15 --- /dev/null +++ b/.planning/phases/27-hermes-adapter/27-RESEARCH.md @@ -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 (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. + + +--- + + +## 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. | + + +--- + +## 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)