higpertext-cli 0.8.0__py3-none-any.whl

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 (335) hide show
  1. config/adapters_config.json +450 -0
  2. config/antigravity_agent_template.json +31 -0
  3. config/app_config.json +174 -0
  4. config/context_engine.json +33 -0
  5. config/environments/model_defaults.json +5 -0
  6. config/governance/branching_strategy.json +36 -0
  7. config/governance/deployment_gates.json +30 -0
  8. config/governance/guidelines_contract.json +54 -0
  9. config/governance/quality_gates.json +39 -0
  10. config/governance/section_rules.json +22 -0
  11. config/governance/security_guardrails.json +52 -0
  12. config/hooks/README.md +35 -0
  13. config/hooks/custom/test_output_limiter.json +9 -0
  14. config/hooks/global/session_prompt.json +9 -0
  15. config/htx_config.json +24 -0
  16. config/profile_learner.json +18 -0
  17. config/profiles/base_agent.json +40 -0
  18. config/profiles/base_auditor.json +19 -0
  19. config/profiles/base_developer.json +19 -0
  20. config/profiles/base_operator.json +16 -0
  21. config/profiles/global.json +33 -0
  22. config/profiles/software_developer.json +23 -0
  23. config/router_content.json +137 -0
  24. config/semantic_graph.json +66 -0
  25. config/workflows/ado_release_flow.json +38 -0
  26. config/workflows/docs-update.json +33 -0
  27. config/workflows/governance-check.yaml +26 -0
  28. config/workflows/guidelines-sync.json +40 -0
  29. config/workflows/higpertext-build.json +73 -0
  30. config/workflows/higpertext-plan.json +38 -0
  31. config/workflows/higpertext-review.json +41 -0
  32. config/workflows/pr-quality-check.json +56 -0
  33. config/workflows/quality-remediation.json +57 -0
  34. higpertext/__init__.py +18 -0
  35. higpertext/adapters/__init__.py +27 -0
  36. higpertext/adapters/adapter_utils.py +604 -0
  37. higpertext/adapters/claude_adapter/__init__.py +0 -0
  38. higpertext/adapters/claude_adapter/claude_adapter.py +154 -0
  39. higpertext/adapters/copilot_adapter/__init__.py +0 -0
  40. higpertext/adapters/copilot_adapter/copilot_adapter.py +231 -0
  41. higpertext/adapters/gemini_adapter/__init__.py +0 -0
  42. higpertext/adapters/gemini_adapter/gemini_adapter.py +211 -0
  43. higpertext/adapters/llm_formatter.py +46 -0
  44. higpertext/adapters/open_code_adapter/__init__.py +0 -0
  45. higpertext/adapters/open_code_adapter/open_code_adapter.py +480 -0
  46. higpertext/capabilities/capabilities_runner.py +216 -0
  47. higpertext/capabilities/common/agent-builder.json +54 -0
  48. higpertext/capabilities/common/agent-sync.json +34 -0
  49. higpertext/capabilities/common/code-skeletonizer.json +35 -0
  50. higpertext/capabilities/common/commit-report.json +42 -0
  51. higpertext/capabilities/common/context-assembler.json +37 -0
  52. higpertext/capabilities/common/context-budget-report.json +15 -0
  53. higpertext/capabilities/common/dep-manager.json +43 -0
  54. higpertext/capabilities/common/docs-sync.json +14 -0
  55. higpertext/capabilities/common/doctor.json +18 -0
  56. higpertext/capabilities/common/efficiency-meter.json +31 -0
  57. higpertext/capabilities/common/env-catalog.json +13 -0
  58. higpertext/capabilities/common/env-clean.json +14 -0
  59. higpertext/capabilities/common/env-logs.json +16 -0
  60. higpertext/capabilities/common/env-runner.json +23 -0
  61. higpertext/capabilities/common/env-status.json +13 -0
  62. higpertext/capabilities/common/env-stop.json +14 -0
  63. higpertext/capabilities/common/env-template.json +14 -0
  64. higpertext/capabilities/common/error-context-locator.json +23 -0
  65. higpertext/capabilities/common/eval-agent.json +33 -0
  66. higpertext/capabilities/common/file-map.json +17 -0
  67. higpertext/capabilities/common/governance-exception.json +54 -0
  68. higpertext/capabilities/common/graph-query.json +59 -0
  69. higpertext/capabilities/common/graph-rebuild.json +31 -0
  70. higpertext/capabilities/common/graph-visualize.json +37 -0
  71. higpertext/capabilities/common/grep-search.json +176 -0
  72. higpertext/capabilities/common/higpertext-tester.json +25 -0
  73. higpertext/capabilities/common/hook-health.json +19 -0
  74. higpertext/capabilities/common/hook-sync-check.json +19 -0
  75. higpertext/capabilities/common/hooks-manager.json +55 -0
  76. higpertext/capabilities/common/knowledge-asker.json +27 -0
  77. higpertext/capabilities/common/list-rules.json +27 -0
  78. higpertext/capabilities/common/llm-invoke.json +59 -0
  79. higpertext/capabilities/common/load-rules.json +37 -0
  80. higpertext/capabilities/common/memory-manager.json +65 -0
  81. higpertext/capabilities/common/quality-scan.json +21 -0
  82. higpertext/capabilities/common/quality-updater.json +35 -0
  83. higpertext/capabilities/common/rag-index.json +17 -0
  84. higpertext/capabilities/common/report-viewer.json +24 -0
  85. higpertext/capabilities/common/roadmap-report.json +37 -0
  86. higpertext/capabilities/common/scripts/_env_cli.py +65 -0
  87. higpertext/capabilities/common/scripts/agent_builder.py +60 -0
  88. higpertext/capabilities/common/scripts/agent_sync.py +56 -0
  89. higpertext/capabilities/common/scripts/ask_higpertext.py +38 -0
  90. higpertext/capabilities/common/scripts/code_skeletonizer.py +225 -0
  91. higpertext/capabilities/common/scripts/commit_report.py +134 -0
  92. higpertext/capabilities/common/scripts/context_assembler.py +70 -0
  93. higpertext/capabilities/common/scripts/context_budget_report.py +53 -0
  94. higpertext/capabilities/common/scripts/dep_manager.py +81 -0
  95. higpertext/capabilities/common/scripts/docs_sync.py +981 -0
  96. higpertext/capabilities/common/scripts/doctor.py +144 -0
  97. higpertext/capabilities/common/scripts/efficiency_meter.py +83 -0
  98. higpertext/capabilities/common/scripts/env_catalog.py +47 -0
  99. higpertext/capabilities/common/scripts/env_clean.py +30 -0
  100. higpertext/capabilities/common/scripts/env_logs.py +32 -0
  101. higpertext/capabilities/common/scripts/env_runner.py +53 -0
  102. higpertext/capabilities/common/scripts/env_status.py +38 -0
  103. higpertext/capabilities/common/scripts/env_stop.py +30 -0
  104. higpertext/capabilities/common/scripts/env_template.py +73 -0
  105. higpertext/capabilities/common/scripts/error_context_locator.py +138 -0
  106. higpertext/capabilities/common/scripts/eval_agent.py +80 -0
  107. higpertext/capabilities/common/scripts/file_map.py +95 -0
  108. higpertext/capabilities/common/scripts/governance_exception.py +116 -0
  109. higpertext/capabilities/common/scripts/graph_query.py +104 -0
  110. higpertext/capabilities/common/scripts/graph_rebuild.py +107 -0
  111. higpertext/capabilities/common/scripts/graph_visualize.py +76 -0
  112. higpertext/capabilities/common/scripts/grep_search.py +648 -0
  113. higpertext/capabilities/common/scripts/higpertext_tester.py +102 -0
  114. higpertext/capabilities/common/scripts/hook_health.py +149 -0
  115. higpertext/capabilities/common/scripts/hook_sync_check.py +134 -0
  116. higpertext/capabilities/common/scripts/hooks_manager.py +171 -0
  117. higpertext/capabilities/common/scripts/list_rules.py +175 -0
  118. higpertext/capabilities/common/scripts/llm_invoke.py +135 -0
  119. higpertext/capabilities/common/scripts/load_rules.py +379 -0
  120. higpertext/capabilities/common/scripts/memory_manager.py +210 -0
  121. higpertext/capabilities/common/scripts/presentation_engine.py +63 -0
  122. higpertext/capabilities/common/scripts/quality_scan.py +132 -0
  123. higpertext/capabilities/common/scripts/rag_index.py +39 -0
  124. higpertext/capabilities/common/scripts/report_viewer.py +106 -0
  125. higpertext/capabilities/common/scripts/roadmap_report.py +73 -0
  126. higpertext/capabilities/common/scripts/search_router.py +111 -0
  127. higpertext/capabilities/common/scripts/semantic_diff.py +166 -0
  128. higpertext/capabilities/common/scripts/semantic_search.py +43 -0
  129. higpertext/capabilities/common/scripts/session_control.py +136 -0
  130. higpertext/capabilities/common/scripts/smart_read.py +232 -0
  131. higpertext/capabilities/common/scripts/subagent_executor.py +143 -0
  132. higpertext/capabilities/common/scripts/sync_agents.py +353 -0
  133. higpertext/capabilities/common/scripts/task_decomposer.py +78 -0
  134. higpertext/capabilities/common/scripts/telemetry_report.py +36 -0
  135. higpertext/capabilities/common/search-router.json +24 -0
  136. higpertext/capabilities/common/semantic-diff.json +40 -0
  137. higpertext/capabilities/common/semantic-search.json +19 -0
  138. higpertext/capabilities/common/session-clean.json +20 -0
  139. higpertext/capabilities/common/session-start.json +44 -0
  140. higpertext/capabilities/common/smart-read.json +28 -0
  141. higpertext/capabilities/common/subagent-executor.json +25 -0
  142. higpertext/capabilities/common/sync-agents.json +32 -0
  143. higpertext/capabilities/common/task-decomposer.json +37 -0
  144. higpertext/capabilities/common/telemetry-report.json +23 -0
  145. higpertext/capabilities/git/__init__.py +0 -0
  146. higpertext/capabilities/git/committer.json +61 -0
  147. higpertext/capabilities/git/diff.json +33 -0
  148. higpertext/capabilities/git/ls-files.json +44 -0
  149. higpertext/capabilities/git/rm.json +27 -0
  150. higpertext/capabilities/git/scripts/__init__.py +0 -0
  151. higpertext/capabilities/git/scripts/commit_changes.py +1077 -0
  152. higpertext/capabilities/git/scripts/git_diff.py +171 -0
  153. higpertext/capabilities/git/scripts/git_ls_files.py +376 -0
  154. higpertext/capabilities/git/scripts/git_rm.py +62 -0
  155. higpertext/capabilities/security/k8s-auditor.json +33 -0
  156. higpertext/capabilities/security/scripts/k8s_auditor.py +307 -0
  157. higpertext/capabilities/security/scripts/secret_scanner.py +235 -0
  158. higpertext/capabilities/security/secret-scanner.json +32 -0
  159. higpertext/hooks/__init__.py +28 -0
  160. higpertext/hooks/_compat.py +27 -0
  161. higpertext/hooks/hook_tasks/__init__.py +1 -0
  162. higpertext/hooks/hook_tasks/_rules/__init__.py +0 -0
  163. higpertext/hooks/hook_tasks/_rules/bash_rules.py +635 -0
  164. higpertext/hooks/hook_tasks/_rules/context_engine_rule.py +79 -0
  165. higpertext/hooks/hook_tasks/_rules/context_rules.py +199 -0
  166. higpertext/hooks/hook_tasks/_rules/governance_adapter.py +72 -0
  167. higpertext/hooks/hook_tasks/_rules/profile_rules.json +25 -0
  168. higpertext/hooks/hook_tasks/_rules/quality_rules.py +86 -0
  169. higpertext/hooks/hook_tasks/_rules/security_rules.py +214 -0
  170. higpertext/hooks/hook_tasks/_rules/session_rules.py +316 -0
  171. higpertext/hooks/hook_tasks/_rules/telemetry_rules.py +121 -0
  172. higpertext/hooks/hook_tasks/audit_logger_hook.py +28 -0
  173. higpertext/hooks/hook_tasks/hook_bash_guard.py +101 -0
  174. higpertext/hooks/hook_tasks/hook_code_quality.py +48 -0
  175. higpertext/hooks/hook_tasks/hook_context_hint.py +46 -0
  176. higpertext/hooks/hook_tasks/hook_context_manager.py +44 -0
  177. higpertext/hooks/hook_tasks/hook_io.py +122 -0
  178. higpertext/hooks/hook_tasks/hook_loop_guard.py +182 -0
  179. higpertext/hooks/hook_tasks/hook_post_observer.py +54 -0
  180. higpertext/hooks/hook_tasks/hook_read_guard.py +85 -0
  181. higpertext/hooks/hook_tasks/hook_security_guard.py +81 -0
  182. higpertext/hooks/hook_tasks/hook_session_prompt.py +83 -0
  183. higpertext/hooks/hook_tasks/hook_session_stop.py +115 -0
  184. higpertext/hooks/hook_tasks/hook_utils.py +144 -0
  185. higpertext/hooks/hook_tasks/session_guard_hook.py +23 -0
  186. higpertext/hooks/hook_tasks/telemetry_utils.py +176 -0
  187. higpertext/hooks/hook_tasks/test_echo_hook.py +33 -0
  188. higpertext/hooks/hook_tasks/webhook_hook.py +54 -0
  189. higpertext/hooks/hook_tasks/workflow_runner_hook.py +49 -0
  190. higpertext/hooks/hooks_catalog.json +116 -0
  191. higpertext/kernel/__init__.py +63 -0
  192. higpertext/kernel/_compat.py +138 -0
  193. higpertext/kernel/app_config.py +117 -0
  194. higpertext/kernel/application/__init__.py +13 -0
  195. higpertext/kernel/application/agent_registry.py +102 -0
  196. higpertext/kernel/application/capability_manager.py +61 -0
  197. higpertext/kernel/application/commit_reporter.py +247 -0
  198. higpertext/kernel/application/context_builder.py +166 -0
  199. higpertext/kernel/application/context_engine.py +409 -0
  200. higpertext/kernel/application/engine.py +41 -0
  201. higpertext/kernel/application/env_runtime.py +174 -0
  202. higpertext/kernel/application/environment_manager.py +154 -0
  203. higpertext/kernel/application/governance.py +192 -0
  204. higpertext/kernel/application/hook_registry.py +102 -0
  205. higpertext/kernel/application/hook_renderer.py +720 -0
  206. higpertext/kernel/application/ports.py +49 -0
  207. higpertext/kernel/application/profile_learner.py +358 -0
  208. higpertext/kernel/application/profile_service.py +205 -0
  209. higpertext/kernel/application/profile_services.py +6 -0
  210. higpertext/kernel/application/profile_use_cases.py +93 -0
  211. higpertext/kernel/application/rag_service.py +75 -0
  212. higpertext/kernel/application/roadmap_reporter.py +178 -0
  213. higpertext/kernel/application/semantic_engine.py +258 -0
  214. higpertext/kernel/application/session_services.py +33 -0
  215. higpertext/kernel/application/skill_hook_compiler.py +85 -0
  216. higpertext/kernel/application/telemetry.py +326 -0
  217. higpertext/kernel/application/workflow_manager.py +176 -0
  218. higpertext/kernel/config_paths.py +66 -0
  219. higpertext/kernel/domain/__init__.py +12 -0
  220. higpertext/kernel/domain/agent_registry.py +23 -0
  221. higpertext/kernel/domain/commit_reporter.py +155 -0
  222. higpertext/kernel/domain/compilers.py +7 -0
  223. higpertext/kernel/domain/context_engine.py +319 -0
  224. higpertext/kernel/domain/entities.py +51 -0
  225. higpertext/kernel/domain/env_runtime.py +62 -0
  226. higpertext/kernel/domain/governance.py +198 -0
  227. higpertext/kernel/domain/hook_models.py +29 -0
  228. higpertext/kernel/domain/profile_learner.py +186 -0
  229. higpertext/kernel/domain/rag.py +70 -0
  230. higpertext/kernel/domain/repositories.py +8 -0
  231. higpertext/kernel/domain/roadmap_reporter.py +80 -0
  232. higpertext/kernel/domain/semantic_engine.py +107 -0
  233. higpertext/kernel/engine.py +42 -0
  234. higpertext/kernel/htx_resolver.py +69 -0
  235. higpertext/kernel/infrastructure/__init__.py +13 -0
  236. higpertext/kernel/infrastructure/agent_registry.py +40 -0
  237. higpertext/kernel/infrastructure/cache/capability_cache.py +319 -0
  238. higpertext/kernel/infrastructure/capability_helper.py +40 -0
  239. higpertext/kernel/infrastructure/cli/__init__.py +1 -0
  240. higpertext/kernel/infrastructure/cli/agent_commands.py +62 -0
  241. higpertext/kernel/infrastructure/cli/arguments.py +39 -0
  242. higpertext/kernel/infrastructure/cli/capability_command_builder.py +86 -0
  243. higpertext/kernel/infrastructure/cli/capability_task_service.py +234 -0
  244. higpertext/kernel/infrastructure/cli/cli_search.py +234 -0
  245. higpertext/kernel/infrastructure/cli/parameter_contracts.py +83 -0
  246. higpertext/kernel/infrastructure/cli/parser_builder.py +122 -0
  247. higpertext/kernel/infrastructure/cli/profile_commands.py +89 -0
  248. higpertext/kernel/infrastructure/cli/roadmap_commands.py +117 -0
  249. higpertext/kernel/infrastructure/cli/router.py +1110 -0
  250. higpertext/kernel/infrastructure/cli/session_commands.py +36 -0
  251. higpertext/kernel/infrastructure/cli/task_commands.py +23 -0
  252. higpertext/kernel/infrastructure/cli/task_result_reporter.py +56 -0
  253. higpertext/kernel/infrastructure/cli/workflow_commands.py +25 -0
  254. higpertext/kernel/infrastructure/compilers/__init__.py +3 -0
  255. higpertext/kernel/infrastructure/compilers/factory.py +27 -0
  256. higpertext/kernel/infrastructure/compilers/graph_compiler.py +20 -0
  257. higpertext/kernel/infrastructure/compilers/guide_compiler.py +50 -0
  258. higpertext/kernel/infrastructure/compilers/hook_compiler.py +69 -0
  259. higpertext/kernel/infrastructure/compilers/playbook_compiler.py +154 -0
  260. higpertext/kernel/infrastructure/context_engine.py +303 -0
  261. higpertext/kernel/infrastructure/database/local_vector_store.py +99 -0
  262. higpertext/kernel/infrastructure/deployment/__init__.py +1 -0
  263. higpertext/kernel/infrastructure/deployment/resource_deployer.py +283 -0
  264. higpertext/kernel/infrastructure/diagnostics/__init__.py +1 -0
  265. higpertext/kernel/infrastructure/diagnostics/health.py +191 -0
  266. higpertext/kernel/infrastructure/env_runtime.py +227 -0
  267. higpertext/kernel/infrastructure/execution/__init__.py +1 -0
  268. higpertext/kernel/infrastructure/execution/parallel.py +188 -0
  269. higpertext/kernel/infrastructure/execution/resilience.py +155 -0
  270. higpertext/kernel/infrastructure/file_repositories.py +213 -0
  271. higpertext/kernel/infrastructure/governance.py +198 -0
  272. higpertext/kernel/infrastructure/hook_config_loader.py +53 -0
  273. higpertext/kernel/infrastructure/hook_webhook_dispatcher.py +61 -0
  274. higpertext/kernel/infrastructure/hook_workflow_bridge.py +60 -0
  275. higpertext/kernel/infrastructure/llm/__init__.py +6 -0
  276. higpertext/kernel/infrastructure/llm/provider.py +46 -0
  277. higpertext/kernel/infrastructure/llm/providers/__init__.py +0 -0
  278. higpertext/kernel/infrastructure/llm/providers/anthropic_provider.py +94 -0
  279. higpertext/kernel/infrastructure/llm/providers/gemini_embeddings.py +74 -0
  280. higpertext/kernel/infrastructure/llm/providers/gemini_provider.py +101 -0
  281. higpertext/kernel/infrastructure/llm/providers/ollama_provider.py +110 -0
  282. higpertext/kernel/infrastructure/llm/providers/openai_provider.py +98 -0
  283. higpertext/kernel/infrastructure/llm/registry.py +81 -0
  284. higpertext/kernel/infrastructure/logger.py +303 -0
  285. higpertext/kernel/infrastructure/output_store.py +70 -0
  286. higpertext/kernel/infrastructure/parser/__init__.py +1 -0
  287. higpertext/kernel/infrastructure/parser/code_chunker.py +144 -0
  288. higpertext/kernel/infrastructure/parser/language/__init__.py +14 -0
  289. higpertext/kernel/infrastructure/parser/language/base.py +41 -0
  290. higpertext/kernel/infrastructure/parser/language/powershell_parser.py +35 -0
  291. higpertext/kernel/infrastructure/parser/language/python_parser.py +98 -0
  292. higpertext/kernel/infrastructure/parser/language/typescript_parser.py +91 -0
  293. higpertext/kernel/infrastructure/parser/semantic_graph.py +409 -0
  294. higpertext/kernel/infrastructure/presentation/__init__.py +1 -0
  295. higpertext/kernel/infrastructure/presentation/html_renderer.py +137 -0
  296. higpertext/kernel/infrastructure/presentation/markdown_renderer.py +84 -0
  297. higpertext/kernel/infrastructure/presentation/markdown_report_renderer.py +97 -0
  298. higpertext/kernel/infrastructure/profile_store.py +28 -0
  299. higpertext/kernel/infrastructure/semantic_engine.py +289 -0
  300. higpertext/kernel/infrastructure/telemetry_reporter.py +132 -0
  301. higpertext/kernel/infrastructure/validation/__init__.py +1 -0
  302. higpertext/kernel/infrastructure/validation/contract_validator.py +163 -0
  303. higpertext/kernel/pkg_resources.py +38 -0
  304. higpertext/kernel/session_manager.py +319 -0
  305. higpertext/templates/env/generic-shell.yaml +21 -0
  306. higpertext/templates/env/node-vitest.yaml +27 -0
  307. higpertext/templates/env/python-pytest.yaml +29 -0
  308. higpertext/templates/html/commit_body.html +20 -0
  309. higpertext/templates/html/commit_diff.html +4 -0
  310. higpertext/templates/html/commit_index.html +29 -0
  311. higpertext/templates/html/commit_layer.html +11 -0
  312. higpertext/templates/html/commit_shell.html +28 -0
  313. higpertext/templates/html/graph_visualize.html +86 -0
  314. higpertext/templates/html/roadmap_body.html +12 -0
  315. higpertext/templates/html/roadmap_phase.html +5 -0
  316. higpertext/templates/html/roadmap_shell.html +29 -0
  317. higpertext/templates/markdown/commit_report.md +18 -0
  318. higpertext/templates/markdown/efficiency_report.md +12 -0
  319. higpertext/templates/markdown/roadmap_report.md +25 -0
  320. higpertext/templates/skills/best-practices.md +7 -0
  321. higpertext/templates/skills/clean-code.md +8 -0
  322. higpertext/templates/skills/ddd-standards.md +7 -0
  323. higpertext/templates/skills/tdd-practices.md +7 -0
  324. higpertext/templates/subagents/architect.md +7 -0
  325. higpertext/templates/subagents/test-engineer.md +7 -0
  326. higpertext/templates/workflows/build.json +23 -0
  327. higpertext/templates/workflows/compact.json +21 -0
  328. higpertext/templates/workflows/plan.json +59 -0
  329. higpertext/templates/workflows/review.json +26 -0
  330. higpertext/templates/workflows/spec.json +27 -0
  331. higpertext_cli-0.8.0.dist-info/METADATA +35 -0
  332. higpertext_cli-0.8.0.dist-info/RECORD +335 -0
  333. higpertext_cli-0.8.0.dist-info/WHEEL +5 -0
  334. higpertext_cli-0.8.0.dist-info/entry_points.txt +2 -0
  335. higpertext_cli-0.8.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,155 @@
