feat: Phase 45 — Content as Skills (9 SKILL.md files, Creative group, gap fixes)

This commit is contained in:
Nexus Dev 2026-04-05 09:57:20 +00:00
parent d25d88d053
commit 1c8a26dbb4
27 changed files with 2554 additions and 59 deletions

View file

@ -1,5 +1,35 @@
# Milestones
## v1.7 Content Generation (Shipped: 2026-04-05)
**Phases completed:** 6 phases, 21 plans, 38 tasks
**Key accomplishments:**
- One-liner:
- One-liner:
- culori/resvg/svgo deps installed, RenderResult bundle types defined, content-job-runner wired to diagram/icon-set/theme-palette switch, and useContentJob SSE hook ready for UI plans
- Diagram renderer synthesizes Mermaid from natural language via LLM (DIAG-01), strips unsafe directives (DIAG-05), renders SVG+PNG via Playwright+DOMPurify+Resvg; icon renderer generates SVG icon sets via LLM, cleans with SVGO, rasterizes to 3 PNG sizes via sharp
- OKLCH palette engine with 7-role dark/light generation from a single hex seed, WCAG AA validation via culori+wcag-contrast, four export formatters (CSS custom props, Tailwind config, VS Code theme, JSON), and nexus-settings.json extended with customTheme persistence
- ContentStudio page with Diagrams and Icons tabs fully functional: prompt+type selector, generate button, SSE progress, SVG preview with download, collapsible source editor (TDD-tested), icon grid with selection and bulk download bar
- All theme UI components built and wired: seed input, palette grid with WCAG badges, scoped live preview (with TDD test coverage for THEME-04), 4-format export tabs, apply confirmation dialog, ThemeContext custom theme extension, ContentStudio Themes tab fully functional
- Full test suite run (30 server + 13 UI component tests passing), server tsc clean, UI tsc clean for Phase 41 files; ThemeContext backward-compat exports restored to fix regression from 41-05 worktree commit
- file-type/xlsx/csv-parse installed, four bundle types added to types.ts, wallpaper/social-post/convert cases wired in content-job-runner, and converter-capabilities service probing pandoc/libreoffice via new execFileNoThrow utility
- LLM SVG wallpaper generation rasterized via sharp at 12 platform dimensions, plus platform-aware social post JSON renderer with hashtags and Instagram carousel support
- Full convert-renderer.ts routing sharp/ffmpeg/xlsx/AI-bridge by format pair, multipart upload route with magic-byte MIME validation returning 202, and GET /api/system/converters capability endpoint
- useSystemProviders hook wires /api/system/providers to ChatInput offline badge, surfacing local Whisper availability via WifiOff icon when voice mode is active
- Four React components + extended ContentStudio tabs: wallpaper generator with 12-platform grouped selector and multi-size app icon grid, social post generator with live character count and hashtag chip copy, carousel collapsible slides
- Drag-drop ConvertPanel with grouped format chips, AI fallback notice, MIME error display, SSE job tracking, and deep-link /convert/:src/:tgt routing
- Playwright HTML-to-PDF renderer for 4 document types (report/invoice/api-docs/one-pager) with PdfDocumentBundle/BrandKitBundle types and content-job-runner wiring
- Full brand identity kit renderer orchestrating logo SVG, 5 avatar sizes, 5 social platform images, email signature HTML, letterhead HTML, guidelines PDF, and ZIP packaging via archiver
- DocumentGeneratePanel, BrandKitPanel, BrandKitResult UI components with pdf-document and brand-kit job submission, PDF/ZIP download, and Documents/Brand tabs added to ContentStudio (7 tabs total)
- Remotion 4.0.445 workspace package with PitchDeck/DemoVideo compositions, cached getBundlePath, concurrency:1 renderPresentationComposition, and presentation job wiring in content-job-runner
- renderPresentation function with LLM pitch-deck/demo-video slide generation (puterChatComplete), Remotion MP4 rendering (concurrency:1 via content-renderer), and SSE content_job.progress events
- PresentationPanel with real-time SSE progress bar, MP4 video player + download, and Presentations tab in ContentStudio — backed by fine-grained data.progress in useContentJob
- 9 content generators registered as installable Nexus skills via local-nexus-content source type, with Creative group seeded and startup sequence unified
---
## v1.6 Voice Pipeline + Minimal Message Bridge (Shipped: 2026-04-04)
**Phases completed:** 4 phases, 12 plans, 14 tasks

View file

@ -93,9 +93,9 @@ Requirements for Content Generation milestone. Each maps to roadmap phases.
### Content as Skills
- [ ] **SKILL-01**: Each content type is implemented as an installable Nexus skill
- [ ] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
- [ ] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
- [x] **SKILL-01**: Each content type is implemented as an installable Nexus skill
- [x] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
- [x] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
## Future Requirements
@ -179,9 +179,9 @@ Which phases cover which requirements. Updated during roadmap creation.
| PRES-02 | Phase 44 | Complete |
| PRES-03 | Phase 44 | Complete |
| PRES-04 | Phase 44 | Complete |
| SKILL-01 | Phase 45 | Pending |
| SKILL-02 | Phase 45 | Pending |
| SKILL-03 | Phase 45 | Pending |
| SKILL-01 | Phase 45 | Complete |
| SKILL-02 | Phase 45 | Complete |
| SKILL-03 | Phase 45 | Complete |
**Coverage:**
- v1.7 requirements: 52 total

View file

@ -187,7 +187,7 @@ Plans:
- [x] **Phase 42: Wallpapers, Social, Format Conversion & Voice** — LLM SVG + sharp wallpapers, social content, format conversion registry with AI fallback, Whisper web chat mic (WALL-01..04, SOCIAL-01..03, CONV-01..09, VOICE-01..03) (completed 2026-04-04)
- [x] **Phase 43: Documents & Branding** — Playwright PDF reports and invoices, full brand identity kit with zip export (DOC-01..03, BRAND-01..06) (completed 2026-04-04)
- [x] **Phase 44: Video & Presentations** — Remotion workspace package, pitch decks and demo videos, SSE render progress (PRES-01..04) (completed 2026-04-04)
- [ ] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03)
- [x] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03) (completed 2026-04-04)
## Phase Details
@ -278,8 +278,8 @@ Plans:
Plans:
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
- [ ] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
- [ ] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
**UI hint**: yes
### Phase 45: Content as Skills
@ -290,7 +290,10 @@ Plans:
1. Each content type (diagram, theme, icon, wallpaper, social post, PDF, brand kit, video) has a corresponding skill file that an agent can load and use to call the correct content job API
2. A freshly created generalist agent has the Creative skill group pre-loaded — it can generate diagrams and themes without any manual skill configuration
3. A user can add or remove individual content type skills through the Skill Aggregator UI without touching configuration files
**Plans**: TBD
**Plans**: 1 plan
Plans:
- [x] 45-01-PLAN.md — 9 SKILL.md files, local-nexus-content source type, Creative group seeding, startup wiring
**UI hint**: yes
---
@ -387,4 +390,4 @@ All 52 v1.7 requirements are mapped to exactly one phase. No orphans.
| 42. Wallpapers, Social, Format Conversion & Voice | v1.7 | 6/6 | Complete | 2026-04-04 |
| 43. Documents & Branding | v1.7 | 3/3 | Complete | 2026-04-04 |
| 44. Video & Presentations | v1.7 | 3/3 | Complete | 2026-04-04 |
| 45. Content as Skills | v1.7 | 0/TBD | Not started | - |
| 45. Content as Skills | v1.7 | 1/1 | Complete | 2026-04-04 |

View file

@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.7
milestone_name: Content Generation
status: verifying
stopped_at: Completed 44-02-PLAN.md — presentation-renderer with LLM slide generation, Remotion render pipeline, SSE progress events, tsc compilation verified
last_updated: "2026-04-04T23:36:43.205Z"
last_activity: 2026-04-04
stopped_at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean
last_updated: "2026-04-05T08:46:16.382Z"
last_activity: 2026-04-05
progress:
total_phases: 6
completed_phases: 5
total_plans: 20
completed_plans: 20
completed_phases: 6
total_plans: 21
completed_plans: 21
percent: 0
---
@ -21,14 +21,14 @@ progress:
See: .planning/PROJECT.md (updated 2026-04-04)
**Core value:** A fresh onboard asks for ONE thing (root directory), auto-creates PM + Engineer agents, and drops you in the dashboard.
**Current focus:** Phase 44 — video-presentations
**Current focus:** Phase 45 — content-as-skills
## Current Position
Phase: 45
Plan: Not started
Status: Phase complete — ready for verification
Last activity: 2026-04-04
Last activity: 2026-04-05
Progress: [░░░░░░░░░░] 0%
@ -99,6 +99,7 @@ Key constraints for v1.7:
- [Phase 44-video-presentations]: mp4Base64 blob URL created in useMemo and revoked in useEffect return — correct lifecycle for React renders
- [Phase 44-video-presentations]: Ambient module declaration in server/src/types/content-renderer.d.ts provides type safety for dynamic import without pulling JSX into server tsc context
- [Phase 44-video-presentations]: content-renderer NOT added as workspace dep in server/package.json — symlink causes tsc to walk JSX source; ambient declaration is sufficient for type safety and runtime works via monorepo node_modules
- [Phase 45-content-as-skills]: SkillSourceConfig changed to discriminated union; local-nexus-content source uses SHA-1 content hash for idempotency; seedCreativeGroupMembers runs after fetchAll in unified startup block
### Pending Todos
@ -113,6 +114,6 @@ None yet.
## Session Continuity
Last session: 2026-04-04T23:35:57.656Z
Stopped at: Completed 44-02-PLAN.md — presentation-renderer with LLM slide generation, Remotion render pipeline, SSE progress events, tsc compilation verified
Last session: 2026-04-04T23:55:26.416Z
Stopped at: Completed 45-01-PLAN.md — 9 content SKILL.md files, local-nexus-content source type, Creative group seeded, unified startup sequence, tsc clean
Resume file: None

View file

@ -0,0 +1,115 @@
---
milestone: v1.7
audited: 2026-04-05T00:00:00Z
status: gaps_found
scores:
requirements: 49/55
phases: 6/6
integration: 49/55
flows: 7/9
gaps:
requirements:
- id: "THEME-01"
status: "unsatisfied"
phase: "41"
evidence: "ThemeSeedInput prop interface mismatch — ContentStudio passes wrong props"
- id: "THEME-02"
status: "partial"
phase: "41"
evidence: "Server renderer works but UI never displays palette (bundle undefined)"
- id: "THEME-03"
status: "unsatisfied"
phase: "41"
evidence: "themeJob.bundle does not exist on useContentJob — palette grid never renders"
- id: "THEME-04"
status: "unsatisfied"
phase: "41"
evidence: "ThemePreviewPanel never renders — gated on undefined bundle"
- id: "THEME-05"
status: "unsatisfied"
phase: "41"
evidence: "ThemeExportTabs never renders — gated on undefined bundle"
- id: "THEME-06"
status: "unsatisfied"
phase: "41"
evidence: "ThemeApplyConfirmDialog receives onOpenChange but expects onCancel prop"
- id: "CONV-09"
status: "unsatisfied"
phase: "42"
evidence: "convert.ts passes {} as StorageService — storage.putFile throws at runtime"
- id: "SKILL-02"
status: "partial"
phase: "45"
evidence: "All 9 SKILL.md files document wrong API path and 3 have wrong input field names"
integration:
- from: "Phase 41-05 ThemeSeedInput"
to: "Phase 41-06 ContentStudio Themes tab"
issue: "Incompatible prop interfaces from parallel worktree execution"
- from: "Phase 42-03 convert route"
to: "Phase 40 content-job-runner"
issue: "StorageService passed as empty object — runtime crash on job completion"
- from: "Phase 45 SKILL.md files"
to: "Phase 40-42 routes and renderers"
issue: "Wrong API path and input field names in skill documentation"
flows:
- name: "Themes tab"
breaks_at: "ContentStudio.tsx — 3 prop mismatches + missing bundle state"
- name: "Format conversion delivery"
breaks_at: "convert.ts line 142 — StorageService is empty object"
tech_debt: []
nyquist:
compliant_phases: 0
partial_phases: 1
missing_phases: 5
overall: "partial"
---
# Milestone v1.7 — Content Generation Audit
## Summary
| Metric | Score |
|--------|-------|
| Requirements | 49/55 satisfied |
| Phases | 6/6 executed |
| Integration | 49/55 wired |
| E2E Flows | 7/9 complete |
## Unsatisfied Requirements
### THEME-01 through THEME-06 (Phase 41)
The theme engine server-side implementation is complete (palette generation, WCAG validation, exports, nexus-settings persistence). However, the ContentStudio Themes tab has 3 compile-time errors from worktree merge divergence:
1. `ThemeSeedInput` receives wrong props (`companyId/onSubmit/isLoading` vs `value/onChange/className`)
2. `themeJob.bundle` does not exist on `useContentJob` — palette display gated on undefined
3. `ThemeApplyConfirmDialog` receives `onOpenChange` but expects `onCancel`
### CONV-09 (Phase 42)
Convert route passes `{} as StorageService` to `contentJobRunner.dispatch`. Jobs start but crash on completion when `storage.putFile` is called.
### SKILL-02 (Phase 45) — Partial
All 9 SKILL.md files document `POST /api/content-jobs` but actual route is `POST /api/companies/:companyId/content-jobs`. Three files also have input field name mismatches.
## Working Flows (7/9)
| Flow | Status |
|------|--------|
| Diagrams | ✓ Complete |
| Icons | ✓ Complete |
| Wallpapers | ✓ Complete |
| Social Posts | ✓ Complete |
| Documents | ✓ Complete |
| Brand Kit | ✓ Complete |
| Presentations | ✓ Complete |
| Themes | ✗ Broken (UI compile errors) |
| Format Conversion | ✗ Broken (runtime crash on delivery) |
## Nyquist Compliance
| Phase | VALIDATION.md | Compliant |
|-------|---------------|-----------|
| 40 | exists | partial |
| 41 | exists | partial |
| 42 | missing | — |
| 43 | missing | — |
| 44 | missing | — |
| 45 | missing | — |

