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.
Files changed (445) hide show
  1. package/.brv/.obsidian/app.json +1 -0
  2. package/.brv/.obsidian/appearance.json +1 -0
  3. package/.brv/.obsidian/core-plugins.json +33 -0
  4. package/.brv/.obsidian/graph.json +22 -0
  5. package/.brv/.obsidian/workspace.json +195 -0
  6. package/.brv/Sin ti/314/201tulo 1.canvas" +1 -0
  7. package/.brv/Sin ti/314/201tulo 2.canvas" +1 -0
  8. package/.brv/Sin ti/314/201tulo.canvas" +1 -0
  9. package/.brv/_queue_status.json +1 -0
  10. package/.brv/config.json +5 -0
  11. package/.brv/context-tree/_index.md +60 -0
  12. package/.brv/context-tree/_manifest.json +165 -0
  13. package/.brv/context-tree/backend/_index.md +24 -0
  14. package/.brv/context-tree/backend/backend/_index.md +40 -0
  15. package/.brv/context-tree/backend/backend/init.abstract.md +0 -0
  16. package/.brv/context-tree/backend/backend/init.md +27 -0
  17. package/.brv/context-tree/backend/backend/init.overview.md +29 -0
  18. package/.brv/context-tree/backend/backend/job_tracker.abstract.md +1 -0
  19. package/.brv/context-tree/backend/backend/job_tracker.md +273 -0
  20. package/.brv/context-tree/backend/backend/job_tracker.overview.md +31 -0
  21. package/.brv/context-tree/backend/backend/main.abstract.md +0 -0
  22. package/.brv/context-tree/backend/backend/main.md +1292 -0
  23. package/.brv/context-tree/backend/backend/main.overview.md +30 -0
  24. package/.brv/context-tree/backend/backend/requirements.abstract.md +1 -0
  25. package/.brv/context-tree/backend/backend/requirements.md +37 -0
  26. package/.brv/context-tree/backend/backend/requirements.overview.md +28 -0
  27. package/.brv/context-tree/docs/_index.md +37 -0
  28. package/.brv/context-tree/docs/api/_index.md +54 -0
  29. package/.brv/context-tree/docs/api/context.md +11 -0
  30. package/.brv/context-tree/docs/api/hermes_api_openapi_specification.abstract.md +0 -0
  31. package/.brv/context-tree/docs/api/hermes_api_openapi_specification.md +468 -0
  32. package/.brv/context-tree/docs/api/hermes_api_openapi_specification.overview.md +44 -0
  33. package/.brv/context-tree/frontend/_index.md +48 -0
  34. package/.brv/context-tree/frontend/hermes_dashboard/_index.md +31 -0
  35. package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.abstract.md +0 -0
  36. package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.md +41 -0
  37. package/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.overview.md +34 -0
  38. package/.brv/context-tree/frontend/src/_index.md +53 -0
  39. package/.brv/context-tree/frontend/src/components/_index.md +52 -0
  40. package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.abstract.md +0 -0
  41. package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.md +161 -0
  42. package/.brv/context-tree/frontend/src/components/sidebar_navigation_component.overview.md +32 -0
  43. package/.brv/context-tree/frontend/src/context.md +10 -0
  44. package/.brv/context-tree/frontend/src/functioncallingpage.abstract.md +0 -0
  45. package/.brv/context-tree/frontend/src/functioncallingpage.md +34 -0
  46. package/.brv/context-tree/frontend/src/functioncallingpage.overview.md +26 -0
  47. package/.brv/context-tree/frontend/src/lib/_index.md +48 -0
  48. package/.brv/context-tree/frontend/src/lib/api_client_library.abstract.md +1 -0
  49. package/.brv/context-tree/frontend/src/lib/api_client_library.md +403 -0
  50. package/.brv/context-tree/frontend/src/lib/api_client_library.overview.md +69 -0
  51. package/.brv/context-tree/frontend/src/page.abstract.md +0 -0
  52. package/.brv/context-tree/frontend/src/page.md +103 -0
  53. package/.brv/context-tree/frontend/src/page.overview.md +7 -0
  54. package/.brv/context-tree/frontend/src/settingspage.abstract.md +0 -0
  55. package/.brv/context-tree/frontend/src/settingspage.md +124 -0
  56. package/.brv/context-tree/frontend/src/settingspage.overview.md +34 -0
  57. package/.brv/context-tree/frontend/src/sidebar.abstract.md +0 -0
  58. package/.brv/context-tree/frontend/src/sidebar.md +170 -0
  59. package/.brv/context-tree/frontend/src/sidebar.overview.md +25 -0
  60. package/.brv/context-tree/meta/_index.md +24 -0
  61. package/.brv/context-tree/meta/curation_context/_index.md +24 -0
  62. package/.brv/context-tree/meta/curation_context/empty_context.abstract.md +4 -0
  63. package/.brv/context-tree/meta/curation_context/empty_context.md +35 -0
  64. package/.brv/context-tree/meta/curation_context/empty_context.overview.md +20 -0
  65. package/.brv/dream-log/drm-1777341062653.json +33 -0
  66. package/.brv/dream-state.json +8 -0
  67. package/.brv/dream.lock +0 -0
  68. package/.brv/review-backups/docs/api/hermes_api_openapi_specification.md +468 -0
  69. package/.claude/settings.local.json +7 -0
  70. package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/app.json +1 -0
  71. package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/appearance.json +1 -0
  72. package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/core-plugins.json +33 -0
  73. package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/graph.json +22 -0
  74. package/.claude/worktrees/phase-2-mcp/.brv/.obsidian/workspace.json +195 -0
  75. package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo 1.canvas" +1 -0
  76. package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo 2.canvas" +1 -0
  77. package/.claude/worktrees/phase-2-mcp/.brv/Sin t/303/255tulo.canvas" +1 -0
  78. package/.claude/worktrees/phase-2-mcp/.brv/_queue_status.json +1 -0
  79. package/.claude/worktrees/phase-2-mcp/.brv/config.json +5 -0
  80. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/_index.md +60 -0
  81. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/_manifest.json +165 -0
  82. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/_index.md +24 -0
  83. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/_index.md +40 -0
  84. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.abstract.md +0 -0
  85. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.md +27 -0
  86. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/init.overview.md +29 -0
  87. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.abstract.md +1 -0
  88. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.md +273 -0
  89. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/job_tracker.overview.md +31 -0
  90. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.abstract.md +0 -0
  91. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.md +1292 -0
  92. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/main.overview.md +30 -0
  93. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.abstract.md +1 -0
  94. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.md +37 -0
  95. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/backend/backend/requirements.overview.md +28 -0
  96. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/_index.md +37 -0
  97. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/_index.md +54 -0
  98. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/context.md +11 -0
  99. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.abstract.md +0 -0
  100. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.md +468 -0
  101. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/docs/api/hermes_api_openapi_specification.overview.md +44 -0
  102. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/_index.md +48 -0
  103. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/_index.md +31 -0
  104. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.abstract.md +0 -0
  105. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.md +41 -0
  106. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/hermes_dashboard/architecture_overview.overview.md +34 -0
  107. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/_index.md +53 -0
  108. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/_index.md +52 -0
  109. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.abstract.md +0 -0
  110. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.md +161 -0
  111. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/components/sidebar_navigation_component.overview.md +32 -0
  112. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/context.md +10 -0
  113. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.abstract.md +0 -0
  114. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.md +34 -0
  115. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/functioncallingpage.overview.md +26 -0
  116. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/_index.md +48 -0
  117. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.abstract.md +1 -0
  118. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.md +403 -0
  119. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/lib/api_client_library.overview.md +69 -0
  120. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.abstract.md +0 -0
  121. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.md +103 -0
  122. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/page.overview.md +7 -0
  123. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.abstract.md +0 -0
  124. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.md +124 -0
  125. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/settingspage.overview.md +34 -0
  126. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.abstract.md +0 -0
  127. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.md +170 -0
  128. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/frontend/src/sidebar.overview.md +25 -0
  129. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/_index.md +24 -0
  130. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/_index.md +24 -0
  131. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.abstract.md +4 -0
  132. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.md +35 -0
  133. package/.claude/worktrees/phase-2-mcp/.brv/context-tree/meta/curation_context/empty_context.overview.md +20 -0
  134. package/.claude/worktrees/phase-2-mcp/.brv/dream-log/drm-1777341062653.json +33 -0
  135. package/.claude/worktrees/phase-2-mcp/.brv/dream-state.json +8 -0
  136. package/.claude/worktrees/phase-2-mcp/.brv/dream.lock +0 -0
  137. package/.claude/worktrees/phase-2-mcp/.brv/review-backups/docs/api/hermes_api_openapi_specification.md +468 -0
  138. package/.claude/worktrees/phase-2-mcp/.claude/settings.local.json +13 -0
  139. package/.claude/worktrees/phase-2-mcp/.kilocode/package-lock.json +378 -0
  140. package/.claude/worktrees/phase-2-mcp/.kilocode/package.json +5 -0
  141. package/.claude/worktrees/phase-2-mcp/AGENTS.md +5 -0
  142. package/.claude/worktrees/phase-2-mcp/CLAUDE.md +29 -0
  143. package/.claude/worktrees/phase-2-mcp/QA_AUDIT_PLAN.md +156 -0
  144. package/.claude/worktrees/phase-2-mcp/README.md +316 -0
  145. package/.claude/worktrees/phase-2-mcp/agent-agnostic-evolution-dashboard.md +405 -0
  146. package/.claude/worktrees/phase-2-mcp/backend/__init__.py +0 -0
  147. package/.claude/worktrees/phase-2-mcp/backend/collectors/__init__.py +0 -0
  148. package/.claude/worktrees/phase-2-mcp/backend/collectors/claude_code_collector.py +277 -0
  149. package/.claude/worktrees/phase-2-mcp/backend/collectors/hermes_collector.py +68 -0
  150. package/.claude/worktrees/phase-2-mcp/backend/curator.py +512 -0
  151. package/.claude/worktrees/phase-2-mcp/backend/eval/__init__.py +19 -0
  152. package/.claude/worktrees/phase-2-mcp/backend/eval/engine.py +116 -0
  153. package/.claude/worktrees/phase-2-mcp/backend/eval/scorers.py +201 -0
  154. package/.claude/worktrees/phase-2-mcp/backend/generate_dataset.py +86 -0
  155. package/.claude/worktrees/phase-2-mcp/backend/job_tracker.py +232 -0
  156. package/.claude/worktrees/phase-2-mcp/backend/main.py +1746 -0
  157. package/.claude/worktrees/phase-2-mcp/backend/mcp_server.py +250 -0
  158. package/.claude/worktrees/phase-2-mcp/backend/promethean/__init__.py +24 -0
  159. package/.claude/worktrees/phase-2-mcp/backend/promethean/cycle_orchestrator.py +270 -0
  160. package/.claude/worktrees/phase-2-mcp/backend/promethean/delta_validator.py +191 -0
  161. package/.claude/worktrees/phase-2-mcp/backend/promethean/dspy_compiler.py +315 -0
  162. package/.claude/worktrees/phase-2-mcp/backend/promethean/gepa_strategist.py +213 -0
  163. package/.claude/worktrees/phase-2-mcp/backend/promethean/models.py +260 -0
  164. package/.claude/worktrees/phase-2-mcp/backend/promethean/skill_deployer.py +195 -0
  165. package/.claude/worktrees/phase-2-mcp/backend/promethean/trace_ingestion.py +142 -0
  166. package/.claude/worktrees/phase-2-mcp/backend/requirements.txt +6 -0
  167. package/.claude/worktrees/phase-2-mcp/backend/sdd_evolve.py +459 -0
  168. package/.claude/worktrees/phase-2-mcp/backend/skill_detector.py +227 -0
  169. package/.claude/worktrees/phase-2-mcp/backend/skill_registry.py +289 -0
  170. package/.claude/worktrees/phase-2-mcp/backend/storage/__init__.py +5 -0
  171. package/.claude/worktrees/phase-2-mcp/backend/storage/run_store.py +393 -0
  172. package/.claude/worktrees/phase-2-mcp/backend/storage/schema.sql +99 -0
  173. package/.claude/worktrees/phase-2-mcp/backend/validate_evolution.py +267 -0
  174. package/.claude/worktrees/phase-2-mcp/components.json +28 -0
  175. package/.claude/worktrees/phase-2-mcp/docs/api/hermes-api.openapi.yaml +438 -0
  176. package/.claude/worktrees/phase-2-mcp/docs/hero.svg +148 -0
  177. package/.claude/worktrees/phase-2-mcp/eslint.config.mjs +18 -0
  178. package/.claude/worktrees/phase-2-mcp/install.sh +245 -0
  179. package/.claude/worktrees/phase-2-mcp/next-env.d.ts +6 -0
  180. package/.claude/worktrees/phase-2-mcp/next.config.ts +32 -0
  181. package/.claude/worktrees/phase-2-mcp/package-lock.json +11936 -0
  182. package/.claude/worktrees/phase-2-mcp/package.json +41 -0
  183. package/.claude/worktrees/phase-2-mcp/pnpm-workspace.yaml +4 -0
  184. package/.claude/worktrees/phase-2-mcp/postcss.config.mjs +7 -0
  185. package/.claude/worktrees/phase-2-mcp/public/file.svg +1 -0
  186. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Bold.otf +0 -0
  187. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Heavy.otf +0 -0
  188. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Medium.otf +0 -0
  189. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Regular.otf +0 -0
  190. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Display-Semibold.otf +0 -0
  191. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Bold.otf +0 -0
  192. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Heavy.otf +0 -0
  193. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Medium.otf +0 -0
  194. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Regular.otf +0 -0
  195. package/.claude/worktrees/phase-2-mcp/public/fonts/SF-Pro-Text-Semibold.otf +0 -0
  196. package/.claude/worktrees/phase-2-mcp/public/globe.svg +1 -0
  197. package/.claude/worktrees/phase-2-mcp/public/next.svg +1 -0
  198. package/.claude/worktrees/phase-2-mcp/public/theme-preview.html +257 -0
  199. package/.claude/worktrees/phase-2-mcp/public/vercel.svg +1 -0
  200. package/.claude/worktrees/phase-2-mcp/public/window.svg +1 -0
  201. package/.claude/worktrees/phase-2-mcp/run.sh +26 -0
  202. package/.claude/worktrees/phase-2-mcp/skills-lock.json +10 -0
  203. package/.claude/worktrees/phase-2-mcp/specs/event-schema.md +223 -0
  204. package/.claude/worktrees/phase-2-mcp/specs/examples/run.jsonl +3 -0
  205. package/.claude/worktrees/phase-2-mcp/src/app/api/[...path]/route.ts +55 -0
  206. package/.claude/worktrees/phase-2-mcp/src/app/api/auth/token/route.ts +22 -0
  207. package/.claude/worktrees/phase-2-mcp/src/app/evolution/page.tsx +589 -0
  208. package/.claude/worktrees/phase-2-mcp/src/app/favicon.ico +0 -0
  209. package/.claude/worktrees/phase-2-mcp/src/app/globals.css +321 -0
  210. package/.claude/worktrees/phase-2-mcp/src/app/layout.tsx +63 -0
  211. package/.claude/worktrees/phase-2-mcp/src/app/page.tsx +70 -0
  212. package/.claude/worktrees/phase-2-mcp/src/app/skills/page.tsx +369 -0
  213. package/.claude/worktrees/phase-2-mcp/src/components/ApiConfigCard.tsx +199 -0
  214. package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.css +1 -0
  215. package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.d.ts +1 -0
  216. package/.claude/worktrees/phase-2-mcp/src/components/ColorBends.jsx +1 -0
  217. package/.claude/worktrees/phase-2-mcp/src/components/CoreLoopToggle.tsx +111 -0
  218. package/.claude/worktrees/phase-2-mcp/src/components/EnvironmentStatus.tsx +176 -0
  219. package/.claude/worktrees/phase-2-mcp/src/components/EvolutionBackground.tsx +1 -0
  220. package/.claude/worktrees/phase-2-mcp/src/components/ReactQueryProvider.tsx +24 -0
  221. package/.claude/worktrees/phase-2-mcp/src/components/Sidebar.tsx +247 -0
  222. package/.claude/worktrees/phase-2-mcp/src/components/SkillDiffViewer.tsx +154 -0
  223. package/.claude/worktrees/phase-2-mcp/src/components/ThemeAwareBackground.tsx +67 -0
  224. package/.claude/worktrees/phase-2-mcp/src/components/ThemeToggle.tsx +54 -0
  225. package/.claude/worktrees/phase-2-mcp/src/components/WelcomeHero.tsx +77 -0
  226. package/.claude/worktrees/phase-2-mcp/src/components/bits/ClickSpark.tsx +116 -0
  227. package/.claude/worktrees/phase-2-mcp/src/components/bits/CountUp.tsx +98 -0
  228. package/.claude/worktrees/phase-2-mcp/src/components/bits/DarkSelect.tsx +95 -0
  229. package/.claude/worktrees/phase-2-mcp/src/components/bits/DecryptedText.tsx +161 -0
  230. package/.claude/worktrees/phase-2-mcp/src/components/bits/ElectricBorder.tsx +184 -0
  231. package/.claude/worktrees/phase-2-mcp/src/components/bits/GlitchText.tsx +34 -0
  232. package/.claude/worktrees/phase-2-mcp/src/components/bits/ShinyText.tsx +55 -0
  233. package/.claude/worktrees/phase-2-mcp/src/components/bits/SpotlightCard.tsx +42 -0
  234. package/.claude/worktrees/phase-2-mcp/src/components/bits/TextType.tsx +95 -0
  235. package/.claude/worktrees/phase-2-mcp/src/components/bits/index.ts +9 -0
  236. package/.claude/worktrees/phase-2-mcp/src/components/pages/CuratorPage.tsx +632 -0
  237. package/.claude/worktrees/phase-2-mcp/src/components/pages/DatasetPage.tsx +271 -0
  238. package/.claude/worktrees/phase-2-mcp/src/components/pages/EvolutionPage.tsx +676 -0
  239. package/.claude/worktrees/phase-2-mcp/src/components/pages/FunctionCallingPage.tsx +1 -0
  240. package/.claude/worktrees/phase-2-mcp/src/components/pages/LogsPage.tsx +272 -0
  241. package/.claude/worktrees/phase-2-mcp/src/components/pages/MetricsPage.tsx +246 -0
  242. package/.claude/worktrees/phase-2-mcp/src/components/pages/OverviewPage.tsx +420 -0
  243. package/.claude/worktrees/phase-2-mcp/src/components/pages/SettingsPage.tsx +88 -0
  244. package/.claude/worktrees/phase-2-mcp/src/components/pages/SkillStudioPage.tsx +376 -0
  245. package/.claude/worktrees/phase-2-mcp/src/components/ui/animated-theme-toggler.tsx +97 -0
  246. package/.claude/worktrees/phase-2-mcp/src/components/ui/button.tsx +67 -0
  247. package/.claude/worktrees/phase-2-mcp/src/components/ui/card.tsx +103 -0
  248. package/.claude/worktrees/phase-2-mcp/src/components/ui/input.tsx +19 -0
  249. package/.claude/worktrees/phase-2-mcp/src/components/ui/separator.tsx +28 -0
  250. package/.claude/worktrees/phase-2-mcp/src/components/ui/sheet.tsx +147 -0
  251. package/.claude/worktrees/phase-2-mcp/src/components/ui/sidebar.tsx +702 -0
  252. package/.claude/worktrees/phase-2-mcp/src/components/ui/skeleton.tsx +13 -0
  253. package/.claude/worktrees/phase-2-mcp/src/components/ui/theme-toggle.tsx +272 -0
  254. package/.claude/worktrees/phase-2-mcp/src/components/ui/tooltip.tsx +57 -0
  255. package/.claude/worktrees/phase-2-mcp/src/hooks/use-mobile.ts +19 -0
  256. package/.claude/worktrees/phase-2-mcp/src/lib/api.ts +455 -0
  257. package/.claude/worktrees/phase-2-mcp/src/lib/queryClient.ts +12 -0
  258. package/.claude/worktrees/phase-2-mcp/src/lib/utils.ts +6 -0
  259. package/.claude/worktrees/phase-2-mcp/stitch/agent_dashboard/DESIGN_SPEC.md +521 -0
  260. package/.claude/worktrees/phase-2-mcp/stitch/agent_dashboard/prototype.html +676 -0
  261. package/.claude/worktrees/phase-2-mcp/stitch/curator_workspace/code.html +448 -0
  262. package/.claude/worktrees/phase-2-mcp/stitch/curator_workspace/screen.png +0 -0
  263. package/.claude/worktrees/phase-2-mcp/stitch/datasets/code.html +479 -0
  264. package/.claude/worktrees/phase-2-mcp/stitch/datasets/screen.png +0 -0
  265. package/.claude/worktrees/phase-2-mcp/stitch/evolution_history/code.html +461 -0
  266. package/.claude/worktrees/phase-2-mcp/stitch/evolution_history/screen.png +0 -0
  267. package/.claude/worktrees/phase-2-mcp/stitch/hermes_dashboard/DESIGN.md +192 -0
  268. package/.claude/worktrees/phase-2-mcp/stitch/hermes_dashboard/DESIGN_SPEC.md +455 -0
  269. package/.claude/worktrees/phase-2-mcp/stitch/hermes_overview/code.html +399 -0
  270. package/.claude/worktrees/phase-2-mcp/stitch/hermes_overview/screen.png +0 -0
  271. package/.claude/worktrees/phase-2-mcp/stitch/live_logs/code.html +324 -0
  272. package/.claude/worktrees/phase-2-mcp/stitch/live_logs/screen.png +0 -0
  273. package/.claude/worktrees/phase-2-mcp/stitch/skill_hub/code.html +596 -0
  274. package/.claude/worktrees/phase-2-mcp/stitch/skill_hub/screen.png +0 -0
  275. package/.claude/worktrees/phase-2-mcp/stitch/system_metrics/code.html +527 -0
  276. package/.claude/worktrees/phase-2-mcp/stitch/system_metrics/screen.png +0 -0
  277. package/.claude/worktrees/phase-2-mcp/stitch/system_settings/code.html +257 -0
  278. package/.claude/worktrees/phase-2-mcp/stitch/system_settings/screen.png +0 -0
  279. package/.claude/worktrees/phase-2-mcp/test_dashboard.py +201 -0
  280. package/.claude/worktrees/phase-2-mcp/tests/collectors/__init__.py +0 -0
  281. package/.claude/worktrees/phase-2-mcp/tests/collectors/fixtures/sample_session.jsonl +7 -0
  282. package/.claude/worktrees/phase-2-mcp/tests/collectors/test_claude_code_collector.py +171 -0
  283. package/.claude/worktrees/phase-2-mcp/tests/collectors/test_hermes_collector.py +167 -0
  284. package/.claude/worktrees/phase-2-mcp/tests/eval/test_engine.py +234 -0
  285. package/.claude/worktrees/phase-2-mcp/tests/eval/test_scorers.py +249 -0
  286. package/.claude/worktrees/phase-2-mcp/tests/storage/__init__.py +0 -0
  287. package/.claude/worktrees/phase-2-mcp/tests/storage/test_run_store.py +359 -0
  288. package/.claude/worktrees/phase-2-mcp/tests/test_curator.py +559 -0
  289. package/.claude/worktrees/phase-2-mcp/tests/test_mcp_server.py +114 -0
  290. package/.claude/worktrees/phase-2-mcp/tsconfig.json +34 -0
  291. package/.env.example +72 -0
  292. package/.kilocode/package-lock.json +378 -0
  293. package/.kilocode/package.json +5 -0
  294. package/AGENTS.md +5 -0
  295. package/CLAUDE.md +29 -0
  296. package/QA_AUDIT_PLAN.md +156 -0
  297. package/README.md +355 -0
  298. package/agent-agnostic-evolution-dashboard.md +405 -0
  299. package/backend/__init__.py +0 -0
  300. package/backend/collectors/__init__.py +0 -0
  301. package/backend/collectors/claude_code_collector.py +277 -0
  302. package/backend/collectors/hermes_collector.py +68 -0
  303. package/backend/curator.py +512 -0
  304. package/backend/eval/__init__.py +19 -0
  305. package/backend/eval/engine.py +116 -0
  306. package/backend/eval/scorers.py +201 -0
  307. package/backend/generate_dataset.py +86 -0
  308. package/backend/job_tracker.py +232 -0
  309. package/backend/main.py +1746 -0
  310. package/backend/mcp_server.py +250 -0
  311. package/backend/promethean/__init__.py +24 -0
  312. package/backend/promethean/cycle_orchestrator.py +270 -0
  313. package/backend/promethean/delta_validator.py +191 -0
  314. package/backend/promethean/dspy_compiler.py +315 -0
  315. package/backend/promethean/gepa_strategist.py +213 -0
  316. package/backend/promethean/models.py +260 -0
  317. package/backend/promethean/skill_deployer.py +195 -0
  318. package/backend/promethean/trace_ingestion.py +142 -0
  319. package/backend/requirements.txt +6 -0
  320. package/backend/sdd_evolve.py +459 -0
  321. package/backend/skill_detector.py +227 -0
  322. package/backend/skill_registry.py +289 -0
  323. package/backend/storage/__init__.py +5 -0
  324. package/backend/storage/run_store.py +393 -0
  325. package/backend/storage/schema.sql +99 -0
  326. package/backend/validate_evolution.py +267 -0
  327. package/bin/genoma.js +250 -0
  328. package/components.json +28 -0
  329. package/docs/api/hermes-api.openapi.yaml +438 -0
  330. package/docs/hero.svg +148 -0
  331. package/eslint.config.mjs +18 -0
  332. package/install.sh +245 -0
  333. package/next-env.d.ts +6 -0
  334. package/next.config.ts +32 -0
  335. package/package.json +46 -0
  336. package/pnpm-workspace.yaml +4 -0
  337. package/postcss.config.mjs +7 -0
  338. package/public/file.svg +1 -0
  339. package/public/fonts/SF-Pro-Display-Bold.otf +0 -0
  340. package/public/fonts/SF-Pro-Display-Heavy.otf +0 -0
  341. package/public/fonts/SF-Pro-Display-Medium.otf +0 -0
  342. package/public/fonts/SF-Pro-Display-Regular.otf +0 -0
  343. package/public/fonts/SF-Pro-Display-Semibold.otf +0 -0
  344. package/public/fonts/SF-Pro-Text-Bold.otf +0 -0
  345. package/public/fonts/SF-Pro-Text-Heavy.otf +0 -0
  346. package/public/fonts/SF-Pro-Text-Medium.otf +0 -0
  347. package/public/fonts/SF-Pro-Text-Regular.otf +0 -0
  348. package/public/fonts/SF-Pro-Text-Semibold.otf +0 -0
  349. package/public/globe.svg +1 -0
  350. package/public/next.svg +1 -0
  351. package/public/theme-preview.html +257 -0
  352. package/public/vercel.svg +1 -0
  353. package/public/window.svg +1 -0
  354. package/run.sh +26 -0
  355. package/scripts/postinstall.js +50 -0
  356. package/skills-lock.json +10 -0
  357. package/specs/event-schema.md +223 -0
  358. package/specs/examples/run.jsonl +3 -0
  359. package/src/app/api/[...path]/route.ts +55 -0
  360. package/src/app/api/auth/token/route.ts +22 -0
  361. package/src/app/evolution/page.tsx +589 -0
  362. package/src/app/favicon.ico +0 -0
  363. package/src/app/globals.css +321 -0
  364. package/src/app/layout.tsx +63 -0
  365. package/src/app/page.tsx +70 -0
  366. package/src/app/skills/page.tsx +369 -0
  367. package/src/components/ApiConfigCard.tsx +199 -0
  368. package/src/components/ColorBends.css +1 -0
  369. package/src/components/ColorBends.d.ts +1 -0
  370. package/src/components/ColorBends.jsx +1 -0
  371. package/src/components/CoreLoopToggle.tsx +111 -0
  372. package/src/components/EnvironmentStatus.tsx +176 -0
  373. package/src/components/EvolutionBackground.tsx +1 -0
  374. package/src/components/ReactQueryProvider.tsx +24 -0
  375. package/src/components/Sidebar.tsx +247 -0
  376. package/src/components/SkillDiffViewer.tsx +154 -0
  377. package/src/components/ThemeAwareBackground.tsx +67 -0
  378. package/src/components/ThemeToggle.tsx +54 -0
  379. package/src/components/WelcomeHero.tsx +77 -0
  380. package/src/components/bits/ClickSpark.tsx +116 -0
  381. package/src/components/bits/CountUp.tsx +98 -0
  382. package/src/components/bits/DarkSelect.tsx +95 -0
  383. package/src/components/bits/DecryptedText.tsx +161 -0
  384. package/src/components/bits/ElectricBorder.tsx +184 -0
  385. package/src/components/bits/GlitchText.tsx +34 -0
  386. package/src/components/bits/ShinyText.tsx +55 -0
  387. package/src/components/bits/SpotlightCard.tsx +42 -0
  388. package/src/components/bits/TextType.tsx +95 -0
  389. package/src/components/bits/index.ts +9 -0
  390. package/src/components/pages/CuratorPage.tsx +632 -0
  391. package/src/components/pages/DatasetPage.tsx +271 -0
  392. package/src/components/pages/EvolutionPage.tsx +676 -0
  393. package/src/components/pages/FunctionCallingPage.tsx +1 -0
  394. package/src/components/pages/LogsPage.tsx +272 -0
  395. package/src/components/pages/MetricsPage.tsx +246 -0
  396. package/src/components/pages/OverviewPage.tsx +420 -0
  397. package/src/components/pages/SettingsPage.tsx +88 -0
  398. package/src/components/pages/SkillStudioPage.tsx +376 -0
  399. package/src/components/ui/animated-theme-toggler.tsx +97 -0
  400. package/src/components/ui/button.tsx +67 -0
  401. package/src/components/ui/card.tsx +103 -0
  402. package/src/components/ui/input.tsx +19 -0
  403. package/src/components/ui/separator.tsx +28 -0
  404. package/src/components/ui/sheet.tsx +147 -0
  405. package/src/components/ui/sidebar.tsx +702 -0
  406. package/src/components/ui/skeleton.tsx +13 -0
  407. package/src/components/ui/theme-toggle.tsx +272 -0
  408. package/src/components/ui/tooltip.tsx +57 -0
  409. package/src/hooks/use-mobile.ts +19 -0
  410. package/src/lib/api.ts +455 -0
  411. package/src/lib/queryClient.ts +12 -0
  412. package/src/lib/utils.ts +6 -0
  413. package/stitch/agent_dashboard/DESIGN_SPEC.md +521 -0
  414. package/stitch/agent_dashboard/prototype.html +676 -0
  415. package/stitch/curator_workspace/code.html +448 -0
  416. package/stitch/curator_workspace/screen.png +0 -0
  417. package/stitch/datasets/code.html +479 -0
  418. package/stitch/datasets/screen.png +0 -0
  419. package/stitch/evolution_history/code.html +461 -0
  420. package/stitch/evolution_history/screen.png +0 -0
  421. package/stitch/hermes_dashboard/DESIGN.md +192 -0
  422. package/stitch/hermes_dashboard/DESIGN_SPEC.md +455 -0
  423. package/stitch/hermes_overview/code.html +399 -0
  424. package/stitch/hermes_overview/screen.png +0 -0
  425. package/stitch/live_logs/code.html +324 -0
  426. package/stitch/live_logs/screen.png +0 -0
  427. package/stitch/skill_hub/code.html +596 -0
  428. package/stitch/skill_hub/screen.png +0 -0
  429. package/stitch/system_metrics/code.html +527 -0
  430. package/stitch/system_metrics/screen.png +0 -0
  431. package/stitch/system_settings/code.html +257 -0
  432. package/stitch/system_settings/screen.png +0 -0
  433. package/test_dashboard.py +201 -0
  434. package/tests/collectors/__init__.py +0 -0
  435. package/tests/collectors/fixtures/sample_session.jsonl +7 -0
  436. package/tests/collectors/test_claude_code_collector.py +171 -0
  437. package/tests/collectors/test_hermes_collector.py +167 -0
  438. package/tests/eval/test_engine.py +234 -0
  439. package/tests/eval/test_scorers.py +249 -0
  440. package/tests/storage/__init__.py +0 -0
  441. package/tests/storage/test_run_store.py +359 -0
  442. package/tests/test_curator.py +559 -0
  443. package/tests/test_e2e_npm.py +621 -0
  444. package/tests/test_mcp_server.py +114 -0
  445. 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