--- phase: 27-hermes-adapter plan: 01 type: execute wave: 1 depends_on: [] files_modified: - server/src/services/heartbeat.ts - packages/shared/src/constants.ts - ui/src/adapters/hermes-local/config-fields.tsx - server/src/__tests__/adapter-session-codecs.test.ts autonomous: true requirements: [HERM-01, HERM-02, HERM-03, HERM-04] must_haves: truths: - "hermes_local is treated as a sessioned local adapter for orphan-process liveness checks" - "Toolsets field does not corrupt extraArgs when creating a new Hermes agent" - "Hermes session codec round-trip is tested (serialize, deserialize, getDisplayId, legacy key)" - "AGENT_ADAPTER_TYPES has no duplicate entries" artifacts: - path: "server/src/services/heartbeat.ts" provides: "hermes_local in SESSIONED_LOCAL_ADAPTERS set" contains: "hermes_local" - path: "packages/shared/src/constants.ts" provides: "Deduplicated AGENT_ADAPTER_TYPES array" contains: "hermes_local" - path: "ui/src/adapters/hermes-local/config-fields.tsx" provides: "Toolsets field hidden in create mode" - path: "server/src/__tests__/adapter-session-codecs.test.ts" provides: "Hermes session codec test block" contains: "hermes sessionCodec" key_links: - from: "server/src/services/heartbeat.ts" to: "server/src/adapters/registry.ts" via: "SESSIONED_LOCAL_ADAPTERS set membership check" pattern: "hermes_local" - from: "server/src/__tests__/adapter-session-codecs.test.ts" to: "hermes-paperclip-adapter/server" via: "import sessionCodec" pattern: "hermes-paperclip-adapter/server" --- Close the four integration gaps preventing full HERM-01 through HERM-04 compliance for the already-installed hermes-paperclip-adapter. Purpose: The Hermes adapter is fully implemented and registered but has four small wiring issues: missing SESSIONED_LOCAL_ADAPTERS entry (orphan reaping broken), create-mode toolsets bug (extraArgs corruption), duplicate gemini_local in constants, and missing session codec test. Output: All four gaps closed; existing hermes-dual-source tests still pass; new session codec test passes. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/27-hermes-adapter/27-RESEARCH.md From server/src/services/heartbeat.ts (line 72-79): ```typescript const SESSIONED_LOCAL_ADAPTERS = new Set([ "claude_local", "codex_local", "cursor", "gemini_local", "opencode_local", "pi_local", ]); ``` From packages/shared/src/constants.ts (line 24-36): ```typescript export const AGENT_ADAPTER_TYPES = [ "process", "http", "claude_local", "codex_local", "gemini_local", "opencode_local", "pi_local", "cursor", "openclaw_gateway", "hermes_local", "gemini_local", // <-- duplicate to remove ] as const; ``` From hermes-paperclip-adapter/server sessionCodec API: ```typescript sessionCodec.deserialize(raw: Record): { sessionId: string } sessionCodec.serialize(params: { sessionId: string }): { sessionId: string } sessionCodec.getDisplayId(serialized: Record | null): string ``` From ui/src/adapters/hermes-local/config-fields.tsx: - `isCreate` boolean prop controls create vs edit mode - In create mode: `values!` and `set!()` for CreateConfigValues - In edit mode: `eff()` and `mark()` for adapterConfig fields - Toolsets field (lines 70-89) incorrectly uses `values!.extraArgs` / `set!({ extraArgs: v })` in create mode Task 1: Fix heartbeat sessioned adapters and deduplicate constants server/src/services/heartbeat.ts, packages/shared/src/constants.ts - server/src/services/heartbeat.ts (lines 70-80 — SESSIONED_LOCAL_ADAPTERS set) - packages/shared/src/constants.ts (lines 24-36 — AGENT_ADAPTER_TYPES array) 1. In `server/src/services/heartbeat.ts`, add `"hermes_local"` to the `SESSIONED_LOCAL_ADAPTERS` Set (line 72-79). Add it after `"pi_local"` to keep alphabetical grouping. This ensures the orphan-process liveness check in heartbeat correctly handles detached Hermes child processes after server restart. (HERM-03) 2. In `packages/shared/src/constants.ts`, remove the duplicate `"gemini_local"` entry from `AGENT_ADAPTER_TYPES` array. The array currently has `"gemini_local"` at positions ~line 29 AND ~line 35. Remove the SECOND occurrence (the one after `"hermes_local"`). Keep the first one. (Cleanup supporting HERM-01) cd /opt/nexus && grep -n "hermes_local" server/src/services/heartbeat.ts && node -e "const c = require('./packages/shared/src/constants.ts'); " 2>/dev/null; grep -c "gemini_local" packages/shared/src/constants.ts | xargs -I{} test {} -eq 1 && echo "DEDUP OK" || echo "DEDUP FAIL" - grep "hermes_local" server/src/services/heartbeat.ts returns a match inside SESSIONED_LOCAL_ADAPTERS - grep -c "gemini_local" packages/shared/src/constants.ts returns exactly 1 - pnpm --filter server exec tsc --noEmit passes hermes_local is in SESSIONED_LOCAL_ADAPTERS; AGENT_ADAPTER_TYPES has no duplicate gemini_local; TypeScript compiles cleanly Task 2: Fix create-mode toolsets field in HermesLocalConfigFields ui/src/adapters/hermes-local/config-fields.tsx - ui/src/adapters/hermes-local/config-fields.tsx (full file — 128 lines) - ui/node_modules/hermes-paperclip-adapter/dist/ui/build-config.js (buildHermesConfig — to understand extraArgs handling) In `ui/src/adapters/hermes-local/config-fields.tsx`, wrap the Toolsets field (the `` block, lines 70-89) inside the existing `{!isCreate && ( ... )}` guard that already wraps "Persist session" and "Timeout" fields (lines 90-125). This hides toolsets from the create form entirely. (HERM-02) Rationale: `CreateConfigValues` has no `toolsets` field. The current code maps toolsets input to `extraArgs`, but `buildHermesConfig` splits `extraArgs` by whitespace into raw CLI flags — so "terminal,file,web" would become a broken CLI arg, not `-t terminal,file,web`. Toolsets default to "all" when unset, which is the correct default for new agents. Users configure toolsets post-creation via the edit form where `mark("adapterConfig", "toolsets", ...)` works correctly. The fix: Move the Toolsets `` block (lines 70-89) to be inside the `{!isCreate && (<> ... )}` block that starts at line 90. The final structure should be: ``` {!isCreate && ( <> ... ... ... )} ``` Remove the create-mode branch from the Toolsets DraftInput value/onCommit props since the field will only render in edit mode now. cd /opt/nexus && grep -A2 "Toolsets" ui/src/adapters/hermes-local/config-fields.tsx | head -5 && pnpm --filter ui exec tsc --noEmit 2>&1 | tail -5 - grep -B5 "Toolsets" ui/src/adapters/hermes-local/config-fields.tsx shows it is inside !isCreate guard - The Toolsets Field no longer references values!.extraArgs - pnpm --filter ui exec tsc --noEmit passes Toolsets field only renders in edit mode; create-mode agents get default "all" toolsets; no extraArgs corruption; TypeScript compiles Task 3: Add hermes session codec test server/src/__tests__/adapter-session-codecs.test.ts - server/src/__tests__/adapter-session-codecs.test.ts (full file — existing codec tests as pattern) In `server/src/__tests__/adapter-session-codecs.test.ts`, add a hermes session codec test block. (HERM-04) 1. Add import at top of file: ```typescript import { sessionCodec as hermesSessionCodec } from "hermes-paperclip-adapter/server"; ``` 2. Add a new test inside the `describe("adapter session codecs", ...)` block, following the exact pattern of the existing tests (e.g., the claude test at lines 18-34): ```typescript it("normalizes hermes session params", () => { const parsed = hermesSessionCodec.deserialize({ sessionId: "hermes-session-1", }); expect(parsed).toEqual({ sessionId: "hermes-session-1", }); const serialized = hermesSessionCodec.serialize(parsed); expect(serialized).toEqual({ sessionId: "hermes-session-1", }); expect(hermesSessionCodec.getDisplayId?.(serialized ?? null)).toBe("hermes-session-1"); }); it("normalizes hermes legacy session_id key", () => { const parsed = hermesSessionCodec.deserialize({ session_id: "hermes-legacy-456", }); expect(parsed).toEqual({ sessionId: "hermes-legacy-456", }); expect(hermesSessionCodec.getDisplayId?.(hermesSessionCodec.serialize(parsed) ?? null)).toBe("hermes-legacy-456"); }); ``` Note: Hermes session params do NOT include a `cwd` field (unlike claude/codex/cursor/gemini). The hermes adapter only tracks `sessionId`. cd /opt/nexus && pnpm --filter server exec vitest run src/__tests__/adapter-session-codecs.test.ts 2>&1 | tail -20 - grep "hermes" server/src/__tests__/adapter-session-codecs.test.ts returns matches for import and test cases - pnpm --filter server exec vitest run src/__tests__/adapter-session-codecs.test.ts shows all tests passing including new hermes tests - Test covers both camelCase sessionId and legacy snake_case session_id deserialization Hermes session codec has round-trip tests covering serialize, deserialize, getDisplayId, and legacy key variant; all adapter-session-codecs tests pass After all tasks complete: 1. Hermes-specific tests pass: ``` pnpm --filter server exec vitest run src/__tests__/adapter-session-codecs.test.ts src/__tests__/hermes-dual-source.test.ts ``` 2. TypeScript compiles for both server and UI: ``` pnpm --filter server exec tsc --noEmit && pnpm --filter ui exec tsc --noEmit ``` 3. Full test suite (sampling): ``` pnpm --filter server exec vitest run ``` - hermes_local is in SESSIONED_LOCAL_ADAPTERS — orphan process liveness checks work - AGENT_ADAPTER_TYPES has exactly one gemini_local entry — no duplicates - Toolsets field only appears in edit mode — no extraArgs corruption on create - Hermes session codec has 2 passing tests covering standard and legacy key formats - All existing tests continue to pass (no regressions) After completion, create `.planning/phases/27-hermes-adapter/27-01-SUMMARY.md`