View file

@ -0,0 +1,202 @@
# Requirements Archive: v1.7 Content Generation
**Archived:** 2026-04-05
**Status:** SHIPPED
For current requirements, see `.planning/REQUIREMENTS.md`.
---
# Requirements: Nexus v1.7 — Content Generation
**Defined:** 2026-04-04
**Core Value:** A fresh onboard asks for ONE thing (root directory), auto-creates PM + Engineer agents, and drops you in the dashboard — no company names, missions, or corporate language anywhere.
## v1.7 Requirements
Requirements for Content Generation milestone. Each maps to roadmap phases.
### Infrastructure
- [x] **INFRA-01**: System processes content generation jobs asynchronously with queued → running → done/failed lifecycle
- [x] **INFRA-02**: System pushes job progress updates via SSE to connected clients
- [x] **INFRA-03**: Generated content stored in namespaced storage without size restrictions blocking video/images
- [x] **INFRA-04**: All generated content tracked in database with source conversation linkage
### Diagram Generation
- [x] **DIAG-01**: User can generate diagrams from natural language description
- [x] **DIAG-02**: System renders Mermaid syntax to SVG and PNG formats
- [x] **DIAG-03**: User can view and edit the Mermaid source for refinement
- [x] **DIAG-04**: System supports architecture, flowchart, ERD, sequence, and mind map diagram types
- [x] **DIAG-05**: Mermaid rendering enforces strict security level to prevent XSS
### Theme & Palette
- [x] **THEME-01**: User can pick a seed color and receive a complete palette (background, surface, overlay, text, accents)
- [x] **THEME-02**: System generates palette in OKLCH color space with Catppuccin-style naming
- [x] **THEME-03**: System validates WCAG AA contrast for all foreground/background pairs
- [x] **THEME-04**: User can preview Nexus UI with the generated palette live
- [x] **THEME-05**: User can export palette as CSS custom properties, Tailwind config, VS Code theme, or JSON
- [x] **THEME-06**: System generates dark and light variants from single seed color
- [x] **THEME-07**: User can apply generated theme to their Nexus instance in one click
### Document Generation
- [x] **DOC-01**: User can generate formatted PDF reports from conversation content
- [x] **DOC-02**: User can generate invoices and contracts from templates
- [x] **DOC-03**: User can generate one-pagers and API documentation
### Icon Generation
- [x] **ICON-01**: User can generate SVG icons from a text description
- [x] **ICON-02**: System produces icon sets with consistent visual style
- [x] **ICON-03**: User can export icons in multiple sizes and formats (SVG, PNG)
### Wallpapers & Visual Assets
- [x] **WALL-01**: User can generate desktop and mobile wallpapers from a description
- [x] **WALL-02**: User can generate social media banners with correct dimensions per platform
- [x] **WALL-03**: User can generate Open Graph and social preview images
- [x] **WALL-04**: User can generate app icons and favicons in multiple sizes
### Presentations & Video
- [x] **PRES-01**: User can generate pitch deck presentations from a conversation
- [x] **PRES-02**: System renders presentations via Remotion to interactive web or MP4
- [x] **PRES-03**: User can generate demo and explainer videos from conversation content
- [x] **PRES-04**: System shows render progress via SSE during video generation
### Social Media Content
- [x] **SOCIAL-01**: User can generate platform-ready posts respecting character limits (Twitter, LinkedIn)
- [x] **SOCIAL-02**: User can generate Instagram carousels and thread sequences
- [x] **SOCIAL-03**: System suggests relevant hashtags for generated content
### Branding Media Kit
- [x] **BRAND-01**: User can generate a full brand identity from a single conversation
- [x] **BRAND-02**: System produces logo mark (SVG), avatar in multiple sizes
- [x] **BRAND-03**: System produces social media profile images and banners per platform
- [x] **BRAND-04**: System produces email signature and letterhead templates
- [x] **BRAND-05**: System produces a brand guidelines document (PDF)
- [x] **BRAND-06**: User can download all brand assets as a zip package
### Format Conversion
- [x] **CONV-01**: User can convert between image formats (PNG, JPG, SVG, WebP, GIF) via sharp
- [x] **CONV-02**: User can convert between audio/video formats via ffmpeg
- [x] **CONV-03**: User can convert between document formats (Markdown, HTML, PDF, DOCX) via Pandoc/LibreOffice
- [x] **CONV-04**: User can convert between data formats (CSV, JSON, XLSX) via direct tooling
- [x] **CONV-05**: User can convert between any format pair via AI-bridged conversion for semantically complex transforms
- [x] **CONV-06**: System provides a conversion UI with source/target format selection and drag-drop input
- [x] **CONV-07**: User can deep-link to specific conversion flows via URL (e.g. `/convert/png/svg`)
- [x] **CONV-08**: System detects available direct converters at startup and degrades gracefully — unavailable direct paths fall through to AI-bridged conversion rather than showing as blocked
- [x] **CONV-09**: System validates uploaded file MIME type via magic-byte detection before processing
### Whisper Web Chat
- [x] **VOICE-01**: User can click a mic button in web chat to record and auto-transcribe via Whisper
- [x] **VOICE-02**: User can toggle between text-only, voice-input, and full-voice modes
- [x] **VOICE-03**: Voice input works offline with local Whisper model
### Content as Skills
- [x] **SKILL-01**: Each content type is implemented as an installable Nexus skill
- [x] **SKILL-02**: Generalist agent is pre-loaded with a "Creative" skill group
- [x] **SKILL-03**: Users can add or remove content type skills through the Skill Aggregator
## Future Requirements
Deferred to future release. Tracked but not in current roadmap.
### AI Image Generation
- **AIGEN-01**: User can generate images via local Stable Diffusion / ComfyUI
- **AIGEN-02**: User can generate images via cloud APIs (DALL-E, Midjourney)
### Advanced Voice
- **AVOICE-01**: Wake word detection ("Hey Nexus")
- **AVOICE-02**: Voice call / real-time audio streaming
## Out of Scope
| Feature | Reason |
|---------|--------|
| AI image generation (SD/DALL-E) | VRAM conflicts with LLM on M4; cloud sends data externally |
| Social media publishing | API rate limits, auth complexity; generation only for v1.7 |
| Batch conversion queue | Single-user deployment; one-at-a-time sufficient |
| Real-time collaborative editing of generated content | Single-user target |
| Wake word detection | Future consideration |
| Voice call / real-time audio streaming | Future consideration |
## Traceability
Which phases cover which requirements. Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| INFRA-01 | Phase 40 | Complete |
| INFRA-02 | Phase 40 | Complete |
| INFRA-03 | Phase 40 | Complete |
| INFRA-04 | Phase 40 | Complete |
| DIAG-01 | Phase 41 | Complete |
| DIAG-02 | Phase 41 | Complete |
| DIAG-03 | Phase 41 | Complete |
| DIAG-04 | Phase 41 | Complete |
| DIAG-05 | Phase 41 | Complete |
| THEME-01 | Phase 41 | Complete |
| THEME-02 | Phase 41 | Complete |
| THEME-03 | Phase 41 | Complete |
| THEME-04 | Phase 41 | Complete |
| THEME-05 | Phase 41 | Complete |
| THEME-06 | Phase 41 | Complete |
| THEME-07 | Phase 41 | Complete |
| ICON-01 | Phase 41 | Complete |
| ICON-02 | Phase 41 | Complete |
| ICON-03 | Phase 41 | Complete |
| WALL-01 | Phase 42 | Complete |
| WALL-02 | Phase 42 | Complete |
| WALL-03 | Phase 42 | Complete |
| WALL-04 | Phase 42 | Complete |
| SOCIAL-01 | Phase 42 | Complete |
| SOCIAL-02 | Phase 42 | Complete |
| SOCIAL-03 | Phase 42 | Complete |
| CONV-01 | Phase 42 | Complete |
| CONV-02 | Phase 42 | Complete |
| CONV-03 | Phase 42 | Complete |
| CONV-04 | Phase 42 | Complete |
| CONV-05 | Phase 42 | Complete |
| CONV-06 | Phase 42 | Complete |
| CONV-07 | Phase 42 | Complete |
| CONV-08 | Phase 42 | Complete |
| CONV-09 | Phase 42 | Complete |
| VOICE-01 | Phase 42 | Complete |
| VOICE-02 | Phase 42 | Complete |
| VOICE-03 | Phase 42 | Complete |
| DOC-01 | Phase 43 | Complete |
| DOC-02 | Phase 43 | Complete |
| DOC-03 | Phase 43 | Complete |
| BRAND-01 | Phase 43 | Complete |
| BRAND-02 | Phase 43 | Complete |
| BRAND-03 | Phase 43 | Complete |
| BRAND-04 | Phase 43 | Complete |
| BRAND-05 | Phase 43 | Complete |
| BRAND-06 | Phase 43 | Complete |
| PRES-01 | Phase 44 | Complete |
| PRES-02 | Phase 44 | Complete |
| PRES-03 | Phase 44 | Complete |
| PRES-04 | Phase 44 | Complete |
| SKILL-01 | Phase 45 | Complete |
| SKILL-02 | Phase 45 | Complete |
| SKILL-03 | Phase 45 | Complete |
**Coverage:**
- v1.7 requirements: 52 total
- Mapped to phases: 52
- Unmapped: 0
---
*Requirements defined: 2026-04-04*
*Last updated: 2026-04-04 after roadmap creation (v1.7)*

View file

