---
phase: 19-adapter-aware-install-uninstall
plan: "03"
type: execute
wave: 2
depends_on: ["19-01"]
files_modified:
  - ui/src/api/skillRegistry.ts
  - ui/src/pages/SkillBrowser.tsx
  - ui/src/components/SkillCard.tsx
autonomous: true
requirements: [HERM-01, HERM-02, HERM-03]

must_haves:
  truths:
    - "Installed tab for Hermes agents shows two labelled sections: Managed and Native"
    - "Native skills display no remove/update/rollback buttons"
    - "Managed skills on Hermes agents display full writable actions (install, update, remove)"
    - "Install dialog sends agentId in body, not agentSkillsDir"
    - "Uninstall sends agentId as query param, not in body"
  artifacts:
    - path: "ui/src/api/skillRegistry.ts"
      provides: "Updated API types and calls using agentId instead of agentSkillsDir"
      contains: "agentId"
    - path: "ui/src/pages/SkillBrowser.tsx"
      provides: "Dual-section Installed tab with Managed/Native labels for Hermes agents"
      contains: "managedSkills"
    - path: "ui/src/components/SkillCard.tsx"
      provides: "isReadOnly prop that hides action buttons for native skills"
      contains: "isReadOnly"
  key_links:
    - from: "ui/src/pages/SkillBrowser.tsx"
      to: "ui/src/api/skillRegistry.ts"
      via: "listAgentSkills returns AgentSkillEntry[] with source field"
      pattern: "listAgentSkills"
    - from: "ui/src/pages/SkillBrowser.tsx"
      to: "ui/src/components/SkillCard.tsx"
      via: "isReadOnly prop passed for native skills"
      pattern: "isReadOnly.*native"
---

<objective>
Update the Skill Browser UI to show managed vs native sections for Hermes agents, make native skills read-only, and switch all API calls from agentSkillsDir to agentId.

Purpose: Users managing Hermes agents need to clearly distinguish Nexus-managed skills (full control) from built-in native skills (view/rate only). The UI must also stop sending filesystem paths to the server.

Output: Updated API client, dual-section Installed tab, read-only SkillCard variant.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/19-adapter-aware-install-uninstall/19-RESEARCH.md
@.planning/phases/19-adapter-aware-install-uninstall/19-01-SUMMARY.md

Read these source files before modifying:
- ui/src/api/skillRegistry.ts (or similar — the API client for skill registry)
- ui/src/pages/SkillBrowser.tsx
- ui/src/components/SkillCard.tsx (if exists — may be inline in SkillBrowser)

<interfaces>
<!-- From Plan 01 (updated API response shape) -->
GET /skill-registry/agents/:agentId/skills
  Response: Array<{ skillId: string; source: "managed" | "native"; installedAt: number }>

POST /skill-registry/skills/:sourceId/:slug/install
  Body: { agentId: string }  (was: { agentSkillsDir: string })

DELETE /skill-registry/skills/:sourceId/:slug?agentId=xxx
  Query: agentId  (was: body.agentSkillsDir)

POST /skill-registry/skills/:sourceId/:slug/rollback
  Body: { versionId: string, agentId: string }  (was: { versionId, agentSkillsDir })

POST /skill-registry/agents/:agentId/groups
  Body: { groupId: string }  (was: { groupId, agentSkillsDir? })

DELETE /skill-registry/agents/:agentId/groups/:groupId
  (no body needed — agentId in URL)
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Update API client types and calls</name>
  <files>
    ui/src/api/skillRegistry.ts
  </files>
  <read_first>
    ui/src/api/skillRegistry.ts (or search for skill registry API functions — may be in a different file like ui/src/api/skillGroups.ts or similar)
  </read_first>
  <action>
    1. Add the `AgentSkillEntry` type:
       ```typescript
       export type AgentSkillEntry = {
         skillId: string;
         source: "managed" | "native";
         installedAt: number;
       };
       ```

    2. Update `listAgentSkills` return type from `string[]` to `AgentSkillEntry[]`.

    3. Update `installSkill` (or equivalent) to send `{ agentId }` in the POST body instead of `{ agentSkillsDir }`.

    4. Update `uninstallSkill` (or equivalent) to pass `agentId` as a query parameter on the DELETE request instead of in the body.

    5. Update `rollbackSkill` (or equivalent) to send `{ versionId, agentId }` instead of `{ versionId, agentSkillsDir }`.

    6. Update group assign/remove calls to NOT send `agentSkillsDir` in the body (agentId is already in the URL).

    7. Remove any `agentSkillsDir` parameter from all exported API functions. Replace with `agentId: string` where needed.
  </action>
  <verify>
    <automated>cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - AgentSkillEntry type exported with skillId, source, installedAt fields
    - listAgentSkills returns AgentSkillEntry[] not string[]
    - installSkill sends agentId in body, not agentSkillsDir
    - uninstallSkill sends agentId as query param
    - rollbackSkill sends agentId in body, not agentSkillsDir
    - No references to agentSkillsDir in any API function
    - TypeScript compiles without errors
  </acceptance_criteria>
  <done>API client uses agentId for all skill operations, returns typed AgentSkillEntry objects</done>
