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