@ -0,0 +1,393 @@
# Roadmap: Nexus
## Milestones
- ✅ **v1.2.1 Universal Skill Management** - Phase 1 (shipped 2026-04-01)
- ✅ **v1.3 Chat & PWA** - Phases 21-26 (shipped 2026-04-02)
- ✅ **v1.4 Hermes Default Provider** - Phases 27-29 (shipped 2026-04-02)
- ✅ **v1.5 Smart Onboarding + Personal AI Assistant** - Phases 30-35 (shipped 2026-04-03)
- ✅ **v1.6 Voice Pipeline + Minimal Message Bridge** - Phases 36-39 (shipped 2026-04-04)
- 🚧 **v1.7 Content Generation** - Phases 40-45 (in progress)
---
<details>
<summary>✅ v1.2.1 Universal Skill Management (Phase 1) - SHIPPED 2026-04-01</summary>
### Phase 1: Foundation
**Goal**: Establish the display-layer rename infrastructure, git hygiene tooling, and rebase safety primitives that all subsequent phases depend on
**Plans**: 2/2 plans complete
Plans:
- [x] 01-01-PLAN.md — Branding package, VOCAB constants, commit-msg hook
- [x] 01-02-PLAN.md — Zone taxonomy, rerere config, rebase safety infrastructure
</details>
<details>
<summary>✅ v1.3 Chat & PWA (Phases 21-26) - SHIPPED 2026-04-02</summary>
### Phase 21: Chat Foundation
**Goal**: Users can have real-time chat conversations with agents
**Plans**: 7/7 plans complete
### Phase 22: Agent Streaming
**Goal**: Agent responses stream in real-time with identity, edit, retry, and stop controls
**Plans**: 5/5 plans complete
### Phase 23: Brainstormer Flow
**Goal**: Users can turn a chat conversation into a tracked project with one handoff action
**Plans**: 4/4 plans complete
### Phase 24: Search, History & Branching
**Goal**: Users can find, bookmark, branch, and export any conversation
**Plans**: 4/4 plans complete
### Phase 25: File System
**Goal**: Users can upload, preview, and version files within chat; voice input transcribes speech to text
**Plans**: 9/9 plans complete
### Phase 26: PWA & Performance
**Goal**: Nexus installs as a PWA, works offline, and loads fast on mobile
**Plans**: 5/5 plans complete
</details>
<details>
<summary>✅ v1.4 Hermes Default Provider (Phases 27-29) - SHIPPED 2026-04-02</summary>
### Phase 27: Hermes Adapter
**Goal**: Users can create a Hermes agent in Nexus, configure it, and have it execute heartbeats that spawn `hermes chat -q`, return a result, and persist the session across runs
**Plans**: 1/1 plans complete
### Phase 28: Ollama Integration & Agent Surface
**Goal**: Users can see which Ollama models are available, get a recommendation for their hardware, configure any Hermes agent to use a local model, and see Hermes-specific runtime data in the dashboard and agent config
**Plans**: 3/3 plans complete
### Phase 29: Default Provider & End-to-End
**Goal**: A fresh Nexus install with only Hermes and Ollama works end-to-end — onboarding offers Hermes as the default, PM and Engineer templates run correctly on the Hermes runtime, and GSD workflow tasks complete successfully
**Plans**: 2/2 plans complete
</details>
<details>
<summary>✅ v1.5 Smart Onboarding + Personal AI Assistant (Phases 30-35) - SHIPPED 2026-04-03</summary>
### Phase 30: Hardware Detection + Mode Selection
**Goal**: Users see accurate hardware information during onboarding, get a model recommendation matched to their machine, and choose a mode that correctly gates all downstream features
**Plans**: 2/2 plans complete
### Phase 31: Puter.js Zero-Config Cloud
**Goal**: Users without Ollama installed can reach working AI in one click via Puter.js
**Plans**: 4/4 plans complete
### Phase 32: Multi-Step Onboarding Wizard
**Goal**: Users move through a complete, skippable onboarding flow that assembles hardware data, provider selection, and voice options into a summary screen
**Plans**: 1/1 plans complete
### Phase 33: Persistent Memory + Personal Assistant Mode
**Goal**: Users in Personal AI Assistant mode accumulate memory across sessions that shapes future responses
**Plans**: 3/3 plans complete
### Phase 34: Voice
**Goal**: Users can speak to the assistant (Whisper STT) and hear responses read aloud (Piper TTS)
**Plans**: 2/2 plans complete
### Phase 35: npx buildthis CLI
**Goal**: A developer can run `npx buildthis` on a fresh machine and either open an already-running Nexus or be guided through install
**Plans**: 1/1 plans complete
</details>
<details>
<summary>✅ v1.6 Voice Pipeline + Minimal Message Bridge (Phases 36-39) - SHIPPED 2026-04-04</summary>
### Phase 36: Voice Pipeline Foundation
**Goal**: The transport-agnostic voice pipeline is live and callable from any consumer — web chat, Telegram, or future integrations — with correct audio transcoding, voice mode flag propagation, and dual output formatting baked in from the start
**Depends on**: Phase 35 (v1.5 shipped)
**Requirements**: VPIPE-01, VPIPE-02, VPIPE-03, VPIPE-04, VPIPE-05, VPIPE-06
**Success Criteria** (what must be TRUE):
1. Posting a WAV audio file to `POST /api/transcribe` returns a transcription with detected language, regardless of whether the request came from the web UI or a test harness
2. Calling `POST /api/synthesize` with a markdown-heavy agent response returns two outputs: a voice-optimized prose version (no markdown) and the original full text with code blocks
3. A WebM/Opus browser recording and an OGG/Opus Telegram voice note both produce identical Whisper transcription quality after ffmpeg transcodes each to WAV 16kHz mono
4. The `voiceMode` flag on a chat message survives from client request through Express route to message persistence — verifiable in the DB record
5. `nexus-settings.json` accepts `voiceMode: "text" | "voice_input" | "full_voice"` and `telegramToken` fields without breaking existing settings reads
**Plans**: 3 plans
Plans:
- [x] 36-01-PLAN.md — VoicePipelineService: ffmpeg transcoding, Whisper STT, Piper TTS, formatForVoice
- [x] 36-02-PLAN.md — Schema extensions: voiceMode in shared validators/types + nexus-settings
- [ ] 36-03-PLAN.md — Voice routes, chat.ts voiceMode wiring, app.ts mount, old transcribe removal
### Phase 37: Web Chat Voice UI
**Goal**: Users can speak to any agent in web chat — recording auto-stops on silence, a live waveform confirms the mic is active, responses play back automatically (toggleable), and voice mode is a first-class setting
**Depends on**: Phase 36
**Requirements**: WCHAT-01, WCHAT-02, WCHAT-03, WCHAT-04, WCHAT-05, WCHAT-06
**Success Criteria** (what must be TRUE):
1. Clicking the mic button starts recording; the waveform animates to show audio levels; speaking and then pausing for 1.5 seconds auto-submits the recording without pressing any button
2. The voice mode toggle has three visible states (text only / voice input / full voice) and persists the selected mode across page refreshes
3. An agent response delivered in full voice mode plays back automatically in the chat thread; the auto-play can be turned off in settings and stays off after a page reload
4. The chat message for a voice interaction shows a voice badge and an expandable section revealing the full markdown response with code blocks intact
5. Voice recording and VAD work correctly in Chrome and Firefox on the Mac Mini (COOP/COEP headers satisfy SharedArrayBuffer requirements)
**Plans**: 3 plans
Plans:
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
**UI hint**: yes
**UI hint**: yes
### Phase 38: Telegram Bridge
**Goal**: The user can message any Nexus agent from their phone via Telegram — text and voice notes both work, agent identity is visible on every reply, and the bot is set up through guided onboarding with no manual token entry in config files
**Depends on**: Phase 36
**Requirements**: TGRAM-01, TGRAM-02, TGRAM-03, TGRAM-04, TGRAM-05, TGRAM-06, ONBRD-03
**Success Criteria** (what must be TRUE):
1. Sending a text message to the Nexus Telegram bot from a phone produces an agent reply prefixed with the agent name (e.g. `[PM]: response`) within 10 seconds
2. Sending a voice note to the Telegram bot produces a transcription confirmation message followed by the agent's text reply — the bot does not silently fail or miss the update
3. Requesting a voice reply from the bot returns an OGG voice note that plays back correctly in the Telegram mobile app
4. The Telegram bridge runs via long polling with no public HTTPS endpoint required — verified by running on the Mac Mini behind NAT
5. The entire `telegram.ts` service file is under 500 lines
6. The onboarding wizard includes a BotFather setup step that walks through creating a bot token and saves it to `nexus-settings.json` without manual file editing
**Plans**: 3 plans
Plans:
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
- [ ] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
**UI hint**: yes
### Phase 39: Voice Polish
**Goal**: Voice responses begin playing before synthesis is complete (sentence-buffered), a single response can be synthesized in multiple languages simultaneously, and new installs can detect STT/TTS hardware capability during onboarding and enable voice in one step
**Depends on**: Phase 37
**Requirements**: VPIPE-07, VPIPE-08, ONBRD-01, ONBRD-02
**Success Criteria** (what must be TRUE):
1. For a multi-sentence agent response, the first sentence begins playing in the browser before the second sentence has finished synthesizing — the gap between text completion and first audio is under 1 second
2. A user can request the same agent response as audio in both English and Danish; both OGG files are generated and available for playback without a second agent call
3. On a fresh install, the onboarding hardware probe reports whether Whisper STT and Piper TTS are runnable on the detected hardware tier
4. The onboarding voice step activates (showing enable/skip options) only when the hardware probe confirms sufficient capability; on hardware below threshold it shows a capability note and skips to the next step
**Plans**: 2 plans
Plans:
- [x] 39-01-PLAN.md — Sentence-buffered TTS streaming + multi-language synthesis
- [ ] 39-02-PLAN.md — Onboarding voice hardware capability probe
</details>
---
### 🚧 v1.7 Content Generation (In Progress)
**Milestone Goal:** Agents produce real deliverables — diagrams, themes, PDFs, wallpapers, social assets, icons, and video — entirely on-device. Every content type is an installable skill. Long-running renders are async with SSE progress from the first request.
## Phases
- [x] **Phase 40: Job Infrastructure** — content_jobs table, async render lifecycle, SSE progress events, namespaced storage without size limit (INFRA-01..04) (completed 2026-04-04)
- [x] **Phase 41: Diagrams, Icons & Theme Engine** — Mermaid diagrams, SVG icon generation, OKLCH theme palette with WCAG AA and live preview (DIAG-01..05, ICON-01..03, THEME-01..07) (completed 2026-04-04)
- [x] **Phase 42: Wallpapers, Social, Format Conversion & Voice** — LLM SVG + sharp wallpapers, social content, format conversion registry with AI fallback, Whisper web chat mic (WALL-01..04, SOCIAL-01..03, CONV-01..09, VOICE-01..03) (completed 2026-04-04)
- [x] **Phase 43: Documents & Branding** — Playwright PDF reports and invoices, full brand identity kit with zip export (DOC-01..03, BRAND-01..06) (completed 2026-04-04)
- [x] **Phase 44: Video & Presentations** — Remotion workspace package, pitch decks and demo videos, SSE render progress (PRES-01..04) (completed 2026-04-04)
- [x] **Phase 45: Content as Skills** — Markdown skill files for all content types, Creative skill group on generalist agent (SKILL-01..03) (completed 2026-04-04)
## Phase Details
### Phase 40: Job Infrastructure
**Goal**: Every content generation request returns a job ID immediately, progresses through a tracked lifecycle, and stores its output in namespaced storage — so nothing blocks and nothing is orphaned
**Depends on**: Phase 39 (v1.6 shipped)
**Requirements**: INFRA-01, INFRA-02, INFRA-03, INFRA-04
**Success Criteria** (what must be TRUE):
1. Submitting a content generation request returns HTTP 202 with a job ID within 200ms, regardless of how long the render takes
2. A connected browser receives SSE events as a job progresses through queued → generating → ready (or error), with no polling required
3. A generated video file larger than 10MB can be stored and retrieved without a size-limit error — the generated/ storage namespace bypasses the upload route limit
4. Every generated asset in the database has a sourceTaskId linking it to the originating conversation task, visible via the asset list API
**Plans**: 2 plans
Plans:
- [x] 40-01-PLAN.md — Schema, constants, migrations, contentJobStore + contentJobRunner services
- [x] 40-02-PLAN.md — HTTP routes (POST 202, GET, SSE), app.ts wiring, integration tests
### Phase 41: Diagrams, Icons & Theme Engine
**Goal**: Users can generate diagrams from natural language, produce SVG icon sets from descriptions, and create a complete OKLCH color theme from a single seed color — all without binary dependencies beyond what is already installed
**Depends on**: Phase 40
**Requirements**: DIAG-01, DIAG-02, DIAG-03, DIAG-04, DIAG-05, ICON-01, ICON-02, ICON-03, THEME-01, THEME-02, THEME-03, THEME-04, THEME-05, THEME-06, THEME-07
**Success Criteria** (what must be TRUE):
1. Describing an architecture in chat produces a rendered Mermaid diagram (SVG and PNG) attached to the conversation, with the editable Mermaid source visible in a collapsible panel
2. Mermaid rendering uses strict security level — a diagram with a `click` directive or `%%{init}%%` override is stripped before render, and SVG output passes DOMPurify before reaching the DOM
3. Requesting an icon set from a description returns a cohesive set of SVG icons downloadable in SVG and PNG formats at multiple sizes
4. Picking a seed color produces a full palette (background, surface, overlay, text, accents) in OKLCH with separate dark and light variants, all passing WCAG AA contrast checks
5. The generated theme can be previewed live in the Nexus UI via CSS custom property injection and applied permanently in one click; export works for CSS variables, Tailwind config, VS Code theme, and JSON
**Plans**: 6 plans
Plans:
- [x] 41-01-PLAN.md — Dependencies, shared types, content-job-runner switch, useContentJob hook
- [x] 41-02-PLAN.md — Diagram renderer (Playwright Mermaid + DOMPurify) and icon renderer (LLM SVG + SVGO)
- [x] 41-03-PLAN.md — OKLCH theme palette engine, WCAG validation, export formatters, nexus-settings extension
- [x] 41-04-PLAN.md — ContentStudio page, Diagram UI (generate, preview, source editor), Icon UI (grid, download)
- [x] 41-05-PLAN.md — Theme UI (seed input, palette grid, live preview, export tabs, apply flow)
- [x] 41-06-PLAN.md — Full test suite + visual checkpoint verification
**UI hint**: yes
### Phase 42: Wallpapers, Social, Format Conversion & Voice
**Goal**: Users can generate platform-ready images (wallpapers, OG images, social banners) via LLM SVG + sharp rasterization, convert between any file format pair, and record voice directly in web chat via the Whisper mic button
**Depends on**: Phase 40
**Requirements**: WALL-01, WALL-02, WALL-03, WALL-04, SOCIAL-01, SOCIAL-02, SOCIAL-03, CONV-01, CONV-02, CONV-03, CONV-04, CONV-05, CONV-06, CONV-07, CONV-08, CONV-09, VOICE-01, VOICE-02, VOICE-03
**Success Criteria** (what must be TRUE):
1. Requesting a desktop wallpaper returns a 2560x1440 PNG; requesting an Instagram banner returns a correctly-dimensioned image — platform dimensions are constants, not magic numbers
2. The format conversion UI allows drag-drop of a source file, selection of a target format, and download of the converted file; direct conversion pairs (image, audio/video, document, data) use native tools; any unsupported pair falls through to AI-bridged conversion rather than showing as unavailable
3. Navigating to `/convert/png/svg` deep-links directly to the PNG->SVG conversion flow with source and target pre-selected
4. An uploaded file is validated against its magic bytes before processing — a JPEG renamed to `.png` is rejected with a clear error, not silently misprocessed
5. Clicking the mic button in web chat records audio, transcribes it via local Whisper, and populates the chat input — works offline with the locally cached model
**Plans**: 6 plans
Plans:
- [x] 42-01-PLAN.md — Dependencies, bundle types, job-runner switch, converter capabilities probe
- [x] 42-02-PLAN.md — Wallpaper renderer (LLM SVG + sharp) and social post renderer (LLM JSON + hashtags)
- [x] 42-03-PLAN.md — Convert renderer (sharp/ffmpeg/xlsx/AI-bridge) and multipart upload route with MIME validation
- [x] 42-04-PLAN.md — Voice offline badge wiring (useSystemProviders hook + ChatInput badge)
- [x] 42-05-PLAN.md — Wallpaper/Social UI panels + ContentStudio tab extensions
- [x] 42-06-PLAN.md — Format conversion UI page with drag-drop, format chips, deep-link routing
**UI hint**: yes
### Phase 43: Documents & Branding
**Goal**: Users can generate polished PDF reports and invoices via Playwright, and create a complete brand identity (logo, avatars, social profiles, letterhead, guidelines PDF, zip package) from a single conversation
**Depends on**: Phase 41
**Requirements**: DOC-01, DOC-02, DOC-03, BRAND-01, BRAND-02, BRAND-03, BRAND-04, BRAND-05, BRAND-06
**Success Criteria** (what must be TRUE):
1. Generating a PDF report from a conversation produces a downloadable PDF with correct layout; generating an invoice from a template produces a filled invoice PDF with correct line items
2. Generating a one-pager or API reference document produces a styled PDF with navigable headings
3. Starting a brand identity conversation produces a logo mark (SVG), avatar at multiple sizes, platform-specific social images, an email signature, and a brand guidelines PDF — all in a single brand kit
4. The complete brand kit can be downloaded as a single zip file with assets organized by type
**Plans**: 3 plans
Plans:
- [x] 43-01-PLAN.md — Types, archiver install, PDF renderer (Playwright HTML-to-PDF), job-runner wiring
- [x] 43-02-PLAN.md — Brand kit renderer (logo, avatars, social images, templates, guidelines PDF, ZIP packaging)
- [x] 43-03-PLAN.md — Document and Brand UI panels, ContentStudio tab extensions
**UI hint**: yes
### Phase 44: Video & Presentations
**Goal**: Agents can produce pitch deck presentations and demo videos rendered by Remotion from a conversation, with SSE progress updates throughout the render — which may take several minutes on the M4
**Depends on**: Phase 40
**Requirements**: PRES-01, PRES-02, PRES-03, PRES-04
**Success Criteria** (what must be TRUE):
1. Requesting a pitch deck from a conversation description produces a Remotion-rendered interactive web presentation or MP4; the render runs in a separate workspace package and does not block the main server process
2. The Remotion bundle is compiled once at server startup and reused for all renders — submitting a second render request does not trigger a second webpack compilation
3. A browser connected during a video render receives SSE progress events (percentage complete) throughout the render; the final event delivers the download URL
4. Concurrent LLM inference and video rendering do not cause the server to become unresponsive — render concurrency is capped and serialized with LLM workloads
**Plans**: 3 plans
Plans:
- [x] 44-01-PLAN.md — Remotion workspace package, compositions, shared constants, types, job-runner wiring
- [x] 44-02-PLAN.md — Presentation renderer with LLM slide generation, Remotion render, SSE progress
- [x] 44-03-PLAN.md — PresentationPanel UI, useContentJob progress extension, ContentStudio tab
**UI hint**: yes
### Phase 45: Content as Skills
**Goal**: Every content type built in Phases 41-44 is accessible to agents as an installable Markdown skill, and the generalist agent ships pre-loaded with the Creative skill group
**Depends on**: Phase 44
**Requirements**: SKILL-01, SKILL-02, SKILL-03
**Success Criteria** (what must be TRUE):
1. Each content type (diagram, theme, icon, wallpaper, social post, PDF, brand kit, video) has a corresponding skill file that an agent can load and use to call the correct content job API
2. A freshly created generalist agent has the Creative skill group pre-loaded — it can generate diagrams and themes without any manual skill configuration
3. A user can add or remove individual content type skills through the Skill Aggregator UI without touching configuration files
**Plans**: 1 plan
Plans:
- [x] 45-01-PLAN.md — 9 SKILL.md files, local-nexus-content source type, Creative group seeding, startup wiring
**UI hint**: yes
---
## Coverage Validation
All 52 v1.7 requirements are mapped to exactly one phase. No orphans.
| Requirement | Phase |
|-------------|-------|
| INFRA-01 | 40 |
| INFRA-02 | 40 |
| INFRA-03 | 40 |
| INFRA-04 | 40 |
| DIAG-01 | 41 |
| DIAG-02 | 41 |
| DIAG-03 | 41 |
| DIAG-04 | 41 |
| DIAG-05 | 41 |
| THEME-01 | 41 |
| THEME-02 | 41 |
| THEME-03 | 41 |
| THEME-04 | 41 |
| THEME-05 | 41 |
| THEME-06 | 41 |
| THEME-07 | 41 |
| ICON-01 | 41 |
| ICON-02 | 41 |
| ICON-03 | 41 |
| WALL-01 | 42 |
| WALL-02 | 42 |
| WALL-03 | 42 |
| WALL-04 | 42 |
| SOCIAL-01 | 42 |
| SOCIAL-02 | 42 |
| SOCIAL-03 | 42 |
| CONV-01 | 42 |
| CONV-02 | 42 |
| CONV-03 | 42 |
| CONV-04 | 42 |
| CONV-05 | 42 |
| CONV-06 | 42 |
| CONV-07 | 42 |
| CONV-08 | 42 |
| CONV-09 | 42 |
| VOICE-01 | 42 |
| VOICE-02 | 42 |
| VOICE-03 | 42 |
| DOC-01 | 43 |
| DOC-02 | 43 |
| DOC-03 | 43 |
| BRAND-01 | 43 |
| BRAND-02 | 43 |
| BRAND-03 | 43 |
| BRAND-04 | 43 |
| BRAND-05 | 43 |
| BRAND-06 | 43 |
| PRES-01 | 44 |
| PRES-02 | 44 |
| PRES-03 | 44 |
| PRES-04 | 44 |
| SKILL-01 | 45 |
| SKILL-02 | 45 |
| SKILL-03 | 45 |
---
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. Foundation | v1.2.1 | 2/2 | Complete | 2026-04-01 |
| 21. Chat Foundation | v1.3 | 7/7 | Complete | 2026-04-02 |
| 22. Agent Streaming | v1.3 | 5/5 | Complete | 2026-04-02 |
| 23. Brainstormer Flow | v1.3 | 4/4 | Complete | 2026-04-02 |
| 24. Search, History & Branching | v1.3 | 4/4 | Complete | 2026-04-02 |
| 25. File System | v1.3 | 9/9 | Complete | 2026-04-02 |
| 26. PWA & Performance | v1.3 | 5/5 | Complete | 2026-04-02 |
| 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 |
| 28. Ollama Integration & Agent Surface | v1.4 | 3/3 | Complete | 2026-04-02 |
| 29. Default Provider & End-to-End | v1.4 | 2/2 | Complete | 2026-04-02 |
| 30. Hardware Detection + Mode Selection | v1.5 | 2/2 | Complete | 2026-04-03 |
| 31. Puter.js Zero-Config Cloud | v1.5 | 4/4 | Complete | 2026-04-03 |
| 32. Multi-Step Onboarding Wizard | v1.5 | 1/1 | Complete | 2026-04-03 |
| 33. Persistent Memory + Personal Assistant Mode | v1.5 | 3/3 | Complete | 2026-04-03 |
| 34. Voice | v1.5 | 2/2 | Complete | 2026-04-03 |
| 35. npx buildthis CLI | v1.5 | 1/1 | Complete | 2026-04-03 |
| 36. Voice Pipeline Foundation | v1.6 | 2/3 | Complete | 2026-04-04 |
| 37. Web Chat Voice UI | v1.6 | 3/4 | Complete | 2026-04-04 |
| 38. Telegram Bridge | v1.6 | 3/3 | Complete | 2026-04-04 |
| 39. Voice Polish | v1.6 | 1/2 | Complete | 2026-04-04 |
| 40. Job Infrastructure | v1.7 | 2/2 | Complete | 2026-04-04 |
| 41. Diagrams, Icons & Theme Engine | v1.7 | 6/6 | Complete | 2026-04-04 |
| 42. Wallpapers, Social, Format Conversion & Voice | v1.7 | 6/6 | Complete | 2026-04-04 |
| 43. Documents & Branding | v1.7 | 3/3 | Complete | 2026-04-04 |
| 44. Video & Presentations | v1.7 | 3/3 | Complete | 2026-04-04 |
| 45. Content as Skills | v1.7 | 1/1 | Complete | 2026-04-04 |

