nexus/.planning/milestones/v1.2.1-phases/19-adapter-aware-install-uninstall/19-01-PLAN.md
Mikkel Georgsen 63636228d3 [nexus] chore: migrate .planning/ from agent repo to nexus repo
Planning artifacts (milestones v1.0-v1.2.1, v1.3 queue, PROJECT.md,
STATE.md, config) now live alongside the code they describe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:24:31 +02:00

12 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
19-adapter-aware-install-uninstall 01 execute 1
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
true
INST-01
INST-02
INST-03
INST-04
HERM-01
HERM-02
HERM-03
truths artifacts key_links
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
path provides contains
server/src/services/skill-registry-schema.ts source column on agentSkills table source.*text.*managed
path provides contains
server/src/services/skill-registry-db.ts ALTER TABLE migration guard for source column ALTER TABLE agent_skills ADD COLUMN source
path provides contains
server/src/services/skill-registry.ts Updated install/uninstall/rollback methods, syncHermesNativeSkills, listAgentSkills returning objects syncHermesNativeSkills
path provides
server/src/services/skill-registry-groups.ts assignGroup/removeGroup using passed agentSkillsDir, no defaultSkillsDir fallback
from to via pattern
server/src/services/skill-registry-db.ts server/src/services/skill-registry-schema.ts ALTER TABLE matches Drizzle schema source column source.*text.*managed
from to via pattern
server/src/services/skill-registry.ts server/src/services/skill-registry-schema.ts agentSkills.source used in insert/select queries 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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
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

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