nexus/.planning/phases/45-content-as-skills/45-01-PLAN.md

19 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
45-content-as-skills 01 execute 1
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
true
SKILL-01
SKILL-02
SKILL-03
truths artifacts key_links
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
path provides contains
server/src/skills/content/diagram.SKILL.md Diagram generation skill definition jobType
path provides contains
server/src/skills/content/presentation.SKILL.md Presentation generation skill definition jobType
path provides contains
server/src/services/skill-registry-fetcher.ts local-nexus-content source type handler local-nexus-content
path provides contains
server/src/services/skill-registry-db.ts Creative group member seeding seedCreativeGroupMembers
path provides contains
server/src/__tests__/skill-registry-content-skills.test.ts Tests for SKILL-01, SKILL-02, SKILL-03 content skills
from to via pattern
server/src/services/skill-registry-fetcher.ts server/src/skills/content/*.SKILL.md readdir + readFile in fetchLocalNexusContent readdir.*SKILL.md
from to via pattern
server/src/index.ts server/src/services/skill-registry.ts skillRegistryService().fetchAll() on startup fetchAll
from to via pattern
server/src/services/skill-registry-db.ts skill_group_members table seedCreativeGroupMembers after getSkillRegistryDb 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.

<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/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:

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:

export type SkillRegistryDb = /* drizzle instance */;
export async function getSkillRegistryDb(): Promise<SkillRegistryDb>;
// seedBuiltinGroups(client) is called inside getSkillRegistryDb — creates builtin/creative group row

From server/src/services/skill-registry.ts:

export function skillRegistryService() {
  return {
    async list(opts?: { includeRemoved?: boolean }): Promise<SkillListItem[]>;
    async install(skillId: string, agentId: string, agentSkillsDir: string): Promise<void>;
    async uninstall(skillId: string, agentId: string, agentSkillsDir: string): Promise<void>;
    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:

// 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<void> {
     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<void> {
     // 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

<success_criteria>

  • 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 </success_criteria>
After completion, create `.planning/phases/45-content-as-skills/45-01-SUMMARY.md`