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>
232 lines
12 KiB
Markdown
232 lines
12 KiB
Markdown
---
|
|
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>
|