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