diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md index 54305313..30d4f207 100644 --- a/.planning/MILESTONES.md +++ b/.planning/MILESTONES.md @@ -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 diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 7a5e0502..700cf6d3 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -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 diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8a10ef5b..eaa5040e 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -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 | diff --git a/.planning/STATE.md b/.planning/STATE.md index 62e6dec7..efc4efce 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -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 diff --git a/.planning/milestones/v1.7-MILESTONE-AUDIT.md b/.planning/milestones/v1.7-MILESTONE-AUDIT.md new file mode 100644 index 00000000..6c0a9f57 --- /dev/null +++ b/.planning/milestones/v1.7-MILESTONE-AUDIT.md @@ -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 | — | diff --git a/.planning/milestones/v1.7-REQUIREMENTS.md b/.planning/milestones/v1.7-REQUIREMENTS.md new file mode 100644 index 00000000..153207e2 --- /dev/null +++ b/.planning/milestones/v1.7-REQUIREMENTS.md @@ -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)* diff --git a/.planning/milestones/v1.7-ROADMAP.md b/.planning/milestones/v1.7-ROADMAP.md new file mode 100644 index 00000000..eaa5040e --- /dev/null +++ b/.planning/milestones/v1.7-ROADMAP.md @@ -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) + +--- + +
+✅ v1.2.1 Universal Skill Management (Phase 1) - SHIPPED 2026-04-01 + +### 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 + +
+ +
+✅ v1.3 Chat & PWA (Phases 21-26) - SHIPPED 2026-04-02 + +### 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 + +
+ +
+✅ v1.4 Hermes Default Provider (Phases 27-29) - SHIPPED 2026-04-02 + +### 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 + +
+ +
+✅ v1.5 Smart Onboarding + Personal AI Assistant (Phases 30-35) - SHIPPED 2026-04-03 + +### 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 + +
+ +
+✅ v1.6 Voice Pipeline + Minimal Message Bridge (Phases 36-39) - SHIPPED 2026-04-04 + +### 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 + +
+ +--- + +### 🚧 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 | diff --git a/.planning/phases/45-content-as-skills/45-01-PLAN.md b/.planning/phases/45-content-as-skills/45-01-PLAN.md new file mode 100644 index 00000000..f14689ca --- /dev/null +++ b/.planning/phases/45-content-as-skills/45-01-PLAN.md @@ -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" +--- + + +Register all 9 content generation types as installable Nexus skills with a local filesystem source, seed the Creative group, and wire startup. + +Purpose: Completes the final v1.7 phase by exposing content generators (diagrams, themes, icons, wallpapers, social posts, format conversion, PDFs, brand kits, presentations) as skills that agents can discover and use. +Output: 9 SKILL.md files, extended fetcher with local-nexus-content source type, Creative group membership, startup wiring, unit tests. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/45-content-as-skills/45-RESEARCH.md + +@server/src/services/skill-registry-fetcher.ts +@server/src/services/skill-registry-db.ts +@server/src/services/skill-registry.ts +@server/src/services/content-job-runner.ts +@server/src/index.ts + + + + +From server/src/services/skill-registry-fetcher.ts: +```typescript +export type SkillSourceConfig = { + id: string; + type: "anthropic-marketplace" | "github-tree"; + owner: string; + repo: string; + ref: string; + label: string; +}; + +export const BUILT_IN_SOURCES: SkillSourceConfig[] = [ /* remote sources */ ]; + +export function parseSkillFrontmatter(markdown: string): { name?: string; description?: string }; + +// Private helpers used inside fetcher (same pattern needed for local): +// upsertSkill(db, { skillId, sourceId, name, description, sourceUrl }) +// cacheSkillVersion(db, { skillId, sha, skillMdContent, skillMdUrl }) +// upsertCommunityRatingsStub(db, skillId, sourceId) +// versionExists(db, versionId) — idempotency guard + +export async function fetchAllSources(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>; +``` + +From server/src/services/skill-registry-db.ts: +```typescript +export type SkillRegistryDb = /* drizzle instance */; +export async function getSkillRegistryDb(): Promise; +// seedBuiltinGroups(client) is called inside getSkillRegistryDb — creates builtin/creative group row +``` + +From server/src/services/skill-registry.ts: +```typescript +export function skillRegistryService() { + return { + async list(opts?: { includeRemoved?: boolean }): Promise; + async install(skillId: string, agentId: string, agentSkillsDir: string): Promise; + async uninstall(skillId: string, agentId: string, agentSkillsDir: string): Promise; + async fetchAll(sources?: SkillSourceConfig[]): Promise<{ fetched: number; errors: string[] }>; + }; +} +``` + +From server/src/services/content-job-runner.ts — jobType values: +``` +"diagram", "icon-set", "theme-palette", "wallpaper", "social-post", +"convert", "pdf-document", "brand-kit", "presentation" +``` + +From server/src/index.ts — startup blocks: +```typescript +// Line 617-626: skill registry init (fire-and-forget) +void (async () => { + const { getSkillRegistryDb } = await import("./services/skill-registry-db.js"); + await getSkillRegistryDb(); + logger.info("skill registry database initialized"); +})(); + +// Line 628-659: pendingSkillGroups reconciler (fire-and-forget) +// Assigns groups to agents with metadata.pendingSkillGroups +// GROUP_NAME_MAP: { "Creative": "builtin/creative", ... } +``` + + + + + + + Task 1: Author 9 SKILL.md files and extend fetcher with local-nexus-content source + + server/src/skills/content/diagram.SKILL.md + server/src/skills/content/icon-set.SKILL.md + server/src/skills/content/theme-palette.SKILL.md + server/src/skills/content/wallpaper.SKILL.md + server/src/skills/content/social-post.SKILL.md + server/src/skills/content/convert.SKILL.md + server/src/skills/content/pdf-document.SKILL.md + server/src/skills/content/brand-kit.SKILL.md + server/src/skills/content/presentation.SKILL.md + server/src/services/skill-registry-fetcher.ts + server/src/__tests__/skill-registry-content-skills.test.ts + + + server/src/services/skill-registry-fetcher.ts + server/src/services/skill-registry-db.ts + server/src/services/content-job-runner.ts + server/src/__tests__/skill-registry-fetch.test.ts + + + - Test: 9 .SKILL.md files exist in server/src/skills/content/ with correct names + - Test: Each SKILL.md has valid frontmatter with name and description fields + - Test: fetchLocalNexusContent reads all 9 files, upserts skills, caches versions + - Test: fetchAllSources with local-nexus-content source returns fetched=9 + - Test: Duplicate fetch (restart) does not create duplicate rows (idempotency) + - Test: Missing directory returns 0 fetched, no error + + + 1. Create directory `server/src/skills/content/`. + + 2. Author 9 SKILL.md files, one per content type. Each file MUST have: + - YAML frontmatter with single-line `name` and single-line `description` (parseSkillFrontmatter uses single-line regex — do NOT use YAML block scalars like `>` or `|`) + - H1 heading matching the content type + - "Usage" section with the exact `POST /api/content-jobs` payload: `jobType` value and `input` fields + - "Output" section describing what the job returns + - Keep each file under 40 lines — these are agent-consumable instructions, not documentation + + Skill files and their jobType values: + - diagram.SKILL.md — jobType: "diagram", input: { description, type? } + - icon-set.SKILL.md — jobType: "icon-set", input: { description, style?, count? } + - theme-palette.SKILL.md — jobType: "theme-palette", input: { seedColor, name? } + - wallpaper.SKILL.md — jobType: "wallpaper", input: { description, resolution? } + - social-post.SKILL.md — jobType: "social-post", input: { topic, platform?, includeHashtags? } + - convert.SKILL.md — jobType: "convert", input: { sourceAssetId, targetFormat } + - pdf-document.SKILL.md — jobType: "pdf-document", input: { content, docType? } + - brand-kit.SKILL.md — jobType: "brand-kit", input: { companyName, description, seedColor? } + - presentation.SKILL.md — jobType: "presentation", input: { topic, slideCount?, style? } + + 3. Extend `SkillSourceConfig` type in skill-registry-fetcher.ts to be a discriminated union: + ```typescript + export type SkillSourceConfig = + | { id: string; type: "anthropic-marketplace"; owner: string; repo: string; ref: string; label: string } + | { id: string; type: "github-tree"; owner: string; repo: string; ref: string; label: string } + | { id: string; type: "local-nexus-content"; dir: string; label: string }; + ``` + + 4. Add `fetchLocalNexusContent` function (NOT exported — private like the other fetch handlers): + - Reads `source.dir` with `readdir({ withFileTypes: true })` + - Filters for `.SKILL.md` suffix + - For each file: derives `slug` from filename, `skillId` = `${source.id}/${slug}` + - Reads file content, computes SHA-1 content hash + - Calls `upsertSkill`, `cacheSkillVersion`, `upsertCommunityRatingsStub` (same pattern as fetchGitHubTree) + - Wraps `readdir` in try/catch — returns 0 if dir missing + - Uses `readdir` and `readFile` from `node:fs/promises` (already imported) + + 5. Add `"local-nexus-content"` branch in `fetchAllSources`: + ```typescript + } else if (source.type === "local-nexus-content") { + fetched += await fetchLocalNexusContent(source, db); + } + ``` + + 6. Add local source to `BUILT_IN_SOURCES` array: + ```typescript + { + id: "nexus-content", + type: "local-nexus-content", + dir: path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "skills", "content"), + label: "Nexus Content Tools", + }, + ``` + Add `import { fileURLToPath } from "node:url";` if not already present. + + 7. Write test file `server/src/__tests__/skill-registry-content-skills.test.ts`: + - Use vitest + temp directory pattern from existing skill-registry-fetch.test.ts + - Test that all 9 SKILL.md files exist and have parseable frontmatter + - Test fetchLocalNexusContent with a temp dir containing mock SKILL.md files + - Test idempotency (calling twice does not duplicate rows) + - Test missing directory returns 0 + - Test Creative group members (see Task 2 — leave placeholder describe block) + + + cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30 + + + - grep -r "jobType" server/src/skills/content/*.SKILL.md | wc -l returns 9 + - grep "local-nexus-content" server/src/services/skill-registry-fetcher.ts returns matches + - grep "nexus-content" server/src/services/skill-registry-fetcher.ts returns match in BUILT_IN_SOURCES + - ls server/src/skills/content/*.SKILL.md | wc -l returns 9 + + 9 SKILL.md files authored with correct frontmatter, local-nexus-content source type added to fetcher, all tests pass + + + + Task 2: Seed Creative group members and wire startup sequence + + server/src/services/skill-registry-db.ts + server/src/index.ts + server/src/__tests__/skill-registry-content-skills.test.ts + + + server/src/services/skill-registry-db.ts + server/src/index.ts + server/src/__tests__/skill-registry-content-skills.test.ts + + + 1. In `skill-registry-db.ts`, add a `seedCreativeGroupMembers` function (exported for testing): + ```typescript + const NEXUS_CONTENT_SKILL_IDS = [ + "nexus-content/diagram", + "nexus-content/icon-set", + "nexus-content/theme-palette", + "nexus-content/wallpaper", + "nexus-content/social-post", + "nexus-content/convert", + "nexus-content/pdf-document", + "nexus-content/brand-kit", + "nexus-content/presentation", + ] as const; + + export async function seedCreativeGroupMembers(client: LibSQLClient): Promise { + const now = Date.now(); + for (const skillId of NEXUS_CONTENT_SKILL_IDS) { + await client.execute({ + sql: `INSERT OR IGNORE INTO skill_group_members (group_id, skill_id, added_at) VALUES (?, ?, ?)`, + args: ["builtin/creative", skillId, now], + }); + } + } + ``` + IMPORTANT: Do NOT call seedCreativeGroupMembers inside getSkillRegistryDb(). The skills must be registered first (via fetchAll), then group members seeded. This is wired in index.ts startup. + + 2. In `server/src/index.ts`, modify the existing skill registry init block (lines ~617-626) to also call fetchAll and seed Creative group members. The sequence must be: + a. `getSkillRegistryDb()` — creates tables + builtin group rows + b. `skillRegistryService().fetchAll()` — registers local skills (+ remote) + c. `seedCreativeGroupMembers(client)` — adds skill IDs to builtin/creative group + + Updated block: + ```typescript + // [nexus] Initialize skill registry, seed content skills, and populate Creative group + void (async () => { + try { + const { getSkillRegistryDb } = await import("./services/skill-registry-db.js"); + const db = await getSkillRegistryDb(); + const { skillRegistryService } = await import("./services/skill-registry.js"); + await skillRegistryService().fetchAll(); + // Seed Creative group members AFTER skills are registered + const { seedCreativeGroupMembers } = await import("./services/skill-registry-db.js"); + // getSkillRegistryDb uses libSQL client internally — get the raw client for SQL + // Use a direct import of createClient since we need the raw client for INSERT OR IGNORE + const { _getClient } = await import("./services/skill-registry-db.js"); + await seedCreativeGroupMembers(_getClient()); + logger.info("skill registry initialized, content skills seeded, Creative group populated"); + } catch (err) { + logger.error({ err }, "skill registry init failed"); + } + })(); + ``` + + ALTERNATIVELY (simpler): Export a helper from skill-registry-db.ts that wraps the client access: + ```typescript + export async function seedCreativeGroupMembersFromDb(): Promise { + // Re-use the existing singleton client from getSkillRegistryDb + const db = await getSkillRegistryDb(); + // Access the raw client via drizzle's session + // ... or just use db.run with raw SQL + ``` + + The simplest approach: expose the raw `_client` from skill-registry-db.ts (already has the singleton pattern), or use `db.run(sql\`INSERT OR IGNORE...\`)` with drizzle's raw SQL. + + Check how `seedBuiltinGroups` accesses the client — it receives `client: LibSQLClient` directly inside `getSkillRegistryDb()`. For the external call, either: + - Export a `getClient()` function that returns the singleton LibSQLClient + - Or have `seedCreativeGroupMembers` accept the drizzle db and use `db.run()` + + Choose the approach that requires minimal changes. The `_db` singleton is already module-scoped in skill-registry-db.ts — adding an exported `getRawClient()` that returns the `_client` singleton is the cleanest path. + + 3. CRITICAL ORDERING: The Creative group seeding MUST happen AFTER fetchAll completes, and BEFORE the pendingSkillGroups reconciler runs. Since both are fire-and-forget async blocks, merge them into a single sequential block: + - Move the pendingSkillGroups reconciler (lines ~628-659) inside the skill registry init block, after seedCreativeGroupMembers + - This guarantees: DB init -> fetchAll -> seed group members -> reconcile pending groups + + 4. Add tests to the placeholder describe block in skill-registry-content-skills.test.ts: + - Test: After fetchAll + seedCreativeGroupMembers, querying skill_group_members for "builtin/creative" returns 9 rows + - Test: Content skills appear in skillRegistryService().list() result + - Test: A content skill can be installed to a temp agent dir via svc.install() + + 5. Run `cd /opt/nexus/server && npx tsc --noEmit` to verify no type errors. + + + cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts --reporter=verbose 2>&1 | tail -30 && npx tsc --noEmit 2>&1 | tail -20 + + + - grep "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts returns matches + - grep "fetchAll" server/src/index.ts returns match in the skill registry init block + - grep "seedCreativeGroupMembers" server/src/index.ts returns match + - grep "builtin/creative" server/src/__tests__/skill-registry-content-skills.test.ts returns matches + - npx tsc --noEmit exits 0 + + Creative group has 9 content skill members after startup, pendingSkillGroups reconciler runs after group is populated, all tests pass, tsc clean + + + + + +1. `ls server/src/skills/content/*.SKILL.md | wc -l` returns 9 +2. `cd /opt/nexus/server && npx vitest run src/__tests__/skill-registry-content-skills.test.ts` — all tests pass +3. `cd /opt/nexus/server && npx tsc --noEmit` — no type errors +4. `grep -c "local-nexus-content" server/src/services/skill-registry-fetcher.ts` returns >= 2 (type + BUILT_IN_SOURCES) +5. `grep -c "seedCreativeGroupMembers" server/src/services/skill-registry-db.ts` returns >= 1 +6. `grep "fetchAll\|seedCreativeGroupMembers" server/src/index.ts` shows both in the startup block + + + +- 9 SKILL.md files exist with valid frontmatter and jobType documentation +- Local-nexus-content source type registered and functional +- Creative group populated with all 9 content skill IDs +- Startup sequence correct: DB init -> fetchAll -> seed group -> reconcile pending +- All unit tests pass, tsc clean + + + +After completion, create `.planning/phases/45-content-as-skills/45-01-SUMMARY.md` + diff --git a/.planning/phases/45-content-as-skills/45-01-SUMMARY.md b/.planning/phases/45-content-as-skills/45-01-SUMMARY.md new file mode 100644 index 00000000..43714bc1 --- /dev/null +++ b/.planning/phases/45-content-as-skills/45-01-SUMMARY.md @@ -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` and `Extract` 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 diff --git a/.planning/phases/45-content-as-skills/45-CONTEXT.md b/.planning/phases/45-content-as-skills/45-CONTEXT.md new file mode 100644 index 00000000..59715552 --- /dev/null +++ b/.planning/phases/45-content-as-skills/45-CONTEXT.md @@ -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) + + +## 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 + + + + +## 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. + + + + +## Existing Code Insights + +Codebase context will be gathered during plan-phase research. + + + + +## Specific Ideas + +No specific requirements — discuss phase skipped. Refer to ROADMAP phase description and success criteria. + + + + +## Deferred Ideas + +None — discuss phase skipped. + + diff --git a/.planning/phases/45-content-as-skills/45-RESEARCH.md b/.planning/phases/45-content-as-skills/45-RESEARCH.md new file mode 100644 index 00000000..4bc04552 --- /dev/null +++ b/.planning/phases/45-content-as-skills/45-RESEARCH.md @@ -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 (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. + + +--- + + +## 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 | + + +--- + +## 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, + db: SkillRegistryDb, +): Promise { + 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 { + 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, + db: SkillRegistryDb, +): Promise { + let entries: Awaited>; + 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 { + 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///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) diff --git a/server/src/__tests__/skill-registry-content-skills.test.ts b/server/src/__tests__/skill-registry-content-skills.test.ts new file mode 100644 index 00000000..ac614f40 --- /dev/null +++ b/server/src/__tests__/skill-registry-content-skills.test.ts @@ -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); + }); +}); diff --git a/server/src/__tests__/skill-registry-fetch.test.ts b/server/src/__tests__/skill-registry-fetch.test.ts index bf10c3bd..8ef03b5f 100644 --- a/server/src/__tests__/skill-registry-fetch.test.ts +++ b/server/src/__tests__/skill-registry-fetch.test.ts @@ -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(); + } } }); }); diff --git a/server/src/index.ts b/server/src/index.ts index d9957d79..ec29185a 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -614,19 +614,21 @@ export async function startServer(): Promise { 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"); diff --git a/server/src/routes/convert.ts b/server/src/routes/convert.ts index e0cfbfed..fe1dd32c 100644 --- a/server/src/routes/convert.ts +++ b/server/src/routes/convert.ts @@ -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, diff --git a/server/src/services/skill-registry-db.ts b/server/src/services/skill-registry-db.ts index 6cefb498..a81a681f 100644 --- a/server/src/services/skill-registry-db.ts +++ b/server/src/services/skill-registry-db.ts @@ -8,6 +8,7 @@ import { resolveSkillRegistryDbPath } from "../home-paths.js"; export type SkillRegistryDb = ReturnType>; 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 { 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 { 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 { + 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; } diff --git a/server/src/services/skill-registry-fetcher.ts b/server/src/services/skill-registry-fetcher.ts index 5e01fc70..9ba244d9 100644 --- a/server/src/services/skill-registry-fetcher.ts +++ b/server/src/services/skill-registry-fetcher.ts @@ -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, db: SkillRegistryDb, ): Promise { const marketplaceUrl = resolveRawGitHubUrl( @@ -308,7 +311,7 @@ async function fetchAnthropicMarketplace( } async function fetchGitHubTree( - source: SkillSourceConfig, + source: Extract, db: SkillRegistryDb, ): Promise { 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, + db: SkillRegistryDb, +): Promise { + 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); diff --git a/server/src/skills/content/brand-kit.SKILL.md b/server/src/skills/content/brand-kit.SKILL.md new file mode 100644 index 00000000..7b570931 --- /dev/null +++ b/server/src/skills/content/brand-kit.SKILL.md @@ -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 diff --git a/server/src/skills/content/convert.SKILL.md b/server/src/skills/content/convert.SKILL.md new file mode 100644 index 00000000..697855f4 --- /dev/null +++ b/server/src/skills/content/convert.SKILL.md @@ -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` diff --git a/server/src/skills/content/diagram.SKILL.md b/server/src/skills/content/diagram.SKILL.md new file mode 100644 index 00000000..5e4fe08a --- /dev/null +++ b/server/src/skills/content/diagram.SKILL.md @@ -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 diff --git a/server/src/skills/content/icon-set.SKILL.md b/server/src/skills/content/icon-set.SKILL.md new file mode 100644 index 00000000..b0c2d35f --- /dev/null +++ b/server/src/skills/content/icon-set.SKILL.md @@ -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 diff --git a/server/src/skills/content/pdf-document.SKILL.md b/server/src/skills/content/pdf-document.SKILL.md new file mode 100644 index 00000000..1936697f --- /dev/null +++ b/server/src/skills/content/pdf-document.SKILL.md @@ -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 diff --git a/server/src/skills/content/presentation.SKILL.md b/server/src/skills/content/presentation.SKILL.md new file mode 100644 index 00000000..006cf023 --- /dev/null +++ b/server/src/skills/content/presentation.SKILL.md @@ -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 diff --git a/server/src/skills/content/social-post.SKILL.md b/server/src/skills/content/social-post.SKILL.md new file mode 100644 index 00000000..823aa8d9 --- /dev/null +++ b/server/src/skills/content/social-post.SKILL.md @@ -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 diff --git a/server/src/skills/content/theme-palette.SKILL.md b/server/src/skills/content/theme-palette.SKILL.md new file mode 100644 index 00000000..ecebba50 --- /dev/null +++ b/server/src/skills/content/theme-palette.SKILL.md @@ -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 diff --git a/server/src/skills/content/wallpaper.SKILL.md b/server/src/skills/content/wallpaper.SKILL.md new file mode 100644 index 00000000..ed5f4784 --- /dev/null +++ b/server/src/skills/content/wallpaper.SKILL.md @@ -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 diff --git a/ui/src/pages/ContentStudio.tsx b/ui/src/pages/ContentStudio.tsx index 9147aad3..5205e03a 100644 --- a/ui/src/pages/ContentStudio.tsx +++ b/ui/src/pages/ContentStudio.tsx @@ -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 (
@@ -57,17 +71,24 @@ export function ContentStudio() { {companyId ? (
{ + value={seedColor} + onChange={setSeedColor} + /> + + {themeBundle && ( <> - ).palette as Array>} /> - ).palette as Array>} /> - ).exports as Record} /> + + + { setShowApplyDialog(false); }} + onCancel={() => setShowApplyDialog(false)} /> )}