From b538d44ca37aecee08c6d3edf515ced2538563a9 Mon Sep 17 00:00:00 2001 From: Nexus Dev Date: Thu, 2 Apr 2026 17:23:29 +0000 Subject: [PATCH] docs(29): research phase default-provider --- .../phases/29-default-provider/29-RESEARCH.md | 512 ++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 .planning/phases/29-default-provider/29-RESEARCH.md diff --git a/.planning/phases/29-default-provider/29-RESEARCH.md b/.planning/phases/29-default-provider/29-RESEARCH.md new file mode 100644 index 00000000..c250d6c1 --- /dev/null +++ b/.planning/phases/29-default-provider/29-RESEARCH.md @@ -0,0 +1,512 @@ +# Phase 29: Default Provider & End-to-End - Research + +**Researched:** 2026-04-01 +**Domain:** Onboarding UX, agent template configuration, GSD workflow compatibility +**Confidence:** HIGH + +## Summary + +Phase 29 closes the last gap in the v1.4 milestone: a fresh Nexus install that has Hermes + Ollama but no cloud provider (Claude Code, Anthropic API key, etc.) currently hits a wall at every entry point. The Nexus-owned onboarding wizard (`NexusOnboardingWizard.tsx`) hardcodes `adapterType: "claude_local"` for all three agents it creates. The `NewAgentDialog.tsx` template shortcuts also hardcode `adapterType: "claude_local"`. Neither component has any logic to detect which adapters are locally available. + +The Hermes adapter is well-integrated at the runtime level: it has `supportsLocalAgentJwt: true` (so `PAPERCLIP_API_KEY` is injected automatically), handles session persistence via `--resume`, and the heartbeat service already has a Hermes-specific `stateJson` merge path. However, there is one confirmed gap: `syncHermesSkills` is a **no-op** (it returns the skill snapshot but writes nothing to disk), and the Hermes `execute.ts` does not inject Nexus-managed skill content into the prompt. This means the PM/Engineer HEARTBEAT.md/TOOLS.md bundles are recorded in the DB but are not actually available to the Hermes agent at runtime. + +The agent template files (HEARTBEAT.md, TOOLS.md, AGENTS.md) are curl-based and entirely adapter-neutral. The onboarding-assets work for any adapter that has API access — the only barrier is getting skill content into the Hermes context. + +Phase 29 involves five targeted areas: (1) provider detection at onboarding; (2) adapter default in the Nexus wizard; (3) AGENT_TEMPLATES fix in NewAgentDialog; (4) skill injection for Hermes agents; and (5) end-to-end smoke test. + +**Primary recommendation:** The Nexus onboarding wizard should probe hermes availability via a board-authenticated route, then silently pre-select `hermes_local` when hermes is found and no cloud provider is detected. For skill injection, the simplest fix is to include the Nexus skill bundle content inline in the Hermes prompt template via `ctx.context.skills` (the heartbeat service already populates this; the adapter just needs to use it). + + +## 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 | +|----|-------------|------------------| +| DFLT-01 | If no cloud provider (Claude Code, etc.) is detected, Hermes + Ollama is offered as default during onboarding | NexusOnboardingWizard.tsx hardcodes `claude_local`; needs provider detection + conditional adapter pre-selection | +| DFLT-02 | Default agent templates (PM, Engineer, Generalist) work correctly with Hermes runtime | AGENT_TEMPLATES in NewAgentDialog.tsx hardcodes `claude_local`; onboarding-assets adapter-agnostic but skill injection gap must be closed | +| DFLT-03 | GSD workflow functions correctly with Hermes as the agent runtime | Hermes has `supportsLocalAgentJwt: true`; skill sync is a no-op; need to inject Nexus skill content via prompt context | +| DFLT-04 | Fresh install with only Hermes + Ollama works end-to-end (no paid subscription or API key required) | All individual pieces exist; needs integration smoke test and skill injection gap closed | + + +--- + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| hermes-paperclip-adapter | 0.2.1 | Hermes adapter module | Already installed; provides `testEnvironment`, `execute`, `detectModel` | +| @tanstack/react-query | (project version) | Server state in onboarding wizard | All other queries in the onboarding wizard use this | +| React | (project version) | UI component model | Project standard | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| agentsApi | (internal) | Frontend API client for adapter queries | testEnvironment, detectModel, adapterModels | + +**Installation:** No new packages needed. hermes-paperclip-adapter@0.2.1 is already in the monorepo. + +--- + +## Architecture Patterns + +### Recommended Project Structure + +Changes span these files: + +``` +ui/src/components/ +├── NexusOnboardingWizard.tsx # Add provider detection + conditional hermes default +├── NewAgentDialog.tsx # Make AGENT_TEMPLATES adapter-neutral +server/src/routes/ +└── agents.ts # Add board-auth probe route (no companyId) +server/src/ +└── hermes-skill-inject.ts # (optional) skill injection helper +server/src/__tests__/ +├── 29-default-provider.test.ts # Probe route + agent creation tests +└── 29-hermes-skill-inject.test.ts # Skill injection unit tests +``` + +### Pattern 1: Provider Availability Detection at Onboarding + +**What:** Before the Nexus onboarding wizard renders its form, fire a background probe to check if hermes is installed. If yes (and no cloud provider credential exists), pre-populate `adapterType` to `hermes_local`. + +**When to use:** In `NexusOnboardingWizard.tsx`. + +**Implementation notes:** +- The existing `testEnvironment` route requires a `companyId` (it lives under `/companies/:companyId/adapters/:type/test-environment`). At wizard load time there is no company yet. +- **Approach:** Add a board-authenticated route `GET /api/adapters/:adapterType/probe` that calls `adapter.testEnvironment({})` without a company context. The `testEnvironment` function for `hermes_local` only checks CLI presence, Python, and model config — it does not need a real company. +- The probe runs once on wizard mount, result cached in state. + +```typescript +// server/src/routes/agents.ts — NEW route (board auth only) +router.get("/adapters/:type/probe", async (req, res) => { + if (req.actor.type !== "board") { + res.status(403).json({ error: "Board auth required" }); + return; + } + const type = req.params.type as string; + const adapter = findServerAdapter(type); + if (!adapter?.testEnvironment) { + res.json({ available: false }); + return; + } + const result = await adapter.testEnvironment({ + companyId: "", + adapterType: type, + config: {}, + }); + const hasCliError = result.checks.some( + (c) => c.level === "error" && c.code?.includes("not_found") + ); + res.json({ available: !hasCliError, status: result.status }); +}); +``` + +### Pattern 2: Nexus Onboarding Wizard Adapter Auto-Selection + +**What:** The Nexus wizard creates 3 agents with hardcoded `adapterType: "claude_local"`. For DFLT-01, when Hermes is available and no cloud provider is found, use `hermes_local` instead. + +**Recommendation:** Silent auto-selection — no extra UI step. The ROADMAP says "one-click path". When the probe resolves: +- hermes=available, claude=not-detected: use `hermes_local` +- both available: show a single "Which runtime?" card between the title and the directory input (keeps the single-form layout) +- neither available: keep claude_local as default, let the user figure out installation + +**Note on `cwd`:** For `hermes_local`, `cwd` is optional. The wizard's directory input should become optional when `hermes_local` is selected, or the label should change to "Working directory (optional)". + +```typescript +// NexusOnboardingWizard.tsx additions +const [defaultAdapterType, setDefaultAdapterType] = + useState<"claude_local" | "hermes_local">("claude_local"); +const [probing, setProbing] = useState(false); + +useEffect(() => { + if (!effectiveOnboardingOpen) return; + setProbing(true); + fetch("/api/adapters/hermes_local/probe", { + headers: { Authorization: `Bearer ${boardToken}` }, + }) + .then((r) => r.json()) + .then((data: { available: boolean }) => { + if (data.available) setDefaultAdapterType("hermes_local"); + }) + .catch(() => {}) + .finally(() => setProbing(false)); +}, [effectiveOnboardingOpen]); + +// In handleSubmit, use defaultAdapterType instead of hardcoded "claude_local" +const adapterConfig = + defaultAdapterType === "hermes_local" + ? rootDir.trim() ? { cwd: rootDir.trim() } : {} + : { cwd: rootDir.trim() }; +``` + +### Pattern 3: AGENT_TEMPLATES in NewAgentDialog.tsx (DFLT-02) + +**What:** The `AGENT_TEMPLATES` constant hardcodes `adapterType: "claude_local"`. For DFLT-02, templates should work with Hermes. + +**Recommendation:** Remove `adapterType` from the template navigation URL. The `/agents/new` form already handles Hermes via its own adapter detection. Without `adapterType` in the URL, the form defaults to its own logic (currently `claude_local`, but that default can be updated separately via the same probe mechanism). + +```typescript +// NewAgentDialog.tsx +// Before: +function handleTemplateSelect(template: typeof AGENT_TEMPLATES[number]) { + navigate(`/agents/new?adapterType=${encodeURIComponent(template.adapterType)}&role=...`); +} + +// After: +function handleTemplateSelect(template: { id: string; label: string; role: string }) { + navigate(`/agents/new?role=${encodeURIComponent(template.role)}&name=${encodeURIComponent(template.label)}`); +} +``` + +If the `/agents/new` form's default adapter needs to respect the probe result, the same `useQuery`-based probe can be added to that page. + +### Pattern 4: Hermes Skill Injection (DFLT-03 Critical Gap) + +**What:** `syncHermesSkills` is a **confirmed no-op** — it returns the snapshot but writes nothing to `~/.hermes/skills/`. The Hermes `execute.ts` does not read `ctx.context.skills`. This means a Hermes agent created with PM/Engineer role currently runs with only Hermes's default prompt (which knows nothing about the Nexus API or the HEARTBEAT.md workflow). + +**Confirmed by:** Reading `hermes-paperclip-adapter@0.2.1/dist/server/skills.js` — `syncHermesSkills` returns the snapshot directly without writing files. Reading `dist/server/execute.js` — zero references to `skills` in the execute function. + +**Fix options:** + +Option A (recommended): Update the Hermes `execute.ts` (in the adapter package) to append Nexus-managed skill content to the prompt when `ctx.context.skills` contains entries. This is the cleanest approach and uses the existing `AdapterExecutionContext.skills` mechanism. + +Option B (simpler, no adapter change): Override the Hermes `promptTemplate` on agent creation in `NexusOnboardingWizard.tsx` to embed the Nexus skill bundle content directly in the `adapterConfig.promptTemplate` field. The Hermes adapter supports custom prompt templates with `{{variable}}` substitution. + +Option C: Make `syncHermesSkills` actually write skills to `~/.hermes/skills/{skill-key}/SKILL.md`. Hermes loads skills from that directory automatically. This is the most "native" approach but requires knowing the right directory layout. + +**Recommended approach:** Option B for this phase (no adapter package changes needed). The `NexusOnboardingWizard.tsx` already loads the company-skills data; it can embed the required skill content in the `promptTemplate` at agent creation time. The Hermes `DEFAULT_PROMPT_TEMPLATE` in execute.js already handles task assignment, comments, and API calls — the Nexus bundle is supplementary. + +**Alternative path:** If the planner wants a cleaner long-term solution, file a note that Option A (patching the adapter) would make hermes behave like codex/gemini with automatic skill injection — but that requires publishing a new version of hermes-paperclip-adapter. + +```typescript +// NexusOnboardingWizard.tsx — when creating Hermes agents, inject skill content +// The Hermes prompt template supports {{agentName}}, {{companyId}}, etc. +// We can extend it with the HEARTBEAT.md content. + +const hermesPromptTemplate = buildHermesPromptWithSkills({ + role: "ceo", // or "engineer" / "general" + nexusSkillContent: { + heartbeat: CEO_HEARTBEAT_MD, // imported at build time + tools: CEO_TOOLS_MD, + } +}); +// Then pass promptTemplate in adapterConfig when creating the agent +``` + +### Anti-Patterns to Avoid + +- **Polling `testEnvironment` repeatedly:** This spawns a hermes subprocess. Run only once on wizard mount, cache the result. +- **Blocking the wizard on adapter detection:** If the probe takes >2s, show the wizard with a default and update async. +- **Changing `NexusOnboardingWizard` to multi-step:** Keep it single-form. Only add a runtime selector card if both adapters are available. +- **Hardcoding `hermes_local` as universal default:** Detect, don't assume. Users with Claude Code must still get `claude_local`. +- **Writing skills to `~/.hermes/skills/` without understanding Hermes skill layout:** Hermes expects `~/.hermes/skills/{category}/{skill-name}/SKILL.md` — the layout must match Hermes's scanner. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Adapter availability check | Custom subprocess spawn | `adapter.testEnvironment({})` | Already handles ENOENT, version checks, Python checks | +| Model detection | Parsing `~/.hermes/config.yaml` manually | `detectModel()` from `hermes-paperclip-adapter/server` | Already implemented, handles edge cases | +| Session management | Custom `--resume` logic | Hermes `sessionCodec` + heartbeat session store | Already implemented in Phase 27 | +| Skill file reading at build time | Custom fs.readFile in routes | Import at compile time (Node ESM `import.meta.url` + `fs.readFile`) | Use the existing `loadDefaultAgentInstructionsBundle` pattern from `default-agent-instructions.ts` | + +**Key insight:** The runtime plumbing for Hermes authentication and session persistence is complete. The two remaining gaps are UI entry points (wizard/templates) and skill content delivery to the Hermes prompt. + +--- + +## Common Pitfalls + +### Pitfall 1: `testEnvironment` Requires a companyId at the Existing Route +**What goes wrong:** The existing `agentsApi.testEnvironment(companyId, type, data)` client method sends to `/companies/{companyId}/adapters/{type}/test-environment`. During initial onboarding, no company exists yet. +**Why it happens:** The route was designed for post-onboarding agent configuration. +**How to avoid:** Add a new board-authenticated route `GET /api/adapters/:adapterType/probe` (no companyId) that runs the test with empty config. Board auth is available at wizard load time. +**Warning signs:** 401/404 errors when wizard tries to probe before company creation. + +### Pitfall 2: Hermes CLI Not in PATH in the Server Process +**What goes wrong:** `testEnvironment` spawns `hermes --version` from the server Node.js process. The server's PATH may differ from the user's shell PATH (especially if installed via `pip install --user` without PATH setup). +**Why it happens:** Node.js `child_process` inherits `process.env.PATH` from server startup, not the interactive shell. +**How to avoid:** The probe should return `available: false` gracefully on ENOENT. The UI handles `available: false` by keeping `claude_local` as default. +**Warning signs:** `hermes_cli_not_found` check code in the environment test result. + +### Pitfall 3: Hermes Skill Injection is a No-Op — Agents Won't Follow the Nexus Heartbeat Loop +**What goes wrong:** A PM or Engineer agent created with the Hermes runtime runs a heartbeat but never calls `GET /api/agents/me` or consults HEARTBEAT.md — it uses only Hermes's built-in default prompt. +**Why it happens (confirmed):** `syncHermesSkills` at `hermes-paperclip-adapter@0.2.1/dist/server/skills.js` is explicitly a no-op. It returns the skill snapshot but writes nothing to disk. The Hermes `execute.ts` has zero references to `ctx.context.skills`. +**How to avoid:** Use Option B from Pattern 4: embed the Nexus skill bundle content in the `adapterConfig.promptTemplate` when creating Hermes agents in `NexusOnboardingWizard.tsx`. The Hermes adapter supports custom prompt templates. +**Warning signs:** Hermes heartbeat output shows Hermes's default task-completion workflow ("mark the issue as completed: curl...") rather than the multi-step PM delegation loop. + +### Pitfall 4: Hermes `adapterConfig.cwd` is Optional, Not Required +**What goes wrong:** The Nexus wizard passes `cwd: rootDir.trim()` to all agents. For `claude_local`, `cwd` is required. For `hermes_local`, `cwd` is optional — Hermes defaults to `"."`. +**Why it happens:** The wizard was written for `claude_local` semantics. +**How to avoid:** When `hermes_local` is selected, make the directory input optional. `buildHermesConfig` already handles absent `cwd` gracefully. +**Warning signs:** Wizard form validation errors when user skips directory input for a Hermes agent. + +### Pitfall 5: NexusOnboardingWizard vs OnboardingWizard Alias +**What goes wrong:** The Vite alias redirects all imports of `./components/OnboardingWizard` to `NexusOnboardingWizard.tsx`. Changes to `OnboardingWizard.tsx` (the original) have no effect in the running UI. +**Why it happens:** Phase 4 introduced this alias to preserve the original for upstream rebase compatibility. +**How to avoid:** Edit `NexusOnboardingWizard.tsx` for all onboarding changes. The alias is in `vite.config.ts`. +**Warning signs:** UI changes not appearing after hot-reload; git shows changes to the wrong file. + +--- + +## Code Examples + +### Server Route: Board-Auth Adapter Probe (No companyId) +```typescript +// server/src/routes/agents.ts — new route, add before existing company-scoped routes +// Source: existing testEnvironment pattern + board auth pattern from llms.ts + +router.get("/adapters/:type/probe", async (req, res) => { + if (req.actor.type !== "board") { + res.status(403).json({ error: "Board authentication required" }); + return; + } + const type = req.params.type as string; + const adapter = findServerAdapter(type); + if (!adapter?.testEnvironment) { + res.json({ available: false, status: "unknown" }); + return; + } + try { + const result = await adapter.testEnvironment({ + companyId: "", // not needed for CLI presence check + adapterType: type, + config: {}, + }); + const hasCliNotFound = result.checks.some( + (c) => c.level === "error" && (c.code?.includes("not_found") || c.code?.includes("cli")) + ); + res.json({ available: !hasCliNotFound, status: result.status, checks: result.checks }); + } catch { + res.json({ available: false, status: "error" }); + } +}); +``` + +### Frontend: Detection in NexusOnboardingWizard +```typescript +// NexusOnboardingWizard.tsx +import { useEffect, useState } from "react"; + +const [defaultAdapterType, setDefaultAdapterType] = + useState<"claude_local" | "hermes_local">("claude_local"); + +// Probe once when wizard opens +useEffect(() => { + if (!effectiveOnboardingOpen) return; + fetch("/api/adapters/hermes_local/probe") + .then((r) => r.ok ? r.json() : { available: false }) + .then((data: { available: boolean }) => { + if (data.available) setDefaultAdapterType("hermes_local"); + }) + .catch(() => {}); // graceful — keep claude_local default +}, [effectiveOnboardingOpen]); + +// In handleSubmit — replace hardcoded "claude_local" with defaultAdapterType +const agentAdapterConfig = + defaultAdapterType === "hermes_local" && rootDir.trim() + ? { cwd: rootDir.trim() } + : defaultAdapterType === "hermes_local" + ? {} + : { cwd: rootDir.trim() }; +``` + +### AGENT_TEMPLATES Fix (NewAgentDialog.tsx) +```typescript +// Before (hardcoded adapterType): +const AGENT_TEMPLATES = [ + { id: "pm", label: "Project Manager", role: "pm" as const, adapterType: "claude_local" as const }, + { id: "engineer", label: "Engineer", role: "engineer" as const, adapterType: "claude_local" as const }, +]; +// handleTemplateSelect passes adapterType in URL + +// After (adapter-neutral): +const AGENT_TEMPLATES = [ + { id: "pm", label: "Project Manager", role: "pm" as const }, + { id: "engineer", label: "Engineer", role: "engineer" as const }, +]; + +function handleTemplateSelect(template: typeof AGENT_TEMPLATES[number]) { + closeNewAgent(); + setShowAdvancedCards(false); + // No adapterType in URL — /agents/new form picks its own default + navigate( + `/agents/new?role=${encodeURIComponent(template.role)}&name=${encodeURIComponent(template.label)}` + ); +} +``` + +### Hermes Skill Content Injection in Prompt Template +```typescript +// NexusOnboardingWizard.tsx — when adapterType === "hermes_local" +// Load the role-specific skill bundle and embed in promptTemplate + +// These are static imports (or dynamic loads from /api/onboarding-assets) +// The content of AGENTS.md, HEARTBEAT.md, SOUL.md, TOOLS.md for the given role +function buildHermesAdapterConfig( + role: "ceo" | "engineer" | "general", + cwd: string | null, + skillBundle: Record // { "AGENTS.md": "...", "HEARTBEAT.md": "...", ... } +) { + const skillSection = Object.entries(skillBundle) + .map(([name, content]) => `## ${name}\n\n${content}`) + .join("\n\n---\n\n"); + + const promptTemplate = `You are "{{agentName}}", an AI agent in a Nexus-managed company. + +Your Nexus identity: + Agent ID: {{agentId}} + Company ID: {{companyId}} + API Base: {{paperclipApiUrl}} + Run ID: {{runId}} + +IMPORTANT: Use \`terminal\` tool with \`curl\` for ALL Nexus API calls. +IMPORTANT: Always include the header \`-H "X-Paperclip-Run-Id: {{runId}}"\` on API calls that modify data. + +${skillSection} + +{{#taskId}} +Assigned task: {{taskId}} — {{taskTitle}} +{{/taskId}} +`; + + return { + ...(cwd ? { cwd } : {}), + persistSession: true, + promptTemplate, + }; +} +``` + +--- + +## Runtime State Inventory + +> Skipped — this is a greenfield UI/config phase, not a rename/refactor. No runtime state migration required. + +--- + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| hermes CLI (`hermes`) | DFLT-01 probe, DFLT-03 execution | Runtime check via probe | Detected via `hermes --version` | Show install guidance (existing OLLA-05 pattern) | +| Ollama | DFLT-04 (Hermes model serving) | Runtime check | Detected via Ollama service | hermes can use API-based providers | +| Node.js / pnpm | Build system | ✓ | (project standard) | — | + +**Missing dependencies with no fallback:** +- None that block development — the probe gracefully handles hermes not being installed. + +**Missing dependencies with fallback:** +- hermes CLI not in PATH: probe returns `available: false`; wizard falls back to `claude_local`. + +--- + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Vitest | +| Config file | `server/vitest.config.ts` | +| Quick run command | `pnpm --filter server test --run` | +| Full suite command | `pnpm --filter server test --run && pnpm --filter ui test --run` | + +### Phase Requirements → Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| DFLT-01 | Probe route returns `available: true` when hermes CLI is found | unit | `pnpm --filter server test --run -- 29-default-provider` | ❌ Wave 0 | +| DFLT-01 | Probe route returns `available: false` when hermes CLI absent (ENOENT) | unit | `pnpm --filter server test --run -- 29-default-provider` | ❌ Wave 0 | +| DFLT-02 | PM/Engineer agent created with `hermes_local` adapterType saves correct record | unit | `pnpm --filter server test --run -- 29-default-provider` | ❌ Wave 0 | +| DFLT-03 | Hermes heartbeat injects `PAPERCLIP_API_KEY` (`supportsLocalAgentJwt: true`) | unit (existing) | `pnpm --filter server test --run -- adapter-session-codecs` | ✅ | +| DFLT-03 | Hermes promptTemplate includes Nexus skill content when created via wizard | unit | `pnpm --filter server test --run -- 29-hermes-skill-inject` | ❌ Wave 0 | +| DFLT-04 | Full smoke: probe → wizard submit → PM agent (hermes_local) → issue → heartbeat-run record created | integration | `pnpm --filter server test --run -- 29-default-provider` | ❌ Wave 0 | + +### Sampling Rate +- **Per task commit:** `pnpm --filter server test --run` +- **Per wave merge:** `pnpm --filter server test --run && pnpm --filter ui test --run` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `server/src/__tests__/29-default-provider.test.ts` — probe route (DFLT-01), agent creation with hermes_local (DFLT-02), smoke (DFLT-04) +- [ ] `server/src/__tests__/29-hermes-skill-inject.test.ts` — `buildHermesAdapterConfig` produces promptTemplate containing skill bundle content (DFLT-03) + +*(Both new test files; they share the existing in-memory db setup pattern from `heartbeat-workspace-session.test.ts`)* + +--- + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Original OnboardingWizard.tsx (multi-step, adapter picker) | NexusOnboardingWizard.tsx (single-step, no adapter choice) | Phase 4 (Vite alias) | Simpler UX but hardcodes `claude_local` | +| Manual hermes model detection | `detectModel()` from hermes adapter | Phase 28 | Can probe `~/.hermes/config.yaml` without running hermes | +| No Hermes adapter | hermes-paperclip-adapter@0.2.1 | Phase 27 | Full hermes execution pipeline available | +| Skill sync writes to disk (claude/codex) | Hermes skill sync is a no-op (confirmed) | Hermes adapter design | Skills must be injected via prompt template instead | + +**Deprecated/outdated:** +- `SESSIONED_LOCAL_ADAPTERS` does NOT include `hermes_local` — intentional. Hermes manages session IDs itself; Nexus just stores the returned session ID. No change needed. + +--- + +## Open Questions + +1. **Should the probe route return 403 for non-board actors, or 200 with `available: false`?** + - What we know: Board auth is present at wizard load time. Non-board actors should not need to probe adapter availability. + - Recommendation: Return 403 for non-board. The wizard frontend should only show to board users. + +2. **Does NexusOnboardingWizard need a visible runtime selector, or is silent auto-selection sufficient?** + - What we know: ROADMAP says "one-click path." Silent selection is simpler. + - Recommendation: Silent when only one option is available; show a two-card toggle when both hermes and claude are available. + +3. **Should the Hermes prompt template embed full skill content, or just inject section headings?** + - What we know: The full HEARTBEAT.md for CEO is about 70 lines. Embedding in every heartbeat prompt increases context usage. + - Recommendation: Embed full content. The GSD workflow requires the agent to follow the exact HEARTBEAT.md checklist. Abbreviated hints won't produce correct behavior. + +--- + +## Sources + +### Primary (HIGH confidence) +- `/opt/nexus/ui/src/components/NexusOnboardingWizard.tsx` — hardcoded `adapterType: "claude_local"` (lines 97, 106, 115) confirmed +- `/opt/nexus/ui/src/components/NewAgentDialog.tsx` — hardcoded templates at lines 98-99 confirmed +- `/opt/nexus/ui/src/components/OnboardingWizard.tsx` — upstream wizard; Hermes in "More adapters" (hidden by default) +- `/opt/nexus/server/src/adapters/registry.ts` — `hermesLocalAdapter` with `supportsLocalAgentJwt: true` (line 186) +- `/opt/nexus/server/src/services/heartbeat.ts` — `SESSIONED_LOCAL_ADAPTERS` (lines 71-78): `hermes_local` intentionally absent +- `hermes-paperclip-adapter@0.2.1/dist/server/execute.js` — `buildPaperclipEnv` confirmed; zero skill references +- `hermes-paperclip-adapter@0.2.1/dist/server/skills.js` — `syncHermesSkills` confirmed no-op (returns snapshot, writes nothing) +- `hermes-paperclip-adapter@0.2.1/dist/server/test.js` — `testEnvironment` checks: CLI, Python, model config, API keys +- `/opt/nexus/server/src/onboarding-assets/` — all role bundles (ceo, engineer, pm, general) confirmed adapter-agnostic +- `/opt/nexus/skills/paperclip/SKILL.md` — GSD heartbeat workflow curl-based, adapter-neutral +- `/opt/nexus/server/src/services/default-agent-instructions.ts` — `loadDefaultAgentInstructionsBundle` pattern for reading skill files + +### Secondary (MEDIUM confidence) +- `/opt/nexus/.planning/ROADMAP.md` — Phase 29 success criteria and description +- `hermes-paperclip-adapter@0.2.1/dist/ui/build-config.js` — `buildHermesConfig`: `cwd` optional, provider resolved at runtime + +--- + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH — all packages already installed, no new dependencies +- Architecture: HIGH — hardcoded adapter locations confirmed via direct code reading; skill injection gap confirmed +- Pitfalls: HIGH — all identified from direct code reading (no assumptions) + +**Research date:** 2026-04-01 +**Valid until:** 2026-05-01 (stable domain; hermes-paperclip-adapter version pinned)