---
phase: 19-adapter-aware-install-uninstall
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - server/src/services/skill-registry-schema.ts
  - server/src/services/skill-registry-db.ts
  - server/src/services/skill-registry.ts
  - server/src/services/skill-registry-groups.ts
autonomous: true
requirements: [INST-01, INST-02, INST-03, INST-04, HERM-01, HERM-02, HERM-03]

must_haves:
  truths:
    - "install() accepts agentSkillsDir (resolved externally) and writes skill files to that directory"
    - "uninstall() removes skill files from disk before soft-deleting the registry row"
    - "rollback() restores previous version files to the provided agentSkillsDir"
    - "assignGroup/removeGroup accept agentSkillsDir (resolved externally) instead of using defaultSkillsDir()"
    - "agentSkills table has a source column distinguishing managed from native skills"
    - "syncHermesNativeSkills populates native skill rows in agentSkills and stub rows in skills table"
    - "listAgentSkills returns objects with source field, not bare string arrays"
  artifacts:
    - path: "server/src/services/skill-registry-schema.ts"
      provides: "source column on agentSkills table"
      contains: "source.*text.*managed"
    - path: "server/src/services/skill-registry-db.ts"
      provides: "ALTER TABLE migration guard for source column"
      contains: "ALTER TABLE agent_skills ADD COLUMN source"
    - path: "server/src/services/skill-registry.ts"
      provides: "Updated install/uninstall/rollback methods, syncHermesNativeSkills, listAgentSkills returning objects"
      contains: "syncHermesNativeSkills"
    - path: "server/src/services/skill-registry-groups.ts"
      provides: "assignGroup/removeGroup using passed agentSkillsDir, no defaultSkillsDir fallback"
  key_links:
    - from: "server/src/services/skill-registry-db.ts"
      to: "server/src/services/skill-registry-schema.ts"
      via: "ALTER TABLE matches Drizzle schema source column"
      pattern: "source.*text.*managed"
    - from: "server/src/services/skill-registry.ts"
      to: "server/src/services/skill-registry-schema.ts"
      via: "agentSkills.source used in insert/select queries"
      pattern: "agentSkills\\.source"
---

<objective>
Update the skill registry service layer to support adapter-aware install/uninstall and Hermes dual-source tracking.

Purpose: The service layer must (1) accept resolved skill directories for all file operations instead of hardcoded paths, (2) actually remove files on uninstall (currently only soft-deletes), (3) track managed vs native skill sources in the libSQL schema, and (4) sync Hermes native skills from disk.

Output: Updated schema, migration guard, service methods ready for route-layer wiring in Plan 02.
</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/18-adapter-path-resolver/18-01-SUMMARY.md

Read these source files before modifying:
- server/src/services/skill-registry-schema.ts
- server/src/services/skill-registry-db.ts
- server/src/services/skill-registry.ts
- server/src/services/skill-registry-groups.ts
</context>

<tasks>