View file

@ -0,0 +1,392 @@
---
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>

View file

@ -0,0 +1,174 @@
---
phase: 45-content-as-skills
plan: 01
subsystem: skills
tags: [skill-registry, content-generation, local-nexus-content, creative-group, libsql, drizzle]
# Dependency graph
requires:
- phase: 40-job-infrastructure
provides: content_jobs table and renderContent dispatcher with all 9 jobTypes
- phase: 41-diagrams-icons-theme-engine
provides: diagram, icon-set, theme-palette renderers
- phase: 42-wallpapers-social-format-conversion-voice
provides: wallpaper, social-post, convert renderers
- phase: 43-documents-branding
provides: pdf-document, brand-kit renderers
- phase: 44-video-presentations
provides: presentation renderer
provides:
- 9 SKILL.md files in server/src/skills/content/ with frontmatter, jobType docs, and output docs
- local-nexus-content source type in skill-registry-fetcher.ts
- fetchLocalNexusContent function that reads .SKILL.md files, computes SHA-1 hash, upserts skills
- nexus-content entry in BUILT_IN_SOURCES pointing to local skills/content dir
- seedCreativeGroupMembers() export in skill-registry-db.ts seeding 9 content skills into builtin/creative group
- getRawClient() export for test access to raw LibSQL client
- Unified startup block in index.ts: DB init -> fetchAll -> seedCreativeGroupMembers -> reconcile pending groups
- 9 unit tests for SKILL.md files, fetchLocalNexusContent, and Creative group seeding
affects: [agents-with-creative-group, skill-registry-api, pendingSkillGroups-reconciler]
# Tech tracking
tech-stack:
added: []
patterns:
- local-nexus-content source type: discriminated union variant in SkillSourceConfig for filesystem-based skill discovery
- SHA-1 content hash for idempotency on local files (no commit SHA available)
- seedCreativeGroupMembers uses INSERT OR IGNORE — safe to call multiple times
- getRawClient() exposes LibSQL singleton for direct SQL outside drizzle context
key-files:
created:
- 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/__tests__/skill-registry-content-skills.test.ts
modified:
- server/src/services/skill-registry-fetcher.ts
- server/src/services/skill-registry-db.ts
- server/src/index.ts
- server/src/__tests__/skill-registry-fetch.test.ts
key-decisions:
- "SkillSourceConfig changed to discriminated union — fetchAnthropicMarketplace/fetchGitHubTree narrowed to Extract types for tsc safety"
- "SHA-1 content hash used for local SKILL.md idempotency — no Git commit SHA available for local files"
- "seedCreativeGroupMembers does NOT run inside getSkillRegistryDb() — must run after fetchAll so skill rows exist first"
- "getRawClient() exported for test access — tests need raw SQL for skill_group_members queries"
- "pendingSkillGroups reconciler merged into skill registry init block — guarantees Creative group is seeded before reconciliation"
- "skill-registry-fetch.test.ts Test 7 updated to expect 4 BUILT_IN_SOURCES — auto-fixed to reflect new local source"
patterns-established:
- "local-nexus-content pattern: filesystem skill source reads .SKILL.md files, derives slug from filename sans extension"
- "Content hash idempotency: SHA-1 of file content serves as version identifier for local files"
requirements-completed: [SKILL-01, SKILL-02, SKILL-03]
# Metrics
duration: 3min
completed: 2026-04-04
---
# Phase 45 Plan 01: Content-as-Skills Summary
**9 content generators registered as installable Nexus skills via local-nexus-content source type, with Creative group seeded and startup sequence unified**
## Performance
- **Duration:** ~3 min
- **Started:** 2026-04-04T23:51:00Z
- **Completed:** 2026-04-04T23:54:15Z
- **Tasks:** 2
- **Files modified:** 14
## Accomplishments
- Authored 9 SKILL.md files (diagram, icon-set, theme-palette, wallpaper, social-post, convert, pdf-document, brand-kit, presentation) with valid YAML frontmatter, jobType usage docs, and output sections
- Extended skill-registry-fetcher.ts with local-nexus-content source type: discriminated union SkillSourceConfig, fetchLocalNexusContent private handler, nexus-content entry in BUILT_IN_SOURCES
- Added seedCreativeGroupMembers() and getRawClient() to skill-registry-db.ts; merged skill registry init and pendingSkillGroups reconciler into a single sequential startup block ensuring correct ordering
## Task Commits
1. **Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source** - `5138572d` (feat)
2. **Task 2: Seed Creative group members and wire startup sequence** - `98f0b8f8` (feat)
## Files Created/Modified
- `server/src/skills/content/diagram.SKILL.md` - Diagram generation skill with Mermaid jobType docs
- `server/src/skills/content/icon-set.SKILL.md` - SVG icon set generation skill docs
- `server/src/skills/content/theme-palette.SKILL.md` - OKLCH theme palette generation skill docs
- `server/src/skills/content/wallpaper.SKILL.md` - Desktop wallpaper generation skill docs
- `server/src/skills/content/social-post.SKILL.md` - Social media post generation skill docs
- `server/src/skills/content/convert.SKILL.md` - Format conversion skill docs
- `server/src/skills/content/pdf-document.SKILL.md` - PDF document generation skill docs
- `server/src/skills/content/brand-kit.SKILL.md` - Brand identity kit generation skill docs
- `server/src/skills/content/presentation.SKILL.md` - Video presentation generation skill docs
- `server/src/services/skill-registry-fetcher.ts` - Added local-nexus-content source type and handler
- `server/src/services/skill-registry-db.ts` - Added getRawClient(), seedCreativeGroupMembers(), _client singleton
- `server/src/index.ts` - Unified startup block with correct sequence
- `server/src/__tests__/skill-registry-content-skills.test.ts` - 9 tests for SKILL.md files, fetch handler, Creative group
- `server/src/__tests__/skill-registry-fetch.test.ts` - Updated Test 7 to expect 4 BUILT_IN_SOURCES
## Decisions Made
- SkillSourceConfig changed from flat type to discriminated union — required narrowing fetchAnthropicMarketplace/fetchGitHubTree to Extract types to avoid TypeScript access errors on local-nexus-content variant
- SHA-1 of file content used as version identifier for local SKILL.md files — no Git commit SHA available; provides idempotency across server restarts
- seedCreativeGroupMembers() separated from getSkillRegistryDb() initialization — must run after fetchAll() so the skills rows exist for foreign key constraints; wired sequentially in index.ts
- getRawClient() exported to give tests direct SQL access for querying skill_group_members table
- pendingSkillGroups reconciler moved inside the skill registry init block — eliminates race where reconciler could run before Creative group was seeded, causing missed group assignments
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 1 - Bug] Updated skill-registry-fetch.test.ts Test 7 to expect 4 BUILT_IN_SOURCES**
- **Found during:** Task 1 (extending fetcher)
- **Issue:** Existing test asserted `toHaveLength(3)` and `type` to match remote-only regex — both failed after adding nexus-content local source
- **Fix:** Updated count to 4, added nexus-content to expected IDs, made type check conditional on non-local sources
- **Files modified:** server/src/__tests__/skill-registry-fetch.test.ts
- **Verification:** All 7 existing fetch tests pass after update
- **Committed in:** 5138572d (Task 1 commit)
**2. [Rule 1 - Bug] Narrowed fetchAnthropicMarketplace/fetchGitHubTree parameter types**
- **Found during:** Task 2 (tsc check)
- **Issue:** Converting SkillSourceConfig to discriminated union caused tsc errors — functions accessed `.owner`, `.repo`, `.ref` which don't exist on local-nexus-content variant
- **Fix:** Changed parameter types to `Extract<SkillSourceConfig, { type: "anthropic-marketplace" }>` and `Extract<SkillSourceConfig, { type: "github-tree" }>` respectively
- **Files modified:** server/src/services/skill-registry-fetcher.ts
- **Verification:** `npx tsc --noEmit` exits 0
- **Committed in:** 98f0b8f8 (Task 2 commit)
---
**Total deviations:** 2 auto-fixed (2 Rule 1 bugs)
**Impact on plan:** Both auto-fixes required for correctness. No scope creep.
## Issues Encountered
None — plan executed cleanly with 2 small tsc/test fixes applied automatically.
## Known Stubs
None — all 9 SKILL.md files contain complete documentation with jobType, input fields, and output descriptions. The skills point to fully-implemented renderers from Phases 41-44.
## Next Phase Readiness
Phase 45 is the final phase of milestone v1.7. All content generators are now registered as installable Nexus skills discoverable by agents. The Creative group is seeded with all 9 skill IDs and the startup sequence ensures agents with `pendingSkillGroups: ["Creative"]` receive their skill group assignments.
---
*Phase: 45-content-as-skills*
*Completed: 2026-04-04*
## Self-Check: PASSED
- FOUND: server/src/skills/content/diagram.SKILL.md
- FOUND: server/src/skills/content/presentation.SKILL.md
- FOUND: server/src/services/skill-registry-fetcher.ts
- FOUND: server/src/services/skill-registry-db.ts
- FOUND: server/src/__tests__/skill-registry-content-skills.test.ts
- FOUND: .planning/phases/45-content-as-skills/45-01-SUMMARY.md
- COMMIT 5138572d: feat(45-01): author 9 content SKILL.md files and extend fetcher with local-nexus-content source
- COMMIT 98f0b8f8: feat(45-01): seed Creative group members and wire unified startup sequence

