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,1746 @@
|
|
|
1
|
+
"""Hermes Evolution Dashboard — FastAPI Backend
|
|
2
|
+
|
|
3
|
+
Bridges the Next.js frontend to the hermes-agent-self-evolution Python modules.
|
|
4
|
+
Exposes REST endpoints + WebSocket streaming for real-time evolution monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import shutil
|
|
11
|
+
import asyncio
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from dotenv import dotenv_values
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
|
21
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
22
|
+
from pydantic import BaseModel
|
|
23
|
+
|
|
24
|
+
from .skill_registry import get_registry
|
|
25
|
+
from .job_tracker import tracker, EvolutionJob, JobStatus
|
|
26
|
+
|
|
27
|
+
# ── Paths ──────────────────────────────────────────────────────────
|
|
28
|
+
HERMES_REPO = Path(os.environ.get("HERMES_AGENT_REPO", Path.home() / ".hermes" / "hermes-agent"))
|
|
29
|
+
|
|
30
|
+
# Evolution dir: env var > sibling > home
|
|
31
|
+
_env_ev = os.environ.get("EVOLUTION_DIR", "")
|
|
32
|
+
if _env_ev:
|
|
33
|
+
EVOLUTION_DIR = Path(_env_ev)
|
|
34
|
+
else:
|
|
35
|
+
_sibling = Path(__file__).parent.parent.parent / "hermes-agent-self-evolution"
|
|
36
|
+
_home = Path.home() / ".hermes" / "hermes-agent-self-evolution"
|
|
37
|
+
EVOLUTION_DIR = _sibling if _sibling.exists() else _home
|
|
38
|
+
|
|
39
|
+
SKILLS_DIR = HERMES_REPO / "skills"
|
|
40
|
+
MEMORY_DIR = Path.home() / ".hermes" / "memory"
|
|
41
|
+
DATASETS_DIR = Path.home() / ".hermes" / "datasets"
|
|
42
|
+
|
|
43
|
+
# ── Skill Registry (lazy loaded) ────────────────────────────────
|
|
44
|
+
_skill_registry = None
|
|
45
|
+
|
|
46
|
+
def get_skill_registry():
|
|
47
|
+
global _skill_registry
|
|
48
|
+
if _skill_registry is None:
|
|
49
|
+
_skill_registry = get_registry()
|
|
50
|
+
return _skill_registry
|
|
51
|
+
|
|
52
|
+
SESSIONS_DIR = Path.home() / ".hermes" / "sessions"
|
|
53
|
+
|
|
54
|
+
# ── Python executable ──────────────────────────────────────────────
|
|
55
|
+
def _find_python() -> str:
|
|
56
|
+
"""Find the best Python executable (supports evolution modules)."""
|
|
57
|
+
# 1. Env var override
|
|
58
|
+
env_py = os.environ.get("PYTHON", "")
|
|
59
|
+
if env_py and Path(env_py).exists():
|
|
60
|
+
return env_py
|
|
61
|
+
|
|
62
|
+
# 2. System python3.12 (where dspy is installed)
|
|
63
|
+
for candidate in ["/usr/bin/python3", "python3.12", "python3.11", "python3"]:
|
|
64
|
+
try:
|
|
65
|
+
result = subprocess.run(
|
|
66
|
+
[candidate, "-c", "import dspy; print('ok')"],
|
|
67
|
+
capture_output=True, timeout=5
|
|
68
|
+
)
|
|
69
|
+
if result.returncode == 0:
|
|
70
|
+
return candidate
|
|
71
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# 3. Fallback
|
|
75
|
+
return sys.executable
|
|
76
|
+
|
|
77
|
+
PYTHON_BIN = _find_python()
|
|
78
|
+
|
|
79
|
+
app = FastAPI(title="Hermes Evolution API", version="0.2.0")
|
|
80
|
+
|
|
81
|
+
app.add_middleware(
|
|
82
|
+
CORSMiddleware,
|
|
83
|
+
allow_origins=["http://localhost:3001", "http://localhost:3000", "http://127.0.0.1:3001"],
|
|
84
|
+
allow_credentials=True,
|
|
85
|
+
allow_methods=["*"],
|
|
86
|
+
allow_headers=["*"],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# ── WebSocket manager ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
class ConnectionManager:
|
|
92
|
+
def __init__(self):
|
|
93
|
+
self.active: list[WebSocket] = []
|
|
94
|
+
|
|
95
|
+
async def connect(self, ws: WebSocket):
|
|
96
|
+
await ws.accept()
|
|
97
|
+
self.active.append(ws)
|
|
98
|
+
|
|
99
|
+
def disconnect(self, ws: WebSocket):
|
|
100
|
+
if ws in self.active:
|
|
101
|
+
self.active.remove(ws)
|
|
102
|
+
|
|
103
|
+
async def broadcast(self, message: dict):
|
|
104
|
+
for ws in self.active:
|
|
105
|
+
try:
|
|
106
|
+
await ws.send_json(message)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
manager = ConnectionManager()
|
|
111
|
+
|
|
112
|
+
# ── Models ─────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
class SkillInfo(BaseModel):
|
|
115
|
+
name: str
|
|
116
|
+
path: str
|
|
117
|
+
description: str
|
|
118
|
+
size: int
|
|
119
|
+
last_modified: str
|
|
120
|
+
source: str = "skill" # "skill" or "description"
|
|
121
|
+
|
|
122
|
+
class EvolveRequest(BaseModel):
|
|
123
|
+
skill_name: str
|
|
124
|
+
iterations: int = 3
|
|
125
|
+
eval_source: str = "synthetic"
|
|
126
|
+
dataset_size: int = 10
|
|
127
|
+
|
|
128
|
+
class MemoryEntry(BaseModel):
|
|
129
|
+
key: str
|
|
130
|
+
value: str
|
|
131
|
+
source: str
|
|
132
|
+
timestamp: str
|
|
133
|
+
|
|
134
|
+
class RunMetrics(BaseModel):
|
|
135
|
+
skill_name: str
|
|
136
|
+
timestamp: str
|
|
137
|
+
baseline_score: float
|
|
138
|
+
evolved_score: float
|
|
139
|
+
improvement: float
|
|
140
|
+
elapsed_seconds: float
|
|
141
|
+
constraints_passed: bool
|
|
142
|
+
|
|
143
|
+
# ── Helpers ────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
def _normalize_metrics(raw: dict) -> dict:
|
|
146
|
+
"""Normalize metrics.json from different versions of evolve scripts.
|
|
147
|
+
|
|
148
|
+
Supports:
|
|
149
|
+
- evolve_skill.py format: skill_name, baseline_score, evolved_score, improvement
|
|
150
|
+
- evolve_now.py format: skill, original_size, evolved_size, diff_lines
|
|
151
|
+
"""
|
|
152
|
+
m = dict(raw) # shallow copy
|
|
153
|
+
|
|
154
|
+
# Normalize skill name key
|
|
155
|
+
if "skill" in m and "skill_name" not in m:
|
|
156
|
+
m["skill_name"] = m["skill"]
|
|
157
|
+
|
|
158
|
+
# Normalize scores
|
|
159
|
+
if "baseline_score" not in m:
|
|
160
|
+
if "original_size" in m and m["original_size"] > 0:
|
|
161
|
+
# Derive a pseudo-score from size reduction (lower = more concise = better)
|
|
162
|
+
ratio = m.get("evolved_size", m["original_size"]) / m["original_size"]
|
|
163
|
+
m["baseline_score"] = 0.5 # placeholder
|
|
164
|
+
m["evolved_score"] = min(1.0, 0.5 + (1 - ratio) * 0.3)
|
|
165
|
+
m["improvement"] = m["evolved_score"] - m["baseline_score"]
|
|
166
|
+
else:
|
|
167
|
+
m["baseline_score"] = 0.0
|
|
168
|
+
m["evolved_score"] = 0.0
|
|
169
|
+
m["improvement"] = 0.0
|
|
170
|
+
|
|
171
|
+
# Normalize constraints_passed
|
|
172
|
+
if "constraints_passed" not in m:
|
|
173
|
+
m["constraints_passed"] = m.get("diff_lines", 0) > 0
|
|
174
|
+
|
|
175
|
+
# Normalize elapsed
|
|
176
|
+
if "elapsed_seconds" not in m:
|
|
177
|
+
m["elapsed_seconds"] = 0.0
|
|
178
|
+
|
|
179
|
+
# Normalize iterations
|
|
180
|
+
if "iterations" not in m:
|
|
181
|
+
m["iterations"] = 3
|
|
182
|
+
|
|
183
|
+
return m
|
|
184
|
+
|
|
185
|
+
def _find_skill_file(skill_name: str) -> Optional[Path]:
|
|
186
|
+
"""Find a SKILL.md by name, searching recursively."""
|
|
187
|
+
if not SKILLS_DIR.exists():
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Direct path
|
|
191
|
+
direct = SKILLS_DIR / skill_name / "SKILL.md"
|
|
192
|
+
if direct.exists():
|
|
193
|
+
return direct
|
|
194
|
+
|
|
195
|
+
# Recursive search
|
|
196
|
+
for f in SKILLS_DIR.rglob("SKILL.md"):
|
|
197
|
+
if f.parent.name == skill_name:
|
|
198
|
+
return f
|
|
199
|
+
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def _parse_skill_content(skill_file: Path) -> dict:
|
|
203
|
+
"""Parse a skill file into frontmatter + body."""
|
|
204
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
205
|
+
frontmatter = {}
|
|
206
|
+
body = content
|
|
207
|
+
|
|
208
|
+
if content.startswith("---"):
|
|
209
|
+
try:
|
|
210
|
+
import yaml
|
|
211
|
+
end = content.index("---", 3)
|
|
212
|
+
frontmatter = yaml.safe_load(content[3:end]) or {}
|
|
213
|
+
body = content[end + 3:].strip()
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"frontmatter": frontmatter,
|
|
219
|
+
"body": body,
|
|
220
|
+
"raw": content,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# ── SKILLS endpoints ───────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
class ToggleSkillRequest(BaseModel):
|
|
226
|
+
provider: str
|
|
227
|
+
skill_name: str
|
|
228
|
+
enabled: bool
|
|
229
|
+
|
|
230
|
+
@app.get("/api/skills/providers")
|
|
231
|
+
async def list_providers():
|
|
232
|
+
"""Lista todos los proveedores de skills detectados con sus estadísticas"""
|
|
233
|
+
try:
|
|
234
|
+
providers = get_skill_registry().get_providers()
|
|
235
|
+
return {"status": "ok", "providers": providers}
|
|
236
|
+
except Exception as e:
|
|
237
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
238
|
+
|
|
239
|
+
@app.get("/api/skills/provider/{provider_name}")
|
|
240
|
+
async def get_provider_skills(provider_name: str):
|
|
241
|
+
"""Lista todas las skills de un proveedor específico"""
|
|
242
|
+
try:
|
|
243
|
+
providers = get_skill_registry().get_providers()
|
|
244
|
+
for p in providers:
|
|
245
|
+
if p["name"] == provider_name:
|
|
246
|
+
return {"status": "ok", "provider": p}
|
|
247
|
+
raise HTTPException(status_code=404, detail=f"Proveedor '{provider_name}' no encontrado")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
250
|
+
|
|
251
|
+
@app.get("/api/skills/refresh")
|
|
252
|
+
async def refresh_skills():
|
|
253
|
+
"""Fuerza un re-escaneo de skills (útil para desarrollo)"""
|
|
254
|
+
try:
|
|
255
|
+
global _skill_registry
|
|
256
|
+
_skill_registry = get_registry().rescan()
|
|
257
|
+
providers = _skill_registry.get_providers()
|
|
258
|
+
return {"status": "ok", "providers": providers}
|
|
259
|
+
except Exception as e:
|
|
260
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
261
|
+
|
|
262
|
+
@app.post("/api/skills/toggle")
|
|
263
|
+
async def toggle_skill(request: ToggleSkillRequest):
|
|
264
|
+
"""Activa o desactiva una skill"""
|
|
265
|
+
try:
|
|
266
|
+
get_skill_registry().toggle_provider_skill(request.provider, request.skill_name, request.enabled)
|
|
267
|
+
return {"status": "ok", "enabled": request.enabled}
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
270
|
+
|
|
271
|
+
@app.delete("/api/skills/global/{skill_name}")
|
|
272
|
+
async def delete_global_skill(skill_name: str):
|
|
273
|
+
"""Elimina una skill GLOBALMENTE (de global_skills/ y todos los symlinks)."""
|
|
274
|
+
try:
|
|
275
|
+
registry = get_registry()
|
|
276
|
+
target_gid = None
|
|
277
|
+
target_skill = None
|
|
278
|
+
for gid, s in registry.global_skills.items():
|
|
279
|
+
if s.name == skill_name and not s.is_fork:
|
|
280
|
+
target_gid = gid
|
|
281
|
+
target_skill = s
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
if not target_skill:
|
|
285
|
+
raise HTTPException(status_code=404, detail="Skill global no encontrada o es un fork")
|
|
286
|
+
canonical = Path(target_skill.canonical_path)
|
|
287
|
+
|
|
288
|
+
# 1. Eliminar symlinks en todos los providers PRIMERO
|
|
289
|
+
for provider_name, src_dir in registry.PROVIDER_SOURCES.items():
|
|
290
|
+
if not src_dir.exists():
|
|
291
|
+
continue
|
|
292
|
+
link = src_dir / skill_name
|
|
293
|
+
if link.exists() and link.is_symlink():
|
|
294
|
+
try:
|
|
295
|
+
if link.resolve() == canonical:
|
|
296
|
+
link.unlink()
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# 2. Eliminar directorio canónico
|
|
301
|
+
if canonical.exists() and canonical.is_dir():
|
|
302
|
+
shutil.rmtree(canonical)
|
|
303
|
+
|
|
304
|
+
keys_to_delete = [k for k, v in registry.provider_index.items() if v == target_gid]
|
|
305
|
+
for k in keys_to_delete:
|
|
306
|
+
del registry.provider_index[k]
|
|
307
|
+
del registry.global_skills[target_gid]
|
|
308
|
+
|
|
309
|
+
registry.save()
|
|
310
|
+
return {"status": "ok", "message": f"Skill '{skill_name}' eliminada globalmente"}
|
|
311
|
+
except HTTPException:
|
|
312
|
+
raise
|
|
313
|
+
except Exception as e:
|
|
314
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
315
|
+
|
|
316
|
+
@app.delete("/api/skills/{provider}/{skill_name}")
|
|
317
|
+
async def delete_skill(provider: str, skill_name: str):
|
|
318
|
+
"""Elimina una skill de un provider específico (desenlaza, pero preserva la skill global)."""
|
|
319
|
+
try:
|
|
320
|
+
registry = get_registry()
|
|
321
|
+
key = f"{provider}.{skill_name}"
|
|
322
|
+
if key not in registry.provider_index:
|
|
323
|
+
raise HTTPException(status_code=404, detail="Skill no encontrada para ese provider")
|
|
324
|
+
|
|
325
|
+
gid = registry.provider_index[key]
|
|
326
|
+
if gid not in registry.global_skills:
|
|
327
|
+
del registry.provider_index[key]
|
|
328
|
+
registry.save()
|
|
329
|
+
return {"status": "ok", "message": f"Enlace eliminado (skill global no existía)"}
|
|
330
|
+
|
|
331
|
+
skill = registry.global_skills[gid]
|
|
332
|
+
skill.providers = [p for p in skill.providers if p['name'] != provider]
|
|
333
|
+
del registry.provider_index[key]
|
|
334
|
+
registry.save()
|
|
335
|
+
return {"status": "ok", "message": f"Skill '{skill_name}' desenlazada de '{provider}'"}
|
|
336
|
+
except HTTPException:
|
|
337
|
+
raise
|
|
338
|
+
except Exception as e:
|
|
339
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
340
|
+
|
|
341
|
+
@app.get("/api/skills/{skill_name}")
|
|
342
|
+
async def get_skill(skill_name: str):
|
|
343
|
+
"""Get full skill content + metadata."""
|
|
344
|
+
skill_file = _find_skill_file(skill_name)
|
|
345
|
+
if not skill_file:
|
|
346
|
+
raise HTTPException(404, f"Skill '{skill_name}' not found")
|
|
347
|
+
|
|
348
|
+
parsed = _parse_skill_content(skill_file)
|
|
349
|
+
return {
|
|
350
|
+
"name": skill_name,
|
|
351
|
+
"path": str(skill_file.relative_to(HERMES_REPO)),
|
|
352
|
+
"frontmatter": parsed["frontmatter"],
|
|
353
|
+
"body": parsed["body"],
|
|
354
|
+
"raw": parsed["raw"],
|
|
355
|
+
"size": len(parsed["raw"]),
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
@app.get("/api/evolution/evolvable")
|
|
359
|
+
async def list_evolvable_skills(provider: Optional[str] = None):
|
|
360
|
+
"""List skills that can be evolved, grouped by provider (Hermes, Claude Code, OpenCode)."""
|
|
361
|
+
from .skill_registry import get_registry
|
|
362
|
+
registry = get_registry()
|
|
363
|
+
|
|
364
|
+
providers = registry.get_providers()
|
|
365
|
+
result = []
|
|
366
|
+
for p in providers:
|
|
367
|
+
if provider and p["name"] != provider:
|
|
368
|
+
continue
|
|
369
|
+
# Only include providers with evolvable skills (have a SKILL.md)
|
|
370
|
+
evolvable = [
|
|
371
|
+
s for s in p["skills"]
|
|
372
|
+
if s["enabled"] and not s.get("is_fork")
|
|
373
|
+
]
|
|
374
|
+
if evolvable:
|
|
375
|
+
result.append({
|
|
376
|
+
"provider": p["name"],
|
|
377
|
+
"total": p["total"],
|
|
378
|
+
"enabled": p["enabled"],
|
|
379
|
+
"skills": evolvable,
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
# Sort by total skills desc
|
|
383
|
+
result.sort(key=lambda x: x["total"], reverse=True)
|
|
384
|
+
return {"status": "ok", "providers": result}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@app.get("/api/evolution/runs")
|
|
388
|
+
async def list_evolution_runs():
|
|
389
|
+
"""List all evolution runs — from metrics.json AND job tracker."""
|
|
390
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
391
|
+
all_runs = []
|
|
392
|
+
seen_skills = set()
|
|
393
|
+
|
|
394
|
+
# 1. From metrics.json files (successful runs)
|
|
395
|
+
if output_dir.exists():
|
|
396
|
+
for skill_dir in output_dir.iterdir():
|
|
397
|
+
if skill_dir.is_dir() and skill_dir.name != "skills":
|
|
398
|
+
for run_dir in skill_dir.iterdir():
|
|
399
|
+
mf = run_dir / "metrics.json"
|
|
400
|
+
if mf.exists():
|
|
401
|
+
try:
|
|
402
|
+
data = _normalize_metrics(json.loads(mf.read_text()))
|
|
403
|
+
data.setdefault("run_dir", run_dir.name)
|
|
404
|
+
all_runs.append(data)
|
|
405
|
+
seen_skills.add(skill_dir.name)
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
# 2. From job tracker (completed/failed runs without metrics.json)
|
|
410
|
+
for job in tracker.get_all_jobs(limit=200):
|
|
411
|
+
if job.status in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
412
|
+
# Check if we already have metrics for this skill
|
|
413
|
+
has_metrics = any(
|
|
414
|
+
r.get("skill_name") == job.skill_name
|
|
415
|
+
and r.get("timestamp", "").replace("_", "").startswith(
|
|
416
|
+
job.started_at.replace("-", "").replace(":", "").replace("T", "")[:8]
|
|
417
|
+
)
|
|
418
|
+
for r in all_runs
|
|
419
|
+
)
|
|
420
|
+
if not has_metrics:
|
|
421
|
+
all_runs.append({
|
|
422
|
+
"skill_name": job.skill_name,
|
|
423
|
+
"timestamp": job.started_at.replace("-", "").replace(":", "").replace("T", "_")[:15],
|
|
424
|
+
"iterations": job.iterations,
|
|
425
|
+
"baseline_score": job.baseline_score or 0.0,
|
|
426
|
+
"evolved_score": job.evolved_score or 0.0,
|
|
427
|
+
"improvement": job.improvement or 0.0,
|
|
428
|
+
"elapsed_seconds": 0.0,
|
|
429
|
+
"constraints_passed": job.status == JobStatus.COMPLETED,
|
|
430
|
+
"status": job.status.value,
|
|
431
|
+
"error": job.error,
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
return sorted(all_runs, key=lambda r: r.get("timestamp", ""), reverse=True)
|
|
435
|
+
|
|
436
|
+
# ── EVOLUTION endpoints ────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
@app.get("/api/skills/{skill_name}/evolution-history")
|
|
439
|
+
async def skill_history(skill_name: str):
|
|
440
|
+
"""Get evolution run history for a skill."""
|
|
441
|
+
output_dir = EVOLUTION_DIR / "output" / skill_name
|
|
442
|
+
runs = []
|
|
443
|
+
|
|
444
|
+
if output_dir.exists():
|
|
445
|
+
for run_dir in sorted(output_dir.iterdir(), reverse=True):
|
|
446
|
+
metrics_file = run_dir / "metrics.json"
|
|
447
|
+
if metrics_file.exists():
|
|
448
|
+
try:
|
|
449
|
+
data = json.loads(metrics_file.read_text())
|
|
450
|
+
data["run_dir"] = str(run_dir.name)
|
|
451
|
+
runs.append(_normalize_metrics(data))
|
|
452
|
+
except Exception:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
# Also add from job tracker
|
|
456
|
+
for job in tracker.get_all_jobs(limit=50):
|
|
457
|
+
if job.skill_name == skill_name and job.status in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
458
|
+
has_metrics = any(r.get("timestamp", "").startswith(job.started_at[:10]) for r in runs)
|
|
459
|
+
if not has_metrics:
|
|
460
|
+
runs.append({
|
|
461
|
+
"skill_name": job.skill_name,
|
|
462
|
+
"timestamp": job.started_at.replace("-", "").replace(":", "").replace("T", "_")[:15],
|
|
463
|
+
"iterations": job.iterations,
|
|
464
|
+
"baseline_score": job.baseline_score or 0.0,
|
|
465
|
+
"evolved_score": job.evolved_score or 0.0,
|
|
466
|
+
"improvement": job.improvement or 0.0,
|
|
467
|
+
"elapsed_seconds": 0.0,
|
|
468
|
+
"constraints_passed": job.status == JobStatus.COMPLETED,
|
|
469
|
+
"status": job.status.value,
|
|
470
|
+
"error": job.error,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
return runs
|
|
474
|
+
|
|
475
|
+
@app.get("/api/skills/{skill_name}/evolution/{run_dir}/diff")
|
|
476
|
+
async def skill_diff(skill_name: str, run_dir: str):
|
|
477
|
+
"""Get baseline vs evolved skill content for a specific run."""
|
|
478
|
+
skill_path = EVOLUTION_DIR / "output" / skill_name
|
|
479
|
+
|
|
480
|
+
# Support "latest" keyword
|
|
481
|
+
if run_dir == "latest":
|
|
482
|
+
if not skill_path.exists():
|
|
483
|
+
raise HTTPException(404, f"No evolution runs found for skill '{skill_name}'")
|
|
484
|
+
runs = sorted(
|
|
485
|
+
[d for d in skill_path.iterdir() if d.is_dir()],
|
|
486
|
+
key=lambda d: d.stat().st_mtime,
|
|
487
|
+
reverse=True,
|
|
488
|
+
)
|
|
489
|
+
if not runs:
|
|
490
|
+
raise HTTPException(404, f"No evolution runs found for skill '{skill_name}'")
|
|
491
|
+
run_path = runs[0]
|
|
492
|
+
else:
|
|
493
|
+
run_path = skill_path / run_dir
|
|
494
|
+
|
|
495
|
+
if not run_path.exists():
|
|
496
|
+
raise HTTPException(404, f"Run '{run_dir}' not found for skill '{skill_name}'")
|
|
497
|
+
|
|
498
|
+
baseline_file = run_path / "baseline_skill.md"
|
|
499
|
+
evolved_file = run_path / "evolved_skill.md"
|
|
500
|
+
metrics_file = run_path / "metrics.json"
|
|
501
|
+
|
|
502
|
+
result = {
|
|
503
|
+
"skill_name": skill_name,
|
|
504
|
+
"run_dir": run_dir,
|
|
505
|
+
"baseline": None,
|
|
506
|
+
"evolved": None,
|
|
507
|
+
"metrics": None,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if baseline_file.exists():
|
|
511
|
+
result["baseline"] = baseline_file.read_text(encoding="utf-8", errors="replace")
|
|
512
|
+
if evolved_file.exists():
|
|
513
|
+
result["evolved"] = evolved_file.read_text(encoding="utf-8", errors="replace")
|
|
514
|
+
if metrics_file.exists():
|
|
515
|
+
try:
|
|
516
|
+
result["metrics"] = json.loads(metrics_file.read_text())
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
return result
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@app.post("/api/evolution/validate")
|
|
524
|
+
async def validate_evolution_endpoint(skill_name: str):
|
|
525
|
+
"""Validate latest evolution with LLM Judge + holdout dataset."""
|
|
526
|
+
import subprocess as sp
|
|
527
|
+
|
|
528
|
+
validate_path = Path(__file__).parent / "validate_evolution.py"
|
|
529
|
+
cmd = [PYTHON_BIN, "-u", str(validate_path), "--skill", skill_name]
|
|
530
|
+
env = os.environ.copy()
|
|
531
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
result = sp.run(cmd, capture_output=True, text=True, timeout=180, env=env)
|
|
535
|
+
runs = sorted(
|
|
536
|
+
[d for d in (EVOLUTION_DIR / "output" / skill_name).iterdir() if d.is_dir()],
|
|
537
|
+
key=lambda d: d.stat().st_mtime, reverse=True,
|
|
538
|
+
)
|
|
539
|
+
if runs:
|
|
540
|
+
rp = runs[0] / "validation_report.json"
|
|
541
|
+
if rp.exists():
|
|
542
|
+
return json.loads(rp.read_text())
|
|
543
|
+
return {"skill_name": skill_name, "final_verdict": "FAIL ❌",
|
|
544
|
+
"error": "No report generated", "stdout": result.stdout[-500:]}
|
|
545
|
+
except sp.TimeoutExpired:
|
|
546
|
+
raise HTTPException(504, "Validation timed out (>3 min)")
|
|
547
|
+
except Exception as e:
|
|
548
|
+
raise HTTPException(500, str(e))
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@app.get("/api/evolution/validate/{skill_name}")
|
|
552
|
+
async def get_validation_report(skill_name: str):
|
|
553
|
+
"""Get latest validation report for a skill."""
|
|
554
|
+
output_dir = EVOLUTION_DIR / "output" / skill_name
|
|
555
|
+
if not output_dir.exists():
|
|
556
|
+
raise HTTPException(404, f"No evolution output for '{skill_name}'")
|
|
557
|
+
runs = sorted([d for d in output_dir.iterdir() if d.is_dir()],
|
|
558
|
+
key=lambda d: d.stat().st_mtime, reverse=True)
|
|
559
|
+
for run in runs:
|
|
560
|
+
rp = run / "validation_report.json"
|
|
561
|
+
if rp.exists():
|
|
562
|
+
return json.loads(rp.read_text())
|
|
563
|
+
raise HTTPException(404, f"No validation report for '{skill_name}'")
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
@app.post("/api/evolution/start")
|
|
567
|
+
async def start_evolution(req: EvolveRequest):
|
|
568
|
+
"""Start an evolution run with full job tracking."""
|
|
569
|
+
|
|
570
|
+
# Check for existing active jobs for this skill
|
|
571
|
+
active = tracker.get_active_jobs()
|
|
572
|
+
for job in active:
|
|
573
|
+
if job.skill_name == req.skill_name:
|
|
574
|
+
return {
|
|
575
|
+
"error": f"Evolution already running for '{req.skill_name}'",
|
|
576
|
+
"job_id": job.id,
|
|
577
|
+
"status": job.status.value,
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
# Create tracked job
|
|
581
|
+
job = tracker.create_job(req.skill_name, req.iterations)
|
|
582
|
+
job.add_log(f"Starting evolution for skill: {req.skill_name}")
|
|
583
|
+
job.add_log(f"Iterations: {req.iterations}, Source: {req.eval_source}")
|
|
584
|
+
|
|
585
|
+
# Build command — use SDD evolution engine
|
|
586
|
+
sdd_evolve_path = Path(__file__).parent / "sdd_evolve.py"
|
|
587
|
+
cmd = [
|
|
588
|
+
"/usr/bin/python3", "-u", str(sdd_evolve_path),
|
|
589
|
+
"--skill", req.skill_name,
|
|
590
|
+
"--iterations", str(req.iterations),
|
|
591
|
+
"--eval-source", req.eval_source,
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
env = os.environ.copy()
|
|
595
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
596
|
+
env["HERMES_AGENT_REPO"] = str(HERMES_REPO)
|
|
597
|
+
env["TERM"] = "dumb"
|
|
598
|
+
|
|
599
|
+
# ── Provider selection: Ollama preferred when available ───────────
|
|
600
|
+
def _ollama_up() -> bool:
|
|
601
|
+
import urllib.request
|
|
602
|
+
try:
|
|
603
|
+
with urllib.request.urlopen("http://localhost:11434/v1/models", timeout=2) as r:
|
|
604
|
+
return r.status == 200
|
|
605
|
+
except Exception:
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
_dotenv = dotenv_values(str(Path.home() / ".hermes" / ".env"))
|
|
609
|
+
for key in ("OLLAMA_API_BASE", "SDD_OLLAMA_MODEL", "SDD_EVOLVE_MODEL"):
|
|
610
|
+
if key in os.environ:
|
|
611
|
+
env[key] = os.environ[key]
|
|
612
|
+
elif _dotenv.get(key):
|
|
613
|
+
env[key] = _dotenv[key]
|
|
614
|
+
|
|
615
|
+
if _ollama_up():
|
|
616
|
+
# Local Ollama is alive — use it regardless of stale cloud keys in .env
|
|
617
|
+
base = env.get("OLLAMA_API_BASE", "http://localhost:11434/v1")
|
|
618
|
+
if not base.rstrip("/").endswith("/v1"):
|
|
619
|
+
base = base.rstrip("/") + "/v1"
|
|
620
|
+
env["OPENAI_BASE_URL"] = base
|
|
621
|
+
env["OPENAI_API_KEY"] = "ollama"
|
|
622
|
+
env.setdefault("SDD_EVOLVE_MODEL", env.get("SDD_OLLAMA_MODEL", "gemma4:31b-cloud"))
|
|
623
|
+
job.add_log("Provider: Ollama local (gemma4:31b-cloud)")
|
|
624
|
+
else:
|
|
625
|
+
# Forward cloud keys only when Ollama is NOT available
|
|
626
|
+
for key in ("OPENROUTER_API_KEY", "NOUS_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"):
|
|
627
|
+
if key in os.environ:
|
|
628
|
+
env[key] = os.environ[key]
|
|
629
|
+
elif _dotenv.get(key):
|
|
630
|
+
env[key] = _dotenv[key]
|
|
631
|
+
env.setdefault("OPENAI_BASE_URL", "https://openrouter.ai/api/v1")
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
process = await asyncio.create_subprocess_exec(
|
|
635
|
+
*cmd,
|
|
636
|
+
cwd=str(Path(__file__).parent),
|
|
637
|
+
env=env,
|
|
638
|
+
stdout=asyncio.subprocess.PIPE,
|
|
639
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
640
|
+
)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
job.status = JobStatus.FAILED
|
|
643
|
+
job.error = f"Failed to start process: {e}"
|
|
644
|
+
job.completed_at = datetime.now().isoformat()
|
|
645
|
+
job.save_log()
|
|
646
|
+
return {"error": job.error, "job_id": job.id}
|
|
647
|
+
|
|
648
|
+
job.pid = process.pid
|
|
649
|
+
job.status = JobStatus.LOADING_SKILL
|
|
650
|
+
tracker.set_process(job.id, process)
|
|
651
|
+
|
|
652
|
+
# Stream and parse output
|
|
653
|
+
async def stream_output():
|
|
654
|
+
try:
|
|
655
|
+
while True:
|
|
656
|
+
line = await process.stdout.readline()
|
|
657
|
+
if not line:
|
|
658
|
+
break
|
|
659
|
+
text = line.decode("utf-8", errors="replace").strip()
|
|
660
|
+
|
|
661
|
+
if text:
|
|
662
|
+
# Parse line for structured updates
|
|
663
|
+
tracker.parse_line(job, text)
|
|
664
|
+
|
|
665
|
+
# Broadcast to WebSocket clients
|
|
666
|
+
await manager.broadcast({
|
|
667
|
+
"type": "evolution_log",
|
|
668
|
+
"job_id": job.id,
|
|
669
|
+
"skill": req.skill_name,
|
|
670
|
+
"status": job.status.value,
|
|
671
|
+
"progress": job.progress,
|
|
672
|
+
"iteration": job.current_iteration,
|
|
673
|
+
"total_iterations": job.iterations,
|
|
674
|
+
"message": text,
|
|
675
|
+
"timestamp": datetime.now().isoformat(),
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
# Process ended
|
|
679
|
+
await process.wait()
|
|
680
|
+
if job.status not in (JobStatus.COMPLETED, JobStatus.FAILED):
|
|
681
|
+
if process.returncode == 0:
|
|
682
|
+
job.status = JobStatus.COMPLETED
|
|
683
|
+
job.completed_at = datetime.now().isoformat()
|
|
684
|
+
job.add_log("Process completed successfully")
|
|
685
|
+
else:
|
|
686
|
+
job.status = JobStatus.FAILED
|
|
687
|
+
job.completed_at = datetime.now().isoformat()
|
|
688
|
+
job.error = f"Process exited with code {process.returncode}"
|
|
689
|
+
job.add_log(f"Process failed: {job.error}")
|
|
690
|
+
|
|
691
|
+
# Try to load final metrics from output
|
|
692
|
+
output_dir = EVOLUTION_DIR / "output" / req.skill_name
|
|
693
|
+
if output_dir.exists():
|
|
694
|
+
latest_run = max(
|
|
695
|
+
[d for d in output_dir.iterdir() if d.is_dir()],
|
|
696
|
+
key=lambda d: d.stat().st_mtime,
|
|
697
|
+
default=None,
|
|
698
|
+
)
|
|
699
|
+
if latest_run:
|
|
700
|
+
mf = latest_run / "metrics.json"
|
|
701
|
+
if mf.exists():
|
|
702
|
+
try:
|
|
703
|
+
metrics = json.loads(mf.read_text())
|
|
704
|
+
job.baseline_score = metrics.get("baseline_score")
|
|
705
|
+
job.evolved_score = metrics.get("evolved_score")
|
|
706
|
+
job.improvement = metrics.get("improvement")
|
|
707
|
+
except Exception:
|
|
708
|
+
pass
|
|
709
|
+
|
|
710
|
+
job.save_log()
|
|
711
|
+
tracker.cleanup_process(job.id)
|
|
712
|
+
|
|
713
|
+
# Broadcast completion
|
|
714
|
+
await manager.broadcast({
|
|
715
|
+
"type": "evolution_complete",
|
|
716
|
+
"job_id": job.id,
|
|
717
|
+
"skill": req.skill_name,
|
|
718
|
+
"status": job.status.value,
|
|
719
|
+
"baseline_score": job.baseline_score,
|
|
720
|
+
"evolved_score": job.evolved_score,
|
|
721
|
+
"improvement": job.improvement,
|
|
722
|
+
"timestamp": datetime.now().isoformat(),
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
except Exception as e:
|
|
726
|
+
job.status = JobStatus.FAILED
|
|
727
|
+
job.error = str(e)
|
|
728
|
+
job.completed_at = datetime.now().isoformat()
|
|
729
|
+
job.save_log()
|
|
730
|
+
tracker.cleanup_process(job.id)
|
|
731
|
+
|
|
732
|
+
asyncio.create_task(stream_output())
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
"job_id": job.id,
|
|
736
|
+
"skill": req.skill_name,
|
|
737
|
+
"status": job.status.value,
|
|
738
|
+
"iterations": req.iterations,
|
|
739
|
+
"pid": process.pid,
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
# ── Job tracking endpoints ────────────────────────────────────────────
|
|
743
|
+
|
|
744
|
+
@app.get("/api/jobs")
|
|
745
|
+
async def list_jobs(active_only: bool = False, limit: int = 50):
|
|
746
|
+
"""List evolution jobs."""
|
|
747
|
+
if active_only:
|
|
748
|
+
jobs = tracker.get_active_jobs()
|
|
749
|
+
else:
|
|
750
|
+
jobs = tracker.get_all_jobs(limit)
|
|
751
|
+
return [j.to_dict() for j in jobs]
|
|
752
|
+
|
|
753
|
+
@app.get("/api/jobs/{job_id}")
|
|
754
|
+
async def get_job(job_id: str):
|
|
755
|
+
"""Get detailed job status including logs."""
|
|
756
|
+
job = tracker.get_job(job_id)
|
|
757
|
+
if not job:
|
|
758
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
759
|
+
return job.to_dict()
|
|
760
|
+
|
|
761
|
+
@app.get("/api/jobs/{job_id}/logs")
|
|
762
|
+
async def get_job_logs(job_id: str, since: int = 0):
|
|
763
|
+
"""Get job logs (optionally only lines after index `since`)."""
|
|
764
|
+
job = tracker.get_job(job_id)
|
|
765
|
+
if not job:
|
|
766
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
767
|
+
return {
|
|
768
|
+
"job_id": job_id,
|
|
769
|
+
"total_lines": len(job.logs),
|
|
770
|
+
"logs": job.logs[since:],
|
|
771
|
+
"status": job.status.value,
|
|
772
|
+
"progress": job.progress,
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
@app.delete("/api/jobs/{job_id}")
|
|
776
|
+
async def cancel_job(job_id: str):
|
|
777
|
+
"""Cancel a running evolution job."""
|
|
778
|
+
job = tracker.get_job(job_id)
|
|
779
|
+
if not job:
|
|
780
|
+
raise HTTPException(404, f"Job '{job_id}' not found")
|
|
781
|
+
|
|
782
|
+
process = tracker.get_process(job_id)
|
|
783
|
+
if process:
|
|
784
|
+
try:
|
|
785
|
+
process.terminate()
|
|
786
|
+
await process.wait()
|
|
787
|
+
except Exception:
|
|
788
|
+
pass
|
|
789
|
+
tracker.cleanup_process(job_id)
|
|
790
|
+
|
|
791
|
+
job.status = JobStatus.FAILED
|
|
792
|
+
job.error = "Cancelled by user"
|
|
793
|
+
job.completed_at = datetime.now().isoformat()
|
|
794
|
+
job.save_log()
|
|
795
|
+
|
|
796
|
+
await manager.broadcast({
|
|
797
|
+
"type": "evolution_cancelled",
|
|
798
|
+
"job_id": job_id,
|
|
799
|
+
"skill": job.skill_name,
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
return {"status": "cancelled", "job_id": job_id}
|
|
803
|
+
|
|
804
|
+
@app.get("/api/evolution/{skill_name}/output/{run_id}")
|
|
805
|
+
async def get_run_output(skill_name: str, run_id: str):
|
|
806
|
+
"""Get evolved vs baseline skill diff for a specific run."""
|
|
807
|
+
run_dir = EVOLUTION_DIR / "output" / skill_name / run_id
|
|
808
|
+
|
|
809
|
+
if not run_dir.exists():
|
|
810
|
+
raise HTTPException(404, "Run not found")
|
|
811
|
+
|
|
812
|
+
result = {}
|
|
813
|
+
|
|
814
|
+
for f in ["evolved_skill.md", "baseline_skill.md", "evolved_FAILED.md"]:
|
|
815
|
+
fp = run_dir / f
|
|
816
|
+
if fp.exists():
|
|
817
|
+
result[f.replace(".md", "")] = fp.read_text(encoding="utf-8", errors="replace")
|
|
818
|
+
|
|
819
|
+
metrics_file = run_dir / "metrics.json"
|
|
820
|
+
if metrics_file.exists():
|
|
821
|
+
raw = json.loads(metrics_file.read_text())
|
|
822
|
+
result["metrics"] = _normalize_metrics(raw)
|
|
823
|
+
|
|
824
|
+
return result
|
|
825
|
+
|
|
826
|
+
# ── DATASET endpoints ──────────────────────────────────────────────
|
|
827
|
+
|
|
828
|
+
@app.get("/api/datasets")
|
|
829
|
+
async def list_datasets():
|
|
830
|
+
"""List all eval datasets — from datasets/ dir AND evolution output."""
|
|
831
|
+
datasets = []
|
|
832
|
+
|
|
833
|
+
for base in [DATASETS_DIR, EVOLUTION_DIR / "datasets", EVOLUTION_DIR / "output"]:
|
|
834
|
+
if not base.exists():
|
|
835
|
+
continue
|
|
836
|
+
for skill_dir in base.iterdir():
|
|
837
|
+
if skill_dir.is_dir():
|
|
838
|
+
splits = {}
|
|
839
|
+
for split in ["train.jsonl", "val.jsonl", "holdout.jsonl"]:
|
|
840
|
+
fp = skill_dir / split
|
|
841
|
+
if fp.exists():
|
|
842
|
+
count = sum(1 for _ in open(fp))
|
|
843
|
+
splits[split.replace(".jsonl", "")] = count
|
|
844
|
+
if splits:
|
|
845
|
+
datasets.append({
|
|
846
|
+
"skill": skill_dir.name,
|
|
847
|
+
"path": str(skill_dir),
|
|
848
|
+
"splits": splits,
|
|
849
|
+
"total": sum(splits.values()),
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
return datasets
|
|
853
|
+
|
|
854
|
+
@app.get("/api/datasets/{skill_name}")
|
|
855
|
+
async def get_dataset(skill_name: str):
|
|
856
|
+
"""Get dataset examples for a skill."""
|
|
857
|
+
for base in [DATASETS_DIR, EVOLUTION_DIR / "datasets", EVOLUTION_DIR / "output"]:
|
|
858
|
+
dataset_dir = base / skill_name
|
|
859
|
+
if dataset_dir.exists():
|
|
860
|
+
result = {}
|
|
861
|
+
for split in ["train", "val", "holdout"]:
|
|
862
|
+
fp = dataset_dir / f"{split}.jsonl"
|
|
863
|
+
if fp.exists():
|
|
864
|
+
examples = []
|
|
865
|
+
for line in open(fp):
|
|
866
|
+
try:
|
|
867
|
+
examples.append(json.loads(line))
|
|
868
|
+
except Exception:
|
|
869
|
+
pass
|
|
870
|
+
result[split] = examples
|
|
871
|
+
return result
|
|
872
|
+
|
|
873
|
+
raise HTTPException(404, f"No dataset found for '{skill_name}'")
|
|
874
|
+
|
|
875
|
+
@app.get("/api/datasets/{skill_name}/sessions")
|
|
876
|
+
async def import_sessions(skill_name: str, source: str = "all", max_examples: int = 50):
|
|
877
|
+
"""Import and filter external sessions for dataset building."""
|
|
878
|
+
cmd = [
|
|
879
|
+
PYTHON_BIN, "-m", "evolution.core.external_importers",
|
|
880
|
+
"--source", source,
|
|
881
|
+
"--skill", skill_name,
|
|
882
|
+
"--max-examples", str(max_examples),
|
|
883
|
+
"--dry-run",
|
|
884
|
+
]
|
|
885
|
+
env = os.environ.copy()
|
|
886
|
+
env["PYTHONPATH"] = str(EVOLUTION_DIR)
|
|
887
|
+
env["HERMES_AGENT_REPO"] = str(HERMES_REPO)
|
|
888
|
+
|
|
889
|
+
result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(EVOLUTION_DIR), env=env)
|
|
890
|
+
|
|
891
|
+
return {
|
|
892
|
+
"stdout": result.stdout,
|
|
893
|
+
"stderr": result.stderr,
|
|
894
|
+
"returncode": result.returncode,
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
# ── MEMORY endpoints ───────────────────────────────────────────────
|
|
898
|
+
|
|
899
|
+
@app.get("/api/memory")
|
|
900
|
+
async def list_memory():
|
|
901
|
+
"""List all stored memories."""
|
|
902
|
+
memories = []
|
|
903
|
+
|
|
904
|
+
# Check .hermes/memory directory
|
|
905
|
+
if MEMORY_DIR.exists():
|
|
906
|
+
for f in sorted(MEMORY_DIR.glob("*.json")):
|
|
907
|
+
try:
|
|
908
|
+
data = json.loads(f.read_text())
|
|
909
|
+
memories.append({
|
|
910
|
+
"key": f.stem,
|
|
911
|
+
"data": data,
|
|
912
|
+
"modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
|
|
913
|
+
})
|
|
914
|
+
except Exception:
|
|
915
|
+
memories.append({
|
|
916
|
+
"key": f.stem,
|
|
917
|
+
"data": f.read_text()[:200],
|
|
918
|
+
"modified": datetime.fromtimestamp(f.stat().st_mtime).isoformat(),
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
# Also check SOUL.md memory section
|
|
922
|
+
soul_path = Path.home() / ".hermes" / "SOUL.md"
|
|
923
|
+
if soul_path.exists():
|
|
924
|
+
content = soul_path.read_text(encoding="utf-8", errors="replace")
|
|
925
|
+
if "MEMORY" in content:
|
|
926
|
+
memories.append({
|
|
927
|
+
"key": "SOUL_MEMORY",
|
|
928
|
+
"source": "SOUL.md",
|
|
929
|
+
"size": len(content),
|
|
930
|
+
})
|
|
931
|
+
|
|
932
|
+
return memories
|
|
933
|
+
|
|
934
|
+
# ── GRAPH endpoint (vis.js format — same as graphify) ─────────────────
|
|
935
|
+
|
|
936
|
+
COMMUNITY_COLORS = [
|
|
937
|
+
"#4E79A7", "#F28E2B", "#E15759", "#76B7B2", "#59A14F",
|
|
938
|
+
"#EDC948", "#B07AA1", "#FF9DA7", "#9C755F", "#BAB0AC",
|
|
939
|
+
]
|
|
940
|
+
|
|
941
|
+
@app.get("/api/graph")
|
|
942
|
+
async def get_graph():
|
|
943
|
+
"""Generate a vis.js knowledge graph from skills, memory, and evolution runs.
|
|
944
|
+
|
|
945
|
+
Format matches graphify's export: nodes + edges for vis-network.
|
|
946
|
+
"""
|
|
947
|
+
nodes = {}
|
|
948
|
+
edges = []
|
|
949
|
+
communities = {} # community_id -> [node_ids]
|
|
950
|
+
|
|
951
|
+
# ── 1. Skills as nodes ────────────────────────────────────────
|
|
952
|
+
skill_nodes = {}
|
|
953
|
+
if SKILLS_DIR.exists():
|
|
954
|
+
category_map = {} # category_dir -> community_id
|
|
955
|
+
cat_counter = 0
|
|
956
|
+
|
|
957
|
+
for skill_file in sorted(SKILLS_DIR.rglob("SKILL.md")):
|
|
958
|
+
skill_name = skill_file.parent.name
|
|
959
|
+
category = skill_file.parent.parent.name if skill_file.parent.parent != SKILLS_DIR else "root"
|
|
960
|
+
|
|
961
|
+
# Assign community by category
|
|
962
|
+
if category not in category_map:
|
|
963
|
+
category_map[category] = cat_counter
|
|
964
|
+
communities[cat_counter] = []
|
|
965
|
+
cat_counter += 1
|
|
966
|
+
cid = category_map[category]
|
|
967
|
+
|
|
968
|
+
# Read skill
|
|
969
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
970
|
+
stat = skill_file.stat()
|
|
971
|
+
desc = ""
|
|
972
|
+
if content.startswith("---"):
|
|
973
|
+
try:
|
|
974
|
+
import yaml
|
|
975
|
+
end = content.index("---", 3)
|
|
976
|
+
fm = yaml.safe_load(content[3:end])
|
|
977
|
+
desc = (fm.get("description") or "")[:80]
|
|
978
|
+
except Exception:
|
|
979
|
+
pass
|
|
980
|
+
|
|
981
|
+
node_id = f"skill:{skill_name}"
|
|
982
|
+
deg = 0 # will update after edges
|
|
983
|
+
nodes[node_id] = {
|
|
984
|
+
"id": node_id,
|
|
985
|
+
"label": skill_name,
|
|
986
|
+
"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)]}},
|
|
987
|
+
"size": 15,
|
|
988
|
+
"font": {"size": 11, "color": "#ffffff"},
|
|
989
|
+
"title": f"{skill_name}\n{desc}",
|
|
990
|
+
"community": cid,
|
|
991
|
+
"community_name": category,
|
|
992
|
+
"source_file": str(skill_file.relative_to(HERMES_REPO)),
|
|
993
|
+
"file_type": "skill",
|
|
994
|
+
"degree": 0,
|
|
995
|
+
}
|
|
996
|
+
communities[cid].append(node_id)
|
|
997
|
+
skill_nodes[skill_name] = node_id
|
|
998
|
+
|
|
999
|
+
# Edge: skill -> category
|
|
1000
|
+
cat_node_id = f"cat:{category}"
|
|
1001
|
+
if cat_node_id not in nodes:
|
|
1002
|
+
nodes[cat_node_id] = {
|
|
1003
|
+
"id": cat_node_id,
|
|
1004
|
+
"label": category,
|
|
1005
|
+
"color": {"background": "#555555", "border": "#555555", "highlight": {"background": "#ffffff", "border": "#555555"}},
|
|
1006
|
+
"size": 22,
|
|
1007
|
+
"font": {"size": 13, "color": "#ffffff"},
|
|
1008
|
+
"title": f"Category: {category}",
|
|
1009
|
+
"community": cid,
|
|
1010
|
+
"community_name": category,
|
|
1011
|
+
"source_file": "",
|
|
1012
|
+
"file_type": "category",
|
|
1013
|
+
"degree": 0,
|
|
1014
|
+
}
|
|
1015
|
+
edges.append({
|
|
1016
|
+
"from": node_id,
|
|
1017
|
+
"to": cat_node_id,
|
|
1018
|
+
"label": "belongs_to",
|
|
1019
|
+
"title": "belongs_to",
|
|
1020
|
+
"dashes": False,
|
|
1021
|
+
"width": 1,
|
|
1022
|
+
"color": {"opacity": 0.4},
|
|
1023
|
+
"confidence": "STRUCTURAL",
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
# ── 2. Evolution runs as edges between skill variants ─────────
|
|
1027
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
1028
|
+
if output_dir.exists():
|
|
1029
|
+
for skill_dir in output_dir.iterdir():
|
|
1030
|
+
if skill_dir.is_dir() and skill_dir.name != "skills":
|
|
1031
|
+
for run_dir in skill_dir.iterdir():
|
|
1032
|
+
mf = run_dir / "metrics.json"
|
|
1033
|
+
if mf.exists():
|
|
1034
|
+
try:
|
|
1035
|
+
data = json.loads(mf.read_text())
|
|
1036
|
+
evolved_id = f"evolved:{skill_dir.name}:{run_dir.name}"
|
|
1037
|
+
baseline_id = skill_nodes.get(skill_dir.name)
|
|
1038
|
+
|
|
1039
|
+
nodes[evolved_id] = {
|
|
1040
|
+
"id": evolved_id,
|
|
1041
|
+
"label": f"{skill_dir.name} ✦",
|
|
1042
|
+
"color": {"background": "#22c55e", "border": "#22c55e", "highlight": {"background": "#ffffff", "border": "#22c55e"}},
|
|
1043
|
+
"size": 10 + (data.get("improvement", 0) * 100),
|
|
1044
|
+
"font": {"size": 9, "color": "#a0a0a0"},
|
|
1045
|
+
"title": f"Evolved: {skill_dir.name}\nScore: {data.get('evolved_score', 0):.2f}\nImprovement: {data.get('improvement', 0)*100:.1f}%",
|
|
1046
|
+
"community": 0,
|
|
1047
|
+
"community_name": "evolved",
|
|
1048
|
+
"source_file": str(run_dir),
|
|
1049
|
+
"file_type": "evolution",
|
|
1050
|
+
"degree": 1,
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if baseline_id:
|
|
1054
|
+
edges.append({
|
|
1055
|
+
"from": baseline_id,
|
|
1056
|
+
"to": evolved_id,
|
|
1057
|
+
"label": "evolved_to",
|
|
1058
|
+
"title": f"+{(data.get('improvement', 0)*100):.1f}%",
|
|
1059
|
+
"dashes": True,
|
|
1060
|
+
"width": 2,
|
|
1061
|
+
"color": {"opacity": 0.8},
|
|
1062
|
+
"confidence": "EXTRACTED",
|
|
1063
|
+
})
|
|
1064
|
+
except Exception:
|
|
1065
|
+
pass
|
|
1066
|
+
|
|
1067
|
+
# ── 3. Memory entries as nodes ────────────────────────────────
|
|
1068
|
+
if MEMORY_DIR.exists():
|
|
1069
|
+
mem_cid = cat_counter
|
|
1070
|
+
communities[mem_cid] = []
|
|
1071
|
+
for f in sorted(MEMORY_DIR.glob("*.json")):
|
|
1072
|
+
try:
|
|
1073
|
+
mem_data = json.loads(f.read_text())
|
|
1074
|
+
mem_id = f"memory:{f.stem}"
|
|
1075
|
+
nodes[mem_id] = {
|
|
1076
|
+
"id": mem_id,
|
|
1077
|
+
"label": f.stem,
|
|
1078
|
+
"color": {"background": "#8b5cf6", "border": "#8b5cf6", "highlight": {"background": "#ffffff", "border": "#8b5cf6"}},
|
|
1079
|
+
"size": 12,
|
|
1080
|
+
"font": {"size": 10, "color": "#c4b5fd"},
|
|
1081
|
+
"title": f"Memory: {f.stem}",
|
|
1082
|
+
"community": mem_cid,
|
|
1083
|
+
"community_name": "memory",
|
|
1084
|
+
"source_file": str(f),
|
|
1085
|
+
"file_type": "memory",
|
|
1086
|
+
"degree": 0,
|
|
1087
|
+
}
|
|
1088
|
+
communities[mem_cid].append(mem_id)
|
|
1089
|
+
except Exception:
|
|
1090
|
+
pass
|
|
1091
|
+
|
|
1092
|
+
# ── 4. SOUL.md as hub node ────────────────────────────────────
|
|
1093
|
+
soul_path = Path.home() / ".hermes" / "SOUL.md"
|
|
1094
|
+
if soul_path.exists():
|
|
1095
|
+
soul_id = "soul:SOUL.md"
|
|
1096
|
+
nodes[soul_id] = {
|
|
1097
|
+
"id": soul_id,
|
|
1098
|
+
"label": "SOUL.md",
|
|
1099
|
+
"color": {"background": "#eab308", "border": "#eab308", "highlight": {"background": "#ffffff", "border": "#eab308"}},
|
|
1100
|
+
"size": 25,
|
|
1101
|
+
"font": {"size": 13, "color": "#ffffff"},
|
|
1102
|
+
"title": "Hermes Soul — Core identity and memory",
|
|
1103
|
+
"community": cat_counter + 1,
|
|
1104
|
+
"community_name": "core",
|
|
1105
|
+
"source_file": str(soul_path),
|
|
1106
|
+
"file_type": "core",
|
|
1107
|
+
"degree": 0,
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
# ── Calculate degrees and node sizes ──────────────────────────
|
|
1111
|
+
for e in edges:
|
|
1112
|
+
if e["from"] in nodes:
|
|
1113
|
+
nodes[e["from"]]["degree"] = nodes[e["from"]].get("degree", 0) + 1
|
|
1114
|
+
if e["to"] in nodes:
|
|
1115
|
+
nodes[e["to"]]["degree"] = nodes[e["to"]].get("degree", 0) + 1
|
|
1116
|
+
|
|
1117
|
+
max_deg = max((n.get("degree", 0) for n in nodes.values()), default=1) or 1
|
|
1118
|
+
for n in nodes.values():
|
|
1119
|
+
deg = n.get("degree", 0)
|
|
1120
|
+
n["size"] = round(10 + 30 * (deg / max_deg), 1)
|
|
1121
|
+
n["font"]["size"] = 12 if deg >= max_deg * 0.15 else 0
|
|
1122
|
+
|
|
1123
|
+
# ── Legend ────────────────────────────────────────────────────
|
|
1124
|
+
legend = []
|
|
1125
|
+
for cid, label in enumerate(list(category_map.keys()) if 'category_map' in dir() else []):
|
|
1126
|
+
legend.append({
|
|
1127
|
+
"cid": cid,
|
|
1128
|
+
"color": COMMUNITY_COLORS[cid % len(COMMUNITY_COLORS)],
|
|
1129
|
+
"label": label,
|
|
1130
|
+
"count": len(communities.get(cid, [])),
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
"nodes": list(nodes.values()),
|
|
1135
|
+
"edges": edges,
|
|
1136
|
+
"legend": legend,
|
|
1137
|
+
"stats": {
|
|
1138
|
+
"total_nodes": len(nodes),
|
|
1139
|
+
"total_edges": len(edges),
|
|
1140
|
+
"communities": len(communities),
|
|
1141
|
+
"skills": len(skill_nodes),
|
|
1142
|
+
},
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
# ── METRICS endpoints ──────────────────────────────────────────────
|
|
1146
|
+
|
|
1147
|
+
@app.get("/api/metrics")
|
|
1148
|
+
async def get_metrics():
|
|
1149
|
+
"""Aggregate metrics across all evolution runs."""
|
|
1150
|
+
output_dir = EVOLUTION_DIR / "output"
|
|
1151
|
+
|
|
1152
|
+
all_runs = []
|
|
1153
|
+
if output_dir.exists():
|
|
1154
|
+
for skill_dir in output_dir.iterdir():
|
|
1155
|
+
if skill_dir.is_dir():
|
|
1156
|
+
for run_dir in skill_dir.iterdir():
|
|
1157
|
+
mf = run_dir / "metrics.json"
|
|
1158
|
+
if mf.exists():
|
|
1159
|
+
try:
|
|
1160
|
+
raw = json.loads(mf.read_text())
|
|
1161
|
+
all_runs.append(_normalize_metrics(raw))
|
|
1162
|
+
except Exception:
|
|
1163
|
+
pass
|
|
1164
|
+
|
|
1165
|
+
if not all_runs:
|
|
1166
|
+
return {
|
|
1167
|
+
"total_runs": 0,
|
|
1168
|
+
"skills_evolved": 0,
|
|
1169
|
+
"avg_improvement": 0,
|
|
1170
|
+
"best_improvement": 0,
|
|
1171
|
+
"total_time_seconds": 0,
|
|
1172
|
+
"avg_time_seconds": 0,
|
|
1173
|
+
"success_rate": 0,
|
|
1174
|
+
"runs": [],
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
improvements = [r.get("improvement", 0) for r in all_runs]
|
|
1178
|
+
times = [r.get("elapsed_seconds", 0) for r in all_runs]
|
|
1179
|
+
passed = sum(1 for r in all_runs if r.get("constraints_passed", False))
|
|
1180
|
+
skills = set(r.get("skill_name", "unknown") for r in all_runs)
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
"total_runs": len(all_runs),
|
|
1184
|
+
"skills_evolved": len(skills),
|
|
1185
|
+
"avg_improvement": sum(improvements) / len(improvements) if improvements else 0,
|
|
1186
|
+
"best_improvement": max(improvements) if improvements else 0,
|
|
1187
|
+
"total_time_seconds": sum(times),
|
|
1188
|
+
"avg_time_seconds": sum(times) / len(times) if times else 0,
|
|
1189
|
+
"success_rate": passed / len(all_runs) if all_runs else 0,
|
|
1190
|
+
"runs": sorted(all_runs, key=lambda r: r.get("timestamp", ""), reverse=True)[:20],
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
# ── CONSTRAINTS endpoints ──────────────────────────────────────────
|
|
1194
|
+
|
|
1195
|
+
@app.get("/api/constraints/validate/{skill_name}")
|
|
1196
|
+
async def validate_skill(skill_name: str):
|
|
1197
|
+
"""Validate a skill against all constraints."""
|
|
1198
|
+
skill_file = _find_skill_file(skill_name)
|
|
1199
|
+
if not skill_file:
|
|
1200
|
+
raise HTTPException(404, f"Skill '{skill_name}' not found")
|
|
1201
|
+
|
|
1202
|
+
content = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
1203
|
+
|
|
1204
|
+
# Try evolution module first
|
|
1205
|
+
try:
|
|
1206
|
+
sys.path.insert(0, str(EVOLUTION_DIR))
|
|
1207
|
+
from evolution.core.constraints import ConstraintValidator
|
|
1208
|
+
from evolution.core.config import EvolutionConfig
|
|
1209
|
+
|
|
1210
|
+
config = EvolutionConfig()
|
|
1211
|
+
validator = ConstraintValidator(config)
|
|
1212
|
+
results = validator.validate_all(content, "skill")
|
|
1213
|
+
|
|
1214
|
+
return [
|
|
1215
|
+
{
|
|
1216
|
+
"passed": r.passed,
|
|
1217
|
+
"constraint": r.constraint_name,
|
|
1218
|
+
"message": r.message,
|
|
1219
|
+
"details": r.details,
|
|
1220
|
+
}
|
|
1221
|
+
for r in results
|
|
1222
|
+
]
|
|
1223
|
+
except Exception:
|
|
1224
|
+
pass
|
|
1225
|
+
|
|
1226
|
+
# Fallback: built-in validation
|
|
1227
|
+
results = []
|
|
1228
|
+
size = len(content.encode("utf-8"))
|
|
1229
|
+
results.append({
|
|
1230
|
+
"passed": size <= 15000,
|
|
1231
|
+
"constraint": "size_limit",
|
|
1232
|
+
"message": f"{size} bytes (max 15KB)",
|
|
1233
|
+
"details": None,
|
|
1234
|
+
})
|
|
1235
|
+
results.append({
|
|
1236
|
+
"passed": bool(content.strip()),
|
|
1237
|
+
"constraint": "non_empty",
|
|
1238
|
+
"message": "Skill must contain text",
|
|
1239
|
+
"details": None,
|
|
1240
|
+
})
|
|
1241
|
+
results.append({
|
|
1242
|
+
"passed": content.strip().startswith("---"),
|
|
1243
|
+
"constraint": "skill_structure",
|
|
1244
|
+
"message": "Must start with YAML frontmatter (---)",
|
|
1245
|
+
"details": None,
|
|
1246
|
+
})
|
|
1247
|
+
return results
|
|
1248
|
+
|
|
1249
|
+
# ── WEBSOCKET for live streaming ────────────────────────────────────
|
|
1250
|
+
|
|
1251
|
+
@app.websocket("/ws/stream")
|
|
1252
|
+
async def websocket_stream(ws: WebSocket):
|
|
1253
|
+
"""Real-time evolution log streaming."""
|
|
1254
|
+
await manager.connect(ws)
|
|
1255
|
+
try:
|
|
1256
|
+
while True:
|
|
1257
|
+
data = await ws.receive_text()
|
|
1258
|
+
await ws.send_json({"type": "pong", "echo": data})
|
|
1259
|
+
except WebSocketDisconnect:
|
|
1260
|
+
manager.disconnect(ws)
|
|
1261
|
+
|
|
1262
|
+
# ── Health check ───────────────────────────────────────────────────
|
|
1263
|
+
|
|
1264
|
+
@app.get("/api/health")
|
|
1265
|
+
async def health():
|
|
1266
|
+
from .skill_registry import get_registry
|
|
1267
|
+
registry = get_registry()
|
|
1268
|
+
all_skills = registry.get_global_skills()
|
|
1269
|
+
skill_count = len(all_skills)
|
|
1270
|
+
|
|
1271
|
+
# Calcular categorías desde paths
|
|
1272
|
+
categories = {}
|
|
1273
|
+
for skill in all_skills:
|
|
1274
|
+
cat = 'uncategorized'
|
|
1275
|
+
for path in [skill.get('canonical_path', '')] + [p.get('local_path', '') for p in skill.get('providers', [])]:
|
|
1276
|
+
if '/skills/' in path:
|
|
1277
|
+
parts = path.split('/skills/')
|
|
1278
|
+
if len(parts) > 1:
|
|
1279
|
+
sub = parts[1].split('/')[0]
|
|
1280
|
+
if sub and sub not in ['.', '..']:
|
|
1281
|
+
cat = sub
|
|
1282
|
+
break
|
|
1283
|
+
categories[cat] = categories.get(cat, 0) + 1
|
|
1284
|
+
|
|
1285
|
+
return {
|
|
1286
|
+
"status": "ok",
|
|
1287
|
+
"hermes_repo": str(HERMES_REPO),
|
|
1288
|
+
"hermes_repo_exists": HERMES_REPO.exists(),
|
|
1289
|
+
"evolution_dir": str(EVOLUTION_DIR),
|
|
1290
|
+
"evolution_dir_exists": EVOLUTION_DIR.exists(),
|
|
1291
|
+
"skills_count": skill_count,
|
|
1292
|
+
"categories": categories,
|
|
1293
|
+
"python": PYTHON_BIN,
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1298
|
+
# PROMETHEAN CYCLE ENDPOINTS — GEPA ⊕ DSPy Autonomous Evolution
|
|
1299
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1300
|
+
|
|
1301
|
+
from .promethean.models import TraceRecord, CyclePhase
|
|
1302
|
+
from .promethean.trace_ingestion import get_ingestor
|
|
1303
|
+
from .promethean.gepa_strategist import get_strategist
|
|
1304
|
+
from .promethean.dspy_compiler import get_compiler
|
|
1305
|
+
from .promethean.delta_validator import get_validator
|
|
1306
|
+
from .promethean.skill_deployer import get_deployer
|
|
1307
|
+
from .promethean.cycle_orchestrator import get_orchestrator
|
|
1308
|
+
|
|
1309
|
+
|
|
1310
|
+
@app.post("/api/promethean/traces")
|
|
1311
|
+
async def ingest_trace(trace: dict):
|
|
1312
|
+
"""① PERCIBE: Ingest a standardized trace from any AI agent."""
|
|
1313
|
+
ingestor = get_ingestor()
|
|
1314
|
+
try:
|
|
1315
|
+
trace_obj = TraceRecord.from_json(trace)
|
|
1316
|
+
trace_id = ingestor.ingest(trace_obj)
|
|
1317
|
+
return {"status": "ingested", "trace_id": trace_id}
|
|
1318
|
+
except Exception as e:
|
|
1319
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
@app.post("/api/promethean/traces/batch")
|
|
1323
|
+
async def ingest_traces_batch(traces: list[dict]):
|
|
1324
|
+
"""① PERCIBE: Ingest multiple traces at once."""
|
|
1325
|
+
ingestor = get_ingestor()
|
|
1326
|
+
try:
|
|
1327
|
+
ids = ingestor.ingest_batch(traces)
|
|
1328
|
+
return {"status": "ingested", "count": len(ids), "trace_ids": ids}
|
|
1329
|
+
except Exception as e:
|
|
1330
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
@app.get("/api/promethean/traces/health")
|
|
1334
|
+
async def get_agent_health():
|
|
1335
|
+
"""Get health summary per agent."""
|
|
1336
|
+
ingestor = get_ingestor()
|
|
1337
|
+
return {"agents": ingestor.get_agent_health(), "total_traces": ingestor.get_trace_count()}
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
@app.get("/api/promethean/anomalies")
|
|
1341
|
+
async def get_anomalies(days: int = 7, min_occurrences: int = 3):
|
|
1342
|
+
"""① PERCIBE: Get detected anomalies from recent traces."""
|
|
1343
|
+
ingestor = get_ingestor()
|
|
1344
|
+
anomalies = ingestor.get_recent_failures(days=days, min_occurrences=min_occurrences)
|
|
1345
|
+
return {"anomalies": anomalies, "count": len(anomalies)}
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
@app.get("/api/promethean/diagnose")
|
|
1349
|
+
async def diagnose_gaps(days: int = 7, min_occurrences: int = 3):
|
|
1350
|
+
"""②③ DIAGNOSTICA + FORMULA: GEPA analyzes anomalies and formulates learning objectives."""
|
|
1351
|
+
ingestor = get_ingestor()
|
|
1352
|
+
strategist = get_strategist()
|
|
1353
|
+
anomalies = ingestor.get_recent_failures(days=days, min_occurrences=min_occurrences)
|
|
1354
|
+
gaps = strategist.diagnose(anomalies)
|
|
1355
|
+
return {"gaps": gaps, "actionable": len([g for g in gaps if g["recommended_action"] == "compile_skill"])}
|
|
1356
|
+
|
|
1357
|
+
|
|
1358
|
+
@app.post("/api/promethean/cycle/start")
|
|
1359
|
+
async def start_promethean_cycle(
|
|
1360
|
+
min_anomaly_occurrences: int = 3,
|
|
1361
|
+
anomaly_days: int = 7,
|
|
1362
|
+
auto_deploy: bool = False, # Safety: default to dry-run
|
|
1363
|
+
):
|
|
1364
|
+
"""🔥 Run the FULL Promethean Cycle (all 7 phases).
|
|
1365
|
+
|
|
1366
|
+
Set auto_deploy=true to automatically register compiled skills.
|
|
1367
|
+
"""
|
|
1368
|
+
orchestrator = get_orchestrator(PYTHON_BIN)
|
|
1369
|
+
result = await orchestrator.run_full_cycle(
|
|
1370
|
+
min_anomaly_occurrences=min_anomaly_occurrences,
|
|
1371
|
+
anomaly_days=anomaly_days,
|
|
1372
|
+
auto_deploy=auto_deploy,
|
|
1373
|
+
)
|
|
1374
|
+
return result
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
@app.get("/api/promethean/cycle/history")
|
|
1378
|
+
async def get_cycle_history(limit: int = 10):
|
|
1379
|
+
"""Get recent Promethean Cycle execution history."""
|
|
1380
|
+
orchestrator = get_orchestrator(PYTHON_BIN)
|
|
1381
|
+
return {"cycles": orchestrator.get_cycle_history(limit=limit)}
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
@app.get("/api/promethean/status")
|
|
1385
|
+
async def get_promethean_status():
|
|
1386
|
+
"""Get current Promethean system status."""
|
|
1387
|
+
ingestor = get_ingestor()
|
|
1388
|
+
compiler = get_compiler(PYTHON_BIN)
|
|
1389
|
+
return {
|
|
1390
|
+
"traces_ingested": ingestor.get_trace_count(),
|
|
1391
|
+
"dspy_available": compiler.is_available,
|
|
1392
|
+
"dspy_version": compiler.dspy_version,
|
|
1393
|
+
"python_bin": PYTHON_BIN,
|
|
1394
|
+
"deployments": get_deployer().get_deployment_history(limit=5),
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
@app.get("/api/promethean/deployments")
|
|
1399
|
+
async def get_deployments(limit: int = 20):
|
|
1400
|
+
"""Get auto-deployed skill history."""
|
|
1401
|
+
return {"deployments": get_deployer().get_deployment_history(limit=limit)}
|
|
1402
|
+
|
|
1403
|
+
|
|
1404
|
+
@app.post("/api/promethean/perceive")
|
|
1405
|
+
async def run_perceive(days: int = 7, min_occurrences: int = 3):
|
|
1406
|
+
"""Run only the PERCIBE phase (for debugging/testing)."""
|
|
1407
|
+
orchestrator = get_orchestrator(PYTHON_BIN)
|
|
1408
|
+
return orchestrator.run_perceive(days=days, min_occ=min_occurrences)
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1412
|
+
# CURATOR ENDPOINTS — Skill Lifecycle Management
|
|
1413
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1414
|
+
|
|
1415
|
+
CURATOR_ENABLED = True # Can be toggled via config in the future
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
@app.get("/api/curator/status")
|
|
1419
|
+
async def curator_status():
|
|
1420
|
+
"""Curator status overview — last run, counts, pinned."""
|
|
1421
|
+
if not CURATOR_ENABLED:
|
|
1422
|
+
return {"status": "disabled"}
|
|
1423
|
+
from .curator import get_status
|
|
1424
|
+
return get_status()
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
@app.get("/api/curator/skills")
|
|
1428
|
+
async def curator_skills():
|
|
1429
|
+
"""Full usage telemetry for all skills."""
|
|
1430
|
+
if not CURATOR_ENABLED:
|
|
1431
|
+
return {"status": "disabled"}
|
|
1432
|
+
from .curator import get_skills_usage
|
|
1433
|
+
return {"skills": get_skills_usage()}
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
@app.post("/api/curator/pin/{skill_name:path}")
|
|
1437
|
+
async def curator_pin(skill_name: str):
|
|
1438
|
+
"""Pin a skill to protect it from curator auto-transitions."""
|
|
1439
|
+
if not CURATOR_ENABLED:
|
|
1440
|
+
raise HTTPException(status_code=503, detail="Curator is disabled")
|
|
1441
|
+
from .curator import pin_skill
|
|
1442
|
+
result = pin_skill(skill_name)
|
|
1443
|
+
if result.get("status") == "error":
|
|
1444
|
+
raise HTTPException(status_code=400, detail=result["message"])
|
|
1445
|
+
return result
|
|
1446
|
+
|
|
1447
|
+
|
|
1448
|
+
@app.post("/api/curator/unpin/{skill_name:path}")
|
|
1449
|
+
async def curator_unpin(skill_name: str):
|
|
1450
|
+
"""Unpin a skill to allow curator transitions."""
|
|
1451
|
+
if not CURATOR_ENABLED:
|
|
1452
|
+
raise HTTPException(status_code=503, detail="Curator is disabled")
|
|
1453
|
+
from .curator import unpin_skill
|
|
1454
|
+
result = unpin_skill(skill_name)
|
|
1455
|
+
if result.get("status") == "error":
|
|
1456
|
+
raise HTTPException(status_code=400, detail=result["message"])
|
|
1457
|
+
return result
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
@app.post("/api/curator/restore/{skill_name:path}")
|
|
1461
|
+
async def curator_restore(skill_name: str):
|
|
1462
|
+
"""Restore an archived skill back to active."""
|
|
1463
|
+
if not CURATOR_ENABLED:
|
|
1464
|
+
raise HTTPException(status_code=503, detail="Curator is disabled")
|
|
1465
|
+
from .curator import restore_skill
|
|
1466
|
+
result = restore_skill(skill_name)
|
|
1467
|
+
if result.get("status") == "error":
|
|
1468
|
+
raise HTTPException(status_code=400, detail=result["message"])
|
|
1469
|
+
return result
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
@app.post("/api/curator/run")
|
|
1473
|
+
async def curator_run(sync: bool = False):
|
|
1474
|
+
"""Trigger a curator review pass."""
|
|
1475
|
+
if not CURATOR_ENABLED:
|
|
1476
|
+
raise HTTPException(status_code=503, detail="Curator is disabled")
|
|
1477
|
+
from .curator import run_curator_review
|
|
1478
|
+
result = run_curator_review(sync=sync)
|
|
1479
|
+
if result.get("status") == "error":
|
|
1480
|
+
raise HTTPException(status_code=500, detail=result["message"])
|
|
1481
|
+
return result
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
@app.get("/api/curator/reports")
|
|
1485
|
+
async def curator_reports(limit: int = 10):
|
|
1486
|
+
"""List past curator run reports."""
|
|
1487
|
+
if not CURATOR_ENABLED:
|
|
1488
|
+
return {"status": "disabled"}
|
|
1489
|
+
from .curator import get_reports
|
|
1490
|
+
return {"reports": get_reports(limit=limit)}
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
@app.get("/api/curator/reports/{report_id}")
|
|
1494
|
+
async def curator_report_detail(report_id: str):
|
|
1495
|
+
"""Get detailed curator report for a specific run."""
|
|
1496
|
+
if not CURATOR_ENABLED:
|
|
1497
|
+
raise HTTPException(status_code=503, detail="Curator is disabled")
|
|
1498
|
+
from .curator import get_report_detail
|
|
1499
|
+
result = get_report_detail(report_id)
|
|
1500
|
+
if result.get("status") == "error":
|
|
1501
|
+
raise HTTPException(status_code=404, detail=result["message"])
|
|
1502
|
+
return result
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
# ── Record usage (called by other parts of the dashboard) ──────────
|
|
1506
|
+
|
|
1507
|
+
@app.post("/api/curator/record-use")
|
|
1508
|
+
async def curator_record_usage(data: dict):
|
|
1509
|
+
"""Record a skill usage/view/patch event.
|
|
1510
|
+
|
|
1511
|
+
Body: {"skill": "skill-name", "action": "use"|"view"|"patch"}
|
|
1512
|
+
"""
|
|
1513
|
+
if not CURATOR_ENABLED:
|
|
1514
|
+
return {"status": "disabled"}
|
|
1515
|
+
skill = data.get("skill", "")
|
|
1516
|
+
action = data.get("action", "use")
|
|
1517
|
+
if not skill:
|
|
1518
|
+
raise HTTPException(status_code=400, detail="Missing 'skill' field")
|
|
1519
|
+
if action not in ("use", "view", "patch"):
|
|
1520
|
+
raise HTTPException(status_code=400, detail=f"Invalid action: {action}")
|
|
1521
|
+
from .curator import record_use
|
|
1522
|
+
return record_use(skill, action)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1526
|
+
# CANONICAL RUN ENDPOINTS — Phase 4 (SQLite Storage + Cross-Agent Queries)
|
|
1527
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1528
|
+
|
|
1529
|
+
from .storage import RunStore
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
@app.get("/api/runs")
|
|
1533
|
+
async def list_canonical_runs(
|
|
1534
|
+
agent_name: Optional[str] = None,
|
|
1535
|
+
outcome: Optional[str] = None,
|
|
1536
|
+
repo: Optional[str] = None,
|
|
1537
|
+
since: Optional[str] = None,
|
|
1538
|
+
until: Optional[str] = None,
|
|
1539
|
+
limit: int = 50,
|
|
1540
|
+
offset: int = 0,
|
|
1541
|
+
):
|
|
1542
|
+
"""List canonical runs with optional filters."""
|
|
1543
|
+
store = RunStore()
|
|
1544
|
+
runs = store.list_runs(
|
|
1545
|
+
agent_name=agent_name,
|
|
1546
|
+
outcome=outcome,
|
|
1547
|
+
repo=repo,
|
|
1548
|
+
since=since,
|
|
1549
|
+
until=until,
|
|
1550
|
+
limit=limit,
|
|
1551
|
+
offset=offset,
|
|
1552
|
+
)
|
|
1553
|
+
return {
|
|
1554
|
+
"runs": [r.to_dict() for r in runs],
|
|
1555
|
+
"count": len(runs),
|
|
1556
|
+
"limit": limit,
|
|
1557
|
+
"offset": offset,
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
|
|
1561
|
+
@app.get("/api/runs/{run_id}")
|
|
1562
|
+
async def get_canonical_run(run_id: str):
|
|
1563
|
+
"""Get detailed canonical run by ID."""
|
|
1564
|
+
store = RunStore()
|
|
1565
|
+
run = store.get_run(run_id)
|
|
1566
|
+
if not run:
|
|
1567
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
1568
|
+
return run.to_dict()
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
@app.get("/api/agents")
|
|
1572
|
+
async def get_agent_summary():
|
|
1573
|
+
"""Get per-agent statistics: count, success_rate, avg_tokens."""
|
|
1574
|
+
store = RunStore()
|
|
1575
|
+
summary = store.get_agent_summary()
|
|
1576
|
+
return {"agents": summary}
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
@app.post("/api/runs/migrate")
|
|
1580
|
+
async def migrate_runs_from_flat_files(
|
|
1581
|
+
project_path: Optional[str] = None,
|
|
1582
|
+
limit: int = 100,
|
|
1583
|
+
):
|
|
1584
|
+
"""Migrate runs from flat-file collectors into SQLite.
|
|
1585
|
+
|
|
1586
|
+
Runs as background task. Idempotent: re-running does not duplicate.
|
|
1587
|
+
"""
|
|
1588
|
+
from .collectors.hermes_collector import HermesCollector
|
|
1589
|
+
from .collectors.claude_code_collector import ClaudeCodeCollector
|
|
1590
|
+
|
|
1591
|
+
store = RunStore()
|
|
1592
|
+
|
|
1593
|
+
# Migrate Hermes traces
|
|
1594
|
+
hermes_collector = HermesCollector()
|
|
1595
|
+
hermes_runs = hermes_collector.load_from_disk(limit=limit)
|
|
1596
|
+
hermes_result = store.upsert_batch(hermes_runs)
|
|
1597
|
+
|
|
1598
|
+
# Migrate Claude Code sessions
|
|
1599
|
+
claude_collector = ClaudeCodeCollector()
|
|
1600
|
+
claude_runs = claude_collector.collect_all(project_path=project_path, limit=limit)
|
|
1601
|
+
claude_result = store.upsert_batch(claude_runs)
|
|
1602
|
+
|
|
1603
|
+
return {
|
|
1604
|
+
"status": "completed",
|
|
1605
|
+
"hermes": hermes_result,
|
|
1606
|
+
"claude_code": claude_result,
|
|
1607
|
+
"total_inserted": hermes_result.get("inserted", 0) + claude_result.get("inserted", 0),
|
|
1608
|
+
"total_updated": hermes_result.get("updated", 0) + claude_result.get("updated", 0),
|
|
1609
|
+
"total_failed": hermes_result.get("failed", 0) + claude_result.get("failed", 0),
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
@app.post("/api/runs/{run_id}/evaluate")
|
|
1614
|
+
async def evaluate_run(run_id: str):
|
|
1615
|
+
"""Run evaluation engine on a canonical run. Returns list of scores."""
|
|
1616
|
+
from .eval.engine import EvaluationEngine
|
|
1617
|
+
|
|
1618
|
+
store = RunStore()
|
|
1619
|
+
run = store.get_run(run_id)
|
|
1620
|
+
if not run:
|
|
1621
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
1622
|
+
|
|
1623
|
+
engine = EvaluationEngine(store=store)
|
|
1624
|
+
scores = engine.evaluate(run)
|
|
1625
|
+
|
|
1626
|
+
return {
|
|
1627
|
+
"run_id": run_id,
|
|
1628
|
+
"scores": [
|
|
1629
|
+
{
|
|
1630
|
+
"scorer": s.scorer,
|
|
1631
|
+
"score": s.score,
|
|
1632
|
+
"passed": s.passed,
|
|
1633
|
+
"details": s.details,
|
|
1634
|
+
}
|
|
1635
|
+
for s in scores
|
|
1636
|
+
],
|
|
1637
|
+
"aggregate_score": engine.get_aggregate_score(run),
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
@app.get("/api/runs/{run_id}/scores")
|
|
1642
|
+
async def get_run_scores(run_id: str):
|
|
1643
|
+
"""Get evaluation scores for a run from database."""
|
|
1644
|
+
store = RunStore()
|
|
1645
|
+
run = store.get_run(run_id)
|
|
1646
|
+
if not run:
|
|
1647
|
+
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
1648
|
+
|
|
1649
|
+
# Retrieve eval_scores from run's eval_scores list
|
|
1650
|
+
return {
|
|
1651
|
+
"run_id": run_id,
|
|
1652
|
+
"scores": run.eval_scores or [],
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
|
|
1656
|
+
@app.post("/api/runs/compare")
|
|
1657
|
+
async def compare_runs(baseline_run_id: str, evolved_run_id: str, threshold: float = 0.05):
|
|
1658
|
+
"""Compare baseline and evolved runs for regression detection."""
|
|
1659
|
+
from .eval.engine import EvaluationEngine
|
|
1660
|
+
|
|
1661
|
+
store = RunStore()
|
|
1662
|
+
engine = EvaluationEngine(store=store)
|
|
1663
|
+
result = engine.detect_regression(baseline_run_id, evolved_run_id, threshold=threshold)
|
|
1664
|
+
|
|
1665
|
+
return result
|
|
1666
|
+
|
|
1667
|
+
|
|
1668
|
+
@app.get("/api/skills")
|
|
1669
|
+
async def list_skills():
|
|
1670
|
+
"""Lista skills únicas con metadata de providers y categoría."""
|
|
1671
|
+
from .skill_registry import get_registry
|
|
1672
|
+
import traceback, datetime, pathlib
|
|
1673
|
+
try:
|
|
1674
|
+
registry = get_registry()
|
|
1675
|
+
skills_map = {}
|
|
1676
|
+
for skill in registry.get_global_skills():
|
|
1677
|
+
name = skill['name']
|
|
1678
|
+
providers = [p['name'] for p in skill['providers']]
|
|
1679
|
+
|
|
1680
|
+
# Extraer categoría del path (canonical o local del primer provider)
|
|
1681
|
+
category = 'uncategorized'
|
|
1682
|
+
for path in [skill.get('canonical_path', '')] + [p.get('local_path', '') for p in skill['providers']]:
|
|
1683
|
+
if '/skills/' in path:
|
|
1684
|
+
parts = path.split('/skills/')
|
|
1685
|
+
if len(parts) > 1:
|
|
1686
|
+
sub = parts[1].split('/')[0]
|
|
1687
|
+
if sub and sub not in ['.', '..']:
|
|
1688
|
+
category = sub
|
|
1689
|
+
break
|
|
1690
|
+
|
|
1691
|
+
if name not in skills_map:
|
|
1692
|
+
skills_map[name] = {
|
|
1693
|
+
"name": name,
|
|
1694
|
+
"description": skill['description'],
|
|
1695
|
+
"tags": skill['tags'],
|
|
1696
|
+
"is_fork": skill.get('is_fork', False),
|
|
1697
|
+
"canonical_path": skill['canonical_path'],
|
|
1698
|
+
"providers": providers,
|
|
1699
|
+
"provider_count": len(providers),
|
|
1700
|
+
"category": category,
|
|
1701
|
+
}
|
|
1702
|
+
return list(skills_map.values())
|
|
1703
|
+
except Exception as e:
|
|
1704
|
+
log_path = pathlib.Path("/tmp/list_skills_error.log")
|
|
1705
|
+
with open(log_path, "w") as f:
|
|
1706
|
+
f.write(str(datetime.datetime.now()) + "\n")
|
|
1707
|
+
f.write(traceback.format_exc())
|
|
1708
|
+
|
|
1709
|
+
|
|
1710
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1711
|
+
# SKILL EVOLUTION ENDPOINTS — Optimize skills with DSPy/SDD
|
|
1712
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1713
|
+
|
|
1714
|
+
@app.post("/api/skills/{skill_name}/evolve")
|
|
1715
|
+
async def evolve_skill_endpoint(skill_name: str, iterations: int = 3):
|
|
1716
|
+
"""Evolve a skill using SDD optimizer. Returns metrics."""
|
|
1717
|
+
try:
|
|
1718
|
+
from .sdd_evolve import evolve_skill
|
|
1719
|
+
|
|
1720
|
+
# Run evolution (blocking for now — could be background job)
|
|
1721
|
+
evolve_skill(skill_name=skill_name, iterations=iterations)
|
|
1722
|
+
|
|
1723
|
+
# Read results from output directory
|
|
1724
|
+
output_dir = Path.home() / ".hermes" / "hermes-agent-self-evolution" / "output" / skill_name
|
|
1725
|
+
runs = sorted(output_dir.glob("*/"), reverse=True) if output_dir.exists() else []
|
|
1726
|
+
|
|
1727
|
+
if not runs:
|
|
1728
|
+
raise HTTPException(status_code=500, detail="Evolution completed but no output found")
|
|
1729
|
+
|
|
1730
|
+
latest_run = runs[0]
|
|
1731
|
+
metrics_file = latest_run / "sdd_analysis.json"
|
|
1732
|
+
|
|
1733
|
+
if not metrics_file.exists():
|
|
1734
|
+
raise HTTPException(status_code=500, detail="SDD analysis not found")
|
|
1735
|
+
|
|
1736
|
+
metrics = json.loads(metrics_file.read_text())
|
|
1737
|
+
return {
|
|
1738
|
+
"status": "completed",
|
|
1739
|
+
"skill_name": skill_name,
|
|
1740
|
+
"iterations": iterations,
|
|
1741
|
+
"metrics": metrics,
|
|
1742
|
+
"output_dir": str(latest_run),
|
|
1743
|
+
}
|
|
1744
|
+
except Exception as e:
|
|
1745
|
+
raise HTTPException(status_code=400, detail=f"Evolution failed: {str(e)}")
|
|
1746
|
+
raise
|