</task>

<task type="auto">
  <name>Task 2: Dual-section Installed tab and read-only SkillCard</name>
  <files>
    ui/src/pages/SkillBrowser.tsx,
    ui/src/components/SkillCard.tsx
  </files>
  <read_first>
    ui/src/pages/SkillBrowser.tsx,
    ui/src/components/SkillCard.tsx (if exists)
  </read_first>
  <action>
    **SkillCard.tsx** (or inline skill card component):

    1. Add `isReadOnly?: boolean` and `source?: "managed" | "native"` props to the component's props interface.

    2. When `isReadOnly` is true:
       - Hide the Remove/Uninstall button
       - Hide the Update button
       - Hide the Rollback button
       - Show a small "Native" badge (e.g., a gray `<Badge>` from the UI library or a Tailwind-styled span)
       - Rating/view actions remain visible

    3. When `source === "managed"` and skill is installed:
       - Show all action buttons as before (this is the default behavior, no change needed)

    **SkillBrowser.tsx** — Installed tab changes:

    1. **Remove agentSkillsDir state and input.** The install dialog currently has a text input for `agentSkillsDir`. Remove it entirely. The dialog already shows agent selection buttons with `agent.id` — use that directly.

    2. **Update install dialog** to pass `agentId` to the API call instead of `agentSkillsDir`.

    3. **Update uninstall handler** to pass `agentId` to the API call instead of `agentSkillsDir`.

    4. **Per-agent skill query on Installed tab:**
       ```typescript
       const { data: agentInstalledSkills = [] } = useQuery({
         queryKey: ["agentInstalledSkills", selectedAgentId],
         queryFn: () => skillRegistryApi.listAgentSkills(selectedAgentId),
         enabled: tab === "installed" && !!selectedAgentId,
       });
       ```

    5. **Split skills into managed/native sections:**
       ```typescript
       const managedSkills = agentInstalledSkills.filter((s) => s.source === "managed");
       const nativeSkills = agentInstalledSkills.filter((s) => s.source === "native");
       ```

    6. **Render two labelled sections** when viewing a Hermes agent's installed skills:
       - "Managed" section heading — renders `managedSkills` with full SkillCard actions
       - "Native" section heading — renders `nativeSkills` with `isReadOnly={true}` and `source="native"`
       - Use simple heading elements or dividers:
         ```tsx
         {managedSkills.length > 0 && (
           <>
             <h3 className="text-sm font-medium text-muted-foreground mt-4 mb-2">Managed</h3>
             {managedSkills.map(skill => <SkillCard ... />)}
           </>
         )}
         {nativeSkills.length > 0 && (
           <>
             <h3 className="text-sm font-medium text-muted-foreground mt-4 mb-2">Native</h3>
             {nativeSkills.map(skill => <SkillCard ... isReadOnly={true} source="native" />)}
           </>
         )}
         ```
       - For non-Hermes agents (where all skills are managed), render a single list without section headings — the experience is unchanged.

    7. **Conditional sections:** Only show the Managed/Native split when `nativeSkills.length > 0`. For non-Hermes agents this will always be 0, so no UI change for them.
  </action>
  <verify>
    <automated>cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - SkillCard accepts isReadOnly and source props
    - isReadOnly=true hides remove/update/rollback buttons
    - source="native" shows Native badge
    - SkillBrowser Installed tab splits into Managed/Native sections when native skills exist
    - Install dialog sends agentId not agentSkillsDir
    - No agentSkillsDir text input in the dialog
    - Uninstall handler sends agentId as query param
    - Non-Hermes agents see unchanged single-list UI
    - TypeScript compiles without errors
  </acceptance_criteria>
  <done>Hermes agents show Managed/Native split in Installed tab, native skills are read-only, all API calls use agentId</done>
</task>

</tasks>

<verification>
- TypeScript compiles: `cd nexus && pnpm tsc --noEmit --project ui/tsconfig.json`
- No agentSkillsDir references: grep for `agentSkillsDir` in ui/src/ should return 0 matches
- isReadOnly prop exists: grep for `isReadOnly` in SkillCard component
- Managed/Native split: grep for `managedSkills` and `nativeSkills` in SkillBrowser
</verification>

<success_criteria>
- Hermes agents show two labelled sections: Managed (full actions) and Native (read-only + badge)
- Non-Hermes agents see unchanged UI (no section headings)
- All API calls use agentId instead of agentSkillsDir
- Install dialog no longer has a text input for skill directory path
- TypeScript compiles clean
</success_criteria>

<output>
After completion, create `.planning/phases/19-adapter-aware-install-uninstall/19-03-SUMMARY.md`
</output>