View file

@ -0,0 +1,41 @@
# Phase 45: Content as Skills - Context
**Gathered:** 2026-04-04
**Status:** Ready for planning
**Mode:** Auto-generated (discuss skipped via workflow.skip_discuss)
<domain>
## Phase Boundary
Every content type built in Phases 41-44 is accessible to agents as an installable Markdown skill, and the generalist agent ships pre-loaded with the Creative skill group
</domain>
<decisions>
## Implementation Decisions
### Claude's Discretion
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
</decisions>
<code_context>
## Existing Code Insights
Codebase context will be gathered during plan-phase research.
</code_context>
<specifics>
## Specific Ideas
No specific requirements — discuss phase skipped. Refer to ROADMAP phase description and success criteria.
</specifics>
<deferred>
## Deferred Ideas
None — discuss phase skipped.
</deferred>

View file

@ -0,0 +1,441 @@
# Phase 45: Content as Skills - Research
**Researched:** 2026-04-04
**Domain:** Nexus skill registry — local SKILL.md authoring, source seeding, group membership, generalist agent preloading
**Confidence:** HIGH
## Summary
Phase 45 bridges the content generation work of Phases 41-44 with the skill registry system already in place. Every content type (diagram, icon, theme, wallpaper, social, convert, pdf-document, brand-kit, presentation) must become an installable Markdown skill, the built-in "Creative" group must contain all of them, and the Generalist agent must ship pre-loaded with that group.
The skill registry infrastructure is complete. The registry stores skills in a libSQL database under `~/.paperclip/instances/default/skills/registry.db`. Skills are fetched from remote GitHub sources (`anthropic-marketplace` and `github-tree` types) or from native agent runtimes (Hermes). There is currently no local-filesystem source type. Phase 45 must add one, seed it on server startup, and wire the Creative group to the new skills.
The generalist agent preloading mechanism already exists: `index.ts` seeds `pendingSkillGroups: ["Creative"]` in agent metadata at creation time, and a fire-and-forget startup reconciler assigns the group (using `assignGroup`) to every agent that has that metadata flag. Phase 45 needs the Creative group to have members before the reconciler runs — which means the skill SKILL.md files and their registry entries must be in place before any agent is created.
**Primary recommendation:** Add a `"local-nexus-content"` source type to the skill registry fetcher. On server startup, register nine content SKILL.md files from `server/src/skills/content/` into the registry DB and add all of them to the `builtin/creative` group.
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
All implementation choices are at Claude's discretion — discuss phase was skipped per user setting.
### Claude's Discretion
All implementation choices are at Claude's discretion. Use ROADMAP phase goal, success criteria, and codebase conventions to guide decisions.
### Deferred Ideas (OUT OF SCOPE)
None.
</user_constraints>
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| SKILL-01 | Each content type is implemented as an installable Nexus skill | Nine SKILL.md files authored; registered in skill registry DB via local-nexus-content source type; installable through existing `svc.install()` path |
| SKILL-02 | Generalist agent is pre-loaded with a "Creative" skill group | `builtin/creative` group already exists in DB seed; group members populated on startup; `pendingSkillGroups` reconciler already assigns the group to Generalist agents |
| SKILL-03 | Users can add or remove content type skills through the Skill Aggregator | Content skills appear in the existing SkillBrowser UI once registered; install/uninstall routes already work for any registered skill |
</phase_requirements>
---
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| libSQL / drizzle-orm | Already installed | Skill registry persistence | All skill data lives in this SQLite DB — no new dep needed |
| node:fs/promises | Node built-in | Read SKILL.md files from disk | Used by existing skill cache logic |
| vitest | Already installed | Unit tests | Project standard in `server/` |
### Supporting
No new packages required. The full content generation stack (jobType dispatch, renderers, SSE) is already in place from Phases 40-44.
**Installation:** None required.
---
## Architecture Patterns
### Recommended Project Structure
```
server/src/
├── skills/
│ └── content/
│ ├── diagram.SKILL.md
│ ├── icon-set.SKILL.md
│ ├── theme-palette.SKILL.md
│ ├── wallpaper.SKILL.md
│ ├── social-post.SKILL.md
│ ├── convert.SKILL.md
│ ├── pdf-document.SKILL.md
│ ├── brand-kit.SKILL.md
│ └── presentation.SKILL.md
├── services/
│ └── skill-registry-fetcher.ts ← add local-nexus-content source type
│ └── skill-registry-db.ts ← seed Creative group members on init
└── index.ts ← call seedContentSkills on startup
```
### Pattern 1: Local-Filesystem Source Type
**What:** A new `"local-nexus-content"` entry in `SkillSourceConfig` union and a `fetchLocalNexusContent()` function that reads SKILL.md files from `server/src/skills/content/` and upserts them into the registry DB.
**When to use:** Server startup — called once after `getSkillRegistryDb()` initialises.
**Example:**
```typescript
// In skill-registry-fetcher.ts — extend the union type
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 };
// New handler called by fetchAllSources when type === "local-nexus-content"
async function fetchLocalNexusContent(
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
db: SkillRegistryDb,
): Promise<number> {
const entries = await readdir(source.dir, { withFileTypes: true });
let fetched = 0;
for (const entry of entries) {
if (!entry.name.endsWith(".SKILL.md")) continue;
const slug = entry.name.replace(/\.SKILL\.md$/, "");
const skillId = `${source.id}/${slug}`;
const skillMdContent = await readFile(path.join(source.dir, entry.name), "utf-8");
const { name, description } = parseSkillFrontmatter(skillMdContent);
// Use a content hash as the version SHA (deterministic, no network needed)
const sha = crypto.createHash("sha1").update(skillMdContent).digest("hex");
await upsertSkill(db, { skillId, sourceId: source.id, name: name ?? slug, description, sourceUrl: "" });
await cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl: "" });
await upsertCommunityRatingsStub(db, skillId, source.id);
fetched++;
}
return fetched;
}
```
**BUILT_IN_SOURCES addition:**
```typescript
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
// … existing remote sources …
{
id: "nexus-content",
type: "local-nexus-content",
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
label: "Nexus Content Tools",
},
];
```
### Pattern 2: Creative Group Membership Seeding
**What:** After the local skills are registered, add them as members of the `builtin/creative` group. This should happen inside `getSkillRegistryDb()` (or a startup helper called right after) so the group has members before the `pendingSkillGroups` reconciler runs.
**When to use:** Server startup, idempotent (uses `INSERT OR IGNORE`).
```typescript
// In skill-registry-db.ts — extend seedBuiltinGroups or add seedCreativeGroupMembers
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;
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],
});
}
}
```
### Pattern 3: SKILL.md Authoring Convention
Each content skill SKILL.md must follow the same frontmatter convention as existing skills:
```markdown
---
name: diagram
description: >
Generate diagrams from a natural language description. Produces Mermaid
syntax rendered to SVG/PNG. Supports architecture, flowchart, ERD,
sequence, and mind-map types. Use when a user asks to visualise a
system, process, or data structure as a diagram.
---
# Diagram Generation
Generates diagrams from a text description via the Nexus content API.
## Usage
Submit a `POST /api/content-jobs` with:
- `jobType: "diagram"`
- `input.description` — natural language description of the diagram
- `input.type` (optional) — `"flowchart"`, `"architecture"`, `"erd"`, `"sequence"`, or `"mindmap"`
The job returns a 202 with a `jobId`. Poll `GET /api/content-jobs/:jobId` or subscribe
to SSE `content_job.done` events to retrieve the resulting asset URL.
## Output
Returns an SVG file (also available as PNG via the export button in the UI).
```
### Startup Wiring
In `server/src/index.ts`, the existing startup block already calls `getSkillRegistryDb()` as fire-and-forget. Extend that block to also call `fetchAllSources()` with `BUILT_IN_SOURCES` immediately after DB init so content skills are seeded on every cold start:
```typescript
// [nexus] Initialize skill registry and seed content skills
void (async () => {
try {
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
await getSkillRegistryDb(); // creates tables + builtin groups
const { skillRegistryService } = await import("./services/skill-registry.js");
await skillRegistryService().fetchAll(); // seeds local-nexus-content skills
logger.info("skill registry database initialized and content skills seeded");
} catch (err) {
logger.error({ err }, "skill registry init failed");
}
})();
```
### Anti-Patterns to Avoid
- **Seeding Creative group members before the skills table rows exist:** `skill_group_members.skill_id` has no FK constraint, so INSERT succeeds even if the skill row is absent — but `resolveEffectiveSkills` then returns orphaned IDs. Always seed skill rows first, group members second.
- **Calling `fetchAll()` before `getSkillRegistryDb()`:** DB tables must exist before the local fetcher tries to upsert.
- **Using mutable remote SHA for local file versioning:** Local files don't have a git SHA. Use a SHA-1 of the file content instead — deterministic and cheap.
- **Blocking HTTP on `fetchAll()` at startup:** Keep as fire-and-forget; the external github-tree fetches can be slow. Content skills (local path) are synchronous but the remote sources may timeout.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Skill install/uninstall | Custom file copy logic | `skillRegistryService().install()` / `uninstall()` | Already handles version tracking, cache dirs, agentSkills table |
| Group assignment | Direct INSERT into agentSkillGroups | `skillGroupService().assignGroup()` | Handles effective skill resolution, per-skill install, dedup |
| Skill Browser display | New UI component | Existing SkillBrowser page at `/skills` | Already renders all registered skills with install/remove actions |
| Group membership seeding | New service | `INSERT OR IGNORE INTO skill_group_members` on DB init | Simpler than adding a new route; idempotent |
---
## Common Pitfalls
### Pitfall 1: Creative group populated but skills not yet in registry
**What goes wrong:** `pendingSkillGroups` reconciler runs at startup and calls `assignGroup("builtin/creative", agentId, skillsDir)`. `resolveEffectiveSkills` returns the 9 skill IDs, but `svc.install()` fails with "Skill not found" because local skills haven't been fetched yet.
**Why it happens:** The skill-registry init and the `pendingSkillGroups` reconciler are both fire-and-forget; their order is non-deterministic.
**How to avoid:** Seed creative group members only after `fetchAll()` completes (not at DB init time). Alternatively, sequence the two startup blocks: skill init → fetchAll → then let the reconciler run.
**Warning signs:** `skipped` array non-empty in `assignGroup` result; Generalist agent has group assigned but no SKILL.md files on disk.
### Pitfall 2: Local source dir resolution broken in production
**What goes wrong:** `path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content")` resolves to `server/src/skills/content` in dev but points to a non-existent path in a compiled/packaged build.
**Why it happens:** `import.meta.url` refers to the compiled output location.
**How to avoid:** In `tsconfig.json`, include `server/src/skills/content/*.SKILL.md` in the assets copied to dist, OR read the source dir via `process.cwd()` with a known repo-relative path. Check that the resolved dir exists before iterating; log a clear error if absent.
### Pitfall 3: SKILL.md frontmatter parse returns undefined name
**What goes wrong:** `parseSkillFrontmatter` fails silently on a multi-line YAML description block (`description: >\n line1\n line2`). The `name` field parses correctly but `description` is truncated to `>` literal.
**Why it happens:** `parseSkillFrontmatter` uses a single-line regex (`^description:\s*(.+)$`) — it does not handle YAML block scalars.
**How to avoid:** For the `name` field (used for registry display), keep it on a single line. For `description` use a single-line string or expand the frontmatter parser to handle folded scalars. Alternatively, accept the `>` literal as description — it won't break anything.
### Pitfall 4: Agent SKILL.md dir wrong for Generalist agent
**What goes wrong:** `assignGroup` copies skills to `agentSkillsDir` but the Generalist agent's adapter is `claude_local` — the skills dir is resolved by `resolveAdapterSkillConfig("claude_local").skillDir`. If the agent's workspace does not exist yet (new onboard), `cp` fails.
**Why it happens:** The workspace dir is created lazily when the agent first runs.
**How to avoid:** In `assignGroup`, `mkdir(agentSkillsDir, { recursive: true })` before attempting the copy. Inspect the existing code path — if this `mkdir` is already there, no action needed.
### Pitfall 5: Duplicate registration on restarts
**What goes wrong:** Every server restart calls `fetchAll()`, which re-registers all 9 local skills. Without idempotency guards, this creates duplicate `skill_versions` and `skill_files` rows.
**Why it happens:** The content hash SHA is deterministic, so `versionExists()` returns true and `cacheSkillVersion` is skipped. But `upsertSkill` uses `onConflictDoUpdate` — the name/description are updated but no duplicate is created.
**How to avoid:** The existing `versionExists()` check handles this. Confirm the content-hash versionId format matches: `${skillId}@${sha}`.
---
## Code Examples
### Registering skills from disk (local-nexus-content handler)
```typescript
// Source: server/src/services/skill-registry-fetcher.ts (new function)
import { readdir, readFile } from "node:fs/promises";
import crypto from "node:crypto";
async function fetchLocalNexusContent(
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
db: SkillRegistryDb,
): Promise<number> {
let entries: Awaited<ReturnType<typeof readdir>>;
try {
entries = await readdir(source.dir, { withFileTypes: true });
} catch {
// Directory doesn't exist (e.g. missing after a build step) — log and skip
return 0;
}
let fetched = 0;
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".SKILL.md")) continue;
const slug = entry.name.replace(/\.SKILL\.md$/, "");
const skillId = `${source.id}/${slug}`;
const content = await readFile(path.join(source.dir, entry.name), "utf-8");
const sha = crypto.createHash("sha1").update(content).digest("hex");
const { name, description } = parseSkillFrontmatter(content);
await upsertSkill(db, {
skillId, sourceId: source.id,
name: name ?? slug, description,
sourceUrl: "",
});
await cacheSkillVersion(db, { skillId, sha, skillMdContent: content, skillMdUrl: "" });
await upsertCommunityRatingsStub(db, skillId, source.id);
fetched++;
}
return fetched;
}
```
### Seeding Creative group membership after fetchAll
```typescript
// In skill-registry-db.ts — called after seedBuiltinGroups
async function seedCreativeGroupMembers(client: LibSQLClient): Promise<void> {
const now = Date.now();
const SKILLS = [
"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",
];
for (const skillId of SKILLS) {
await client.execute({
sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`,
args: ["builtin/creative", skillId, now],
});
}
}
```
### Ensuring Creative is assigned to a new Generalist agent
```typescript
// The existing path in index.ts (already present, no change needed):
await agentSvc.create(company.id, {
name: "Generalist",
role: "general",
adapterType: "claude_local",
adapterConfig: {},
runtimeConfig: {},
metadata: { pendingSkillGroups: ["Creative"], backfilled: true },
});
// The reconciler at server startup calls svc.assignGroup("builtin/creative", agentId, skillsDir)
// which installs all Creative group members to the agent's .claude/skills/ directory.
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Skills only from remote GitHub repos | Skills can come from local filesystem too (new local-nexus-content type) | Phase 45 | Nexus-specific content skills don't need a public GitHub repo |
| Creative group is empty (seed only creates the group row) | Creative group has 9 content skill members after server init | Phase 45 | Generalist agents automatically get all content tools on first run |
---
## Open Questions
1. **Should `cacheSkillVersion` write files to a tmpdir for local skills?**
- What we know: `cacheSkillVersion` writes `SKILL.md` to `~/.paperclip/instances/default/skills/cache/nexus-content/<slug>/<sha>/SKILL.md`, then `svc.install()` copies from that cache dir to the agent's skills dir.
- What's unclear: For local skills, caching to disk before installing is redundant — the source file is already on disk.
- Recommendation: Keep the cache write for consistency (install always reads from cache); the file is tiny. If performance matters, skip caching and have install copy the source file directly.
2. **Where exactly is the `server/src/skills/content/` dir resolved in compiled builds?**
- What we know: The server compiles TypeScript but does not currently bundle assets — source files are referenced at runtime via `import.meta.url`.
- What's unclear: Whether `.SKILL.md` files are included in the dist output.
- Recommendation: Add `server/src/skills/content/` to the list of directories copied during build (check the server's build script), or use `process.env.SKILL_CONTENT_DIR` as an override for production.
3. **Should the Skill Aggregator UI show a "Creative" filter tab?**
- What we know: `SkillBrowser.tsx` filters by adapter compatibility; the "Groups" tab shows group membership via `skillGroupsApi`. Content skills installed via the registry appear in the installed tab.
- What's unclear: Whether the browse experience needs a dedicated "Creative" section or if the existing group-based filtering is sufficient.
- Recommendation: No UI changes needed for SKILL-03. The existing SkillBrowser already supports install/remove for any registered skill. Adding a dedicated Creative tab is a future enhancement.
---
## Environment Availability
Step 2.6: SKIPPED (no new external dependencies — local filesystem only)
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest (node environment) |
| Config file | `server/vitest.config.ts` |
| Quick run command | `cd /opt/nexus/server && npx vitest run --reporter=verbose src/__tests__/skill-registry-content-skills.test.ts` |
| Full suite command | `cd /opt/nexus/server && npx vitest run --reporter=verbose` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| SKILL-01 | 9 SKILL.md files are seeded into registry DB via local-nexus-content source | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "seeds content skills"` | ❌ Wave 0 |
| SKILL-01 | Each skill is installable to an agent skills dir | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "installs content skill"` | ❌ Wave 0 |
| SKILL-02 | Creative group has 9 members after DB init | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "Creative group members"` | ❌ Wave 0 |
| SKILL-03 | Content skills appear in registry list | unit | `npx vitest run src/__tests__/skill-registry-content-skills.test.ts -t "list includes content skills"` | ❌ Wave 0 |
### Sampling Rate
- **Per task commit:** `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts`
- **Per wave merge:** `cd /opt/nexus/server && npx vitest run`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `server/src/__tests__/skill-registry-content-skills.test.ts` — covers SKILL-01, SKILL-02, SKILL-03
- [ ] `server/src/skills/content/*.SKILL.md` — 9 skill files (authored, not test infra)
---
## Sources
### Primary (HIGH confidence)
- Direct source code inspection — `server/src/services/skill-registry-fetcher.ts`, `skill-registry-db.ts`, `skill-registry-groups.ts`, `skill-registry.ts`
- Direct source code inspection — `server/src/index.ts` lines 256-279 (ensureGeneralistAgents), 628-658 (pendingSkillGroups reconciler)
- Direct source code inspection — `server/src/adapters/registry.ts`, `server/src/routes/skill-registry.ts`, `server/src/routes/skill-registry-groups.ts`
### Secondary (MEDIUM confidence)
- Existing test patterns from `server/src/__tests__/skill-registry-install.test.ts` and `hermes-dual-source.test.ts` — confirms vitest + real temp dirs as test pattern
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries already installed; no new deps
- Architecture: HIGH — local source type follows identical pattern to existing github-tree handler
- Pitfalls: HIGH — race conditions and dir resolution issues verified against actual startup code
**Research date:** 2026-04-04
**Valid until:** 2026-05-04 (stable codebase)

View file

@ -0,0 +1,258 @@
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function mockSkillMd(name: string, description: string, jobType: string): string {
return `---
name: ${name}
description: ${description}
---
# ${name}
## Usage
POST /api/content-jobs with jobType: "${jobType}"
`;
}
// ---------------------------------------------------------------------------
// Tests — SKILL.md files on disk
// ---------------------------------------------------------------------------
describe("content skill files", () => {
const SKILL_TYPES = [
"diagram",
"icon-set",
"theme-palette",
"wallpaper",
"social-post",
"convert",
"pdf-document",
"brand-kit",
"presentation",
];
const contentDir = path.resolve(
new URL("../skills/content", import.meta.url).pathname,
);
it("Test 1: 9 .SKILL.md files exist in server/src/skills/content/", async () => {
const { readdir } = await import("node:fs/promises");
const entries = await readdir(contentDir);
const skillFiles = entries.filter((e) => e.endsWith(".SKILL.md"));
expect(skillFiles).toHaveLength(9);
});
it("Test 2: Each SKILL.md has parseable frontmatter with name and description", async () => {
const { parseSkillFrontmatter } = await import("../services/skill-registry-fetcher.js");
for (const skillType of SKILL_TYPES) {
const filePath = path.join(contentDir, `${skillType}.SKILL.md`);
const content = await readFile(filePath, "utf-8");
const { name, description } = parseSkillFrontmatter(content);
expect(name, `${skillType}.SKILL.md missing name`).toBeTruthy();
expect(description, `${skillType}.SKILL.md missing description`).toBeTruthy();
}
});
it("Test 3: Each SKILL.md contains its jobType string", async () => {
for (const skillType of SKILL_TYPES) {
const filePath = path.join(contentDir, `${skillType}.SKILL.md`);
const content = await readFile(filePath, "utf-8");
expect(content, `${skillType}.SKILL.md missing jobType reference`).toContain(`jobType`);
}
});
});
// ---------------------------------------------------------------------------
// Tests — fetchLocalNexusContent via fetchAllSources
// ---------------------------------------------------------------------------
describe("fetchLocalNexusContent", () => {
let tmpDir: string;
let tmpContentDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), "nexus-content-skills-test-"));
tmpContentDir = path.join(tmpDir, "skills", "content");
await mkdir(tmpContentDir, { recursive: true });
process.env.PAPERCLIP_HOME = tmpDir;
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
resetSkillRegistryDb();
});
afterEach(async () => {
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
resetSkillRegistryDb();
delete process.env.PAPERCLIP_HOME;
await rm(tmpDir, { recursive: true, force: true });
});
it("Test 4: fetchAllSources with local-nexus-content returns fetched=9 for 9 mock SKILL.md files", async () => {
// Create 9 mock SKILL.md files
const types = ["diagram", "icon-set", "theme-palette", "wallpaper", "social-post", "convert", "pdf-document", "brand-kit", "presentation"];
for (const t of types) {
await writeFile(
path.join(tmpContentDir, `${t}.SKILL.md`),
mockSkillMd(`${t} Skill`, `Does ${t} stuff`, t),
"utf-8",
);
}
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
const localSource = {
id: "nexus-content",
type: "local-nexus-content" as const,
dir: tmpContentDir,
label: "Nexus Content Tools",
};
const result = await fetchAllSources([localSource]);
expect(result.errors).toHaveLength(0);
expect(result.fetched).toBe(9);
});
it("Test 5: Skills are upserted into the DB with correct sourceId and skillId format", async () => {
await writeFile(
path.join(tmpContentDir, "diagram.SKILL.md"),
mockSkillMd("Diagram Generator", "Generates diagrams", "diagram"),
"utf-8",
);
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
const { skills } = await import("../services/skill-registry-schema.js");
const localSource = {
id: "nexus-content",
type: "local-nexus-content" as const,
dir: tmpContentDir,
label: "Nexus Content Tools",
};
await fetchAllSources([localSource]);
const db = await getSkillRegistryDb();
const rows = await db.select().from(skills);
expect(rows).toHaveLength(1);
expect(rows[0]!.id).toBe("nexus-content/diagram");
expect(rows[0]!.sourceId).toBe("nexus-content");
expect(rows[0]!.name).toBe("Diagram Generator");
});
it("Test 6: Duplicate fetch (restart) does not create duplicate rows — idempotent", async () => {
await writeFile(
path.join(tmpContentDir, "diagram.SKILL.md"),
mockSkillMd("Diagram Generator", "Generates diagrams", "diagram"),
"utf-8",
);
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
const { skills, skillVersions } = await import("../services/skill-registry-schema.js");
const localSource = {
id: "nexus-content",
type: "local-nexus-content" as const,
dir: tmpContentDir,
label: "Nexus Content Tools",
};
await fetchAllSources([localSource]);
await fetchAllSources([localSource]);
const db = await getSkillRegistryDb();
const skillRows = await db.select().from(skills);
const versionRows = await db.select().from(skillVersions);
// Should be exactly 1 skill and 1 version (no duplicates)
expect(skillRows).toHaveLength(1);
expect(versionRows).toHaveLength(1);
});
it("Test 7: Missing directory returns fetched=0 with no errors", async () => {
const { fetchAllSources } = await import("../services/skill-registry-fetcher.js");
const localSource = {
id: "nexus-content",
type: "local-nexus-content" as const,
dir: "/nonexistent/path/that/does/not/exist",
label: "Nexus Content Tools",
};
const result = await fetchAllSources([localSource]);
expect(result.fetched).toBe(0);
expect(result.errors).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// Tests — Creative group members (placeholder — filled in by Task 2)
// ---------------------------------------------------------------------------
describe("Creative group members", () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(path.join(os.tmpdir(), "nexus-creative-group-test-"));
process.env.PAPERCLIP_HOME = tmpDir;
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
resetSkillRegistryDb();
});
afterEach(async () => {
const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
resetSkillRegistryDb();
delete process.env.PAPERCLIP_HOME;
await rm(tmpDir, { recursive: true, force: true });
});
it("Test 8: seedCreativeGroupMembers inserts 9 rows for builtin/creative group", async () => {
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("../services/skill-registry-db.js");
// Initialize DB (creates tables and builtin groups)
await getSkillRegistryDb();
// Seed the group members
await seedCreativeGroupMembers();
// Query directly
const { getRawClient } = await import("../services/skill-registry-db.js");
const client = getRawClient();
const result = await client.execute({
sql: `SELECT skill_id FROM skill_group_members WHERE group_id = ?`,
args: ["builtin/creative"],
});
expect(result.rows).toHaveLength(9);
const skillIds = result.rows.map((r) => r[0] as string);
expect(skillIds).toContain("nexus-content/diagram");
expect(skillIds).toContain("nexus-content/presentation");
});
it("Test 9: seedCreativeGroupMembers is idempotent — calling twice does not duplicate rows", async () => {
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("../services/skill-registry-db.js");
await getSkillRegistryDb();
await seedCreativeGroupMembers();
await seedCreativeGroupMembers();
const { getRawClient } = await import("../services/skill-registry-db.js");
const client = getRawClient();
const result = await client.execute({
sql: `SELECT skill_id FROM skill_group_members WHERE group_id = ?`,
args: ["builtin/creative"],
});
// Still exactly 9, not 18
expect(result.rows).toHaveLength(9);
});
});

View file

@ -381,24 +381,28 @@ describe("skill-registry-fetch", () => {
// -------------------------------------------------------------------------
// Test 7: BUILT_IN_SOURCES has exactly 3 entries
// -------------------------------------------------------------------------
it("Test 7: BUILT_IN_SOURCES contains 3 entries (anthropic-official, schwepps-skills, daymade-skills)", async () => {
it("Test 7: BUILT_IN_SOURCES contains 4 entries (anthropic-official, schwepps-skills, daymade-skills, nexus-content)", async () => {
const { BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
expect(BUILT_IN_SOURCES).toHaveLength(3);
expect(BUILT_IN_SOURCES).toHaveLength(4);
const ids = BUILT_IN_SOURCES.map((s) => s.id);
expect(ids).toContain("anthropic-official");
expect(ids).toContain("schwepps-skills");
expect(ids).toContain("daymade-skills");
expect(ids).toContain("nexus-content");
// Verify all sources have required fields
for (const source of BUILT_IN_SOURCES) {
expect(source.id).toBeTruthy();
expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/);
expect(source.owner).toBeTruthy();
expect(source.repo).toBeTruthy();
expect(source.ref).toBeTruthy();
expect(source.label).toBeTruthy();
// local-nexus-content does not have owner/repo/ref
if (source.type !== "local-nexus-content") {
expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/);
expect(source.owner).toBeTruthy();
expect(source.repo).toBeTruthy();
expect(source.ref).toBeTruthy();
}
}
});
});

View file

@ -614,19 +614,21 @@ export async function startServer(): Promise<StartedServer> {
logger.error({ err }, "startup reconciliation of persisted runtime services failed");
});
// [nexus] Initialize skill registry database (fire-and-forget)
// [nexus] Initialize skill registry, seed content skills, populate Creative group, reconcile pending groups
// Sequence: DB init -> fetchAll (registers local skills) -> seed Creative group members -> reconcile pending
void (async () => {
try {
const { getSkillRegistryDb } = await import("./services/skill-registry-db.js");
const { getSkillRegistryDb, seedCreativeGroupMembers } = await import("./services/skill-registry-db.js");
await getSkillRegistryDb();
logger.info("skill registry database initialized");
const { skillRegistryService } = await import("./services/skill-registry.js");
await skillRegistryService().fetchAll();
await seedCreativeGroupMembers();
logger.info("skill registry initialized, content skills seeded, Creative group populated");
} catch (err) {
logger.error({ err }, "skill registry init failed");
}
})();
// [nexus] Reconcile pendingSkillGroups metadata on agents (fire-and-forget)
void (async () => {
// Reconcile pendingSkillGroups metadata on agents — runs after Creative group is seeded
try {
const { join } = await import("node:path");
const { skillGroupService } = await import("./services/skill-registry-groups.js");

View file

@ -47,7 +47,7 @@ async function runSingleFileUpload(
});
}
export function convertRoutes(db: Db, _storage: StorageService) {
export function convertRoutes(db: Db, storage: StorageService) {
const router = Router();
// POST /companies/:companyId/convert — multipart upload + MIME validation + job dispatch
@ -139,7 +139,7 @@ export function convertRoutes(db: Db, _storage: StorageService) {
sourceTaskId: typeof req.body.sourceTaskId === "string" ? req.body.sourceTaskId : null,
});
void contentJobRunner.dispatch(db, {} as StorageService, job!);
void contentJobRunner.dispatch(db, storage, job!);
res.status(202).json({
jobId: job!.id,

View file

@ -8,6 +8,7 @@ import { resolveSkillRegistryDbPath } from "../home-paths.js";
export type SkillRegistryDb = ReturnType<typeof drizzle<typeof schema>>;
let _db: SkillRegistryDb | null = null;
let _client: LibSQLClient | null = null;
const CREATE_SKILLS_TABLE = `
CREATE TABLE IF NOT EXISTS skills (
@ -147,6 +148,7 @@ export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
await mkdir(dirname(dbPath), { recursive: true });
const client = createClient({ url: `file:${dbPath}` });
_client = client;
_db = drizzle({ client, schema });
await client.execute(CREATE_SKILLS_TABLE);
@ -181,7 +183,47 @@ export async function getSkillRegistryDb(): Promise<SkillRegistryDb> {
return _db;
}
/**
* Return the raw LibSQL client singleton.
* Must be called after getSkillRegistryDb() to ensure the client is initialized.
*/
export function getRawClient(): LibSQLClient {
if (_client === null) {
throw new Error("Skill registry DB not initialized — call getSkillRegistryDb() first");
}
return _client;
}
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;
/**
* Seed the builtin/creative skill group with all 9 Nexus content skill IDs.
* Uses INSERT OR IGNORE for idempotency safe to call multiple times.
* Must be called AFTER skillRegistryService().fetchAll() so skill rows exist.
*/
export async function seedCreativeGroupMembers(): Promise<void> {
const client = getRawClient();
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],
});
}
}
/** Reset the singleton — used for test cleanup */
export function resetSkillRegistryDb(): void {
_db = null;
_client = null;
}

