---
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"
---
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.
@$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/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
Task 1: Schema + migration guard + service method signatures
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
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
**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 `?`).
cd nexus && pnpm tsc --noEmit --project server/tsconfig.json 2>&1 | head -30
- 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
Schema has source column, migration guard runs on DB init, uninstall removes files, syncHermesNativeSkills exists, listAgentSkills returns typed objects, group functions require agentSkillsDir
Task 2: Unit tests for adapter-aware install/uninstall and Hermes sync
server/src/__tests__/skill-registry-adapter-install.test.ts,
server/src/__tests__/hermes-dual-source.test.ts
server/src/__tests__/skill-registry.test.ts (if exists — for test patterns),
server/src/services/skill-registry.ts
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
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`
cd nexus && pnpm --filter @paperclipai/server exec vitest run src/__tests__/skill-registry-adapter-install.test.ts src/__tests__/hermes-dual-source.test.ts
- 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
All unit tests for INST-01 through INST-04 and HERM-01 through HERM-03 service layer pass
- 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
- 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