--- 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 After completion, create `.planning/phases/19-adapter-aware-install-uninstall/19-01-SUMMARY.md`