392 lines
19 KiB
Markdown
392 lines
19 KiB
Markdown
---
|
|
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>
|