<task type="auto">
  <name>Task 1: Schema + migration guard + service method signatures</name>
  <files>
    server/src/services/skill-registry-schema.ts,
    server/src/services/skill-registry-db.ts,
    server/src/services/skill-registry.ts,
    server/src/services/skill-registry-groups.ts
  </files>
  <read_first>
    server/src/services/skill-registry-schema.ts,
    server/src/services/skill-registry-db.ts,
    server/src/services/skill-registry.ts,
    server/src/services/skill-registry-groups.ts
  </read_first>
  <action>
    **skill-registry-schema.ts** — Add `source` column to `agentSkills` table:
    ```typescript
    source: text("source").notNull().default("managed"), // 'managed' | 'native'
    ```
    Place it after the `installedAt` column. Do NOT touch any other table definitions.

    **skill-registry-db.ts** — Add migration guard in `getSkillRegistryDb()` (or equivalent init function) AFTER the DB is ready:
    ```typescript
    try {
      await db.run(sql`ALTER TABLE agent_skills ADD COLUMN source TEXT NOT NULL DEFAULT 'managed'`);
    } catch {
      // Column already exists — ignore "duplicate column name" error
    }
    ```
    Import `sql` from drizzle-orm if not already imported.

    **skill-registry.ts** — Make these changes:
    1. `uninstall(skillId, agentSkillsDir)` — Add second param `agentSkillsDir: string`. Before the existing soft-delete (`db.update(skills).set({ removedAt: ... })`), add file removal:
       ```typescript
       const slug = skillId.split("/").pop() ?? skillId;
       const targetDir = path.join(agentSkillsDir, slug);
       await rm(targetDir, { recursive: true, force: true });
       ```
       Import `rm` from `node:fs/promises` if not already imported.

    2. `install()` — Verify it already accepts `agentSkillsDir` as a parameter (research says it does). No change needed if so. If it uses a different name, standardize to `agentSkillsDir`.

    3. `rollback()` — Verify it already accepts `agentSkillsDir` as a parameter. No change needed if so.

    4. Add `syncHermesNativeSkills(agentId: string)` function:
       - Read directory entries from `path.join(os.homedir(), ".hermes", "skills")`
       - For each entry, create a stub `skills` row with `id: "hermes-native/${entry}"`, `sourceId: "hermes-native"`, `name: entry` using `onConflictDoNothing()`
       - Insert `agentSkills` row with `source: "native"` using `onConflictDoNothing()`
       - Wrap the `readdir` in try/catch — return silently if directory doesn't exist
       - Import `readdir` from `node:fs/promises` and `os` from `node:os`

    5. Update `listAgentSkills(agentId)` (or whatever function returns installed skills for an agent):
       - Change return type from `string[]` to `Array<{ skillId: string; source: "managed" | "native"; installedAt: number }>`
       - Select `agentSkills.source` and `agentSkills.installedAt` in addition to `skillId`
       - If the agent is a Hermes type, call `syncHermesNativeSkills(agentId)` first (Note: this function doesn't know the adapter type directly — the caller in the route layer will invoke sync separately. Just update the query to return objects with source field.)

    **skill-registry-groups.ts** — Make these changes:
    1. Remove `defaultSkillsDir()` function entirely — there is no safe default when the caller fails to provide `agentId`
    2. Update `assignGroup()` and `removeGroup()` to require `agentSkillsDir: string` as a mandatory parameter (not optional). Remove any fallback to `defaultSkillsDir()`.
    3. If these functions currently have `agentSkillsDir?: string` with a fallback, make the param required (remove `?`).
  </action>
  <verify>
    <automated>cd nexus && pnpm tsc --noEmit --project server/tsconfig.json 2>&1 | head -30</automated>
  </verify>
  <acceptance_criteria>
    - agentSkills table definition includes `source: text("source").notNull().default("managed")`
    - skill-registry-db.ts contains `ALTER TABLE agent_skills ADD COLUMN source`
    - uninstall function signature includes agentSkillsDir parameter and calls rm()
    - syncHermesNativeSkills function exists and uses onConflictDoNothing
    - listAgentSkills returns objects with source field (not string[])
    - defaultSkillsDir() removed from skill-registry-groups.ts
    - assignGroup and removeGroup require agentSkillsDir as mandatory param
    - TypeScript compiles without errors
  </acceptance_criteria>
  <done>Schema has source column, migration guard runs on DB init, uninstall removes files, syncHermesNativeSkills exists, listAgentSkills returns typed objects, group functions require agentSkillsDir</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Unit tests for adapter-aware install/uninstall and Hermes sync</name>
  <files>
    server/src/__tests__/skill-registry-adapter-install.test.ts,
    server/src/__tests__/hermes-dual-source.test.ts
  </files>
  <read_first>
    server/src/__tests__/skill-registry.test.ts (if exists — for test patterns),
    server/src/services/skill-registry.ts
  </read_first>
  <behavior>
    skill-registry-adapter-install.test.ts:
    - Test: install() writes files to provided agentSkillsDir, not a hardcoded path
    - Test: uninstall() removes skill directory from disk AND soft-deletes the DB row
    - Test: uninstall() with non-existent directory does not throw (force: true)
    - Test: rollback() restores files to provided agentSkillsDir
    - Test: assignGroup() writes to provided agentSkillsDir
    - Test: removeGroup() removes from provided agentSkillsDir

    hermes-dual-source.test.ts:
    - Test: syncHermesNativeSkills creates skills stub rows with sourceId "hermes-native"
    - Test: syncHermesNativeSkills creates agentSkills rows with source "native"
    - Test: syncHermesNativeSkills is idempotent (running twice doesn't duplicate)
    - Test: syncHermesNativeSkills handles missing ~/.hermes/skills/ gracefully
    - Test: listAgentSkills returns objects with { skillId, source, installedAt }
    - Test: listAgentSkills includes both managed and native skills for a Hermes agent
  </behavior>
  <action>
    Create two test files following the existing test patterns in `server/src/__tests__/`.

    For `skill-registry-adapter-install.test.ts`:
    - Use `vi.mock("node:fs/promises")` to mock filesystem operations (rm, cp, readdir, mkdir)
    - Test that `uninstall(skillId, agentSkillsDir)` calls `rm(path.join(agentSkillsDir, slug), { recursive: true, force: true })`
    - Test that `install` and `rollback` use the provided `agentSkillsDir` path
    - Test that `assignGroup` and `removeGroup` use the provided path (not a default)

    For `hermes-dual-source.test.ts`:
    - Mock `readdir` to return sample skill directory entries
    - Test that `syncHermesNativeSkills` inserts correct rows
    - Test that `listAgentSkills` returns the new object shape
    - Use the existing libSQL test database setup pattern (check how other skill-registry tests set up the DB)

    Run tests: `cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-adapter-install.test.ts src/__tests__/hermes-dual-source.test.ts`
  </action>
  <verify>
    <automated>cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-adapter-install.test.ts src/__tests__/hermes-dual-source.test.ts</automated>
  </verify>
  <acceptance_criteria>
    - skill-registry-adapter-install.test.ts exists with tests for install/uninstall/rollback/assignGroup/removeGroup
    - hermes-dual-source.test.ts exists with tests for syncHermesNativeSkills and listAgentSkills
    - All tests pass
    - uninstall test verifies rm() is called with correct path
    - syncHermesNativeSkills test verifies idempotency
    - listAgentSkills test verifies object shape includes source field
  </acceptance_criteria>
  <done>All unit tests for INST-01 through INST-04 and HERM-01 through HERM-03 service layer pass</done>
</task>

</tasks>

<verification>
- TypeScript compiles: `cd nexus && pnpm tsc --noEmit --project server/tsconfig.json`
- Tests pass: `cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-adapter-install.test.ts src/__tests__/hermes-dual-source.test.ts`
- Schema has source column: grep for `source.*text.*managed` in skill-registry-schema.ts
- Migration guard exists: grep for `ALTER TABLE agent_skills ADD COLUMN source` in skill-registry-db.ts
- No defaultSkillsDir: grep should find NO matches for `defaultSkillsDir` in skill-registry-groups.ts
</verification>

<success_criteria>
- Service layer methods accept agentSkillsDir as resolved path (not client-supplied)
- uninstall removes files from disk before soft-deleting
- agentSkills schema tracks managed vs native source
- syncHermesNativeSkills lazily discovers Hermes native skills from disk
- listAgentSkills returns typed objects with source field
- All tests pass
</success_criteria>

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