---
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"
---

<objective>
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.
</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/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

<interfaces>
<!-- Key types and contracts the executor needs. -->

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<SkillRegistryDb>;
// 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<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:
```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", ... }
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source</name>
  <files>
    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
  </files>
  <read_first>
    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
  </read_first>
  <behavior>
    - 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
  </behavior>
  <action>
    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)
  </action>
  <verify>
    <automated>cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>9 SKILL.md files authored with correct frontmatter, local-nexus-content source type added to fetcher, all tests pass</done>
</task>

<task type="auto">
  <name>Task 2: Seed Creative group members and wire startup sequence</name>
  <files>
    server/src/services/skill-registry-db.ts
    server/src/index.ts
    server/src/__tests__/skill-registry-content-skills.test.ts
  </files>
  <read_first>
    server/src/services/skill-registry-db.ts
    server/src/index.ts
    server/src/__tests__/skill-registry-content-skills.test.ts
  </read_first>
  <action>
    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.
  </action>
  <verify>
    <automated>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</automated>
  </verify>
  <acceptance_criteria>
    - 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
  </acceptance_criteria>
  <done>Creative group has 9 content skill members after startup, pendingSkillGroups reconciler runs after group is populated, all tests pass, tsc clean</done>
</task>

</tasks>

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

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

<output>
After completion, create `.planning/phases/45-content-as-skills/45-01-SUMMARY.md`
</output>
