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,409 @@
1
+ """Context Engine Application layer — ContextAssembler, EfficiencyMeter, and TaskDecomposer."""
2
+
3
+ from __future__ import annotations
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+ from higpertext.kernel.domain.context_engine import (
8
+ ContextPack,
9
+ FileSkeleton,
10
+ SymbolRef,
11
+ TaskIntent,
12
+ EfficiencyReport,
13
+ TaskNode,
14
+ TaskGraph,
15
+ )
16
+
17
+ from higpertext.kernel.infrastructure.context_engine import (
18
+ GraphIndex,
19
+ extract_skeleton,
20
+ should_skeletonize,
21
+ TelemetryReader,
22
+ )
23
+
24
+ # Ratios heurísticos de chars-por-token según tipo de contenido.
25
+ _CHARS_PER_TOKEN: dict[str, float] = {
26
+ "markdown": 4.0,
27
+ "code": 3.5,
28
+ "skeleton": 5.0,
29
+ }
30
+ _MEMORY_FILE = Path(".memory/learnings.json")
31
+
32
+
33
+ def iter_file_link_targets(markdown: str) -> list[str]:
34
+ """Extrae destinos `file://` de enlaces Markdown sin regex vulnerable a backtracking."""
35
+ targets: list[str] = []
36
+ cursor = 0
37
+ marker = "](file:///"
38
+ while True:
39
+ marker_index = markdown.find(marker, cursor)
40
+ if marker_index == -1:
41
+ return targets
42
+ label_start = markdown.rfind("[", 0, marker_index)
43
+ label = markdown[label_start + 1:marker_index] if label_start != -1 else ""
44
+ target_start = marker_index + len(marker)
45
+ target_end = markdown.find(")", target_start)
46
+ if target_end == -1:
47
+ return targets
48
+ if label.endswith(".py"):
49
+ targets.append(markdown[target_start:target_end])
50
+ cursor = target_end + 1
51
+
52
+
53
+ class ContextAssembler:
54
+ """Orquesta la selección de símbolos relevantes bajo un presupuesto de tokens."""
55
+
56
+ def __init__(self, project_root: Path, graph_index: GraphIndex | None = None) -> None:
57
+ self.project_root = project_root
58
+ self._index = graph_index or GraphIndex(project_root)
59
+
60
+ def assemble(self, intent: TaskIntent) -> ContextPack:
61
+ """Construye un ContextPack con símbolos, esqueletos y memorias aplicables."""
62
+ candidates = self._index.search(list(intent.keywords))
63
+ ranked = self._rank(candidates, intent)
64
+ selected, skeletons = self._fit_to_budget(ranked, intent)
65
+ memories = self._fetch_memories(intent)
66
+ pack = ContextPack(
67
+ intent=intent,
68
+ relevant_symbols=selected,
69
+ applicable_memories=memories,
70
+ skeletons=skeletons,
71
+ estimated_tokens=0,
72
+ )
73
+ pack.estimated_tokens = self._estimate_tokens(pack)
74
+ return pack
75
+
76
+ def _rank(self, symbols: list[SymbolRef], intent: TaskIntent) -> list[SymbolRef]:
77
+ """Ordena por número de keywords que cada símbolo satisface (desc)."""
78
+ kws = [k.lower() for k in intent.keywords]
79
+
80
+ def score(s: SymbolRef) -> int:
81
+ haystack = f"{s.name} {s.file} {s.doc}".lower()
82
+ return sum(1 for k in kws if k in haystack)
83
+
84
+ return sorted(symbols, key=score, reverse=True)
85
+
86
+ def _fit_to_budget(
87
+ self, ranked: list[SymbolRef], intent: TaskIntent
88
+ ) -> tuple[list[SymbolRef], list[FileSkeleton]]:
89
+ """Agrega símbolos al pack; degrada a esqueleto si el archivo es grande."""
90
+ selected: list[SymbolRef] = []
91
+ skeletons: list[FileSkeleton] = []
92
+ seen_files: set[str] = set()
93
+
94
+ for sym in ranked:
95
+ trial = ContextPack(
96
+ intent=intent,
97
+ relevant_symbols=selected + [sym],
98
+ applicable_memories=[],
99
+ skeletons=skeletons,
100
+ estimated_tokens=0,
101
+ )
102
+ if self._estimate_tokens(trial) > intent.token_budget:
103
+ skeleton = self._try_skeletonize(sym.file, seen_files)
104
+ if skeleton:
105
+ skeletons.append(skeleton)
106
+ continue
107
+ selected.append(sym)
108
+ seen_files.add(sym.file)
109
+
110
+ return selected, skeletons
111
+
112
+ def _try_skeletonize(self, filepath: str, seen: set[str]) -> FileSkeleton | None:
113
+ """Intenta crear un FileSkeleton si el archivo existe y no fue procesado ya."""
114
+ if filepath in seen:
115
+ return None
116
+ full_path = self.project_root / filepath
117
+ if not full_path.exists() or not filepath.endswith(".py"):
118
+ return None
119
+ source = full_path.read_text(encoding="utf-8")
120
+ if not should_skeletonize(source):
121
+ return None
122
+ seen.add(filepath)
123
+ return FileSkeleton(file=filepath, skeleton=extract_skeleton(source))
124
+
125
+ def _fetch_memories(self, intent: TaskIntent) -> list[str]:
126
+ """Busca en learnings.json entradas que coincidan léxica o semánticamente."""
127
+ memory_path = self.project_root / _MEMORY_FILE
128
+ if not memory_path.exists():
129
+ return []
130
+ try:
131
+ learnings: dict = json.loads(memory_path.read_text(encoding="utf-8"))
132
+ except (json.JSONDecodeError, OSError):
133
+ return []
134
+
135
+ # 1. Obtener matches léxicos tradicionales
136
+ matches = self._match_memories(learnings, intent.keywords)
137
+
138
+ # 2. Obtener matches semánticos híbridos si hay API key y provider
139
+ try:
140
+ from higpertext.kernel.infrastructure.llm.providers.gemini_embeddings import GeminiEmbeddingProvider
141
+ from higpertext.kernel.infrastructure.database.local_vector_store import LocalVectorStore
142
+
143
+ embedder = GeminiEmbeddingProvider(self.project_root)
144
+ query_str = " ".join(intent.keywords)
145
+ query_vec = embedder.get_embedding(query_str)
146
+
147
+ # Recolectar todos los aprendizajes individuales
148
+ all_learnings = []
149
+ for tag, items in learnings.items():
150
+ for item in items:
151
+ for learned_str in item.get("learned", []):
152
+ if learned_str not in matches:
153
+ all_learnings.append(learned_str)
154
+
155
+ # Evaluar similitud semántica
156
+ semantic_candidates = []
157
+ for candidate in all_learnings:
158
+ try:
159
+ cand_vec = embedder.get_embedding(candidate)
160
+ score = LocalVectorStore._cosine_similarity(query_vec, cand_vec)
161
+ if score >= 0.70: # Umbral de similitud semántica mínima
162
+ semantic_candidates.append((candidate, score))
163
+ except Exception:
164
+ continue
165
+
166
+ # Ordenar y tomar los mejores 3 matches semánticos adicionales
167
+ semantic_candidates.sort(key=lambda x: x[1], reverse=True)
168
+ for cand, _ in semantic_candidates[:3]:
169
+ matches.append(cand)
170
+ except Exception:
171
+ # Fallback explícito a coincidencia léxica pura si falla la red o API Key.
172
+ return list(set(matches))
173
+
174
+ return list(set(matches))
175
+
176
+ @staticmethod
177
+ def _match_memories(learnings: dict, keywords: tuple[str, ...]) -> list[str]:
178
+ """Filtra aprendizajes cuyas tags coincidan con al menos una keyword."""
179
+ kws = {k.lower() for k in keywords}
180
+ matches: list[str] = []
181
+ for tag, items in learnings.items():
182
+ if tag.lower() not in kws:
183
+ continue
184
+ for item in items:
185
+ matches.extend(item.get("learned", []))
186
+ return matches
187
+
188
+ @staticmethod
189
+ def _estimate_tokens(pack: ContextPack) -> int:
190
+ """Estima tokens usando ratios diferenciados por tipo de contenido."""
191
+ md = pack.to_markdown()
192
+ skeleton_chars = sum(len(sk.skeleton) for sk in pack.skeletons)
193
+ symbol_chars = sum(len(s.doc) + len(s.name) for s in pack.relevant_symbols)
194
+ memory_chars = sum(len(m) for m in pack.applicable_memories)
195
+ other_chars = len(md) - skeleton_chars - symbol_chars - memory_chars
196
+ tokens = (
197
+ max(other_chars, 0) / _CHARS_PER_TOKEN["markdown"]
198
+ + symbol_chars / _CHARS_PER_TOKEN["code"]
199
+ + skeleton_chars / _CHARS_PER_TOKEN["skeleton"]
200
+ + memory_chars / _CHARS_PER_TOKEN["markdown"]
201
+ )
202
+ return int(tokens)
203
+
204
+
205
+ class EfficiencyMeter:
206
+ """Responsabilidad única: medir qué tan eficiente fue el uso del contexto en una sesión."""
207
+
208
+ def __init__(self, project_root: Path) -> None:
209
+ self.project_root = project_root
210
+ self._reader = TelemetryReader(project_root)
211
+
212
+ def measure_session(self, session_id: str) -> EfficiencyReport:
213
+ total_tokens = self._reader.total_tokens(session_id)
214
+ total_cost = self._reader.total_cost(session_id)
215
+ exploration = self._reader.exploration_reads(session_id)
216
+ higpertext_count = self._reader.higpertext_reads(session_id)
217
+ intercepts = self._reader.hook_intercepts(session_id)
218
+ files_read = self._reader.files_read(session_id)
219
+
220
+ total_reads = exploration + higpertext_count
221
+ waste_ratio = (exploration / total_reads) if total_reads > 0 else 0.0
222
+ hit_rate = self._compute_hit_rate(files_read)
223
+ peak_tokens, window_pct = self._read_window_peak()
224
+
225
+ return EfficiencyReport(
226
+ session_id=session_id,
227
+ total_tokens=total_tokens,
228
+ total_cost_usd=round(total_cost, 6),
229
+ exploration_reads=exploration,
230
+ higpertext_reads=higpertext_count,
231
+ hook_intercepts=intercepts,
232
+ context_hit_rate=round(hit_rate, 3),
233
+ exploration_waste_ratio=round(waste_ratio, 3),
234
+ window_usage_pct=round(window_pct, 3),
235
+ peak_window_tokens=peak_tokens,
236
+ )
237
+
238
+ def save_to_reports(self, report: EfficiencyReport) -> Path | None:
239
+ """Persiste el EfficiencyReport en .higpertext/reports/efficiency/ y actualiza el índice."""
240
+ try:
241
+ from higpertext.adapters.adapter_utils import save_report
242
+
243
+ return save_report(
244
+ target_dir=self.project_root,
245
+ report_id=f"context_engine.efficiency.{report.session_id}",
246
+ content=report.to_markdown(),
247
+ category="efficiency",
248
+ )
249
+ except Exception:
250
+ return None
251
+
252
+ def _read_window_peak(self) -> tuple[int, float]:
253
+ """Lee el pico de tokens de ventana desde window_state.json."""
254
+ try:
255
+ from higpertext.kernel.domain.context_engine import (
256
+ load_window_state,
257
+ _window_limit,
258
+ )
259
+
260
+ state = load_window_state(self.project_root)
261
+ limit = _window_limit()
262
+ return state.peak_tokens, state.usage_pct(limit)
263
+ except Exception:
264
+ return 0, 0.0
265
+
266
+ def _compute_hit_rate(self, files_read: set[str]) -> float:
267
+ """Calcula qué fracción de los archivos leídos estaba en algún context pack."""
268
+ from higpertext.kernel.config_paths import WORKSPACE_DIR_NAME
269
+ if not files_read:
270
+ return 0.0
271
+ pack_dir = self.project_root / WORKSPACE_DIR_NAME / "state" / "context_packs"
272
+ if not pack_dir.exists():
273
+ return 0.0
274
+
275
+ covered: set[str] = set()
276
+ for pack_file in pack_dir.glob("*.md"):
277
+ try:
278
+ content = pack_file.read_text(encoding="utf-8")
279
+ covered.update(iter_file_link_targets(content))
280
+ except OSError:
281
+ continue
282
+
283
+ if not covered:
284
+ return 0.0
285
+
286
+ hits = sum(1 for f in files_read if any(f.endswith(c) or c.endswith(f) for c in covered))
287
+ return hits / len(files_read)
288
+
289
+
290
+ # Heurísticas de skills por tipo de tarea
291
+ _SKILLS_BY_TYPE = {
292
+ "refactor": ["clean-code", "ddd-standards"],
293
+ "feature": ["clean-code", "tdd-practices"],
294
+ "bugfix": ["clean-code", "best-practices"],
295
+ "review": ["best-practices", "ddd-standards"],
296
+ }
297
+ _SUBAGENTS_BY_TYPE = {
298
+ "refactor": ["code-auditor", "architect"],
299
+ "feature": ["architect", "test-engineer"],
300
+ "bugfix": ["code-auditor"],
301
+ "review": ["code-auditor"],
302
+ }
303
+
304
+ # Plantillas de fases por tipo de tarea.
305
+ _TEMPLATES: dict[str, list[tuple[str, str, list[str]]]] = {
306
+ "refactor": [
307
+ ("explore", "Explorar el código existente y mapear el área de cambio", []),
308
+ (
309
+ "plan",
310
+ "Diseñar la nueva estructura y definir contratos de interfaces",
311
+ ["explore"],
312
+ ),
313
+ ("implement", "Implementar los cambios de refactorización", ["plan"]),
314
+ (
315
+ "test",
316
+ "Escribir y ejecutar tests que validen el comportamiento",
317
+ ["implement"],
318
+ ),
319
+ ("verify", "Verificar que no hay regresiones y la cobertura es ≥80%", ["test"]),
320
+ ],
321
+ "feature": [
322
+ (
323
+ "explore",
324
+ "Explorar el sistema existente para entender el punto de integración",
325
+ [],
326
+ ),
327
+ (
328
+ "spec",
329
+ "Definir la especificación y los casos de uso de la feature",
330
+ ["explore"],
331
+ ),
332
+ (
333
+ "tdd-red",
334
+ "Escribir tests que fallen (RED) para la nueva funcionalidad",
335
+ ["spec"],
336
+ ),
337
+ (
338
+ "implement",
339
+ "Implementar la feature hasta que los tests pasen (GREEN)",
340
+ ["tdd-red"],
341
+ ),
342
+ ("refactor", "Limpiar el código manteniendo los tests en verde", ["implement"]),
343
+ ],
344
+ "bugfix": [
345
+ ("reproduce", "Reproducir el bug y escribir un test que lo capture", []),
346
+ (
347
+ "diagnose",
348
+ "Identificar la causa raíz usando el semantic graph",
349
+ ["reproduce"],
350
+ ),
351
+ ("fix", "Aplicar el fix mínimo que pase el test de regresión", ["diagnose"]),
352
+ ("verify", "Verificar que el fix no introduce regresiones", ["fix"]),
353
+ ],
354
+ "review": [
355
+ ("explore", "Explorar los cambios del diff y su contexto en el repo", []),
356
+ ("quality", "Evaluar calidad, SOLID y Clean Code en los cambios", ["explore"]),
357
+ (
358
+ "security",
359
+ "Verificar que no hay vulnerabilidades ni secrets expuestos",
360
+ ["explore"],
361
+ ),
362
+ (
363
+ "summary",
364
+ "Producir el resumen de hallazgos con severidad",
365
+ ["quality", "security"],
366
+ ),
367
+ ],
368
+ }
369
+ _DEFAULT_TEMPLATE = _TEMPLATES["feature"]
370
+
371
+
372
+ class TaskDecomposer:
373
+ """Genera un TaskGraph heurístico a partir de un TaskIntent."""
374
+
375
+ def __init__(self, project_root: Path) -> None:
376
+ self.project_root = project_root
377
+
378
+ def decompose(self, intent: TaskIntent) -> TaskGraph:
379
+ template = _TEMPLATES.get(intent.task_type, _DEFAULT_TEMPLATE)
380
+ skills = _SKILLS_BY_TYPE.get(intent.task_type, ["clean-code"])
381
+ subagents = _SUBAGENTS_BY_TYPE.get(intent.task_type, ["architect"])
382
+ prefix = _make_prefix(intent.goal)
383
+
384
+ nodes: list[TaskNode] = []
385
+ for suffix, description, dep_suffixes in template:
386
+ node_id = f"{prefix}-{suffix}"
387
+ depends_on = [f"{prefix}-{ds}" for ds in dep_suffixes]
388
+ nodes.append(
389
+ TaskNode(
390
+ id=node_id,
391
+ description=f"{description}: {_trim(intent.goal, 50)}",
392
+ depends_on=depends_on,
393
+ skills=list(skills),
394
+ subagents=list(subagents),
395
+ )
396
+ )
397
+
398
+ return TaskGraph(nodes=nodes, goal=intent.goal)
399
+
400
+
401
+ def _make_prefix(goal: str) -> str:
402
+ """Genera un slug corto y único para los IDs de nodos."""
403
+ words = re.findall(r"[\wáéíóúñ]{3,}", goal.lower())
404
+ slug = "-".join(words[:3])
405
+ return slug[:20] or "task"
406
+
407
+
408
+ def _trim(text: str, max_len: int) -> str:
409
+ return text if len(text) <= max_len else text[:max_len] + "…"
@@ -0,0 +1,41 @@
1
+ """Nueva implementación limpia de HigpertextEngine (Aplicación)."""
2
+
3
+ from __future__ import annotations
4
+ from pathlib import Path
5
+ from higpertext.kernel.application.profile_use_cases import (
6
+ GetAgentContextUseCase,
7
+ ValidateProfileUseCase,
8
+ )
9
+ from higpertext.kernel.infrastructure.file_repositories import (
10
+ FileProfileRepository,
11
+ FileCapabilityRepository,
12
+ FileWorkflowRepository,
13
+ )
14
+ from higpertext.kernel.pkg_resources import resolve_resource
15
+
16
+
17
+ class ApplicationHigpertextEngine:
18
+ def __init__(self, base_dir: Path):
19
+ self.base_dir = base_dir
20
+ self.profiles_repo = FileProfileRepository(
21
+ resolve_resource(base_dir, "src", "config", "profiles")
22
+ )
23
+ self.caps_repo = FileCapabilityRepository(
24
+ resolve_resource(base_dir, "src", "higpertext", "capabilities")
25
+ )
26
+ self.wfs_repo = FileWorkflowRepository(resolve_resource(base_dir, "src", "workflows"))
27
+
28
+ def get_agent_context(self, profile_name: str, env_data: dict | None = None) -> dict:
29
+ return GetAgentContextUseCase(
30
+ self.profiles_repo,
31
+ self.caps_repo,
32
+ self.wfs_repo,
33
+ self.base_dir,
34
+ ).execute(profile_name, env_data)
35
+
36
+ def validate_profile(self, profile_name: str) -> tuple[bool, list[str], list[str]]:
37
+ return ValidateProfileUseCase(
38
+ self.profiles_repo,
39
+ self.caps_repo,
40
+ self.base_dir,
41
+ ).execute(profile_name)
@@ -0,0 +1,174 @@
1
+ """Servicio principal para ejecutar entornos Docker/Podman locales (Aplicación)."""
2
+
3
+ from __future__ import annotations
4
+ import re
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from higpertext.kernel.domain.env_runtime import EnvironmentTemplate, RunSpec, RunState
10
+ from higpertext.kernel.infrastructure.env_runtime import ComposeRenderer, resolve_backend, RunStore, TemplateLoader
11
+
12
+
13
+ class EnvironmentService:
14
+ """Orquesta templates, compose, backends y estado de ejecuciones."""
15
+
16
+ def __init__(self, project_root: Path) -> None:
17
+ self.project_root = project_root
18
+ self.templates = TemplateLoader(project_root)
19
+ self.store = RunStore(project_root)
20
+ self.renderer = ComposeRenderer()
21
+
22
+ def catalog(self) -> list[EnvironmentTemplate]:
23
+ return self.templates.list_templates()
24
+
25
+ def run(self, spec: RunSpec) -> RunState:
26
+ template = self.templates.load(spec.template_id)
27
+ resolved = self._resolve_spec(template, spec)
28
+ backend = resolve_backend(resolved.engine)
29
+ run_id = self._new_run_id(template.id)
30
+ run_dir = self.store.run_dir(run_id)
31
+ compose_file = self.renderer.render(template, resolved, run_dir)
32
+ project_name = f"higpertext_{run_id}".replace("-", "_")
33
+ state = RunState(
34
+ run_id=run_id,
35
+ template_id=template.id,
36
+ engine=backend.engine,
37
+ status="running",
38
+ project_name=project_name,
39
+ run_dir=str(run_dir),
40
+ service=resolved.service,
41
+ command=resolved.command,
42
+ )
43
+ self.store.save(state)
44
+ up = backend.up(project_name, compose_file, self.project_root, resolved.timeout_seconds)
45
+ self._write_log(run_dir, "up.log", up.stdout, up.stderr)
46
+ if up.rc != 0:
47
+ return self._finish(state, "failed", up.rc, up.stderr or up.stdout)
48
+ if resolved.detach or not resolved.command:
49
+ state.summary = "Entorno iniciado en segundo plano."
50
+ self.store.save(state)
51
+ return state
52
+ result = backend.exec(
53
+ project_name,
54
+ compose_file,
55
+ self.project_root,
56
+ resolved.service,
57
+ resolved.command,
58
+ resolved.timeout_seconds,
59
+ )
60
+ self._write_log(run_dir, "command.log", result.stdout, result.stderr)
61
+ status = "success" if result.rc == 0 else "failed"
62
+ state = self._finish(
63
+ state,
64
+ status,
65
+ result.rc,
66
+ self._summarize_output(result.stdout, result.stderr),
67
+ )
68
+ if resolved.cleanup and not resolved.keep_alive:
69
+ down = backend.down(project_name, compose_file, self.project_root)
70
+ self._write_log(run_dir, "down.log", down.stdout, down.stderr)
71
+ return state
72
+
73
+ def status(self, run_id: str | None = None) -> list[RunState] | RunState:
74
+ return self.store.load(run_id) if run_id else self.store.list()
75
+
76
+ def logs(
77
+ self, run_id: str, service: str = "", tail: int = 200, errors_only: bool = False
78
+ ) -> str:
79
+ state = self.store.load(run_id)
80
+ compose_file = Path(state.run_dir) / "compose.yaml"
81
+ backend = resolve_backend(state.engine)
82
+ result = backend.logs(state.project_name, compose_file, self.project_root, service, tail)
83
+ text = result.stdout or result.stderr
84
+ if errors_only:
85
+ lines = [
86
+ line
87
+ for line in text.splitlines()
88
+ if re.search(r"error|failed|exception|traceback", line, re.I)
89
+ ]
90
+ return "\n".join(lines)
91
+ return text
92
+
93
+ def stop(self, run_id: str, volumes: bool = False) -> RunState:
94
+ state = self.store.load(run_id)
95
+ compose_file = Path(state.run_dir) / "compose.yaml"
96
+ backend = resolve_backend(state.engine)
97
+ result = backend.down(state.project_name, compose_file, self.project_root, volumes)
98
+ self._write_log(Path(state.run_dir), "stop.log", result.stdout, result.stderr)
99
+ return self._finish(
100
+ state,
101
+ "stopped" if result.rc == 0 else "failed",
102
+ result.rc,
103
+ result.stderr or result.stdout,
104
+ )
105
+
106
+ def clean(self, all_runs: bool = False) -> int:
107
+ states = self.store.list()
108
+ count = 0
109
+ for state in states:
110
+ if all_runs or state.status in {"success", "failed", "stopped"}:
111
+ self.store.delete(state.run_id)
112
+ count += 1
113
+ return count
114
+
115
+ def validate_template(self, template_id: str) -> EnvironmentTemplate:
116
+ return self.templates.load(template_id)
117
+
118
+ @staticmethod
119
+ def _resolve_spec(template: EnvironmentTemplate, spec: RunSpec) -> RunSpec:
120
+ task_name = spec.task or template.defaults.get("task", "")
121
+ task = template.tasks.get(task_name, {}) if task_name else {}
122
+ service = (
123
+ spec.service
124
+ or task.get("service")
125
+ or template.defaults.get("service")
126
+ or next(iter(template.services))
127
+ )
128
+ command = spec.command or task.get("command") or template.defaults.get("command", "")
129
+ timeout = spec.timeout_seconds or int(template.defaults.get("timeout_seconds", 300))
130
+ cleanup = (
131
+ spec.cleanup
132
+ if spec.cleanup is not None
133
+ else bool(template.defaults.get("cleanup", True))
134
+ )
135
+ return RunSpec(
136
+ template_id=spec.template_id,
137
+ engine=spec.engine,
138
+ task=task_name,
139
+ command=command,
140
+ service=service,
141
+ detach=spec.detach,
142
+ timeout_seconds=timeout,
143
+ keep_alive=spec.keep_alive,
144
+ cleanup=cleanup,
145
+ env=spec.env,
146
+ )
147
+
148
+ @staticmethod
149
+ def _new_run_id(template_id: str) -> str:
150
+ safe = re.sub(r"[^a-zA-Z0-9]+", "_", template_id).strip("_")[:24]
151
+ return f"env_{safe}_{uuid.uuid4().hex[:8]}"
152
+
153
+ def _finish(self, state: RunState, status: str, rc: int, summary: str) -> RunState:
154
+ state.status = status
155
+ state.exit_code = rc
156
+ state.finished_at = datetime.now(timezone.utc).isoformat()
157
+ state.summary = summary[:2000]
158
+ self.store.save(state)
159
+ return state
160
+
161
+ @staticmethod
162
+ def _write_log(run_dir: Path, name: str, stdout: str, stderr: str) -> None:
163
+ (run_dir / "logs" / name).write_text(
164
+ (stdout or "") + ("\n" + stderr if stderr else ""), encoding="utf-8"
165
+ )
166
+
167
+ @staticmethod
168
+ def _summarize_output(stdout: str, stderr: str) -> str:
169
+ text = stderr or stdout
170
+ lines = [line for line in text.splitlines() if line.strip()]
171
+ for line in lines:
172
+ if re.search(r"error|failed|exception|traceback|assert", line, re.I):
173
+ return line[:500]
174
+ return "\n".join(lines[-10:])[:1000]