1
+ """higpertext Resilience — Retry con backoff exponencial y Circuit Breaker por capability
2
+ (Infraestructura)."""
3
+
4
+ import time
5
+ import threading
6
+ from dataclasses import dataclass, field
7
+ from enum import Enum
8
+
9
+ from higpertext.kernel.infrastructure.logger import get_logger
10
+ _log = get_logger()
11
+
12
+
13
+ class CircuitState(Enum):
14
+ CLOSED = "closed"
15
+ OPEN = "open"
16
+ HALF_OPEN = "half_open"
17
+
18
+
19
+ @dataclass
20
+ class CircuitBreaker:
21
+ failure_threshold: int = 3
22
+ recovery_timeout: float = 60.0
23
+ success_threshold: int = 1
24
+
25
+ _state: CircuitState = field(default=CircuitState.CLOSED, init=False, repr=False)
26
+ _failure_count: int = field(default=0, init=False, repr=False)
27
+ _success_count: int = field(default=0, init=False, repr=False)
28
+ _opened_at: float = field(default=0.0, init=False, repr=False)
29
+ _lock: threading.Lock = field(default_factory=threading.Lock, init=False, repr=False)
30
+
31
+ @property
32
+ def state(self) -> CircuitState:
33
+ with self._lock:
34
+ if self._state == CircuitState.OPEN:
35
+ if time.monotonic() - self._opened_at >= self.recovery_timeout:
36
+ self._state = CircuitState.HALF_OPEN
37
+ self._success_count = 0
38
+ return self._state
39
+
40
+ def is_allowed(self) -> bool:
41
+ return self.state != CircuitState.OPEN
42
+
43
+ def record_success(self) -> None:
44
+ with self._lock:
45
+ self._failure_count = 0
46
+ if self._state == CircuitState.HALF_OPEN:
47
+ self._success_count += 1
48
+ if self._success_count >= self.success_threshold:
49
+ self._state = CircuitState.CLOSED
50
+ _log.info("[Circuit] Circuito CERRADO — servicio recuperado.")
51
+
52
+ def record_failure(self) -> None:
53
+ with self._lock:
54
+ self._failure_count += 1
55
+ if self._state == CircuitState.HALF_OPEN:
56
+ self._state = CircuitState.OPEN
57
+ self._opened_at = time.monotonic()
58
+ _log.info(
59
+ f"[Circuit] Circuito ABIERTO nuevamente — reintentando en {
60
+ self.recovery_timeout}s."
61
+ )
62
+ elif (
63
+ self._state == CircuitState.CLOSED and self._failure_count >= self.failure_threshold
64
+ ):
65
+ self._state = CircuitState.OPEN
66
+ self._opened_at = time.monotonic()
67
+ _log.error(
68
+ f"[Circuit] Circuito ABIERTO tras {
69
+ self._failure_count} fallos consecutivos — reintentando en {
70
+ self.recovery_timeout}s."
71
+ )
72
+
73
+
74
+ class ResilienceManager:
75
+ DEFAULT_MAX_RETRIES = 3
76
+ DEFAULT_BASE_DELAY = 2.0
77
+ DEFAULT_MAX_DELAY = 30.0
78
+ DEFAULT_JITTER = 0.5
79
+
80
+ def __init__(self) -> None:
81
+ self._breakers: dict[str, CircuitBreaker] = {}
82
+ self._lock = threading.Lock()
83
+
84
+ def get_breaker(self, capability_id: str) -> CircuitBreaker:
85
+ with self._lock:
86
+ if capability_id not in self._breakers:
87
+ self._breakers[capability_id] = CircuitBreaker()
88
+ return self._breakers[capability_id]
89
+
90
+ def execute_with_resilience(
91
+ self,
92
+ capability_id: str,
93
+ fn,
94
+ max_retries: int = DEFAULT_MAX_RETRIES,
95
+ base_delay: float = DEFAULT_BASE_DELAY,
96
+ max_delay: float = DEFAULT_MAX_DELAY,
97
+ ):
98
+ import random
99
+
100
+ breaker = self.get_breaker(capability_id)
101
+
102
+ if not breaker.is_allowed():
103
+ timeout_s = f"{breaker.recovery_timeout:.0f}s"
104
+ _log.info(
105
+ f"[Circuit] Capability '{capability_id}' bloqueada — circuito ABIERTO."
106
+ f" Reintentando automáticamente en {timeout_s}."
107
+ )
108
+ return None
109
+
110
+ for attempt in range(1, max_retries + 1):
111
+ result = fn()
112
+
113
+ if result is not None and result.returncode == 0:
114
+ breaker.record_success()
115
+ return result
116
+
117
+ breaker.record_failure()
118
+
119
+ if not breaker.is_allowed():
120
+ _log.info(f"[Retry] Circuito abierto — deteniendo reintentos para '{capability_id}'.")
121
+ return result
122
+
123
+ if attempt < max_retries:
124
+ delay = min(base_delay * (2 ** (attempt - 1)), max_delay)
125
+ jitter = delay * self.DEFAULT_JITTER * random.random()
126
+ wait = delay + jitter
127
+ _log.info(
128
+ f"[Retry] Intento {attempt}/{max_retries} falló para '{capability_id}'."
129
+ f" Reintentando en {wait:.1f}s..."
130
+ )
131
+ time.sleep(wait)
132
+ else:
133
+ _log.info(f"[Retry] Agotados {max_retries} intentos para '{capability_id}'.")
134
+
135
+ return result
136
+
137
+ def get_status(self) -> dict:
138
+ with self._lock:
139
+ return {
140
+ cap_id: {
141
+ "state": breaker.state.value,
142
+ "failures": breaker._failure_count,
143
+ }
144
+ for cap_id, breaker in self._breakers.items()
145
+ }
146
+
147
+
148
+ _resilience_manager: ResilienceManager | None = None
149
+
150
+
151
+ def get_resilience_manager() -> ResilienceManager:
152
+ global _resilience_manager
153
+ if _resilience_manager is None:
154
+ _resilience_manager = ResilienceManager()
155
+ return _resilience_manager
@@ -0,0 +1,213 @@
1
+ """Implementaciones concretas de repositorios JSON (Infraestructura)."""
2
+
3
+ from __future__ import annotations
4
+ from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME, FILE_EXTENSION
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from higpertext.kernel.application.ports import (
9
+ IProfileRepository,
10
+ ICapabilityRepository,
11
+ IWorkflowRepository,
12
+ ISessionRepository,
13
+ )
14
+ from higpertext.kernel.domain.entities import Profile, Capability, Workflow, Session
15
+
16
+
17
+ class FileProfileRepository(IProfileRepository):
18
+ def __init__(self, base_profiles_dir: Path):
19
+ self.profiles_dir = base_profiles_dir
20
+
21
+ def _get_custom_profiles_dir(self) -> Path:
22
+ return Path(os.getcwd()).resolve() / WORKSPACE_DIR_NAME / "profiles"
23
+
24
+ def _get_project_profiles_dir(self) -> Path:
25
+ return Path(os.getcwd()).resolve() / "src" / "config" / "profiles"
26
+
27
+ def _get_all_files(self) -> list[Path]:
28
+ pattern = f"*{FILE_EXTENSION}"
29
+ files = list(self.profiles_dir.glob(pattern))
30
+ for extra_dir in (
31
+ self._get_custom_profiles_dir(),
32
+ self._get_project_profiles_dir(),
33
+ ):
34
+ if extra_dir.exists():
35
+ files.extend(extra_dir.glob(pattern))
36
+ return files
37
+
38
+ def load(self, name: str) -> Profile:
39
+ filename = f"{name}{FILE_EXTENSION}"
40
+ candidates = [
41
+ self._get_custom_profiles_dir() / filename,
42
+ self._get_project_profiles_dir() / filename,
43
+ self.profiles_dir / filename,
44
+ ]
45
+ p = next((c for c in candidates if c.exists()), None)
46
+ if p is None:
47
+ raise FileNotFoundError(f"Perfil no encontrado: {name}")
48
+ data = json.loads(p.read_text(encoding="utf-8"))
49
+ return Profile(
50
+ name=data.get("name", name),
51
+ description=data.get("description", ""),
52
+ system_prompt=data.get("system_prompt", ""),
53
+ capabilities=data.get("capabilities", []),
54
+ session_skills=data.get("session_skills", []),
55
+ session_subagents=data.get("session_subagents", []),
56
+ governance_access=data.get("governance_access", False),
57
+ extends=data.get("extends"),
58
+ model=data.get("model"),
59
+ )
60
+
61
+ def list_all(self) -> list[str]:
62
+ seen = set()
63
+ result = []
64
+ for p in self._get_all_files():
65
+ if p.stem not in seen:
66
+ seen.add(p.stem)
67
+ result.append(p.stem)
68
+ return result
69
+
70
+
71
+ class FileCapabilityRepository(ICapabilityRepository):
72
+ def __init__(self, base_capabilities_dir: Path):
73
+ self.caps_dir = base_capabilities_dir
74
+
75
+ def _get_custom_capabilities_dir(self) -> Path:
76
+ return Path(os.getcwd()).resolve() / WORKSPACE_DIR_NAME / "capabilities"
77
+
78
+ def _get_project_capabilities_dir(self) -> Path:
79
+ return Path(os.getcwd()).resolve() / "src" / "higpertext" / "capabilities"
80
+
81
+ def _get_all_files(self) -> list[Path]:
82
+ pattern = f"*{FILE_EXTENSION}"
83
+ files = list(self.caps_dir.rglob(pattern))
84
+ for extra_dir in (
85
+ self._get_custom_capabilities_dir(),
86
+ self._get_project_capabilities_dir(),
87
+ ):
88
+ if extra_dir.exists() and extra_dir != self.caps_dir:
89
+ files.extend(extra_dir.rglob(pattern))
90
+ return files
91
+
92
+ def load(self, cap_id: str) -> Capability:
93
+ f = next(
94
+ (f for f in self._get_all_files() if f.stem == cap_id or cap_id.endswith(f".{f.stem}")),
95
+ None,
96
+ )
97
+
98
+ if not f or not f.exists():
99
+ raise FileNotFoundError(f"Capacidad no encontrada: {cap_id}")
100
+ data = json.loads(f.read_text(encoding="utf-8"))
101
+ return Capability(
102
+ id=data.get("id", cap_id),
103
+ version=data.get("version", "1.0.0"),
104
+ name=data.get("name", ""),
105
+ description=data.get("description", ""),
106
+ entrypoint=data.get("entrypoint", ""),
107
+ language=data.get("language", "python"),
108
+ parameters=data.get("parameters", []),
109
+ security=data.get("security", {}),
110
+ hook_task_id=data.get("hook_task_id"),
111
+ )
112
+
113
+ def list_all(self) -> list[Capability]:
114
+ caps = []
115
+ for f in self._get_all_files():
116
+ try:
117
+ data = json.loads(f.read_text(encoding="utf-8"))
118
+ caps.append(
119
+ Capability(
120
+ id=data.get("id", f.stem),
121
+ version=data.get("version", "1.0.0"),
122
+ name=data.get("name", ""),
123
+ description=data.get("description", ""),
124
+ entrypoint=data.get("entrypoint", ""),
125
+ language=data.get("language", "python"),
126
+ parameters=data.get("parameters", []),
127
+ security=data.get("security", {}),
128
+ hook_task_id=data.get("hook_task_id"),
129
+ )
130
+ )
131
+ except Exception: # nosec B110
132
+ pass
133
+ return caps
134
+
135
+
136
+ class FileWorkflowRepository(IWorkflowRepository):
137
+ def __init__(self, workflows_dir: Path):
138
+ self.wf_dir = workflows_dir
139
+
140
+ def _get_project_workflows_dir(self) -> Path:
141
+ return Path(os.getcwd()).resolve() / "src" / "workflows"
142
+
143
+ def _get_all_workflow_files(self) -> list[Path]:
144
+ pattern = f"*{FILE_EXTENSION}"
145
+ files = list(self.wf_dir.glob(pattern)) if self.wf_dir.exists() else []
146
+ project_dir = self._get_project_workflows_dir()
147
+ if project_dir.exists() and project_dir != self.wf_dir:
148
+ files.extend(project_dir.glob(pattern))
149
+ return files
150
+
151
+ def list_all(self) -> list[Workflow]:
152
+ wfs = []
153
+ for f in self._get_all_workflow_files():
154
+ try:
155
+ data = json.loads(f.read_text(encoding="utf-8"))
156
+ wfs.append(
157
+ Workflow(
158
+ id=data.get("id", f.stem),
159
+ name=data.get("name", ""),
160
+ description=data.get("description", ""),
161
+ steps=data.get("steps", []),
162
+ required_profile=data.get("required_profile", ""),
163
+ )
164
+ )
165
+ except Exception: # nosec B110
166
+ pass
167
+ return wfs
168
+
169
+
170
+ class FileSessionRepository(ISessionRepository):
171
+ def __init__(self, session_file: Path):
172
+ self.session_file = session_file
173
+
174
+ def load_active(self) -> Session | None:
175
+ if not self.session_file.exists():
176
+ return None
177
+ try:
178
+ data = json.loads(self.session_file.read_text(encoding="utf-8"))
179
+ return Session(
180
+ session_id=data.get("session_id", ""),
181
+ profile=data.get("profile", ""),
182
+ assistant=data.get("assistant", ""),
183
+ status=data.get("status", "inactive"),
184
+ created_at=data.get("created_at", ""),
185
+ active_skills=data.get("active_skills", []),
186
+ active_subagents=data.get("active_subagents", []),
187
+ tasks=data.get("tasks", []),
188
+ )
189
+ except Exception:
190
+ return None
191
+
192
+ def save(self, session: Session) -> None:
193
+ self.session_file.parent.mkdir(parents=True, exist_ok=True)
194
+ data = {
195
+ "session_id": session.session_id,
196
+ "profile": session.profile,
197
+ "assistant": session.assistant,
198
+ "status": session.status,
199
+ "created_at": session.created_at,
200
+ "active_skills": session.active_skills,
201
+ "active_subagents": session.active_subagents,
202
+ "tasks": session.tasks,
203
+ }
204
+ self.session_file.write_text(
205
+ json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8"
206
+ )
207
+
208
+ def delete(self) -> None:
209
+ if self.session_file.exists():
210
+ try:
211
+ self.session_file.unlink()
212
+ except OSError: # nosec B110
213
+ pass
@@ -0,0 +1,198 @@
1
+ """Implementaciones de infraestructura para gobernanza."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ from pathlib import Path
6
+ from datetime import datetime, timezone
7
+
8
+ from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
9
+ from higpertext.kernel.domain.governance import Rule, RuleOverride, Scope, Severity, Verdict
10
+
11
+
12
+ _SECTION_RULES_PATH = Path(__file__).resolve().parents[3] / "config" / "governance" / "section_rules.json"
13
+
14
+
15
+ def _load_section_rules() -> dict:
16
+ """Carga section_rules.json una sola vez. Devuelve dict vacío si no existe."""
17
+ try:
18
+ return json.loads(_SECTION_RULES_PATH.read_text(encoding="utf-8"))
19
+ except (OSError, json.JSONDecodeError):
20
+ return {}
21
+
22
+
23
+ def _rule_id(section: str, index: int) -> str:
24
+ return f"{section}.rule-{index:02d}"
25
+
26
+
27
+ class ContractLoader:
28
+ """Carga y mergea reglas de gobernanza global + perfil activo."""
29
+
30
+ def __init__(self, project_root: Path) -> None:
31
+ self._root = project_root
32
+ self._contract_path = (
33
+ project_root / "src" / "config" / "governance" / "guidelines_contract.json"
34
+ )
35
+ _rules = _load_section_rules()
36
+ # section -> (Scope, Severity)
37
+ self._section_map: dict[str, tuple[Scope, Severity]] = {
38
+ section: (Scope(cfg["scope"]), Severity(cfg["severity"]))
39
+ for section, cfg in _rules.get("section_map", {}).items()
40
+ }
41
+ # frozenset of (section, index) pairs that are auto-checkable
42
+ self._automated_positions: frozenset[tuple[str, int]] = frozenset(
43
+ (entry["section"], entry["index"])
44
+ for entry in _rules.get("automated_rule_positions", [])
45
+ )
46
+ # (section, index) -> canonical rule ID
47
+ self._canonical_ids: dict[tuple[str, int], str] = {
48
+ (entry["section"], entry["index"]): entry["id"]
49
+ for entry in _rules.get("canonical_ids", [])
50
+ }
51
+
52
+ def load(self, profile: str = "") -> list[Rule]:
53
+ """Retorna las reglas efectivas para el perfil dado."""
54
+ base_rules = self._load_base_rules()
55
+ if not profile:
56
+ return base_rules
57
+
58
+ overrides = self._load_profile_overrides(profile)
59
+ additions = self._load_profile_additions(profile)
60
+
61
+ merged = _merge_rules(base_rules, overrides)
62
+ return merged + additions
63
+
64
+ def load_raw_contract(self) -> dict:
65
+ if not self._contract_path.exists():
66
+ return {}
67
+ try:
68
+ data = json.loads(self._contract_path.read_text(encoding="utf-8"))
69
+ security = self._load_domain_file("security_guardrails")
70
+ if security:
71
+ data["security_guardrails"] = security
72
+ return data
73
+ except (OSError, json.JSONDecodeError):
74
+ return {}
75
+
76
+ def load_security_guardrails(self) -> dict:
77
+ return self._load_domain_file("security_guardrails")
78
+
79
+ def load_branching_strategy(self) -> dict:
80
+ return self._load_domain_file("branching_strategy")
81
+
82
+ def load_quality_gates(self) -> dict:
83
+ return self._load_domain_file("quality_gates")
84
+
85
+ def load_deployment_gates(self) -> dict:
86
+ return self._load_domain_file("deployment_gates")
87
+
88
+ def _load_domain_file(self, name: str) -> dict:
89
+ path = self._root / "src" / "config" / "governance" / f"{name}.json"
90
+ if not path.exists():
91
+ return {}
92
+ try:
93
+ return json.loads(path.read_text(encoding="utf-8"))
94
+ except (OSError, json.JSONDecodeError):
95
+ return {}
96
+
97
+ def _load_base_rules(self) -> list[Rule]:
98
+ contract = self.load_raw_contract()
99
+ guidelines = contract.get("guidelines", {})
100
+ rules: list[Rule] = []
101
+ for section, entries in guidelines.items():
102
+ scope, severity = self._section_map.get(section, (Scope.ANY, Severity.MEDIUM))
103
+ for i, text in enumerate(entries, 1):
104
+ pos = (section, i)
105
+ rid = self._canonical_ids.get(pos, _rule_id(section, i))
106
+ automated = pos in self._automated_positions
107
+ rules.append(
108
+ Rule(
109
+ id=rid,
110
+ description=text,
111
+ severity=severity,
112
+ scopes=(scope,),
113
+ automated=automated,
114
+ source="global",
115
+ )
116
+ )
117
+ return rules
118
+
119
+ def _profile_governance_path(self, profile: str) -> Path:
120
+ return self._root / WORKSPACE_DIR_NAME / "governance" / f"{profile}.json"
121
+
122
+ def _load_profile_overrides(self, profile: str) -> list[RuleOverride]:
123
+ path = self._profile_governance_path(profile)
124
+ if not path.exists():
125
+ return []
126
+ try:
127
+ data = json.loads(path.read_text(encoding="utf-8"))
128
+ overrides = []
129
+ for rule_id, cfg in data.get("overrides", {}).items():
130
+ overrides.append(
131
+ RuleOverride(
132
+ rule_id=rule_id,
133
+ source=profile,
134
+ numeric_threshold=cfg.get("threshold"),
135
+ description=cfg.get("description"),
136
+ )
137
+ )
138
+ return overrides
139
+ except (OSError, json.JSONDecodeError):
140
+ return []
141
+
142
+ def _load_profile_additions(self, profile: str) -> list[Rule]:
143
+ path = self._profile_governance_path(profile)
144
+ if not path.exists():
145
+ return []
146
+ try:
147
+ data = json.loads(path.read_text(encoding="utf-8"))
148
+ rules: list[Rule] = []
149
+ for section, entries in data.get("additions", {}).items():
150
+ scope, severity = self._section_map.get(section, (Scope.ANY, Severity.MEDIUM))
151
+ for i, entry in enumerate(entries, 1):
152
+ if isinstance(entry, str):
153
+ text, cfg = entry, {}
154
+ else:
155
+ text = entry.get("rule", "")
156
+ cfg = entry
157
+ rid = cfg.get("id", f"{profile}.{section}.rule-{i:02d}")
158
+ rules.append(
159
+ Rule(
160
+ id=rid,
161
+ description=text,
162
+ severity=Severity(cfg.get("severity", severity.value)),
163
+ scopes=(Scope(cfg.get("scope", scope.value)),),
164
+ automated=cfg.get("automated", False),
165
+ capability=cfg.get("capability"),
166
+ source=profile,
167
+ )
168
+ )
169
+ return rules
170
+ except (OSError, json.JSONDecodeError):
171
+ return []
172
+
173
+
174
+ def _merge_rules(base: list[Rule], overrides: list[RuleOverride]) -> list[Rule]:
175
+ override_map = {o.rule_id: o for o in overrides}
176
+ return [rule.merge(override_map[rule.id]) if rule.id in override_map else rule for rule in base]
177
+
178
+
179
+ class AuditLog:
180
+ def __init__(self, project_root: Path) -> None:
181
+ self._store = project_root / WORKSPACE_DIR_NAME / "state" / "governance_audit.jsonl"
182
+
183
+ def record(self, verdict: Verdict, profile: str, triggered_by: str = "") -> None:
184
+ entry = {
185
+ "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"),
186
+ "profile": profile,
187
+ "scope": verdict.scope,
188
+ "status": verdict.status.value,
189
+ "triggered_by": triggered_by,
190
+ "findings": len(verdict.findings),
191
+ "blocked": len(verdict.blocking_findings),
192
+ }
193
+ try:
194
+ self._store.parent.mkdir(parents=True, exist_ok=True)
195
+ with self._store.open("a", encoding="utf-8") as fh:
196
+ fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
197
+ except OSError:
198
+ pass
@@ -0,0 +1,53 @@
1
+ """Utilidad compartida para cargar y guardar hooks_config.json."""
2
+
3
+ from __future__ import annotations
4
+ from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
5
+ import json
6
+ from pathlib import Path
7
+ from higpertext.kernel.pkg_resources import pkg_data_root
8
+
9
+ _EMPTY: dict = {"hooks": [], "profile_hooks": {}, "webhooks": []}
10
+
11
+ # Ruta canónica del estado activo dentro del proyecto destino
12
+ _CONFIG_PATH = Path(WORKSPACE_DIR_NAME) / "config" / "hooks_config.json"
13
+ # Catálogo canónico de definiciones de hooks, empaquetado junto al código del engine
14
+ _CATALOG_REL_PATH = Path("hooks") / "hooks_catalog.json"
15
+
16
+
17
+ def load_hooks_config(project_root: Path) -> dict:
18
+ """Carga el estado activo del proyecto (profile_hooks, webhooks, hooks del proyecto)."""
19
+ config_path = project_root / _CONFIG_PATH
20
+ if not config_path.exists():
21
+ return dict(_EMPTY)
22
+ try:
23
+ return json.loads(config_path.read_text(encoding="utf-8"))
24
+ except (OSError, json.JSONDecodeError):
25
+ return dict(_EMPTY)
26
+
27
+
28
+ def load_hook_definitions(engine_root: Path) -> list[dict]:
29
+ """Carga el catálogo canónico de definiciones de hooks del engine.
30
+
31
+ Busca primero en el árbol de fuente (modo dev), y si no existe ahí,
32
+ cae a los recursos empaquetados (higpertext_data) cuando se instala como CLI.
33
+ """
34
+ candidates = [engine_root / "src" / "higpertext" / _CATALOG_REL_PATH]
35
+ pkg_root = pkg_data_root()
36
+ if pkg_root is not None:
37
+ candidates.append(pkg_root / "higpertext" / _CATALOG_REL_PATH)
38
+
39
+ for definitions_path in candidates:
40
+ if not definitions_path.exists():
41
+ continue
42
+ try:
43
+ data = json.loads(definitions_path.read_text(encoding="utf-8"))
44
+ return data.get("hooks", [])
45
+ except (OSError, json.JSONDecodeError):
46
+ continue
47
+ return []
48
+
49
+
50
+ def save_hooks_config(project_root: Path, data: dict) -> None:
51
+ config_path = project_root / _CONFIG_PATH
52
+ config_path.parent.mkdir(parents=True, exist_ok=True)
53
+ config_path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
@@ -0,0 +1,61 @@
1
+ """WebhookDispatcher — emite eventos HTTP a sistemas externos sin dependencias extra."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ import os
6
+ import re
7
+ import urllib.request
8
+ import urllib.error
9
+ from pathlib import Path
10
+
11
+ from higpertext.kernel.application.hook_registry import HookRegistry
12
+
13
+ _VAR_RE = re.compile(r"\$\{(\w+)\}")
14
+
15
+
16
+ def _resolve_env(value: str) -> str:
17
+ return _VAR_RE.sub(lambda m: os.environ.get(m.group(1), ""), value)
18
+
19
+
20
+ def _render_payload(template: dict, context: dict) -> dict:
21
+ """Sustituye {{key}} en valores del template con valores del contexto."""
22
+ result: dict = {}
23
+ for k, v in template.items():
24
+ if isinstance(v, str):
25
+ for ctx_key, ctx_val in context.items():
26
+ v = v.replace(f"{{{{{ctx_key}}}}}", str(ctx_val))
27
+ result[k] = v
28
+ else:
29
+ result[k] = v
30
+ return result
31
+
32
+
33
+ class WebhookDispatcher:
34
+ def __init__(self, project_root: Path) -> None:
35
+ self.registry = HookRegistry(project_root)
36
+
37
+ def dispatch(self, event: str, assistant: str, context: dict) -> None:
38
+ """Emite webhooks activos para el evento dado."""
39
+ webhooks = self.registry.get_webhooks_for(assistant)
40
+ for wh in webhooks:
41
+ if wh.event != event:
42
+ continue
43
+ url = _resolve_env(wh.url)
44
+ if not url:
45
+ continue
46
+ payload = _render_payload(wh.payload_template, context)
47
+ self._post(url, payload, wh.timeout)
48
+
49
+ def _post(self, url: str, payload: dict, timeout: int) -> None:
50
+ body = json.dumps(payload).encode("utf-8")
51
+ req = urllib.request.Request(
52
+ url,
53
+ data=body,
54
+ headers={"Content-Type": "application/json"},
55
+ method="POST",
56
+ )
57
+ try:
58
+ with urllib.request.urlopen(req, timeout=timeout):
59
+ pass
60
+ except urllib.error.URLError: # nosec B110
61
+ pass # Webhook failures are non-blocking