genoma-evolution 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.brv/.obsidian/app.json +1 -0
- package/.brv/.obsidian/appearance.json +1 -0
- package/.brv/.obsidian/core-plugins.json +33 -0
- package/.brv/.obsidian/graph.json +22 -0
- package/.brv/.obsidian/workspace.json +195 -0
- package/.brv/Sin ti/314/201tulo 1.canvas" +1 -0
- package/.brv/Sin ti/314/201tulo 2.canvas" +1 -0
- package/.brv/Sin ti/314/201tulo.canvas" +1 -0
- package/.brv/_queue_status.json +1 -0
- package/.brv/config.json +5 -0
- package/.brv/context-tree/_index.md +60 -0
- package/.brv/context-tree/_manifest.json +165 -0
- package/.brv/context-tree/backend/_index.md +24 -0
- package/.brv/context-tree/backend/backend/_index.md +40 -0
- package/.brv/context-tree/backend/backend/init.abstract.md +0 -0
- package/.brv/context-tree/backend/backend/init.md +27 -0
- package/.brv/context-tree/backend/backend/init.overview.md +29 -0
- package/.brv/context-tree/backend/backend/job_tracker.abstract.md +1 -0
- package/.brv/context-tree/backend/backend/job_tracker.md +273 -0
- package/.brv/context-tree/backend/backend/job_tracker.overview.md +31 -0
- package/.brv/context-tree/backend/backend/main.abstract.md +0 -0
- package/.brv/context-tree/backend/backend/main.md +1292 -0
- package/.brv/context-tree/backend/backend/main.overview.md +30 -0
- package/.brv/context-tree/backend/backend/requirements.abstract.md +1 -0
- package/.brv/context-tree/backend/backend/requirements.md +37 -0
- package/.brv/context-tree/backend/backend/requirements.overview.md +28 -0
- package/.brv/context-tree/docs/_index.md +37 -0
- package/.brv/context-tree/docs/api/_index.md +54 -0
- package/.brv/context-tree/docs/api/context.md +11 -0
- package/.brv/context-tree/docs/api/hermes_api_openapi_specification.abstract.md +0 -0
- package/.brv/context-tree/docs/api/hermes_api_openapi_specification.md +468 -0
- package/.brv/context-tree/docs/api/hermes_api_openapi_specification.overview.md +44 -0
- package/.brv/context-tree/frontend/_index.md +48 -0
- package/.brv/context-tree/frontend/hermes_dashboard/_index.md +31 -0
- package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.abstract.md +0 -0
- package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.md +41 -0
- package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.overview.md +34 -0
- package/.brv/context-tree/frontend/src/_index.md +53 -0
- package/.brv/context-tree/frontend/src/components/_index.md +52 -0
- package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.abstract.md +0 -0
- package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.md +161 -0
- package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.overview.md +32 -0
- package/.brv/context-tree/frontend/src/context.md +10 -0
- package/.brv/context-tree/frontend/src/functioncallingpage.abstract.md +0 -0
- package/.brv/context-tree/frontend/src/functioncallingpage.md +34 -0
- package/.brv/context-tree/frontend/src/functioncallingpage.overview.md +26 -0
- package/.brv/context-tree/frontend/src/lib/_index.md +48 -0
- package/.brv/context-tree/frontend/src/lib/api_client_library.abstract.md +1 -0
- package/.brv/context-tree/frontend/src/lib/api_client_library.md +403 -0
- package/.brv/context-tree/frontend/src/lib/api_client_library.overview.md +69 -0
- package/.brv/context-tree/frontend/src/page.abstract.md +0 -0
- package/.brv/context-tree/frontend/src/page.md +103 -0
- package/.brv/context-tree/frontend/src/page.overview.md +7 -0
- package/.brv/context-tree/frontend/src/settingspage.abstract.md +0 -0
- package/.brv/context-tree/frontend/src/settingspage.md +124 -0
- package/.brv/context-tree/frontend/src/settingspage.overview.md +34 -0
- package/.brv/context-tree/frontend/src/sidebar.abstract.md +0 -0
- package/.brv/context-tree/frontend/src/sidebar.md +170 -0
- package/.brv/context-tree/frontend/src/sidebar.overview.md +25 -0
- package/.brv/context-tree/meta/_index.md +24 -0
- package/.brv/context-tree/meta/curation_context/_index.md +24 -0
- package/.brv/context-tree/meta/curation_context/empty_context.abstract.md +4 -0
- package/.brv/context-tree/meta/curation_context/empty_context.md +35 -0
- package/.brv/context-tree/meta/curation_context/empty_context.overview.md +20 -0
- package/.brv/dream-log/drm-1777341062653.json +33 -0
- package/.brv/dream-state.json +8 -0
- package/.brv/dream.lock +0 -0
- package/.brv/review-backups/docs/api/hermes_api_openapi_specification.md +468 -0
- package/.claude/settings.local.json +7 -0
- package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/app.json +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/appearance.json +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/core-plugins.json +33 -0
- package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/graph.json +22 -0
- package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/workspace.json +195 -0
- package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo 1.canvas" +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo 2.canvas" +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo.canvas" +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/_queue_status.json +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/config.json +5 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/_index.md +60 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/_manifest.json +165 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/_index.md +24 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/_index.md +40 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.md +27 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.overview.md +29 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.abstract.md +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.md +273 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.overview.md +31 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.md +1292 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.overview.md +30 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.abstract.md +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.md +37 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.overview.md +28 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/_index.md +37 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/_index.md +54 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/context.md +11 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.md +468 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.overview.md +44 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/_index.md +48 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/_index.md +31 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.md +41 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.overview.md +34 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/_index.md +53 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/_index.md +52 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.md +161 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.overview.md +32 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/context.md +10 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.md +34 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.overview.md +26 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/_index.md +48 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.abstract.md +1 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.md +403 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.overview.md +69 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.md +103 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.overview.md +7 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.md +124 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.overview.md +34 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.abstract.md +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.md +170 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.overview.md +25 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/_index.md +24 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/_index.md +24 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.abstract.md +4 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.md +35 -0
- package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.overview.md +20 -0
- package/.claude/worktrees/phase-2-mcp/.brv/dream-log/drm-1777341062653.json +33 -0
- package/.claude/worktrees/phase-2-mcp/.brv/dream-state.json +8 -0
- package/.claude/worktrees/phase-2-mcp/.brv/dream.lock +0 -0
- package/.claude/worktrees/phase-2-mcp/.brv/review-backups/docs/api/hermes_api_openapi_specification.md +468 -0
- package/.claude/worktrees/phase-2-mcp/.claude/settings.local.json +13 -0
- package/.claude/worktrees/phase-2-mcp/.kilocode/package-lock.json +378 -0
- package/.claude/worktrees/phase-2-mcp/.kilocode/package.json +5 -0
- package/.claude/worktrees/phase-2-mcp/AGENTS.md +5 -0
- package/.claude/worktrees/phase-2-mcp/CLAUDE.md +29 -0
- package/.claude/worktrees/phase-2-mcp/QA_AUDIT_PLAN.md +156 -0
- package/.claude/worktrees/phase-2-mcp/README.md +316 -0
- package/.claude/worktrees/phase-2-mcp/agent-agnostic-evolution-dashboard.md +405 -0
- package/.claude/worktrees/phase-2-mcp/backend/__init__.py +0 -0
- package/.claude/worktrees/phase-2-mcp/backend/collectors/__init__.py +0 -0
- package/.claude/worktrees/phase-2-mcp/backend/collectors/claude_code_collector.py +277 -0
- package/.claude/worktrees/phase-2-mcp/backend/collectors/hermes_collector.py +68 -0
- package/.claude/worktrees/phase-2-mcp/backend/curator.py +512 -0
- package/.claude/worktrees/phase-2-mcp/backend/eval/__init__.py +19 -0
- package/.claude/worktrees/phase-2-mcp/backend/eval/engine.py +116 -0
- package/.claude/worktrees/phase-2-mcp/backend/eval/scorers.py +201 -0
- package/.claude/worktrees/phase-2-mcp/backend/generate_dataset.py +86 -0
- package/.claude/worktrees/phase-2-mcp/backend/job_tracker.py +232 -0
- package/.claude/worktrees/phase-2-mcp/backend/main.py +1746 -0
- package/.claude/worktrees/phase-2-mcp/backend/mcp_server.py +250 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/__init__.py +24 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/cycle_orchestrator.py +270 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/delta_validator.py +191 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/dspy_compiler.py +315 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/gepa_strategist.py +213 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/models.py +260 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/skill_deployer.py +195 -0
- package/.claude/worktrees/phase-2-mcp/backend/promethean/trace_ingestion.py +142 -0
- package/.claude/worktrees/phase-2-mcp/backend/requirements.txt +6 -0
- package/.claude/worktrees/phase-2-mcp/backend/sdd_evolve.py +459 -0
- package/.claude/worktrees/phase-2-mcp/backend/skill_detector.py +227 -0
- package/.claude/worktrees/phase-2-mcp/backend/skill_registry.py +289 -0
- package/.claude/worktrees/phase-2-mcp/backend/storage/__init__.py +5 -0
- package/.claude/worktrees/phase-2-mcp/backend/storage/run_store.py +393 -0
- package/.claude/worktrees/phase-2-mcp/backend/storage/schema.sql +99 -0
- package/.claude/worktrees/phase-2-mcp/backend/validate_evolution.py +267 -0
- package/.claude/worktrees/phase-2-mcp/components.json +28 -0
- package/.claude/worktrees/phase-2-mcp/docs/api/hermes-api.openapi.yaml +438 -0
- package/.claude/worktrees/phase-2-mcp/docs/hero.svg +148 -0
- package/.claude/worktrees/phase-2-mcp/eslint.config.mjs +18 -0
- package/.claude/worktrees/phase-2-mcp/install.sh +245 -0
- package/.claude/worktrees/phase-2-mcp/next-env.d.ts +6 -0
- package/.claude/worktrees/phase-2-mcp/next.config.ts +32 -0
- package/.claude/worktrees/phase-2-mcp/package-lock.json +11936 -0
- package/.claude/worktrees/phase-2-mcp/package.json +41 -0
- package/.claude/worktrees/phase-2-mcp/pnpm-workspace.yaml +4 -0
- package/.claude/worktrees/phase-2-mcp/postcss.config.mjs +7 -0
- package/.claude/worktrees/phase-2-mcp/public/file.svg +1 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Bold.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Heavy.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Medium.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Regular.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Semibold.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Bold.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Heavy.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Medium.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Regular.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Semibold.otf +0 -0
- package/.claude/worktrees/phase-2-mcp/public/globe.svg +1 -0
- package/.claude/worktrees/phase-2-mcp/public/next.svg +1 -0
- package/.claude/worktrees/phase-2-mcp/public/theme-preview.html +257 -0
- package/.claude/worktrees/phase-2-mcp/public/vercel.svg +1 -0
- package/.claude/worktrees/phase-2-mcp/public/window.svg +1 -0
- package/.claude/worktrees/phase-2-mcp/run.sh +26 -0
- package/.claude/worktrees/phase-2-mcp/skills-lock.json +10 -0
- package/.claude/worktrees/phase-2-mcp/specs/event-schema.md +223 -0
- package/.claude/worktrees/phase-2-mcp/specs/examples/run.jsonl +3 -0
- package/.claude/worktrees/phase-2-mcp/src/app/api/[...path]/route.ts +55 -0
- package/.claude/worktrees/phase-2-mcp/src/app/api/auth/token/route.ts +22 -0
- package/.claude/worktrees/phase-2-mcp/src/app/evolution/page.tsx +589 -0
- package/.claude/worktrees/phase-2-mcp/src/app/favicon.ico +0 -0
- package/.claude/worktrees/phase-2-mcp/src/app/globals.css +321 -0
- package/.claude/worktrees/phase-2-mcp/src/app/layout.tsx +63 -0
- package/.claude/worktrees/phase-2-mcp/src/app/page.tsx +70 -0
- package/.claude/worktrees/phase-2-mcp/src/app/skills/page.tsx +369 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ApiConfigCard.tsx +199 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.css +1 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.d.ts +1 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.jsx +1 -0
- package/.claude/worktrees/phase-2-mcp/src/components/CoreLoopToggle.tsx +111 -0
- package/.claude/worktrees/phase-2-mcp/src/components/EnvironmentStatus.tsx +176 -0
- package/.claude/worktrees/phase-2-mcp/src/components/EvolutionBackground.tsx +1 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ReactQueryProvider.tsx +24 -0
- package/.claude/worktrees/phase-2-mcp/src/components/Sidebar.tsx +247 -0
- package/.claude/worktrees/phase-2-mcp/src/components/SkillDiffViewer.tsx +154 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ThemeAwareBackground.tsx +67 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ThemeToggle.tsx +54 -0
- package/.claude/worktrees/phase-2-mcp/src/components/WelcomeHero.tsx +77 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/ClickSpark.tsx +116 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/CountUp.tsx +98 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/DarkSelect.tsx +95 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/DecryptedText.tsx +161 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/ElectricBorder.tsx +184 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/GlitchText.tsx +34 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/ShinyText.tsx +55 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/SpotlightCard.tsx +42 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/TextType.tsx +95 -0
- package/.claude/worktrees/phase-2-mcp/src/components/bits/index.ts +9 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/CuratorPage.tsx +632 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/DatasetPage.tsx +271 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/EvolutionPage.tsx +676 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/FunctionCallingPage.tsx +1 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/LogsPage.tsx +272 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/MetricsPage.tsx +246 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/OverviewPage.tsx +420 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/SettingsPage.tsx +88 -0
- package/.claude/worktrees/phase-2-mcp/src/components/pages/SkillStudioPage.tsx +376 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/animated-theme-toggler.tsx +97 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/button.tsx +67 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/card.tsx +103 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/input.tsx +19 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/separator.tsx +28 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/sheet.tsx +147 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/sidebar.tsx +702 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/skeleton.tsx +13 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/theme-toggle.tsx +272 -0
- package/.claude/worktrees/phase-2-mcp/src/components/ui/tooltip.tsx +57 -0
- package/.claude/worktrees/phase-2-mcp/src/hooks/use-mobile.ts +19 -0
- package/.claude/worktrees/phase-2-mcp/src/lib/api.ts +455 -0
- package/.claude/worktrees/phase-2-mcp/src/lib/queryClient.ts +12 -0
- package/.claude/worktrees/phase-2-mcp/src/lib/utils.ts +6 -0
- package/.claude/worktrees/phase-2-mcp/stitch/agent_dashboard/DESIGN_SPEC.md +521 -0
- package/.claude/worktrees/phase-2-mcp/stitch/agent_dashboard/prototype.html +676 -0
- package/.claude/worktrees/phase-2-mcp/stitch/curator_workspace/code.html +448 -0
- package/.claude/worktrees/phase-2-mcp/stitch/curator_workspace/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/datasets/code.html +479 -0
- package/.claude/worktrees/phase-2-mcp/stitch/datasets/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/evolution_history/code.html +461 -0
- package/.claude/worktrees/phase-2-mcp/stitch/evolution_history/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/hermes_dashboard/DESIGN.md +192 -0
- package/.claude/worktrees/phase-2-mcp/stitch/hermes_dashboard/DESIGN_SPEC.md +455 -0
- package/.claude/worktrees/phase-2-mcp/stitch/hermes_overview/code.html +399 -0
- package/.claude/worktrees/phase-2-mcp/stitch/hermes_overview/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/live_logs/code.html +324 -0
- package/.claude/worktrees/phase-2-mcp/stitch/live_logs/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/skill_hub/code.html +596 -0
- package/.claude/worktrees/phase-2-mcp/stitch/skill_hub/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/system_metrics/code.html +527 -0
- package/.claude/worktrees/phase-2-mcp/stitch/system_metrics/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/stitch/system_settings/code.html +257 -0
- package/.claude/worktrees/phase-2-mcp/stitch/system_settings/screen.png +0 -0
- package/.claude/worktrees/phase-2-mcp/test_dashboard.py +201 -0
- package/.claude/worktrees/phase-2-mcp/tests/collectors/__init__.py +0 -0
- package/.claude/worktrees/phase-2-mcp/tests/collectors/fixtures/sample_session.jsonl +7 -0
- package/.claude/worktrees/phase-2-mcp/tests/collectors/test_claude_code_collector.py +171 -0
- package/.claude/worktrees/phase-2-mcp/tests/collectors/test_hermes_collector.py +167 -0
- package/.claude/worktrees/phase-2-mcp/tests/eval/test_engine.py +234 -0
- package/.claude/worktrees/phase-2-mcp/tests/eval/test_scorers.py +249 -0
- package/.claude/worktrees/phase-2-mcp/tests/storage/__init__.py +0 -0
- package/.claude/worktrees/phase-2-mcp/tests/storage/test_run_store.py +359 -0
- package/.claude/worktrees/phase-2-mcp/tests/test_curator.py +559 -0
- package/.claude/worktrees/phase-2-mcp/tests/test_mcp_server.py +114 -0
- package/.claude/worktrees/phase-2-mcp/tsconfig.json +34 -0
- package/.env.example +72 -0
- package/.kilocode/package-lock.json +378 -0
- package/.kilocode/package.json +5 -0
- package/AGENTS.md +5 -0
- package/CLAUDE.md +29 -0
- package/QA_AUDIT_PLAN.md +156 -0
- package/README.md +355 -0
- package/agent-agnostic-evolution-dashboard.md +405 -0
- package/backend/__init__.py +0 -0
- package/backend/collectors/__init__.py +0 -0
- package/backend/collectors/claude_code_collector.py +277 -0
- package/backend/collectors/hermes_collector.py +68 -0
- package/backend/curator.py +512 -0
- package/backend/eval/__init__.py +19 -0
- package/backend/eval/engine.py +116 -0
- package/backend/eval/scorers.py +201 -0
- package/backend/generate_dataset.py +86 -0
- package/backend/job_tracker.py +232 -0
- package/backend/main.py +1746 -0
- package/backend/mcp_server.py +250 -0
- package/backend/promethean/__init__.py +24 -0
- package/backend/promethean/cycle_orchestrator.py +270 -0
- package/backend/promethean/delta_validator.py +191 -0
- package/backend/promethean/dspy_compiler.py +315 -0
- package/backend/promethean/gepa_strategist.py +213 -0
- package/backend/promethean/models.py +260 -0
- package/backend/promethean/skill_deployer.py +195 -0
- package/backend/promethean/trace_ingestion.py +142 -0
- package/backend/requirements.txt +6 -0
- package/backend/sdd_evolve.py +459 -0
- package/backend/skill_detector.py +227 -0
- package/backend/skill_registry.py +289 -0
- package/backend/storage/__init__.py +5 -0
- package/backend/storage/run_store.py +393 -0
- package/backend/storage/schema.sql +99 -0
- package/backend/validate_evolution.py +267 -0
- package/bin/genoma.js +250 -0
- package/components.json +28 -0
- package/docs/api/hermes-api.openapi.yaml +438 -0
- package/docs/hero.svg +148 -0
- package/eslint.config.mjs +18 -0
- package/install.sh +245 -0
- package/next-env.d.ts +6 -0
- package/next.config.ts +32 -0
- package/package.json +46 -0
- package/pnpm-workspace.yaml +4 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/fonts/SF-Pro-Display-Bold.otf +0 -0
- package/public/fonts/SF-Pro-Display-Heavy.otf +0 -0
- package/public/fonts/SF-Pro-Display-Medium.otf +0 -0
- package/public/fonts/SF-Pro-Display-Regular.otf +0 -0
- package/public/fonts/SF-Pro-Display-Semibold.otf +0 -0
- package/public/fonts/SF-Pro-Text-Bold.otf +0 -0
- package/public/fonts/SF-Pro-Text-Heavy.otf +0 -0
- package/public/fonts/SF-Pro-Text-Medium.otf +0 -0
- package/public/fonts/SF-Pro-Text-Regular.otf +0 -0
- package/public/fonts/SF-Pro-Text-Semibold.otf +0 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/theme-preview.html +257 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/run.sh +26 -0
- package/scripts/postinstall.js +50 -0
- package/skills-lock.json +10 -0
- package/specs/event-schema.md +223 -0
- package/specs/examples/run.jsonl +3 -0
- package/src/app/api/[...path]/route.ts +55 -0
- package/src/app/api/auth/token/route.ts +22 -0
- package/src/app/evolution/page.tsx +589 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +321 -0
- package/src/app/layout.tsx +63 -0
- package/src/app/page.tsx +70 -0
- package/src/app/skills/page.tsx +369 -0
- package/src/components/ApiConfigCard.tsx +199 -0
- package/src/components/ColorBends.css +1 -0
- package/src/components/ColorBends.d.ts +1 -0
- package/src/components/ColorBends.jsx +1 -0
- package/src/components/CoreLoopToggle.tsx +111 -0
- package/src/components/EnvironmentStatus.tsx +176 -0
- package/src/components/EvolutionBackground.tsx +1 -0
- package/src/components/ReactQueryProvider.tsx +24 -0
- package/src/components/Sidebar.tsx +247 -0
- package/src/components/SkillDiffViewer.tsx +154 -0
- package/src/components/ThemeAwareBackground.tsx +67 -0
- package/src/components/ThemeToggle.tsx +54 -0
- package/src/components/WelcomeHero.tsx +77 -0
- package/src/components/bits/ClickSpark.tsx +116 -0
- package/src/components/bits/CountUp.tsx +98 -0
- package/src/components/bits/DarkSelect.tsx +95 -0
- package/src/components/bits/DecryptedText.tsx +161 -0
- package/src/components/bits/ElectricBorder.tsx +184 -0
- package/src/components/bits/GlitchText.tsx +34 -0
- package/src/components/bits/ShinyText.tsx +55 -0
- package/src/components/bits/SpotlightCard.tsx +42 -0
- package/src/components/bits/TextType.tsx +95 -0
- package/src/components/bits/index.ts +9 -0
- package/src/components/pages/CuratorPage.tsx +632 -0
- package/src/components/pages/DatasetPage.tsx +271 -0
- package/src/components/pages/EvolutionPage.tsx +676 -0
- package/src/components/pages/FunctionCallingPage.tsx +1 -0
- package/src/components/pages/LogsPage.tsx +272 -0
- package/src/components/pages/MetricsPage.tsx +246 -0
- package/src/components/pages/OverviewPage.tsx +420 -0
- package/src/components/pages/SettingsPage.tsx +88 -0
- package/src/components/pages/SkillStudioPage.tsx +376 -0
- package/src/components/ui/animated-theme-toggler.tsx +97 -0
- package/src/components/ui/button.tsx +67 -0
- package/src/components/ui/card.tsx +103 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +147 -0
- package/src/components/ui/sidebar.tsx +702 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/theme-toggle.tsx +272 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/api.ts +455 -0
- package/src/lib/queryClient.ts +12 -0
- package/src/lib/utils.ts +6 -0
- package/stitch/agent_dashboard/DESIGN_SPEC.md +521 -0
- package/stitch/agent_dashboard/prototype.html +676 -0
- package/stitch/curator_workspace/code.html +448 -0
- package/stitch/curator_workspace/screen.png +0 -0
- package/stitch/datasets/code.html +479 -0
- package/stitch/datasets/screen.png +0 -0
- package/stitch/evolution_history/code.html +461 -0
- package/stitch/evolution_history/screen.png +0 -0
- package/stitch/hermes_dashboard/DESIGN.md +192 -0
- package/stitch/hermes_dashboard/DESIGN_SPEC.md +455 -0
- package/stitch/hermes_overview/code.html +399 -0
- package/stitch/hermes_overview/screen.png +0 -0
- package/stitch/live_logs/code.html +324 -0
- package/stitch/live_logs/screen.png +0 -0
- package/stitch/skill_hub/code.html +596 -0
- package/stitch/skill_hub/screen.png +0 -0
- package/stitch/system_metrics/code.html +527 -0
- package/stitch/system_metrics/screen.png +0 -0
- package/stitch/system_settings/code.html +257 -0
- package/stitch/system_settings/screen.png +0 -0
- package/test_dashboard.py +201 -0
- package/tests/collectors/__init__.py +0 -0
- package/tests/collectors/fixtures/sample_session.jsonl +7 -0
- package/tests/collectors/test_claude_code_collector.py +171 -0
- package/tests/collectors/test_hermes_collector.py +167 -0
- package/tests/eval/test_engine.py +234 -0
- package/tests/eval/test_scorers.py +249 -0
- package/tests/storage/__init__.py +0 -0
- package/tests/storage/test_run_store.py +359 -0
- package/tests/test_curator.py +559 -0
- package/tests/test_e2e_npm.py +621 -0
- package/tests/test_mcp_server.py +114 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: main
|
|
3
|
+
summary: ''
|
|
4
|
+
tags: []
|
|
5
|
+
related: []
|
|
6
|
+
keywords: []
|
|
7
|
+
createdAt: '2026-04-28T01:33:30.550Z'
|
|
8
|
+
updatedAt: '2026-04-28T01:33:30.550Z'
|
|
9
|
+
---
|
|
10
|
+
## Reason
|
|
11
|
+
Preserving complete backend/main.py from hermes-dashboard project
|
|
12
|
+
|
|
13
|
+
## Raw Concept
|
|
14
|
+
**Task:**
|
|
15
|
+
Preserve backend/main.py
|
|
16
|
+
|
|
17
|
+
**Files:**
|
|
18
|
+
- backend/main.py
|
|
19
|
+
|
|
20
|
+
**Timestamp:** 2026-04-28
|
|
21
|
+
|
|
22
|
+
**Patterns:**
|
|
23
|
+
- `class ConnectionManager` - Python class definition
|
|
24
|
+
- `class SkillInfo` - Python class definition
|
|
25
|
+
- `class EvolveRequest` - Python class definition
|
|
26
|
+
- `class MemoryEntry` - Python class definition
|
|
27
|
+
- `class RunMetrics` - Python class definition
|
|
28
|
+
- `class ToggleSkillRequest` - Python class definition
|
|
29
|
+
- `def _find_python` - Python function
|
|
30
|
+
- `def _normalize_metrics` - Python function
|
|
31
|
+
- `def _find_skill_file` - Python function
|
|
32
|
+
- `def _parse_skill_content` - Python function
|
|
33
|
+
|
|
34
|
+
## Narrative
|
|
35
|
+
### Structure
|
|
36
|
+
Python file with 6 classes and 4 functions
|
|
37
|
+
|
|
38
|
+
### Dependencies
|
|
39
|
+
Classes: ConnectionManager, SkillInfo, EvolveRequest, MemoryEntry, RunMetrics, ToggleSkillRequest
|
|
40
|
+
|
|
41
|
+
### Highlights
|
|
42
|
+
Source: hermes-dashboard/backend/main.py
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
"""Hermes Evolution Dashboard — FastAPI Backend
|
|
48
|
+
|
|
49
|
+
Bridges the Next.js frontend to the hermes-agent-self-evolution Python modules.
|
|
50
|
+
Exposes REST endpoints + WebSocket streaming for real-time evolution monitoring.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
import json
|
|
54
|
+
import os
|
|
55
|
+
import sys
|
|
56
|
+
import shutil
|
|
57
|
+
import asyncio
|
|
58
|
+
import re
|
|
59
|
+
import subprocess
|
|
60
|
+
from datetime import datetime
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
from typing import Optional
|
|
63
|
+
|
|
64
|
+
from dotenv import dotenv_values
|
|
65
|
+
|
|
66
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
|
67
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
68
|
+
from pydantic import BaseModel
|
|
69
|
+
|
|
70
|
+
# --- Hermes Function Calling integration ---
|
|
71
|
+
from typing import List, Dict, Any
|
|
72
|
+
import json as json_module
|
|
73
|
+
|
|
74
|
+
from .skill_registry import get_registry
|
|
75
|
+
|
|
76
|
+
from .job_tracker import tracker, EvolutionJob, JobStatus
|
|
77
|
+
|
|
78
|
+
# ── Paths ──────────────────────────────────────────────────────────
|
|
79
|
+
HERMES_REPO = Path(os.environ.get("HERMES_AGENT_REPO", Path.home() / ".hermes" / "hermes-agent"))
|
|
80
|
+
|
|
81
|
+
# Evolution dir: env var > sibling > home
|
|
82
|
+
_env_ev = os.environ.get("EVOLUTION_DIR", "")
|
|
83
|
+
if _env_ev:
|
|
84
|
+
EVOLUTION_DIR = Path(_env_ev)
|
|
85
|
+
else:
|
|
86
|
+
_sibling = Path(__file__).parent.parent.parent / "hermes-agent-self-evolution"
|
|
87
|
+
_home = Path.home() / ".hermes" / "hermes-agent-self-evolution"
|
|
88
|
+
EVOLUTION_DIR = _sibling if _sibling.exists() else _home
|
|
89
|
+
|
|
90
|
+
SKILLS_DIR = HERMES_REPO / "skills"
|
|
91
|
+
MEMORY_DIR = Path.home() / ".hermes" / "memory"
|
|
92
|
+
|
|
93
|
+
# ── Skill Detector ────────────────────────────────────────────────
|
|
94
|
+
skill_registry = get_registry()
|
|
95
|
+
SESSIONS_DIR = Path.home() / ".hermes" / "sessions"
|
|
96
|
+
|
|
97
|
+
# ── Python executable ──────────────────────────────────────────────
|
|
98
|
+
def _find_python() -> str:
|
|
99
|
+
"""Find the best Python executable (supports evolution modules)."""
|
|
100
|
+
# 1. Env var override
|
|
101
|
+
env_py = os.environ.get("PYTHON", "")
|
|
102
|
+
if env_py and Path(env_py).exists():
|
|
103
|
+
return env_py
|
|
104
|
+
|
|
105
|
+
# 2. System python3.12 (where dspy is installed)
|
|
106
|
+
for candidate in ["/usr/bin/python3", "python3.12", "python3.11", "python3"]:
|
|
107
|
+
try:
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
[candidate, "-c", "import dspy; print('ok')"],
|
|
110
|
+
capture_output=True, timeout=5
|
|
111
|
+
)
|
|
112
|
+
if result.returncode == 0:
|
|
113
|
+
return candidate
|
|
114
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# 3. Fallback
|
|
118
|
+
return sys.executable
|
|
119
|
+
|
|
120
|
+
PYTHON_BIN = _find_python()
|
|
121
|
+
|
|
122
|
+
app = FastAPI(title="Hermes Evolution API", version="0.2.0")
|
|
123
|
+
|
|
124
|
+
app.add_middleware(
|
|
125
|
+
CORSMiddleware,
|
|
126
|
+
allow_origins=["http://localhost:3001", "http://localhost:3000", "http://127.0.0.1:3001"],
|
|
127
|
+
allow_credentials=True,
|
|
128
|
+
allow_methods=["*"],
|
|
129
|
+
allow_headers=["*"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# ── WebSocket manager ──────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
class ConnectionManager:
|
|
135
|
+
def __init__(self):
|
|
136
|
+
self.active: list[WebSocket] = []
|
|
137
|
+
|
|
138
|
+
async def connect(self, ws: WebSocket):
|
|
139
|
+
await ws.accept()
|
|
140
|
+
self.active.append(ws)
|
|
141
|
+
|
|
142
|
+
def disconnect(self, ws: WebSocket):
|
|
143
|
+
if ws in self.active:
|
|
144
|
+
self.active.remove(ws)
|
|
145
|
+
|
|
146
|
+
async def broadcast(self, message: dict):
|
|
147
|
+
for ws in self.active:
|
|
148
|
+
try:
|
|
149
|
+
await ws.send_json(message)
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
manager = ConnectionManager()
|
|
154
|
+
|
|
155
|
+
# ── Models ─────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
class SkillInfo(BaseModel):
|
|
158
|
+
name: str
|
|
159
|
+
path: str
|
|
160
|
+
description: str
|
|
161
|
+
size: int
|
|
162
|
+
last_modified: str
|
|
163
|
+
source: str = "skill" # "skill" or "description"
|
|
164
|
+
|
|
165
|
+
class EvolveRequest(BaseModel):
|
|
166
|
+
skill_name: str
|
|
167
|
+
iterations: int = 3
|
|
168
|
+
eval_source: str = "synthetic"
|
|
169
|
+
dataset_size: int = 10
|
|
170
|
+
|
|
171
|
+
class MemoryEntry(BaseModel):
|
|
172
|
+
key: str
|
|
173
|
+
value: str
|
|
174
|
+
source: str
|
|
175
|
+
timestamp: str
|
|
176
|
+
|
|
177
|
+
class RunMetrics(BaseModel):
|
|
178
|
+
skill_name: str
|
|
179
|
+
timestamp: str
|
|
180
|
+
baseline_score: float
|
|
181
|
+
evolved_score: float
|
|
182
|
+
improvement: float
|
|
183
|
+
elapsed_seconds: float
|
|
184
|
+
constraints_passed: bool
|
|
185
|
+
|
|
186
|
+
# ── Helpers ────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
def _normalize_metrics(raw: dict) -> dict:
|
|
189
|
+
"""Normalize metrics.json from different versions of evolve scripts.
|
|
190
|
+
|
|
191
|
+
Supports:
|
|
192
|
+
- evolve_skill.py format: skill_name, baseline_score, evolved_score, improvement
|
|
193
|
+
- evolve_now.py format: skill, original_size, evolved_size, diff_lines
|
|
194
|
+
"""
|
|
195
|
+
m = dict(raw) # shallow copy
|
|
196
|
+
|
|
197
|
+
# Normalize skill name key
|
|
198
|
+
if "skill" in m and "skill_name" not in m:
|
|
199
|
+
m["skill_name"] = m["skill"]
|
|
200
|
+
|
|
201
|
+
# Normalize scores
|
|
202
|
+
if "baseline_score" not in m:
|
|
203
|
+
if "original_size" in m and m["original_size"] > 0:
|
|
204
|
+
# Derive a pseudo-score from size reduction (lower = more concise = better)
|
|
205
|
+
ratio = m.get("evolved_size", m["original_size"]) / m["original_size"]
|
|
206
|
+
m["baseline_score"] = 0.5 # placeholder
|
|
207
|
+
m["evolved_score"] = min(1.0, 0.5 + (1 - ratio) * 0.3)
|
|
208
|
+
m["improvement"] = m["evolved_score"] - m["baseline_score"]
|
|
209
|
+
else:
|
|
210
|
+
m["baseline_score"] = 0.0
|
|
211
|
+
m["evolved_score"] = 0.0
|
|
212
|
+
m["improvement"] = 0.0
|
|
213
|
+
|
|
214
|
+
# Normalize constraints_passed
|
|
215
|
+
if "constraints_passed" not in m:
|
|
216
|
+
m["constraints_passed"] = m.get("diff_lines", 0) > 0
|
|
217
|
+
|
|
218
|
+
# Normalize elapsed
|
|
219
|
+
if "elapsed_seconds" not in m:
|
|
220
|
+
m["elapsed_seconds"] = 0.0
|
|
221
|
+
|
|
222
|
+
# Normalize iterations
|
|
223
|
+
if "iterations" not in m:
|
|
224
|
+
m["iterations"] = 3
|
|
225
|
+
|
|
226
|
+
return m
|
|
227
|
+
|
|
228
|
+
def _find_skill_file(skill_name: str) -> Optional[Path]:
|
|
229
|
+
"""Find a SKILL.md by name, searching recursively."""
|
|
230
|
+
if not SKILLS_DIR.exists():
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
# Direct path
|
|
234
|
+
direct = SKILLS_DIR / skill_name / "SKILL.md"
|
|
235
|
+
if direct.exists():
|
|
236
|
+
return direct
|
|
237
|
+
|
|
238
|
+
# Recursive search
|
|
239
|
+
for f in SKILLS_DIR.rglob("SKILL.md"):
|
|
240
|
+
if f.parent.name == skill_name:
|
|
241
|
+
return f
|
|
242
|
+
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
def _parse_skill_content(skill_file: Path) -> dict:
|
|
246
|
+
"""Parse a skill file into frontmatter + body."""
|
|
247
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
248
|
+
frontmatter = {}
|
|
249
|
+
body = content
|
|
250
|
+
|
|
251
|
+
if content.startswith("---"):
|
|
252
|
+
try:
|
|
253
|
+
import yaml
|
|
254
|
+
end = content.index("---", 3)
|
|
255
|
+
frontmatter = yaml.safe_load(content[3:end]) or {}
|
|
256
|
+
body = content[end + 3:].strip()
|
|
257
|
+
except Exception:
|
|
258
|
+
pass
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
"frontmatter": frontmatter,
|
|
262
|
+
"body": body,
|
|
263
|
+
"raw": content,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# ── SKILLS endpoints ───────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
class ToggleSkillRequest(BaseModel):
|
|
269
|
+
provider: str
|
|
270
|
+
skill_name: str
|
|
271
|
+
enabled: bool
|
|
272
|
+
|
|
273
|
+
@app.get("/api/skills/providers")
|
|
274
|
+
async def list_providers():
|
|
275
|
+
"""Lista todos los proveedores de skills detectados con sus estadísticas"""
|
|
276
|
+
try:
|
|
277
|
+
providers = skill_registry.get_providers()
|
|
278
|
+
return {"status": "ok", "providers": providers}
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
281
|
+
|
|
282
|
+
@app.get("/api/skills/provider/{provider_name}")
|
|
283
|
+
async def list_provider_skills(provider_name: str):
|
|
284
|
+
"""Lista todas las skills de un proveedor específico"""
|
|
285
|
+
try:
|
|
286
|
+
providers = skill_registry.get_providers()
|
|
287
|
+
for p in providers:
|
|
288
|
+
if p["name"] == provider_name:
|
|
289
|
+
return {"status": "ok", "provider": p}
|
|
290
|
+
raise HTTPException(status_code=404, detail=f"Proveedor '{provider_name}' no encontrado")
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
293
|
+
|
|
294
|
+
@app.get("/api/skills/refresh")
|
|
295
|
+
async def refresh_skills():
|
|
296
|
+
"""Fuerza un re-escaneo de skills (útil para desarrollo)"""
|
|
297
|
+
try:
|
|
298
|
+
global skill_registry
|
|
299
|
+
skill_registry = get_registry().rescan()
|
|
300
|
+
providers = skill_registry.get_providers()
|
|
301
|
+
return {"status": "ok", "providers": providers}
|
|
302
|
+
except Exception as e:
|
|
303
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
304
|
+
|
|
305
|
+
@app.post("/api/skills/toggle")
|
|
306
|
+
async def toggle_skill(request: ToggleSkillRequest):
|
|
307
|
+
"""Activa o desactiva una skill"""
|
|
308
|
+
try:
|
|
309
|
+
skill_registry.toggle_provider_skill(request.provider, request.skill_name, request.enabled)
|
|
310
|
+
return {"status": "ok", "enabled": request.enabled}
|
|
311
|
+
except Exception as e:
|
|
312
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
313
|
+
|
|
314
|
+
@app.delete("/api/skills/global/{skill_name}")
|
|
315
|
+
async def delete_global_skill(skill_name: str):
|
|
316
|
+
"""Elimina una skill GLOBALMENTE (de global_skills/ y todos los symlinks)."""
|
|
317
|
+
try:
|
|
318
|
+
registry = get_registry()
|
|
319
|
+
target_gid = None
|
|
320
|
+
target_skill = None
|
|
321
|
+
for gid, s in registry.global_skills.items():
|
|
322
|
+
if s.name == skill_name and not s.is_fork:
|
|
323
|
+
target_gid = gid
|
|
324
|
+
target_skill = s
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
if not target_skill:
|
|
328
|
+
raise HTTPException(status_code=404, detail="Skill global no encontrada o es un fork")
|
|
329
|
+
canonical = Path(target_skill.canonical_path)
|
|
330
|
+
|
|
331
|
+
# 1. Eliminar symlinks en todos los providers PRIMERO
|
|
332
|
+
for provider_name, src_dir in registry.PROVIDER_SOURCES.items():
|
|
333
|
+
if not src_dir.exists():
|
|
334
|
+
continue
|
|
335
|
+
link = src_dir / skill_name
|
|
336
|
+
if link.exists() and link.is_symlink():
|
|
337
|
+
try:
|
|
338
|
+
if link.resolve() == canonical:
|
|
339
|
+
link.unlink()
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
# 2. Eliminar directorio canónico
|
|
344
|
+
if canonical.exists() and canonical.is_dir():
|
|
345
|
+
shutil.rmtree(canonical)
|
|
346
|
+
|
|
347
|
+
keys_to_delete = [k for k, v in registry.provider_index.items() if v == target_gid]
|
|
348
|
+
for k in keys_to_delete:
|
|
349
|
+
del registry.provider_index[k]
|
|
350
|
+
del registry.global_skills[target_gid]
|
|
351
|
+
|
|
352
|
+
registry.save()
|
|
353
|
+
return {"status": "ok", "message": f"Skill '{skill_name}' eliminada globalmente"}
|
|
354
|
+
except HTTPException:
|
|
355
|
+
raise
|
|
356
|
+
except Exception as e:
|
|
357
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
358
|
+
|
|
359
|
+
@app.delete("/api/skills/{provider}/{skill_name}")
|
|
360
|
+
async def delete_skill(provider: str, skill_name: str):
|
|
361
|
+
"""Elimina una skill de un provider específico (desenlaza, pero preserva la skill global)."""
|
|
362
|
+
try:
|
|
363
|
+
registry = get_registry()
|
|
364
|
+
key = f"{provider}.{skill_name}"
|
|
365
|
+
if key not in registry.provider_index:
|
|
366
|
+
raise HTTPException(status_code=404, detail="Skill no encontrada para ese provider")
|
|
367
|
+
|
|
368
|
+
gid = registry.provider_index[key]
|
|
369
|
+
if gid not in registry.global_skills:
|
|
370
|
+
del registry.provider_index[key]
|
|
371
|
+
registry.save()
|
|
372
|
+
return {"status": "ok", "message": f"Enlace eliminado (skill global no existía)"}
|
|
373
|
+
|
|
374
|
+
skill = registry.global_skills[gid]
|
|
375
|
+
skill.providers = [p for p in skill.providers if p['name'] != provider]
|
|
376
|
+
del registry.provider_index[key]
|
|
377
|
+
registry.save()
|
|
378
|
+
return {"status": "ok", "message": f"Skill '{skill_name}' desenlazada de '{provider}'"}
|
|
379
|
+
except HTTPException:
|
|
380
|
+
raise
|
|
381
|
+
except Exception as e:
|
|
382
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
383
|
+
|
|
384
|
+
@app.get("/api/skills/{skill_name}")
|
|
385
|
+
async def get_skill(skill_name: str):
|
|
386
|
+
"""Get full skill content + metadata."""
|
|
387
|
+
skill_file = _find_skill_file(skill_name)
|
|
388
|
+
if not skill_file:
|
|
389
|
+
raise HTTPException(404, f"Skill '{skill_name}' not found")
|
|
390
|
+
|
|
391
|
+
parsed = _parse_skill_content(skill_file)
|
|
392
|
+
return {
|
|
393
|
+
"name": skill_name,
|
|
394
|
+
"path": str(skill_file.relative_to(HERMES_REPO)),
|
|
395
|
+
"frontmatter": parsed["frontmatter"],
|
|
396
|
+
"body": parsed["body"],
|
|
397
|
+
"raw": parsed["raw"],
|
|
398
|
+
"size": len(parsed["raw"]),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
@app.get("/api/evolution/runs")
|
|
402
|
+
async def list_evolution_runs():
|
|
403
|
+
"""List all evolution runs — from metrics.json AND job tracker."""
|
|
404
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
405
|
+
all_runs = []
|
|
406
|
+
seen_skills = set()
|
|
407
|
+
|
|
408
|
+
# 1. From metrics.json files (successful runs)
|
|
409
|
+
if output_dir.exists():
|
|
410
|
+
for skill_dir in output_dir.iterdir():
|
|
411
|
+
if skill_dir.is_dir() and skill_dir.name != "skills":
|
|
412
|
+
for run_dir in skill_dir.iterdir():
|
|
413
|
+
mf = run_dir / "metrics.json"
|
|
414
|
+
if mf.exists():
|
|
415
|
+
try:
|
|
416
|
+
data = _normalize_metrics(json.loads(mf.read_text()))
|
|
417
|
+
data.setdefault("run_dir", run_dir.name)
|
|
418
|
+
all_runs.append(data)
|
|
419
|
+
seen_skills.add(skill_dir.name)
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
# 2. From job tracker (completed/failed runs without metrics.json)
|
|
424
|
+
for job in tracker.get_all_jobs(limit=200):
|
|
425
|
+
if job.status in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
426
|
+
# Check if we already have metrics for this skill
|
|
427
|
+
has_metrics = any(
|
|
428
|
+
r.get("skill_name") == job.skill_name
|
|
429
|
+
and r.get("timestamp", "").replace("_", "").startswith(
|
|
430
|
+
job.started_at.replace("-", "").replace(":", "").replace("T", "")[:8]
|
|
431
|
+
)
|
|
432
|
+
for r in all_runs
|
|
433
|
+
)
|
|
434
|
+
if not has_metrics:
|
|
435
|
+
all_runs.append({
|
|
436
|
+
"skill_name": job.skill_name,
|
|
437
|
+
"timestamp": job.started_at.replace("-", "").replace(":", "").replace("T", "_")[:15],
|
|
438
|
+
"iterations": job.iterations,
|
|
439
|
+
"baseline_score": job.baseline_score or 0.0,
|
|
440
|
+
"evolved_score": job.evolved_score or 0.0,
|
|
441
|
+
"improvement": job.improvement or 0.0,
|
|
442
|
+
"elapsed_seconds": 0.0,
|
|
443
|
+
"constraints_passed": job.status == JobStatus.COMPLETED,
|
|
444
|
+
"status": job.status.value,
|
|
445
|
+
"error": job.error,
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
return sorted(all_runs, key=lambda r: r.get("timestamp", ""), reverse=True)
|
|
449
|
+
|
|
450
|
+
# ── EVOLUTION endpoints ────────────────────────────────────────────
|
|
451
|
+
|
|
452
|
+
@app.get("/api/skills/{skill_name}/evolution-history")
|
|
453
|
+
async def skill_history(skill_name: str):
|
|
454
|
+
"""Get evolution run history for a skill."""
|
|
455
|
+
output_dir = EVOLUTION_DIR / "output" / skill_name
|
|
456
|
+
runs = []
|
|
457
|
+
|
|
458
|
+
if output_dir.exists():
|
|
459
|
+
for run_dir in sorted(output_dir.iterdir(), reverse=True):
|
|
460
|
+
metrics_file = run_dir / "metrics.json"
|
|
461
|
+
if metrics_file.exists():
|
|
462
|
+
try:
|
|
463
|
+
data = json.loads(metrics_file.read_text())
|
|
464
|
+
data["run_dir"] = str(run_dir.name)
|
|
465
|
+
runs.append(_normalize_metrics(data))
|
|
466
|
+
except Exception:
|
|
467
|
+
pass
|
|
468
|
+
|
|
469
|
+
# Also add from job tracker
|
|
470
|
+
for job in tracker.get_all_jobs(limit=50):
|
|
471
|
+
if job.skill_name == skill_name and job.status in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
472
|
+
has_metrics = any(r.get("timestamp", "").startswith(job.started_at[:10]) for r in runs)
|
|
473
|
+
if not has_metrics:
|
|
474
|
+
runs.append({
|
|
475
|
+
"skill_name": job.skill_name,
|
|
476
|
+
"timestamp": job.started_at.replace("-", "").replace(":", "").replace("T", "_")[:15],
|
|
477
|
+
"iterations": job.iterations,
|
|
478
|
+
"baseline_score": job.baseline_score or 0.0,
|
|
479
|
+
"evolved_score": job.evolved_score or 0.0,
|
|
480
|
+
"improvement": job.improvement or 0.0,
|
|
481
|
+
"elapsed_seconds": 0.0,
|
|
482
|
+
"constraints_passed": job.status == JobStatus.COMPLETED,
|
|
483
|
+
"status": job.status.value,
|
|
484
|
+
"error": job.error,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
return runs
|
|
488
|
+
|
|
489
|
+
@app.get("/api/skills/{skill_name}/evolution/{run_dir}/diff")
|
|
490
|
+
async def skill_diff(skill_name: str, run_dir: str):
|
|
491
|
+
"""Get baseline vs evolved skill content for a specific run."""
|
|
492
|
+
run_path = EVOLUTION_DIR / "output" / skill_name / run_dir
|
|
493
|
+
if not run_path.exists():
|
|
494
|
+
raise HTTPException(404, f"Run '{run_dir}' not found for skill '{skill_name}'")
|
|
495
|
+
|
|
496
|
+
baseline_file = run_path / "baseline_skill.md"
|
|
497
|
+
evolved_file = run_path / "evolved_skill.md"
|
|
498
|
+
metrics_file = run_path / "metrics.json"
|
|
499
|
+
|
|
500
|
+
result = {
|
|
501
|
+
"skill_name": skill_name,
|
|
502
|
+
"run_dir": run_dir,
|
|
503
|
+
"baseline": None,
|
|
504
|
+
"evolved": None,
|
|
505
|
+
"metrics": None,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if baseline_file.exists():
|
|
509
|
+
result["baseline"] = baseline_file.read_text(encoding="utf-8", errors="replace")
|
|
510
|
+
if evolved_file.exists():
|
|
511
|
+
result["evolved"] = evolved_file.read_text(encoding="utf-8", errors="replace")
|
|
512
|
+
if metrics_file.exists():
|
|
513
|
+
try:
|
|
514
|
+
result["metrics"] = json.loads(metrics_file.read_text())
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
|
|
518
|
+
return result
|
|
519
|
+
|
|
520
|
+
@app.post("/api/evolution/start")
|
|
521
|
+
async def start_evolution(req: EvolveRequest):
|
|
522
|
+
"""Start an evolution run with full job tracking."""
|
|
523
|
+
|
|
524
|
+
# Check for existing active jobs for this skill
|
|
525
|
+
active = tracker.get_active_jobs()
|
|
526
|
+
for job in active:
|
|
527
|
+
if job.skill_name == req.skill_name:
|
|
528
|
+
return {
|
|
529
|
+
"error": f"Evolution already running for '{req.skill_name}'",
|
|
530
|
+
"job_id": job.id,
|
|
531
|
+
"status": job.status.value,
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
# Create tracked job
|
|
535
|
+
job = tracker.create_job(req.skill_name, req.iterations)
|
|
536
|
+
job.add_log(f"Starting evolution for skill: {req.skill_name}")
|
|
537
|
+
job.add_log(f"Iterations: {req.iterations}, Source: {req.eval_source}")
|
|
538
|
+
|
|
539
|
+
# Build command — use SDD evolution engine
|
|
540
|
+
sdd_evolve_path = Path(__file__).parent / "sdd_evolve.py"
|
|
541
|
+
cmd = [
|
|
542
|
+
"/usr/bin/python3", "-u", str(sdd_evolve_path),
|
|
543
|
+
"--skill", req.skill_name,
|
|
544
|
+
"--iterations", str(req.iterations),
|
|
545
|
+
"--eval-source", req.eval_source,
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
env = os.environ.copy()
|
|
549
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
550
|
+
env["HERMES_AGENT_REPO"] = str(HERMES_REPO)
|
|
551
|
+
env["TERM"] = "dumb"
|
|
552
|
+
|
|
553
|
+
# ── Provider selection: Ollama preferred when available ───────────
|
|
554
|
+
def _ollama_up() -> bool:
|
|
555
|
+
import urllib.request
|
|
556
|
+
try:
|
|
557
|
+
with urllib.request.urlopen("http://localhost:11434/v1/models", timeout=2) as r:
|
|
558
|
+
return r.status == 200
|
|
559
|
+
except Exception:
|
|
560
|
+
return False
|
|
561
|
+
|
|
562
|
+
_dotenv = dotenv_values(str(Path.home() / ".hermes" / ".env"))
|
|
563
|
+
for key in ("OLLAMA_API_BASE", "SDD_OLLAMA_MODEL", "SDD_EVOLVE_MODEL"):
|
|
564
|
+
if key in os.environ:
|
|
565
|
+
env[key] = os.environ[key]
|
|
566
|
+
elif _dotenv.get(key):
|
|
567
|
+
env[key] = _dotenv[key]
|
|
568
|
+
|
|
569
|
+
if _ollama_up():
|
|
570
|
+
# Local Ollama is alive — use it regardless of stale cloud keys in .env
|
|
571
|
+
base = env.get("OLLAMA_API_BASE", "http://localhost:11434/v1")
|
|
572
|
+
if not base.rstrip("/").endswith("/v1"):
|
|
573
|
+
base = base.rstrip("/") + "/v1"
|
|
574
|
+
env["OPENAI_BASE_URL"] = base
|
|
575
|
+
env["OPENAI_API_KEY"] = "ollama"
|
|
576
|
+
env.setdefault("SDD_EVOLVE_MODEL", env.get("SDD_OLLAMA_MODEL", "gemma4:31b-cloud"))
|
|
577
|
+
job.add_log("Provider: Ollama local (gemma4:31b-cloud)")
|
|
578
|
+
else:
|
|
579
|
+
# Forward cloud keys only when Ollama is NOT available
|
|
580
|
+
for key in ("OPENROUTER_API_KEY", "NOUS_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"):
|
|
581
|
+
if key in os.environ:
|
|
582
|
+
env[key] = os.environ[key]
|
|
583
|
+
elif _dotenv.get(key):
|
|
584
|
+
env[key] = _dotenv[key]
|
|
585
|
+
env.setdefault("OPENAI_BASE_URL", "https://openrouter.ai/api/v1")
|
|
586
|
+
|
|
587
|
+
try:
|
|
588
|
+
process = await asyncio.create_subprocess_exec(
|
|
589
|
+
*cmd,
|
|
590
|
+
cwd=str(Path(__file__).parent),
|
|
591
|
+
env=env,
|
|
592
|
+
stdout=asyncio.subprocess.PIPE,
|
|
593
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
594
|
+
)
|
|
595
|
+
except Exception as e:
|
|
596
|
+
job.status = JobStatus.FAILED
|
|
597
|
+
job.error = f"Failed to start process: {e}"
|
|
598
|
+
job.completed_at = datetime.now().isoformat()
|
|
599
|
+
job.save_log()
|
|
600
|
+
return {"error": job.error, "job_id": job.id}
|
|
601
|
+
|
|
602
|
+
job.pid = process.pid
|
|
603
|
+
job.status = JobStatus.LOADING_SKILL
|
|
604
|
+
tracker.set_process(job.id, process)
|
|
605
|
+
|
|
606
|
+
# Stream and parse output
|
|
607
|
+
async def stream_output():
|
|
608
|
+
try:
|
|
609
|
+
while True:
|
|
610
|
+
line = await process.stdout.readline()
|
|
611
|
+
if not line:
|
|
612
|
+
break
|
|
613
|
+
text = line.decode("utf-8", errors="replace").strip()
|
|
614
|
+
|
|
615
|
+
if text:
|
|
616
|
+
# Parse line for structured updates
|
|
617
|
+
tracker.parse_line(job, text)
|
|
618
|
+
|
|
619
|
+
# Broadcast to WebSocket clients
|
|
620
|
+
await manager.broadcast({
|
|
621
|
+
"type": "evolution_log",
|
|
622
|
+
"job_id": job.id,
|
|
623
|
+
"skill": req.skill_name,
|
|
624
|
+
"status": job.status.value,
|
|
625
|
+
"progress": job.progress,
|
|
626
|
+
"iteration": job.current_iteration,
|
|
627
|
+
"total_iterations": job.iterations,
|
|
628
|
+
"message": text,
|
|
629
|
+
"timestamp": datetime.now().isoformat(),
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
# Process ended
|
|
633
|
+
await process.wait()
|
|
634
|
+
if job.status not in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
635
|
+
if process.returncode == 0:
|
|
636
|
+
job.status = JobStatus.COMPLETED
|
|
637
|
+
job.completed_at = datetime.now().isoformat()
|
|
638
|
+
job.add_log("Process completed successfully")
|
|
639
|
+
else:
|
|
640
|
+
job.status = JobStatus.FAILED
|
|
641
|
+
job.completed_at = datetime.now().isoformat()
|
|
642
|
+
job.error = f"Process exited with code {process.returncode}"
|
|
643
|
+
job.add_log(f"Process failed: {job.error}")
|
|
644
|
+
|
|
645
|
+
# Try to load final metrics from output
|
|
646
|
+
output_dir = EVOLUTION_DIR / "output" / req.skill_name
|
|
647
|
+
if output_dir.exists():
|
|
648
|
+
latest_run = max(
|
|
649
|
+
[d for d in output_dir.iterdir() if d.is_dir()],
|
|
650
|
+
key=lambda d: d.stat().st_mtime,
|
|
651
|
+
default=None,
|
|
652
|
+
)
|
|
653
|
+
if latest_run:
|
|
654
|
+
mf = latest_run / "metrics.json"
|
|
655
|
+
if mf.exists():
|
|
656
|
+
try:
|
|
657
|
+
metrics = json.loads(mf.read_text())
|
|
658
|
+
job.baseline_score = metrics.get("baseline_score")
|
|
659
|
+
job.evolved_score = metrics.get("evolved_score")
|
|
660
|
+
job.improvement = metrics.get("improvement")
|
|
661
|
+
except Exception:
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
job.save_log()
|
|
665
|
+
tracker.cleanup_process(job.id)
|
|
666
|
+
|
|
667
|
+
# Broadcast completion
|
|
668
|
+
await manager.broadcast({
|
|
669
|
+
"type": "evolution_complete",
|
|
670
|
+
"job_id": job.id,
|
|
671
|
+
"skill": req.skill_name,
|
|
672
|
+
"status": job.status.value,
|
|
673
|
+
"baseline_score": job.baseline_score,
|
|
674
|
+
"evolved_score": job.evolved_score,
|
|
675
|
+
"improvement": job.improvement,
|
|
676
|
+
"timestamp": datetime.now().isoformat(),
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
except Exception as e:
|
|
680
|
+
job.status = JobStatus.FAILED
|
|
681
|
+
job.error = str(e)
|
|
682
|
+
job.completed_at = datetime.now().isoformat()
|
|
683
|
+
job.save_log()
|
|
684
|
+
tracker.cleanup_process(job.id)
|
|
685
|
+
|
|
686
|
+
asyncio.create_task(stream_output())
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
"job_id": job.id,
|
|
690
|
+
"skill": req.skill_name,
|
|
691
|
+
"status": job.status.value,
|
|
692
|
+
"iterations": req.iterations,
|
|
693
|
+
"pid": process.pid,
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
# ── Job tracking endpoints ────────────────────────────────────────────
|
|
697
|
+
|
|
698
|
+
@app.get("/api/jobs")
|
|
699
|
+
async def list_jobs(active_only: bool = False, limit: int = 50):
|
|
700
|
+
"""List evolution jobs."""
|
|
701
|
+
if active_only:
|
|
702
|
+
jobs = tracker.get_active_jobs()
|
|
703
|
+
else:
|
|
704
|
+
jobs = tracker.get_all_jobs(limit)
|
|
705
|
+
return [j.to_dict() for j in jobs]
|
|
706
|
+
|
|
707
|
+
@app.get("/api/jobs/{job_id}")
|
|
708
|
+
async def get_job(job_id: str):
|
|
709
|
+
"""Get detailed job status including logs."""
|
|
710
|
+
job = tracker.get_job(job_id)
|
|
711
|
+
if not job:
|
|
712
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
713
|
+
return job.to_dict()
|
|
714
|
+
|
|
715
|
+
@app.get("/api/jobs/{job_id}/logs")
|
|
716
|
+
async def get_job_logs(job_id: str, since: int = 0):
|
|
717
|
+
"""Get job logs (optionally only lines after index `since`)."""
|
|
718
|
+
job = tracker.get_job(job_id)
|
|
719
|
+
if not job:
|
|
720
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
721
|
+
return {
|
|
722
|
+
"job_id": job_id,
|
|
723
|
+
"total_lines": len(job.logs),
|
|
724
|
+
"logs": job.logs[since:],
|
|
725
|
+
"status": job.status.value,
|
|
726
|
+
"progress": job.progress,
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
@app.delete("/api/jobs/{job_id}")
|
|
730
|
+
async def cancel_job(job_id: str):
|
|
731
|
+
"""Cancel a running evolution job."""
|
|
732
|
+
job = tracker.get_job(job_id)
|
|
733
|
+
if not job:
|
|
734
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
735
|
+
|
|
736
|
+
process = tracker.get_process(job_id)
|
|
737
|
+
if process:
|
|
738
|
+
try:
|
|
739
|
+
process.terminate()
|
|
740
|
+
await process.wait()
|
|
741
|
+
except Exception:
|
|
742
|
+
pass
|
|
743
|
+
tracker.cleanup_process(job_id)
|
|
744
|
+
|
|
745
|
+
job.status = JobStatus.FAILED
|
|
746
|
+
job.error = "Cancelled by user"
|
|
747
|
+
job.completed_at = datetime.now().isoformat()
|
|
748
|
+
job.save_log()
|
|
749
|
+
|
|
750
|
+
await manager.broadcast({
|
|
751
|
+
"type": "evolution_cancelled",
|
|
752
|
+
"job_id": job_id,
|
|
753
|
+
"skill": job.skill_name,
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
return {"status": "cancelled", "job_id": job_id}
|
|
757
|
+
|
|
758
|
+
@app.get("/api/evolution/{skill_name}/output/{run_id}")
|
|
759
|
+
async def get_run_output(skill_name: str, run_id: str):
|
|
760
|
+
"""Get evolved vs baseline skill diff for a specific run."""
|
|
761
|
+
run_dir = EVOLUTION_DIR / "output" / skill_name / run_id
|
|
762
|
+
|
|
763
|
+
if not run_dir.exists():
|
|
764
|
+
raise HTTPException(404, "Run not found")
|
|
765
|
+
|
|
766
|
+
result = {}
|
|
767
|
+
|
|
768
|
+
for f in ["evolved_skill.md", "baseline_skill.md", "evolved_FAILED.md"]:
|
|
769
|
+
fp = run_dir / f
|
|
770
|
+
if fp.exists():
|
|
771
|
+
result[f.replace(".md", "")] = fp.read_text(encoding="utf-8", errors="replace")
|
|
772
|
+
|
|
773
|
+
metrics_file = run_dir / "metrics.json"
|
|
774
|
+
if metrics_file.exists():
|
|
775
|
+
raw = json.loads(metrics_file.read_text())
|
|
776
|
+
result["metrics"] = _normalize_metrics(raw)
|
|
777
|
+
|
|
778
|
+
return result
|
|
779
|
+
|
|
780
|
+
# ── DATASET endpoints ──────────────────────────────────────────────
|
|
781
|
+
|
|
782
|
+
@app.get("/api/datasets")
|
|
783
|
+
async def list_datasets():
|
|
784
|
+
"""List all eval datasets."""
|
|
785
|
+
datasets = []
|
|
786
|
+
|
|
787
|
+
for base in [EVOLUTION_DIR / "datasets", EVOLUTION_DIR / "output"]:
|
|
788
|
+
if not base.exists():
|
|
789
|
+
continue
|
|
790
|
+
for skill_dir in base.iterdir():
|
|
791
|
+
if skill_dir.is_dir():
|
|
792
|
+
splits = {}
|
|
793
|
+
for split in ["train.jsonl", "val.jsonl", "holdout.jsonl"]:
|
|
794
|
+
fp = skill_dir / split
|
|
795
|
+
if fp.exists():
|
|
796
|
+
count = sum(1 for _ in open(fp))
|
|
797
|
+
splits[split.replace(".jsonl", "")] = count
|
|
798
|
+
if splits:
|
|
799
|
+
datasets.append({
|
|
800
|
+
"skill": skill_dir.name,
|
|
801
|
+
"path": str(skill_dir),
|
|
802
|
+
"splits": splits,
|
|
803
|
+
"total": sum(splits.values()),
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
return datasets
|
|
807
|
+
|
|
808
|
+
@app.get("/api/datasets/{skill_name}")
|
|
809
|
+
async def get_dataset(skill_name: str):
|
|
810
|
+
"""Get dataset examples for a skill."""
|
|
811
|
+
for base in [EVOLUTION_DIR / "datasets", EVOLUTION_DIR / "output"]:
|
|
812
|
+
dataset_dir = base / skill_name
|
|
813
|
+
if dataset_dir.exists():
|
|
814
|
+
result = {}
|
|
815
|
+
for split in ["train", "val", "holdout"]:
|
|
816
|
+
fp = dataset_dir / f"{split}.jsonl"
|
|
817
|
+
if fp.exists():
|
|
818
|
+
examples = []
|
|
819
|
+
for line in open(fp):
|
|
820
|
+
try:
|
|
821
|
+
examples.append(json.loads(line))
|
|
822
|
+
except Exception:
|
|
823
|
+
pass
|
|
824
|
+
result[split] = examples
|
|
825
|
+
return result
|
|
826
|
+
|
|
827
|
+
raise HTTPException(404, f"No dataset found for '{skill_name}'")
|
|
828
|
+
|
|
829
|
+
@app.get("/api/datasets/{skill_name}/sessions")
|
|
830
|
+
async def import_sessions(skill_name: str, source: str = "all", max_examples: int = 50):
|
|
831
|
+
"""Import and filter external sessions for dataset building."""
|
|
832
|
+
cmd = [
|
|
833
|
+
PYTHON_BIN, "-m", "evolution.core.external_importers",
|
|
834
|
+
"--source", source,
|
|
835
|
+
"--skill", skill_name,
|
|
836
|
+
"--max-examples", str(max_examples),
|
|
837
|
+
"--dry-run",
|
|
838
|
+
]
|
|
839
|
+
env = os.environ.copy()
|
|
840
|
+
env["PYTHONPATH"] = str(EVOLUTION_DIR)
|
|
841
|
+
env["HERMES_AGENT_REPO"] = str(HERMES_REPO)
|
|
842
|
+
|
|
843
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(EVOLUTION_DIR), env=env)
|
|
844
|
+
|
|
845
|
+
return {
|
|
846
|
+
"stdout": result.stdout,
|
|
847
|
+
"stderr": result.stderr,
|
|
848
|
+
"returncode": result.returncode,
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
# ── MEMORY endpoints ───────────────────────────────────────────────
|
|
852
|
+
|
|
853
|
+
@app.get("/api/memory")
|
|
854
|
+
async def list_memory():
|
|
855
|
+
"""List all stored memories."""
|
|
856
|
+
memories = []
|
|
857
|
+
|
|
858
|
+
# Check .hermes/memory directory
|
|
859
|
+
if MEMORY_DIR.exists():
|
|
860
|
+
for f in sorted(MEMORY_DIR.glob("*.json")):
|
|
861
|
+
try:
|
|
862
|
+
data = json.loads(f.read_text())
|
|
863
|
+
memories.append({
|
|
864
|
+
"key": f.stem,
|
|
865
|
+
"data": data,
|
|
866
|
+
"modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
|
|
867
|
+
})
|
|
868
|
+
except Exception:
|
|
869
|
+
memories.append({
|
|
870
|
+
"key": f.stem,
|
|
871
|
+
"data": f.read_text()[:200],
|
|
872
|
+
"modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
# Also check SOUL.md memory section
|
|
876
|
+
soul_path = Path.home() / ".hermes" / "SOUL.md"
|
|
877
|
+
if soul_path.exists():
|
|
878
|
+
content = soul_path.read_text(encoding="utf-8", errors="replace")
|
|
879
|
+
if "MEMORY" in content:
|
|
880
|
+
memories.append({
|
|
881
|
+
"key": "SOUL_MEMORY",
|
|
882
|
+
"source": "SOUL.md",
|
|
883
|
+
"size": len(content),
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
return memories
|
|
887
|
+
|
|
888
|
+
# ── GRAPH endpoint (vis.js format — same as graphify) ─────────────────
|
|
889
|
+
|
|
890
|
+
COMMUNITY_COLORS = [
|
|
891
|
+
"#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F",
|
|
892
|
+
"#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC",
|
|
893
|
+
]
|
|
894
|
+
|
|
895
|
+
@app.get("/api/graph")
|
|
896
|
+
async def get_graph():
|
|
897
|
+
"""Generate a vis.js knowledge graph from skills, memory, and evolution runs.
|
|
898
|
+
|
|
899
|
+
Format matches graphify's export: nodes + edges for vis-network.
|
|
900
|
+
"""
|
|
901
|
+
nodes = {}
|
|
902
|
+
edges = []
|
|
903
|
+
communities = {} # community_id -> [node_ids]
|
|
904
|
+
|
|
905
|
+
# ── 1. Skills as nodes ────────────────────────────────────────
|
|
906
|
+
skill_nodes = {}
|
|
907
|
+
if SKILLS_DIR.exists():
|
|
908
|
+
category_map = {} # category_dir -> community_id
|
|
909
|
+
cat_counter = 0
|
|
910
|
+
|
|
911
|
+
for skill_file in sorted(SKILLS_DIR.rglob("SKILL.md")):
|
|
912
|
+
skill_name = skill_file.parent.name
|
|
913
|
+
category = skill_file.parent.parent.name if skill_file.parent.parent != SKILLS_DIR else "root"
|
|
914
|
+
|
|
915
|
+
# Assign community by category
|
|
916
|
+
if category not in category_map:
|
|
917
|
+
category_map[category] = cat_counter
|
|
918
|
+
communities[cat_counter] = []
|
|
919
|
+
cat_counter += 1
|
|
920
|
+
cid = category_map[category]
|
|
921
|
+
|
|
922
|
+
# Read skill
|
|
923
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
924
|
+
stat = skill_file.stat()
|
|
925
|
+
desc = ""
|
|
926
|
+
if content.startswith("---"):
|
|
927
|
+
try:
|
|
928
|
+
import yaml
|
|
929
|
+
end = content.index("---", 3)
|
|
930
|
+
fm = yaml.safe_load(content[3:end])
|
|
931
|
+
desc = (fm.get("description") or "")[:80]
|
|
932
|
+
except Exception:
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
node_id = f"skill:{skill_name}"
|
|
936
|
+
deg = 0 # will update after edges
|
|
937
|
+
nodes[node_id] = {
|
|
938
|
+
"id": node_id,
|
|
939
|
+
"label": skill_name,
|
|
940
|
+
"color": {"background": COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)], "border": COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)], "highlight": {"background": "#ffffff", "border": COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)]}},
|
|
941
|
+
"size": 15,
|
|
942
|
+
"font": {"size": 11, "color": "#ffffff"},
|
|
943
|
+
"title": f"{skill_name}\n{desc}",
|
|
944
|
+
"community": cid,
|
|
945
|
+
"community_name": category,
|
|
946
|
+
"source_file": str(skill_file.relative_to(HERMES_REPO)),
|
|
947
|
+
"file_type": "skill",
|
|
948
|
+
"degree": 0,
|
|
949
|
+
}
|
|
950
|
+
communities[cid].append(node_id)
|
|
951
|
+
skill_nodes[skill_name] = node_id
|
|
952
|
+
|
|
953
|
+
# Edge: skill -> category
|
|
954
|
+
cat_node_id = f"cat:{category}"
|
|
955
|
+
if cat_node_id not in nodes:
|
|
956
|
+
nodes[cat_node_id] = {
|
|
957
|
+
"id": cat_node_id,
|
|
958
|
+
"label": category,
|
|
959
|
+
"color": {"background": "#555555", "border": "#555555", "highlight": {"background": "#ffffff", "border": "#555555"}},
|
|
960
|
+
"size": 22,
|
|
961
|
+
"font": {"size": 13, "color": "#ffffff"},
|
|
962
|
+
"title": f"Category: {category}",
|
|
963
|
+
"community": cid,
|
|
964
|
+
"community_name": category,
|
|
965
|
+
"source_file": "",
|
|
966
|
+
"file_type": "category",
|
|
967
|
+
"degree": 0,
|
|
968
|
+
}
|
|
969
|
+
edges.append({
|
|
970
|
+
"from": node_id,
|
|
971
|
+
"to": cat_node_id,
|
|
972
|
+
"label": "belongs_to",
|
|
973
|
+
"title": "belongs_to",
|
|
974
|
+
"dashes": False,
|
|
975
|
+
"width": 1,
|
|
976
|
+
"color": {"opacity": 0.4},
|
|
977
|
+
"confidence": "STRUCTURAL",
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
# ── 2. Evolution runs as edges between skill variants ─────────
|
|
981
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
982
|
+
if output_dir.exists():
|
|
983
|
+
for skill_dir in output_dir.iterdir():
|
|
984
|
+
if skill_dir.is_dir() and skill_dir.name != "skills":
|
|
985
|
+
for run_dir in skill_dir.iterdir():
|
|
986
|
+
mf = run_dir / "metrics.json"
|
|
987
|
+
if mf.exists():
|
|
988
|
+
try:
|
|
989
|
+
data = json.loads(mf.read_text())
|
|
990
|
+
evolved_id = f"evolved:{skill_dir.name}:{run_dir.name}"
|
|
991
|
+
baseline_id = skill_nodes.get(skill_dir.name)
|
|
992
|
+
|
|
993
|
+
nodes[evolved_id] = {
|
|
994
|
+
"id": evolved_id,
|
|
995
|
+
"label": f"{skill_dir.name} ✦",
|
|
996
|
+
"color": {"background": "#22c55e", "border": "#22c55e", "highlight": {"background": "#ffffff", "border": "#22c55e"}},
|
|
997
|
+
"size": 10 + (data.get("improvement", 0) * 100),
|
|
998
|
+
"font": {"size": 9, "color": "#a0a0a0"},
|
|
999
|
+
"title": f"Evolved: {skill_dir.name}\nScore: {data.get('evolved_score', 0):.2f}\nImprovement: {data.get('improvement', 0)*100:.1f}%",
|
|
1000
|
+
"community": 0,
|
|
1001
|
+
"community_name": "evolved",
|
|
1002
|
+
"source_file": str(run_dir),
|
|
1003
|
+
"file_type": "evolution",
|
|
1004
|
+
"degree": 1,
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if baseline_id:
|
|
1008
|
+
edges.append({
|
|
1009
|
+
"from": baseline_id,
|
|
1010
|
+
"to": evolved_id,
|
|
1011
|
+
"label": "evolved_to",
|
|
1012
|
+
"title": f"+{(data.get('improvement', 0)*100):.1f}%",
|
|
1013
|
+
"dashes": True,
|
|
1014
|
+
"width": 2,
|
|
1015
|
+
"color": {"opacity": 0.8},
|
|
1016
|
+
"confidence": "EXTRACTED",
|
|
1017
|
+
})
|
|
1018
|
+
except Exception:
|
|
1019
|
+
pass
|
|
1020
|
+
|
|
1021
|
+
# ── 3. Memory entries as nodes ────────────────────────────────
|
|
1022
|
+
if MEMORY_DIR.exists():
|
|
1023
|
+
mem_cid = cat_counter
|
|
1024
|
+
communities[mem_cid] = []
|
|
1025
|
+
for f in sorted(MEMORY_DIR.glob("*.json")):
|
|
1026
|
+
try:
|
|
1027
|
+
mem_data = json.loads(f.read_text())
|
|
1028
|
+
mem_id = f"memory:{f.stem}"
|
|
1029
|
+
nodes[mem_id] = {
|
|
1030
|
+
"id": mem_id,
|
|
1031
|
+
"label": f.stem,
|
|
1032
|
+
"color": {"background": "#8b5cf6", "border": "#8b5cf6", "highlight": {"background": "#ffffff", "border": "#8b5cf6"}},
|
|
1033
|
+
"size": 12,
|
|
1034
|
+
"font": {"size": 10, "color": "#c4b5fd"},
|
|
1035
|
+
"title": f"Memory: {f.stem}",
|
|
1036
|
+
"community": mem_cid,
|
|
1037
|
+
"community_name": "memory",
|
|
1038
|
+
"source_file": str(f),
|
|
1039
|
+
"file_type": "memory",
|
|
1040
|
+
"degree": 0,
|
|
1041
|
+
}
|
|
1042
|
+
communities[mem_cid].append(mem_id)
|
|
1043
|
+
except Exception:
|
|
1044
|
+
pass
|
|
1045
|
+
|
|
1046
|
+
# ── 4. SOUL.md as hub node ────────────────────────────────────
|
|
1047
|
+
soul_path = Path.home() / ".hermes" / "SOUL.md"
|
|
1048
|
+
if soul_path.exists():
|
|
1049
|
+
soul_id = "soul:SOUL.md"
|
|
1050
|
+
nodes[soul_id] = {
|
|
1051
|
+
"id": soul_id,
|
|
1052
|
+
"label": "SOUL.md",
|
|
1053
|
+
"color": {"background": "#eab308", "border": "#eab308", "highlight": {"background": "#ffffff", "border": "#eab308"}},
|
|
1054
|
+
"size": 25,
|
|
1055
|
+
"font": {"size": 13, "color": "#ffffff"},
|
|
1056
|
+
"title": "Hermes Soul — Core identity and memory",
|
|
1057
|
+
"community": cat_counter + 1,
|
|
1058
|
+
"community_name": "core",
|
|
1059
|
+
"source_file": str(soul_path),
|
|
1060
|
+
"file_type": "core",
|
|
1061
|
+
"degree": 0,
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
# ── Calculate degrees and node sizes ──────────────────────────
|
|
1065
|
+
for e in edges:
|
|
1066
|
+
if e["from"] in nodes:
|
|
1067
|
+
nodes[e["from"]]["degree"] = nodes[e["from"]].get("degree", 0) + 1
|
|
1068
|
+
if e["to"] in nodes:
|
|
1069
|
+
nodes[e["to"]]["degree"] = nodes[e["to"]].get("degree", 0) + 1
|
|
1070
|
+
|
|
1071
|
+
max_deg = max((n.get("degree", 0) for n in nodes.values()), default=1) or 1
|
|
1072
|
+
for n in nodes.values():
|
|
1073
|
+
deg = n.get("degree", 0)
|
|
1074
|
+
n["size"] = round(10 + 30 * (deg / max_deg), 1)
|
|
1075
|
+
n["font"]["size"] = 12 if deg >= max_deg * 0.15 else 0
|
|
1076
|
+
|
|
1077
|
+
# ── Legend ────────────────────────────────────────────────────
|
|
1078
|
+
legend = []
|
|
1079
|
+
for cid, label in enumerate(list(category_map.keys()) if 'category_map' in dir() else []):
|
|
1080
|
+
legend.append({
|
|
1081
|
+
"cid": cid,
|
|
1082
|
+
"color": COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)],
|
|
1083
|
+
"label": label,
|
|
1084
|
+
"count": len(communities.get(cid, [])),
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
"nodes": list(nodes.values()),
|
|
1089
|
+
"edges": edges,
|
|
1090
|
+
"legend": legend,
|
|
1091
|
+
"stats": {
|
|
1092
|
+
"total_nodes": len(nodes),
|
|
1093
|
+
"total_edges": len(edges),
|
|
1094
|
+
"communities": len(communities),
|
|
1095
|
+
"skills": len(skill_nodes),
|
|
1096
|
+
},
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
# ── METRICS endpoints ──────────────────────────────────────────────
|
|
1100
|
+
|
|
1101
|
+
@app.get("/api/metrics")
|
|
1102
|
+
async def get_metrics():
|
|
1103
|
+
"""Aggregate metrics across all evolution runs."""
|
|
1104
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
1105
|
+
|
|
1106
|
+
all_runs = []
|
|
1107
|
+
if output_dir.exists():
|
|
1108
|
+
for skill_dir in output_dir.iterdir():
|
|
1109
|
+
if skill_dir.is_dir():
|
|
1110
|
+
for run_dir in skill_dir.iterdir():
|
|
1111
|
+
mf = run_dir / "metrics.json"
|
|
1112
|
+
if mf.exists():
|
|
1113
|
+
try:
|
|
1114
|
+
raw = json.loads(mf.read_text())
|
|
1115
|
+
all_runs.append(_normalize_metrics(raw))
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1118
|
+
|
|
1119
|
+
if not all_runs:
|
|
1120
|
+
return {
|
|
1121
|
+
"total_runs": 0,
|
|
1122
|
+
"skills_evolved": 0,
|
|
1123
|
+
"avg_improvement": 0,
|
|
1124
|
+
"best_improvement": 0,
|
|
1125
|
+
"total_time_seconds": 0,
|
|
1126
|
+
"avg_time_seconds": 0,
|
|
1127
|
+
"success_rate": 0,
|
|
1128
|
+
"runs": [],
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
improvements = [r.get("improvement", 0) for r in all_runs]
|
|
1132
|
+
times = [r.get("elapsed_seconds", 0) for r in all_runs]
|
|
1133
|
+
passed = sum(1 for r in all_runs if r.get("constraints_passed", False))
|
|
1134
|
+
skills = set(r.get("skill_name", "unknown") for r in all_runs)
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
"total_runs": len(all_runs),
|
|
1138
|
+
"skills_evolved": len(skills),
|
|
1139
|
+
"avg_improvement": sum(improvements) / len(improvements) if improvements else 0,
|
|
1140
|
+
"best_improvement": max(improvements) if improvements else 0,
|
|
1141
|
+
"total_time_seconds": sum(times),
|
|
1142
|
+
"avg_time_seconds": sum(times) / len(times) if times else 0,
|
|
1143
|
+
"success_rate": passed / len(all_runs) if all_runs else 0,
|
|
1144
|
+
"runs": sorted(all_runs, key=lambda r: r.get("timestamp", ""), reverse=True)[:20],
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
# ── CONSTRAINTS endpoints ──────────────────────────────────────────
|
|
1148
|
+
|
|
1149
|
+
@app.get("/api/constraints/validate/{skill_name}")
|
|
1150
|
+
async def validate_skill(skill_name: str):
|
|
1151
|
+
"""Validate a skill against all constraints."""
|
|
1152
|
+
skill_file = _find_skill_file(skill_name)
|
|
1153
|
+
if not skill_file:
|
|
1154
|
+
raise HTTPException(404, f"Skill '{skill_name}' not found")
|
|
1155
|
+
|
|
1156
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
1157
|
+
|
|
1158
|
+
# Try evolution module first
|
|
1159
|
+
try:
|
|
1160
|
+
sys.path.insert(0, str(EVOLUTION_DIR))
|
|
1161
|
+
from evolution.core.constraints import ConstraintValidator
|
|
1162
|
+
from evolution.core.config import EvolutionConfig
|
|
1163
|
+
|
|
1164
|
+
config = EvolutionConfig()
|
|
1165
|
+
validator = ConstraintValidator(config)
|
|
1166
|
+
results = validator.validate_all(content, "skill")
|
|
1167
|
+
|
|
1168
|
+
return [
|
|
1169
|
+
{
|
|
1170
|
+
"passed": r.passed,
|
|
1171
|
+
"constraint": r.constraint_name,
|
|
1172
|
+
"message": r.message,
|
|
1173
|
+
"details": r.details,
|
|
1174
|
+
}
|
|
1175
|
+
for r in results
|
|
1176
|
+
]
|
|
1177
|
+
except Exception:
|
|
1178
|
+
pass
|
|
1179
|
+
|
|
1180
|
+
# Fallback: built-in validation
|
|
1181
|
+
results = []
|
|
1182
|
+
size = len(content.encode("utf-8"))
|
|
1183
|
+
results.append({
|
|
1184
|
+
"passed": size <= 15000,
|
|
1185
|
+
"constraint": "size_limit",
|
|
1186
|
+
"message": f"{size} bytes (max 15KB)",
|
|
1187
|
+
"details": None,
|
|
1188
|
+
})
|
|
1189
|
+
results.append({
|
|
1190
|
+
"passed": bool(content.strip()),
|
|
1191
|
+
"constraint": "non_empty",
|
|
1192
|
+
"message": "Skill must contain text",
|
|
1193
|
+
"details": None,
|
|
1194
|
+
})
|
|
1195
|
+
results.append({
|
|
1196
|
+
"passed": content.strip().startswith("---"),
|
|
1197
|
+
"constraint": "skill_structure",
|
|
1198
|
+
"message": "Must start with YAML frontmatter (---)",
|
|
1199
|
+
"details": None,
|
|
1200
|
+
})
|
|
1201
|
+
return results
|
|
1202
|
+
|
|
1203
|
+
# ── WEBSOCKET for live streaming ────────────────────────────────────
|
|
1204
|
+
|
|
1205
|
+
@app.websocket("/ws/stream")
|
|
1206
|
+
async def websocket_stream(ws: WebSocket):
|
|
1207
|
+
"""Real-time evolution log streaming."""
|
|
1208
|
+
await manager.connect(ws)
|
|
1209
|
+
try:
|
|
1210
|
+
while True:
|
|
1211
|
+
data = await ws.receive_text()
|
|
1212
|
+
await ws.send_json({"type": "pong", "echo": data})
|
|
1213
|
+
except WebSocketDisconnect:
|
|
1214
|
+
manager.disconnect(ws)
|
|
1215
|
+
|
|
1216
|
+
# ── Health check ───────────────────────────────────────────────────
|
|
1217
|
+
|
|
1218
|
+
@app.get("/api/health")
|
|
1219
|
+
async def health():
|
|
1220
|
+
from .skill_registry import get_registry
|
|
1221
|
+
registry = get_registry()
|
|
1222
|
+
all_skills = registry.get_global_skills()
|
|
1223
|
+
skill_count = len(all_skills)
|
|
1224
|
+
|
|
1225
|
+
# Calcular categorías desde paths
|
|
1226
|
+
categories = {}
|
|
1227
|
+
for skill in all_skills:
|
|
1228
|
+
cat = 'uncategorized'
|
|
1229
|
+
for path in [skill.get('canonical_path', '')] + [p.get('local_path', '') for p in skill.get('providers', [])]:
|
|
1230
|
+
if '/skills/' in path:
|
|
1231
|
+
parts = path.split('/skills/')
|
|
1232
|
+
if len(parts) > 1:
|
|
1233
|
+
sub = parts[1].split('/')[0]
|
|
1234
|
+
if sub and sub not in ['.', '..']:
|
|
1235
|
+
cat = sub
|
|
1236
|
+
break
|
|
1237
|
+
categories[cat] = categories.get(cat, 0) + 1
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
"status": "ok",
|
|
1241
|
+
"hermes_repo": str(HERMES_REPO),
|
|
1242
|
+
"hermes_repo_exists": HERMES_REPO.exists(),
|
|
1243
|
+
"evolution_dir": str(EVOLUTION_DIR),
|
|
1244
|
+
"evolution_dir_exists": EVOLUTION_DIR.exists(),
|
|
1245
|
+
"skills_count": skill_count,
|
|
1246
|
+
"categories": categories,
|
|
1247
|
+
"python": PYTHON_BIN,
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
@app.get("/api/skills")
|
|
1251
|
+
async def list_skills():
|
|
1252
|
+
"""Lista skills únicas con metadata de providers y categoría."""
|
|
1253
|
+
from .skill_registry import get_registry
|
|
1254
|
+
import traceback, datetime, pathlib
|
|
1255
|
+
try:
|
|
1256
|
+
registry = get_registry()
|
|
1257
|
+
skills_map = {}
|
|
1258
|
+
for skill in registry.get_global_skills():
|
|
1259
|
+
name = skill['name']
|
|
1260
|
+
providers = [p['name'] for p in skill['providers']]
|
|
1261
|
+
|
|
1262
|
+
# Extraer categoría del path (canonical o local del primer provider)
|
|
1263
|
+
category = 'uncategorized'
|
|
1264
|
+
for path in [skill.get('canonical_path', '')] + [p.get('local_path', '') for p in skill['providers']]:
|
|
1265
|
+
if '/skills/' in path:
|
|
1266
|
+
parts = path.split('/skills/')
|
|
1267
|
+
if len(parts) > 1:
|
|
1268
|
+
sub = parts[1].split('/')[0]
|
|
1269
|
+
if sub and sub not in ['.', '..']:
|
|
1270
|
+
category = sub
|
|
1271
|
+
break
|
|
1272
|
+
|
|
1273
|
+
if name not in skills_map:
|
|
1274
|
+
skills_map[name] = {
|
|
1275
|
+
"name": name,
|
|
1276
|
+
"description": skill['description'],
|
|
1277
|
+
"tags": skill['tags'],
|
|
1278
|
+
"is_fork": skill.get('is_fork', False),
|
|
1279
|
+
"canonical_path": skill['canonical_path'],
|
|
1280
|
+
"providers": providers,
|
|
1281
|
+
"provider_count": len(providers),
|
|
1282
|
+
"category": category,
|
|
1283
|
+
}
|
|
1284
|
+
return list(skills_map.values())
|
|
1285
|
+
except Exception as e:
|
|
1286
|
+
log_path = pathlib.Path("/tmp/list_skills_error.log")
|
|
1287
|
+
with open(log_path, "w") as f:
|
|
1288
|
+
f.write(str(datetime.datetime.now()) + "\n")
|
|
1289
|
+
f.write(traceback.format_exc())
|
|
1290
|
+
raise
|
|
1291
|
+
|
|
1292
|
+
|