View file

@ -1,7 +1,8 @@
import crypto from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { eq } from "drizzle-orm";
import { getSkillRegistryDb, type SkillRegistryDb } from "./skill-registry-db.js";
import { skills, skillVersions, skillFiles, communityRatings } from "./skill-registry-schema.js";
@ -17,14 +18,10 @@ import { resolveSkillCacheDir } from "../home-paths.js";
// Source config
// ---------------------------------------------------------------------------
export type SkillSourceConfig = {
id: string;
type: "anthropic-marketplace" | "github-tree";
owner: string;
repo: string;
ref: string;
label: string;
};
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 };
export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
{
@ -51,6 +48,12 @@ export const BUILT_IN_SOURCES: SkillSourceConfig[] = [
ref: "main",
label: "Daymade Community",
},
{
id: "nexus-content",
type: "local-nexus-content",
dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"),
label: "Nexus Content Tools",
},
];
// ---------------------------------------------------------------------------
@ -244,7 +247,7 @@ async function cacheSkillVersion(
// ---------------------------------------------------------------------------
async function fetchAnthropicMarketplace(
source: SkillSourceConfig,
source: Extract<SkillSourceConfig, { type: "anthropic-marketplace" }>,
db: SkillRegistryDb,
): Promise<number> {
const marketplaceUrl = resolveRawGitHubUrl(
@ -308,7 +311,7 @@ async function fetchAnthropicMarketplace(
}
async function fetchGitHubTree(
source: SkillSourceConfig,
source: Extract<SkillSourceConfig, { type: "github-tree" }>,
db: SkillRegistryDb,
): Promise<number> {
const treeUrl = `https://api.github.com/repos/${source.owner}/${source.repo}/git/trees/${encodeURIComponent(source.ref)}?recursive=1`;
@ -372,6 +375,77 @@ async function fetchGitHubTree(
return fetched;
}
// ---------------------------------------------------------------------------
// Local filesystem source handler
// ---------------------------------------------------------------------------
async function fetchLocalNexusContent(
source: Extract<SkillSourceConfig, { type: "local-nexus-content" }>,
db: SkillRegistryDb,
): Promise<number> {
let entries;
try {
entries = await readdir(source.dir, { withFileTypes: true });
} catch {
// Directory missing or unreadable — return 0, no error
return 0;
}
const skillMdFiles = entries.filter(
(e) => e.isFile() && e.name.endsWith(".SKILL.md"),
);
let fetched = 0;
for (const entry of skillMdFiles) {
// Derive slug from filename: "diagram.SKILL.md" → "diagram"
const slug = entry.name.replace(/\.SKILL\.md$/, "");
const skillId = `${source.id}/${slug}`;
const filePath = path.join(source.dir, entry.name);
let content: string;
try {
content = await readFile(filePath, "utf-8");
} catch {
continue;
}
// Compute a content-based SHA-1 hash for idempotency
const sha = crypto.createHash("sha1").update(content).digest("hex");
const versionId = `${skillId}@${sha}`;
// Idempotency check — skip DB writes if version already cached
if (await versionExists(db, versionId)) {
fetched++;
continue;
}
const { name, description } = parseSkillFrontmatter(content);
const sourceUrl = `file://${filePath}`;
await upsertSkill(db, {
skillId,
sourceId: source.id,
name: name ?? slug,
description,
sourceUrl,
});
await cacheSkillVersion(db, {
skillId,
sha,
skillMdContent: content,
skillMdUrl: sourceUrl,
});
await upsertCommunityRatingsStub(db, skillId, source.id);
fetched++;
}
return fetched;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
@ -398,8 +472,8 @@ export async function fetchAllSources(
fetched += await fetchAnthropicMarketplace(source, db);
} else if (source.type === "github-tree") {
fetched += await fetchGitHubTree(source, db);
} else {
errors.push(`Unknown source type for ${source.id}`);
} else if (source.type === "local-nexus-content") {
fetched += await fetchLocalNexusContent(source, db);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);

View file

@ -0,0 +1,36 @@
---
name: Brand Kit Generator
description: Generate a complete brand identity kit including logo, color palette, and social assets via the Nexus content job API
---
# Brand Kit Generator
Generate a cohesive brand identity kit from a company name and description. Produces logo SVG, color palette, typography recommendations, and social media image templates.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "brand-kit",
"input": {
"companyName": "Acme Corp",
"description": "A modern SaaS platform for project management",
"seedColor": "#2563eb"
}
}
```
**Fields:**
- `companyName` (required): The company or brand name
- `description` (required): Brief description of the company and its focus
- `seedColor` (optional): Hex color to anchor the brand palette
## Output
The job returns a `BrandKitBundle` with:
- `logoSvg`: Primary logo as SVG
- `palette`: Color tokens (primary, secondary, accent, neutrals)
- `socialImages`: Array of platform-sized social image templates
- `typography`: Recommended font pairings

View file

@ -0,0 +1,33 @@
---
name: Format Converter
description: Convert files between formats using direct conversion or AI-bridged fallback via the Nexus content job API
---
# Format Converter
Convert an existing asset to a different file format. Supports direct converter paths (ffmpeg, sharp, pandoc) with AI-bridged fallback for all format pairs.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "convert",
"input": {
"sourceAssetId": "asset-uuid-here",
"targetFormat": "pdf"
}
}
```
**Fields:**
- `sourceAssetId` (required): UUID of the source asset already stored in Nexus
- `targetFormat` (required): Target format extension — e.g. `pdf`, `png`, `mp4`, `docx`, `svg`
## Output
The job returns a `ConvertBundle` with:
- `assetId`: ID of the converted output asset
- `targetFormat`: Format that was applied
- `method`: Conversion path used — `direct` or `ai-bridge`

View file

@ -0,0 +1,32 @@
---
name: Diagram Generator
description: Generate Mermaid diagrams as SVG via the Nexus content job API
---
# Diagram Generator
Generate diagrams from natural language descriptions. Supports flowcharts, sequence diagrams, ER diagrams, Gantt charts, and more using Mermaid syntax rendered to SVG.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "diagram",
"input": {
"prompt": "A flowchart showing user login flow with success and failure paths",
"diagramType": "flowchart"
}
}
```
**Fields:**
- `prompt` (required): Natural language description of the diagram
- `diagramType` (optional): Hint for diagram type — `flowchart`, `sequence`, `er`, `gantt`, `class`, `pie`
## Output
The job returns a `DiagramBundle` with:
- `svg`: Rendered SVG string
- `mermaidSource`: Raw Mermaid source code

View file

@ -0,0 +1,34 @@
---
name: Icon Set Generator
description: Generate cohesive SVG icon sets with consistent style via the Nexus content job API
---
# Icon Set Generator
Generate a set of SVG icons with a consistent visual style. Icons are produced as clean, scalable SVG suitable for UI, documentation, or branding use.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "icon-set",
"input": {
"description": "Navigation icons for a dashboard app",
"style": "outline",
"count": 6
}
}
```
**Fields:**
- `description` (required): What the icons represent or their purpose
- `style` (optional): Visual style — `outline`, `filled`, `duotone` (default: `outline`)
- `count` (optional): Number of icons to generate (default: 6, max: 12)
## Output
The job returns an `IconSetBundle` with:
- `icons`: Array of `{ name, svg }` objects
- `style`: The style applied

View file

@ -0,0 +1,33 @@
---
name: PDF Document Generator
description: Generate styled PDF documents from structured content via the Nexus content job API
---
# PDF Document Generator
Generate professional PDF documents from Markdown or structured content. Supports multiple document types with appropriate layouts and styling.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "pdf-document",
"input": {
"content": "# Q1 Report\n\nRevenue grew 24% year-over-year...",
"docType": "report"
}
}
```
**Fields:**
- `content` (required): Document body as Markdown or plain text
- `docType` (optional): Layout template — `report`, `invoice`, `api-docs`, `one-pager` (default: `report`)
## Output
The job returns a `PdfBundle` with:
- `assetId`: ID of the stored PDF asset
- `pageCount`: Number of pages in the output
- `docType`: Document type template applied

View file

@ -0,0 +1,35 @@
---
name: Presentation Generator
description: Generate animated video presentations with LLM-authored slides via the Nexus content job API
---
# Presentation Generator
Generate a video presentation from a topic. Uses LLM to author slide content and Remotion to render an MP4 with animations and transitions.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "presentation",
"input": {
"topic": "Introduction to machine learning for product managers",
"slideCount": 8,
"style": "professional"
}
}
```
**Fields:**
- `topic` (required): The subject of the presentation
- `slideCount` (optional): Number of slides to generate (default: 6, max: 20)
- `style` (optional): Visual style — `professional`, `creative`, `minimal` (default: `professional`)
## Output
The job returns a `PresentationBundle` with:
- `assetId`: ID of the rendered MP4 asset
- `slides`: Array of slide data (title, bullets, speakerNotes)
- `durationSeconds`: Approximate video duration

View file

@ -0,0 +1,34 @@
---
name: Social Post Generator
description: Generate platform-optimized social media posts with optional images via the Nexus content job API
---
# Social Post Generator
Generate social media post copy and optional image assets sized for specific platforms. Respects character limits and platform conventions.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "social-post",
"input": {
"prompt": "Launch announcement for our new AI assistant feature",
"platform": "twitter-x"
}
}
```
**Fields:**
- `prompt` (required): The subject or message to communicate
- `platform` (optional): Target platform — `twitter-x`, `linkedin`, `instagram-caption`, `instagram-carousel` (default: `twitter-x`)
## Output
The job returns a `SocialPostBundle` with:
- `copy`: Post text body
- `hashtags`: Array of suggested hashtags
- `slides`: Optional carousel slide content array
- `platform`: The target platform applied

View file

@ -0,0 +1,33 @@
---
name: Theme Palette Generator
description: Generate accessible OKLCH color palettes for UI theming via the Nexus content job API
---
# Theme Palette Generator
Generate a complete UI color palette from a seed color. Uses OKLCH color space for perceptually uniform, accessible color scales with WCAG contrast ratios.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "theme-palette",
"input": {
"seedColor": "#6366f1",
"name": "Indigo Theme"
}
}
```
**Fields:**
- `seedColor` (required): Hex color used as the primary brand color
- `name` (optional): Human-readable name for the theme
## Output
The job returns a `ThemePaletteBundle` with:
- `colors`: Named color tokens (primary, surface, accent, text, border scales)
- `wcagReport`: Contrast ratios for foreground/background pairs
- `cssVars`: Ready-to-use CSS custom property declarations

View file

@ -0,0 +1,32 @@
---
name: Wallpaper Generator
description: Generate desktop wallpapers as high-resolution PNG via the Nexus content job API
---
# Wallpaper Generator
Generate desktop wallpapers from a description. Produces high-resolution PNG images suitable for desktop backgrounds, lock screens, or presentation backdrops.
## Usage
Submit a content job via `POST /api/companies/{companyId}/content-jobs`:
```json
{
"jobType": "wallpaper",
"input": {
"prompt": "Abstract geometric pattern with deep blue and gold tones",
"platform": "desktop-fhd"
}
}
```
**Fields:**
- `prompt` (required): Visual description of the desired wallpaper
- `platform` (optional): Target platform — `desktop-hd`, `desktop-fhd`, `desktop-4k`, `mobile-portrait`, `mobile-landscape`, `og-image`, `twitter-card`, `instagram-post`, `instagram-banner`, `linkedin-banner`, `app-icon`, `favicon` (default: `desktop-fhd`)
## Output
The job returns a `WallpaperBundle` with:
- `assetId`: ID of the stored PNG asset
- `resolution`: Final output dimensions

View file

@ -8,18 +8,32 @@ import { DocumentGeneratePanel } from "../components/DocumentGeneratePanel";
import { BrandKitPanel } from "../components/BrandKitPanel";
import { PresentationPanel } from "../components/PresentationPanel";
import { ThemeSeedInput } from "../components/ThemeSeedInput";
import { ThemePaletteGrid } from "../components/ThemePaletteGrid";
import { ThemePaletteGrid, type PaletteRole } from "../components/ThemePaletteGrid";
import { ThemePreviewPanel } from "../components/ThemePreviewPanel";
import { ThemeExportTabs } from "../components/ThemeExportTabs";
import { ThemeApplyConfirmDialog } from "../components/ThemeApplyConfirmDialog";
import { useContentJob } from "../hooks/useContentJob";
import { useState } from "react";
import { useState, useEffect } from "react";
export function ContentStudio() {
const { selectedCompanyId } = useCompany();
const companyId = selectedCompanyId ?? "";
const themeJob = useContentJob(companyId);
const [showApplyDialog, setShowApplyDialog] = useState(false);
const [seedColor, setSeedColor] = useState("#4f46e5");
const [themeBundle, setThemeBundle] = useState<{
palette: PaletteRole[];
exports: { css: string; tailwind: string; vscode: string; json: string };
} | null>(null);
useEffect(() => {
if (themeJob.status === "done" && themeJob.resultAssetId && companyId) {
fetch(`/api/companies/${companyId}/assets/${themeJob.resultAssetId}`)
.then((r) => r.json())
.then((data) => setThemeBundle(data as typeof themeBundle))
.catch(() => {});
}
}, [themeJob.status, themeJob.resultAssetId, companyId]);
return (
<div className="flex flex-col gap-6 p-6">
@ -57,17 +71,24 @@ export function ContentStudio() {
{companyId ? (
<div className="flex flex-col gap-6">
<ThemeSeedInput
companyId={companyId}
onSubmit={(seedColor) => {
value={seedColor}
onChange={setSeedColor}
/>
<button
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
disabled={themeJob.status === "running" || themeJob.status === "queued"}
onClick={() => {
setThemeBundle(null);
themeJob.submit("theme-palette", { seedColor });
}}
isLoading={themeJob.status === "running" || themeJob.status === "queued"}
/>
{themeJob.bundle && (
>
{themeJob.status === "running" || themeJob.status === "queued" ? "Generating..." : "Generate Palette"}
</button>
{themeBundle && (
<>
<ThemePaletteGrid palette={(themeJob.bundle as Record<string, unknown>).palette as Array<Record<string, unknown>>} />
<ThemePreviewPanel palette={(themeJob.bundle as Record<string, unknown>).palette as Array<Record<string, unknown>>} />
<ThemeExportTabs exports={(themeJob.bundle as Record<string, unknown>).exports as Record<string, string>} />
<ThemePaletteGrid palette={themeBundle.palette} />
<ThemePreviewPanel palette={themeBundle.palette} variant="light" />
<ThemeExportTabs exports={themeBundle.exports} />
<button
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
onClick={() => setShowApplyDialog(true)}
@ -76,10 +97,10 @@ export function ContentStudio() {
</button>
<ThemeApplyConfirmDialog
open={showApplyDialog}
onOpenChange={setShowApplyDialog}
onConfirm={() => {
setShowApplyDialog(false);
}}
onCancel={() => setShowApplyDialog(false)}
/>
</>
)}