--- phase: 45-content-as-skills plan: 01 type: execute wave: 1 depends_on: [] files_modified: - server/src/skills/content/diagram.SKILL.md - server/src/skills/content/icon-set.SKILL.md - server/src/skills/content/theme-palette.SKILL.md - server/src/skills/content/wallpaper.SKILL.md - server/src/skills/content/social-post.SKILL.md - server/src/skills/content/convert.SKILL.md - server/src/skills/content/pdf-document.SKILL.md - server/src/skills/content/brand-kit.SKILL.md - server/src/skills/content/presentation.SKILL.md - server/src/services/skill-registry-fetcher.ts - server/src/services/skill-registry-db.ts - server/src/index.ts - server/src/__tests__/skill-registry-content-skills.test.ts autonomous: true requirements: - SKILL-01 - SKILL-02 - SKILL-03 must_haves: truths: - "Each of the 9 content types (diagram, icon-set, theme-palette, wallpaper, social-post, convert, pdf-document, brand-kit, presentation) has a SKILL.md file on disk" - "Local content skills are registered in the skill registry DB on server startup" - "The builtin/creative group has all 9 content skill IDs as members after startup" - "Content skills appear in skill registry list and are installable/removable through existing API" - "Generalist agent gets Creative group skills via the existing pendingSkillGroups reconciler" artifacts: - path: "server/src/skills/content/diagram.SKILL.md" provides: "Diagram generation skill definition" contains: "jobType" - path: "server/src/skills/content/presentation.SKILL.md" provides: "Presentation generation skill definition" contains: "jobType" - path: "server/src/services/skill-registry-fetcher.ts" provides: "local-nexus-content source type handler" contains: "local-nexus-content" - path: "server/src/services/skill-registry-db.ts" provides: "Creative group member seeding" contains: "seedCreativeGroupMembers" - path: "server/src/__tests__/skill-registry-content-skills.test.ts" provides: "Tests for SKILL-01, SKILL-02, SKILL-03" contains: "content skills" key_links: - from: "server/src/services/skill-registry-fetcher.ts" to: "server/src/skills/content/*.SKILL.md" via: "readdir + readFile in fetchLocalNexusContent" pattern: "readdir.*SKILL\\.md" - from: "server/src/index.ts" to: "server/src/services/skill-registry.ts" via: "skillRegistryService().fetchAll() on startup" pattern: "fetchAll" - from: "server/src/services/skill-registry-db.ts" to: "skill_group_members table" via: "seedCreativeGroupMembers after getSkillRegistryDb" pattern: "builtin/creative" --- Register all 9 content generation types as installable Nexus skills with a local filesystem source, seed the Creative group, and wire startup. Purpose: Completes the final v1.7 phase by exposing content generators (diagrams, themes, icons, wallpapers, social posts, format conversion, PDFs, brand kits, presentations) as skills that agents can discover and use. Output: 9 SKILL.md files, extended fetcher with local-nexus-content source type, Creative group membership, startup wiring, unit tests. @$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/45-content-as-skills/45-RESEARCH.md @server/src/services/skill-registry-fetcher.ts @server/src/services/skill-registry-db.ts @server/src/services/skill-registry.ts @server/src/services/content-job-runner.ts @server/src/index.ts From server/src/services/skill-registry-fetcher.ts: ```typescript export type SkillSourceConfig = { id: string; type: "anthropic-marketplace" | "github-tree"; owner: string; repo: string; ref: string; label: string; }; export const BUILT_IN_SOURCES: SkillSourceConfig[] = [ /* remote sources */ ]; export function parseSkillFrontmatter(markdown: string): { name?: string; description?: string }; // Private helpers used inside fetcher (same pattern needed for local): // upsertSkill(db, { skillId, sourceId, name, description, sourceUrl }) // cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl }) // upsertCommunityRatingsStub(db, skillId, sourceId) // versionExists(db, versionId) — idempotency guard export async function fetchAllSources(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>; ``` From server/src/services/skill-registry-db.ts: ```typescript export type SkillRegistryDb = /* drizzle instance */; export async function getSkillRegistryDb(): Promise; // seedBuiltinGroups(client) is called inside getSkillRegistryDb — creates builtin/creative group row ``` From server/src/services/skill-registry.ts: ```typescript export function skillRegistryService() { return { async list(opts?: { includeRemoved?: boolean }): Promise; async install(skillId: string, agentId: string, agentSkillsDir: string): Promise; async uninstall(skillId: string, agentId: string, agentSkillsDir: string): Promise; async fetchAll(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>; }; } ``` From server/src/services/content-job-runner.ts — jobType values: ``` "diagram", "icon-set", "theme-palette", "wallpaper", "social-post", "convert", "pdf-document", "brand-kit", "presentation" ``` From server/src/index.ts — startup blocks: ```typescript // Line 617-626: skill registry init (fire-and-forget) void (async () => { const { getSkillRegistryDb } = await import("./services/skill-registry-db.js"); await getSkillRegistryDb(); logger.info("skill registry database initialized"); })(); // Line 628-659: pendingSkillGroups reconciler (fire-and-forget) // Assigns groups to agents with metadata.pendingSkillGroups // GROUP_NAME_MAP: { "Creative": "builtin/creative", ... } ``` Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source server/src/skills/content/diagram.SKILL.md server/src/skills/content/icon-set.SKILL.md server/src/skills/content/theme-palette.SKILL.md server/src/skills/content/wallpaper.SKILL.md server/src/skills/content/social-post.SKILL.md server/src/skills/content/convert.SKILL.md server/src/skills/content/pdf-document.SKILL.md server/src/skills/content/brand-kit.SKILL.md server/src/skills/content/presentation.SKILL.md server/src/services/skill-registry-fetcher.ts server/src/__tests__/skill-registry-content-skills.test.ts server/src/services/skill-registry-fetcher.ts server/src/services/skill-registry-db.ts server/src/services/content-job-runner.ts server/src/__tests__/skill-registry-fetch.test.ts - Test: 9 .SKILL.md files exist in server/src/skills/content/ with correct names - Test: Each SKILL.md has valid frontmatter with name and description fields - Test: fetchLocalNexusContent reads all 9 files, upserts skills, caches versions - Test: fetchAllSources with local-nexus-content source returns fetched=9 - Test: Duplicate fetch (restart) does not create duplicate rows (idempotency) - Test: Missing directory returns 0 fetched, no error 1. Create directory `server/src/skills/content/`. 2. Author 9 SKILL.md files, one per content type. Each file MUST have: - YAML frontmatter with single-line `name` and single-line `description` (parseSkillFrontmatter uses single-line regex — do NOT use YAML block scalars like `>` or `|`) - H1 heading matching the content type - "Usage" section with the exact `POST /api/content-jobs` payload: `jobType` value and `input` fields - "Output" section describing what the job returns - Keep each file under 40 lines — these are agent-consumable instructions, not documentation Skill files and their jobType values: - diagram.SKILL.md — jobType: "diagram", input: { description, type? } - icon-set.SKILL.md — jobType: "icon-set", input: { description, style?, count? } - theme-palette.SKILL.md — jobType: "theme-palette", input: { seedColor, name? } - wallpaper.SKILL.md — jobType: "wallpaper", input: { description, resolution? } - social-post.SKILL.md — jobType: "social-post", input: { topic, platform?, includeHashtags? } - convert.SKILL.md — jobType: "convert", input: { sourceAssetId, targetFormat } - pdf-document.SKILL.md — jobType: "pdf-document", input: { content, docType? } - brand-kit.SKILL.md — jobType: "brand-kit", input: { companyName, description, seedColor? } - presentation.SKILL.md — jobType: "presentation", input: { topic, slideCount?, style? } 3. Extend `SkillSourceConfig` type in skill-registry-fetcher.ts to be a discriminated union: ```typescript export type SkillSourceConfig = | { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string } | { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string } | { id: string; type: "local-nexus-content"; dir: string; label: string }; ``` 4. Add `fetchLocalNexusContent` function (NOT exported — private like the other fetch handlers): - Reads `source.dir` with `readdir({ withFileTypes: true })` - Filters for `.SKILL.md` suffix - For each file: derives `slug` from filename, `skillId` = `${source.id}/${slug}` - Reads file content, computes SHA-1 content hash - Calls `upsertSkill`, `cacheSkillVersion`, `upsertCommunityRatingsStub` (same pattern as fetchGitHubTree) - Wraps `readdir` in try/catch — returns 0 if dir missing - Uses `readdir` and `readFile` from `node:fs/promises` (already imported) 5. Add `"local-nexus-content"` branch in `fetchAllSources`: ```typescript } else if (source.type === "local-nexus-content") { fetched += await fetchLocalNexusContent(source, db); } ``` 6. Add local source to `BUILT_IN_SOURCES` array: ```typescript { id: "nexus-content", type: "local-nexus-content", dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"), label: "Nexus Content Tools", }, ``` Add `import { fileURLToPath } from "node:url";` if not already present. 7. Write test file `server/src/__tests__/skill-registry-content-skills.test.ts`: - Use vitest + temp directory pattern from existing skill-registry-fetch.test.ts - Test that all 9 SKILL.md files exist and have parseable frontmatter - Test fetchLocalNexusContent with a temp dir containing mock SKILL.md files - Test idempotency (calling twice does not duplicate rows) - Test missing directory returns 0 - Test Creative group members (see Task 2 — leave placeholder describe block) cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30 - grep -r "jobType" server/src/skills/content/*.SKILL.md | wc -l returns 9 - grep "local-nexus-content" server/src/services/skill-registry-fetcher.ts returns matches - grep "nexus-content" server/src/services/skill-registry-fetcher.ts returns match in BUILT_IN_SOURCES - ls server/src/skills/content/*.SKILL.md | wc -l returns 9 9 SKILL.md files authored with correct frontmatter, local-nexus-content source type added to fetcher, all tests pass Task 2: Seed Creative group members and wire startup sequence server/src/services/skill-registry-db.ts server/src/index.ts server/src/__tests__/skill-registry-content-skills.test.ts server/src/services/skill-registry-db.ts server/src/index.ts server/src/__tests__/skill-registry-content-skills.test.ts 1. In `skill-registry-db.ts`, add a `seedCreativeGroupMembers` function (exported for testing): ```typescript const NEXUS_CONTENT_SKILL_IDS = [ "nexus-content/diagram", "nexus-content/icon-set", "nexus-content/theme-palette", "nexus-content/wallpaper", "nexus-content/social-post", "nexus-content/convert", "nexus-content/pdf-document", "nexus-content/brand-kit", "nexus-content/presentation", ] as const; export async function seedCreativeGroupMembers(client: LibSQLClient): Promise { const now = Date.now(); for (const skillId of NEXUS_CONTENT_SKILL_IDS) { await client.execute({ sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`, args: ["builtin/creative", skillId, now], }); } } ``` IMPORTANT: Do NOT call seedCreativeGroupMembers inside getSkillRegistryDb(). The skills must be registered first (via fetchAll), then group members seeded. This is wired in index.ts startup. 2. In `server/src/index.ts`, modify the existing skill registry init block (lines ~617-626) to also call fetchAll and seed Creative group members. The sequence must be: a. `getSkillRegistryDb()` — creates tables + builtin group rows b. `skillRegistryService().fetchAll()` — registers local skills (+ remote) c. `seedCreativeGroupMembers(client)` — adds skill IDs to builtin/creative group Updated block: ```typescript // [nexus] Initialize skill registry, seed content skills, and populate Creative group void (async () => { try { const { getSkillRegistryDb } = await import("./services/skill-registry-db.js"); const db = await getSkillRegistryDb(); const { skillRegistryService } = await import("./services/skill-registry.js"); await skillRegistryService().fetchAll(); // Seed Creative group members AFTER skills are registered const { seedCreativeGroupMembers } = await import("./services/skill-registry-db.js"); // getSkillRegistryDb uses libSQL client internally — get the raw client for SQL // Use a direct import of createClient since we need the raw client for INSERT OR IGNORE const { _getClient } = await import("./services/skill-registry-db.js"); await seedCreativeGroupMembers(_getClient()); logger.info("skill registry initialized, content skills seeded, Creative group populated"); } catch (err) { logger.error({ err }, "skill registry init failed"); } })(); ``` ALTERNATIVELY (simpler): Export a helper from skill-registry-db.ts that wraps the client access: ```typescript export async function seedCreativeGroupMembersFromDb(): Promise { // Re-use the existing singleton client from getSkillRegistryDb const db = await getSkillRegistryDb(); // Access the raw client via drizzle's session // ... or just use db.run with raw SQL ``` The simplest approach: expose the raw `_client` from skill-registry-db.ts (already has the singleton pattern), or use `db.run(sql\`INSERT OR IGNORE...\`)` with drizzle's raw SQL. Check how `seedBuiltinGroups` accesses the client — it receives `client: LibSQLClient` directly inside `getSkillRegistryDb()`. For the external call, either: - Export a `getClient()` function that returns the singleton LibSQLClient - Or have `seedCreativeGroupMembers` accept the drizzle db and use `db.run()` Choose the approach that requires minimal changes. The `_db` singleton is already module-scoped in skill-registry-db.ts — adding an exported `getRawClient()` that returns the `_client` singleton is the cleanest path. 3. CRITICAL ORDERING: The Creative group seeding MUST happen AFTER fetchAll completes, and BEFORE the pendingSkillGroups reconciler runs. Since both are fire-and-forget async blocks, merge them into a single sequential block: - Move the pendingSkillGroups reconciler (lines ~628-659) inside the skill registry init block, after seedCreativeGroupMembers - This guarantees: DB init -> fetchAll -> seed group members -> reconcile pending groups 4. Add tests to the placeholder describe block in skill-registry-content-skills.test.ts: - Test: After fetchAll + seedCreativeGroupMembers, querying skill_group_members for "builtin/creative" returns 9 rows - Test: Content skills appear in skillRegistryService().list() result - Test: A content skill can be installed to a temp agent dir via svc.install() 5. Run `cd /opt/nexus/server && npx tsc --noEmit` to verify no type errors. cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30 && npx tsc --noEmit 2>&1 | tail -20 - grep "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts returns matches - grep "fetchAll" server/src/index.ts returns match in the skill registry init block - grep "seedCreativeGroupMembers" server/src/index.ts returns match - grep "builtin/creative" server/src/__tests__/skill-registry-content-skills.test.ts returns matches - npx tsc --noEmit exits 0 Creative group has 9 content skill members after startup, pendingSkillGroups reconciler runs after group is populated, all tests pass, tsc clean 1. `ls server/src/skills/content/*.SKILL.md | wc -l` returns 9 2. `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts` — all tests pass 3. `cd /opt/nexus/server && npx tsc --noEmit` — no type errors 4. `grep -c "local-nexus-content" server/src/services/skill-registry-fetcher.ts` returns >= 2 (type + BUILT_IN_SOURCES) 5. `grep -c "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts` returns >= 1 6. `grep "fetchAll\|seedCreativeGroupMembers" server/src/index.ts` shows both in the startup block - 9 SKILL.md files exist with valid frontmatter and jobType documentation - Local-nexus-content source type registered and functional - Creative group populated with all 9 content skill IDs - Startup sequence correct: DB init -> fetchAll -> seed group -> reconcile pending - All unit tests pass, tsc clean After completion, create `.planning/phases/45-content-as-skills/45-01-SUMMARY.md`