htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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 (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,257 @@
1
+ """
2
+ PostToolUse Enhancement - Duration Calculation and Tool Trace Updates
3
+
4
+ This module handles the PostToolUse hook event and updates tool traces with:
5
+ 1. Execution end time (when the tool completed)
6
+ 2. Duration in milliseconds (end_time - start_time)
7
+ 3. Tool output (result of the tool execution)
8
+ 4. Status (Ok or Error)
9
+ 5. Error message (if status is Error)
10
+
11
+ The module correlates with PreToolUse via tool_use_id environment variable
12
+ and gracefully handles missing pre-events (logs warning, continues).
13
+
14
+ Design:
15
+ - Query tool_traces for matching tool_use_id
16
+ - Get start_time from pre-event
17
+ - Calculate duration_ms (end_time - start_time)
18
+ - Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
19
+ - Handle missing pre-event gracefully (log warning, continue)
20
+ - Non-blocking - errors don't prevent tool execution continuation
21
+ """
22
+
23
+ import json
24
+ import logging
25
+ import os
26
+ from datetime import datetime, timezone
27
+ from typing import Any
28
+
29
+ from htmlgraph.db.schema import HtmlGraphDB
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def calculate_duration(start_time_iso: str, end_time_iso: str) -> int:
35
+ """
36
+ Calculate duration in milliseconds between two ISO8601 UTC timestamps.
37
+
38
+ Args:
39
+ start_time_iso: ISO8601 UTC timestamp from PreToolUse (e.g., "2025-01-07T12:34:56.789000+00:00")
40
+ end_time_iso: ISO8601 UTC timestamp (now, e.g., "2025-01-07T12:34:57.123000+00:00")
41
+
42
+ Returns:
43
+ duration_ms: Integer milliseconds between timestamps (accurate within 1ms)
44
+
45
+ Raises:
46
+ ValueError: If timestamps cannot be parsed
47
+ TypeError: If inputs are not strings
48
+ """
49
+ try:
50
+ # Parse ISO8601 timestamps (handles timezone-aware datetimes)
51
+ start_dt = datetime.fromisoformat(start_time_iso.replace("Z", "+00:00"))
52
+ end_dt = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
53
+
54
+ # Calculate difference and convert to milliseconds
55
+ delta = end_dt - start_dt
56
+ duration_ms = int(delta.total_seconds() * 1000)
57
+
58
+ return duration_ms
59
+ except (ValueError, AttributeError, TypeError) as e:
60
+ logger.warning(f"Error calculating duration: {e}")
61
+ raise
62
+
63
+
64
+ def update_tool_trace(
65
+ tool_use_id: str,
66
+ tool_output: dict[str, Any] | None,
67
+ status: str,
68
+ error_message: str | None = None,
69
+ ) -> bool:
70
+ """
71
+ Update tool_traces table with execution end event.
72
+
73
+ Updates an existing tool trace (created by PreToolUse) with:
74
+ - end_time: Current UTC timestamp
75
+ - duration_ms: Milliseconds between start and end
76
+ - tool_output: Result of tool execution (JSON)
77
+ - status: 'Ok' or 'Error'
78
+ - error_message: Error details if status='Error'
79
+
80
+ Args:
81
+ tool_use_id: Correlation ID from PreToolUse event (from environment)
82
+ tool_output: Tool execution result (dict, will be JSON serialized)
83
+ status: 'Ok' or 'Error'
84
+ error_message: Error details if status='Error'
85
+
86
+ Returns:
87
+ True if update successful, False otherwise
88
+
89
+ Workflow:
90
+ 1. Query tool_traces for matching tool_use_id
91
+ 2. Get start_time from pre-event
92
+ 3. Calculate duration_ms (end_time - start_time)
93
+ 4. Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
94
+ 5. Handle missing pre-event gracefully (log warning, continue)
95
+ """
96
+ try:
97
+ # Connect to database
98
+ db = HtmlGraphDB()
99
+
100
+ if not db.connection:
101
+ db.connect()
102
+
103
+ cursor = db.connection.cursor() # type: ignore[union-attr]
104
+
105
+ # Query tool_traces for matching tool_use_id
106
+ cursor.execute(
107
+ """
108
+ SELECT tool_use_id, start_time FROM tool_traces
109
+ WHERE tool_use_id = ?
110
+ """,
111
+ (tool_use_id,),
112
+ )
113
+
114
+ row = cursor.fetchone()
115
+
116
+ if not row:
117
+ # Missing pre-event - log warning but continue (graceful degradation)
118
+ logger.warning(
119
+ f"Could not find start event for tool_use_id={tool_use_id}. "
120
+ f"PreToolUse event may not have completed. Skipping duration update."
121
+ )
122
+ db.disconnect()
123
+ return False
124
+
125
+ # Get start_time from pre-event
126
+ start_time_iso = row[1]
127
+
128
+ # Calculate end_time (now in UTC)
129
+ end_time_iso = datetime.now(timezone.utc).isoformat()
130
+
131
+ # Calculate duration_ms
132
+ try:
133
+ duration_ms = calculate_duration(start_time_iso, end_time_iso)
134
+ except (ValueError, TypeError) as e:
135
+ logger.warning(
136
+ f"Could not calculate duration for tool_use_id={tool_use_id}: {e}. "
137
+ f"Using None for duration."
138
+ )
139
+ duration_ms = None
140
+
141
+ # Validate status
142
+ valid_statuses = {"Ok", "Error", "completed", "failed", "timeout"}
143
+ if status not in valid_statuses:
144
+ logger.warning(
145
+ f"Invalid status '{status}' for tool_use_id={tool_use_id}. "
146
+ f"Using 'Ok' as default."
147
+ )
148
+ status = "Ok"
149
+
150
+ # JSON serialize tool_output
151
+ tool_output_json = None
152
+ if tool_output:
153
+ try:
154
+ tool_output_json = json.dumps(tool_output)
155
+ except (TypeError, ValueError) as e:
156
+ logger.warning(
157
+ f"Could not JSON serialize tool_output for "
158
+ f"tool_use_id={tool_use_id}: {e}"
159
+ )
160
+ tool_output_json = json.dumps(
161
+ {"error": str(e), "output": str(tool_output)}
162
+ )
163
+
164
+ # Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
165
+ cursor.execute(
166
+ """
167
+ UPDATE tool_traces
168
+ SET end_time = ?, duration_ms = ?, tool_output = ?,
169
+ status = ?, error_message = ?
170
+ WHERE tool_use_id = ?
171
+ """,
172
+ (
173
+ end_time_iso,
174
+ duration_ms,
175
+ tool_output_json,
176
+ status,
177
+ error_message,
178
+ tool_use_id,
179
+ ),
180
+ )
181
+
182
+ if not db.connection:
183
+ db.connect()
184
+
185
+ db.connection.commit() # type: ignore[union-attr]
186
+
187
+ logger.debug(
188
+ f"Updated tool trace: tool_use_id={tool_use_id}, "
189
+ f"duration_ms={duration_ms}, status={status}"
190
+ )
191
+
192
+ db.disconnect()
193
+ return True
194
+
195
+ except Exception as e:
196
+ # Log error but don't block
197
+ logger.error(f"Error updating tool trace for tool_use_id={tool_use_id}: {e}")
198
+ return False
199
+
200
+
201
+ def get_tool_use_id_from_context() -> str | None:
202
+ """
203
+ Get tool_use_id from environment (set by PreToolUse hook).
204
+
205
+ Returns:
206
+ tool_use_id string or None if not set
207
+ """
208
+ return os.environ.get("HTMLGRAPH_TOOL_USE_ID")
209
+
210
+
211
+ def determine_status_from_response(
212
+ tool_response: dict[str, Any] | None,
213
+ ) -> tuple[str, str | None]:
214
+ """
215
+ Determine status (Ok/Error) and error message from tool response.
216
+
217
+ Analyzes tool response to determine if execution was successful.
218
+ Returns (status, error_message) tuple.
219
+
220
+ Args:
221
+ tool_response: Tool execution response (dict)
222
+
223
+ Returns:
224
+ (status, error_message) where:
225
+ - status: 'Ok' or 'Error'
226
+ - error_message: Error details if Error, else None
227
+ """
228
+ if not tool_response:
229
+ return ("Ok", None)
230
+
231
+ if not isinstance(tool_response, dict):
232
+ return ("Ok", None)
233
+
234
+ # Check for explicit error indicators
235
+ # Bash tool: non-empty stderr
236
+ stderr = tool_response.get("stderr", "")
237
+ if stderr and isinstance(stderr, str) and stderr.strip():
238
+ return ("Error", f"stderr: {stderr[:500]}")
239
+
240
+ # Explicit error field
241
+ error_field = tool_response.get("error")
242
+ if error_field and str(error_field).strip():
243
+ return ("Error", str(error_field)[:500])
244
+
245
+ # success=false flag
246
+ if tool_response.get("success") is False:
247
+ reason = tool_response.get("reason", "Unknown error")
248
+ return ("Error", str(reason)[:500])
249
+
250
+ # status field indicating failure
251
+ status_field = tool_response.get("status")
252
+ if status_field and status_field.lower() in {"error", "failed", "failed"}:
253
+ reason = tool_response.get("message", "Unknown error")
254
+ return ("Error", str(reason)[:500])
255
+
256
+ # Default to success
257
+ return ("Ok", None)
@@ -0,0 +1,408 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ """
6
+ Unified PostToolUse Hook - Parallel Execution of Multiple Tasks
7
+
8
+ This module provides a unified PostToolUse hook that runs multiple tasks
9
+ in parallel using asyncio:
10
+ 1. Event tracking - logs tool usage to session events
11
+ 2. Orchestrator reflection - provides delegation suggestions
12
+ 3. Task validation - validates task results
13
+ 4. Error tracking - logs errors and auto-creates debug spikes
14
+ 5. Debugging suggestions - suggests resources when errors detected
15
+ 6. CIGS analysis - cost accounting and reinforcement for delegation
16
+
17
+ Architecture:
18
+ - All tasks run simultaneously via asyncio.gather()
19
+ - Error tracking logs to .htmlgraph/errors.jsonl
20
+ - Auto-creates debug spikes after 3+ similar errors
21
+ - Returns combined response with all feedback
22
+
23
+ Performance:
24
+ - ~40-50% faster than sequential execution
25
+ - Single Python process (no subprocess overhead)
26
+ - Parallel execution maximizes throughput
27
+ """
28
+
29
+ import asyncio
30
+ import json
31
+ import os
32
+ import sys
33
+ from pathlib import Path
34
+ from typing import Any
35
+
36
+ from htmlgraph.cigs import CIGSPostToolAnalyzer
37
+ from htmlgraph.hooks.event_tracker import track_event
38
+ from htmlgraph.hooks.orchestrator_reflector import orchestrator_reflect
39
+ from htmlgraph.hooks.post_tool_use_failure import run as track_error
40
+ from htmlgraph.hooks.task_validator import validate_task_results
41
+
42
+
43
+ async def run_event_tracking(
44
+ hook_type: str, hook_input: dict[str, Any]
45
+ ) -> dict[str, Any]:
46
+ """
47
+ Run event tracking (async wrapper).
48
+
49
+ Args:
50
+ hook_type: "PostToolUse" or "Stop"
51
+ hook_input: Hook input with tool execution details
52
+
53
+ Returns:
54
+ Event tracking response: {"continue": True, "hookSpecificOutput": {...}}
55
+ """
56
+ try:
57
+ loop = asyncio.get_event_loop()
58
+
59
+ # Run in thread pool since it involves I/O
60
+ return await loop.run_in_executor(
61
+ None,
62
+ track_event,
63
+ hook_type,
64
+ hook_input,
65
+ )
66
+ except Exception:
67
+ # Graceful degradation - allow on error
68
+ return {"continue": True}
69
+
70
+
71
+ async def run_orchestrator_reflection(hook_input: dict[str, Any]) -> dict[str, Any]:
72
+ """
73
+ Run orchestrator reflection (async wrapper).
74
+
75
+ Args:
76
+ hook_input: Hook input with tool execution details
77
+
78
+ Returns:
79
+ Reflection response: {"continue": True, "hookSpecificOutput": {...}}
80
+ """
81
+ try:
82
+ loop = asyncio.get_event_loop()
83
+
84
+ # Run in thread pool
85
+ return await loop.run_in_executor(
86
+ None,
87
+ orchestrator_reflect,
88
+ hook_input,
89
+ )
90
+ except Exception:
91
+ # Graceful degradation - allow on error
92
+ return {"continue": True}
93
+
94
+
95
+ async def run_task_validation(hook_input: dict[str, Any]) -> dict[str, Any]:
96
+ """
97
+ Run task result validation (async wrapper).
98
+
99
+ Args:
100
+ hook_input: Hook input with tool execution details
101
+
102
+ Returns:
103
+ Validation response: {"continue": True, "hookSpecificOutput": {...}}
104
+ """
105
+ try:
106
+ loop = asyncio.get_event_loop()
107
+
108
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
109
+ tool_response = hook_input.get("result", {}) or hook_input.get(
110
+ "tool_response", {}
111
+ )
112
+
113
+ # Run task validation
114
+ return await loop.run_in_executor(
115
+ None,
116
+ validate_task_results,
117
+ tool_name,
118
+ tool_response,
119
+ )
120
+ except Exception:
121
+ # Graceful degradation - allow on error
122
+ return {"continue": True}
123
+
124
+
125
+ async def run_error_tracking(hook_input: dict[str, Any]) -> dict[str, Any]:
126
+ """
127
+ Track errors to .htmlgraph/errors.jsonl and auto-create debug spikes.
128
+
129
+ Only tracks ACTUAL errors, not responses containing the word "error".
130
+
131
+ Args:
132
+ hook_input: Hook input with tool execution details
133
+
134
+ Returns:
135
+ Error tracking response: {"continue": True}
136
+ """
137
+ try:
138
+ loop = asyncio.get_event_loop()
139
+
140
+ # Check if this is an ACTUAL error
141
+ has_error = False
142
+ tool_response = hook_input.get("tool_response") or hook_input.get("result", {})
143
+
144
+ if isinstance(tool_response, dict):
145
+ # Bash: non-empty stderr indicates error
146
+ stderr = tool_response.get("stderr", "")
147
+ if stderr and isinstance(stderr, str) and stderr.strip():
148
+ has_error = True
149
+
150
+ # Explicit error field with content
151
+ error_field = tool_response.get("error")
152
+ if error_field and str(error_field).strip():
153
+ has_error = True
154
+
155
+ # success=false flag
156
+ if tool_response.get("success") is False:
157
+ has_error = True
158
+
159
+ # Only track if there's an actual error
160
+ if has_error:
161
+ return await loop.run_in_executor(
162
+ None,
163
+ track_error,
164
+ hook_input,
165
+ )
166
+
167
+ return {"continue": True}
168
+ except Exception:
169
+ # Graceful degradation - allow on error
170
+ return {"continue": True}
171
+
172
+
173
+ async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, Any]:
174
+ """
175
+ Suggest debugging resources based on tool results.
176
+
177
+ Only triggers on ACTUAL errors, not on responses that happen to contain
178
+ the word "error" in their content.
179
+
180
+ Args:
181
+ hook_input: Hook input with tool execution details
182
+
183
+ Returns:
184
+ Suggestion response: {"hookSpecificOutput": {"additionalContext": "..."}}
185
+ """
186
+ try:
187
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
188
+ tool_response = hook_input.get("result", {}) or hook_input.get(
189
+ "tool_response", {}
190
+ )
191
+
192
+ suggestions = []
193
+
194
+ # Check for ACTUAL errors (not just text containing "error")
195
+ has_actual_error = False
196
+
197
+ if isinstance(tool_response, dict):
198
+ # Bash: non-empty stderr indicates error
199
+ stderr = tool_response.get("stderr", "")
200
+ if stderr and isinstance(stderr, str) and stderr.strip():
201
+ has_actual_error = True
202
+
203
+ # Explicit error field
204
+ if tool_response.get("error"):
205
+ has_actual_error = True
206
+
207
+ # success=false flag
208
+ if tool_response.get("success") is False:
209
+ has_actual_error = True
210
+
211
+ if has_actual_error:
212
+ suggestions.append("⚠️ Error detected in tool response")
213
+ suggestions.append("Debugging resources:")
214
+ suggestions.append(" 📚 DEBUGGING.md - Systematic debugging guide")
215
+ suggestions.append(" 🔬 Researcher agent - Research error patterns")
216
+ suggestions.append(" 🐛 Debugger agent - Root cause analysis")
217
+ suggestions.append(" Built-in: /doctor, /hooks, claude --debug")
218
+
219
+ # Check for Task tool without save evidence
220
+ if tool_name == "Task":
221
+ result_text = str(tool_response).lower()
222
+ save_indicators = [".save()", "spike", "htmlgraph", ".create("]
223
+ if not any(ind in result_text for ind in save_indicators):
224
+ suggestions.append("💡 Task completed - remember to document findings")
225
+ suggestions.append(
226
+ " See DEBUGGING.md for research documentation patterns"
227
+ )
228
+
229
+ if suggestions:
230
+ return {
231
+ "hookSpecificOutput": {
232
+ "hookEventName": "PostToolUse",
233
+ "additionalContext": "\n".join(suggestions),
234
+ }
235
+ }
236
+
237
+ return {}
238
+ except Exception:
239
+ # Graceful degradation - no suggestions on error
240
+ return {}
241
+
242
+
243
+ async def run_cigs_analysis(hook_input: dict[str, Any]) -> dict[str, Any]:
244
+ """
245
+ Run CIGS cost accounting and reinforcement analysis.
246
+
247
+ Args:
248
+ hook_input: Hook input with tool execution details
249
+
250
+ Returns:
251
+ CIGS analysis response: {"hookSpecificOutput": {...}}
252
+ """
253
+ try:
254
+ loop = asyncio.get_event_loop()
255
+
256
+ # Extract tool info
257
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
258
+ tool_params = hook_input.get("input", {}) or hook_input.get("tool_input", {})
259
+ tool_response = hook_input.get("result", {}) or hook_input.get(
260
+ "tool_response", {}
261
+ )
262
+
263
+ # Initialize CIGS analyzer
264
+ graph_dir = Path.cwd() / ".htmlgraph"
265
+ analyzer = CIGSPostToolAnalyzer(graph_dir)
266
+
267
+ # Run analysis in executor (may involve I/O)
268
+ return await loop.run_in_executor(
269
+ None,
270
+ analyzer.analyze,
271
+ tool_name,
272
+ tool_params,
273
+ tool_response,
274
+ )
275
+ except Exception:
276
+ # Graceful degradation - allow on error
277
+ return {}
278
+
279
+
280
+ async def posttooluse_hook(
281
+ hook_type: str, hook_input: dict[str, Any]
282
+ ) -> dict[str, Any]:
283
+ """
284
+ Unified PostToolUse hook - runs tracking, reflection, validation, error tracking, debugging suggestions, and CIGS analysis in parallel.
285
+
286
+ Args:
287
+ hook_type: "PostToolUse" or "Stop"
288
+ hook_input: Hook input with tool execution details
289
+
290
+ Returns:
291
+ Claude Code standard format:
292
+ {
293
+ "continue": True,
294
+ "hookSpecificOutput": {
295
+ "hookEventName": "PostToolUse",
296
+ "additionalContext": "Combined feedback",
297
+ "systemMessage": "Warnings/alerts"
298
+ }
299
+ }
300
+ """
301
+ # Run all six in parallel using asyncio.gather
302
+ (
303
+ event_response,
304
+ reflection_response,
305
+ validation_response,
306
+ error_tracking_response,
307
+ debug_suggestions,
308
+ cigs_response,
309
+ ) = await asyncio.gather(
310
+ run_event_tracking(hook_type, hook_input),
311
+ run_orchestrator_reflection(hook_input),
312
+ run_task_validation(hook_input),
313
+ run_error_tracking(hook_input),
314
+ suggest_debugging_resources(hook_input),
315
+ run_cigs_analysis(hook_input),
316
+ )
317
+
318
+ # Combine responses (all should return continue=True)
319
+ # Event tracking is async and shouldn't block
320
+ # Reflection provides optional guidance
321
+ # Validation provides warnings but doesn't block
322
+
323
+ # Collect all guidance and messages
324
+ guidance_parts = []
325
+ system_messages = []
326
+
327
+ # Event tracking guidance (e.g., drift warnings)
328
+ if "hookSpecificOutput" in event_response:
329
+ ctx = event_response["hookSpecificOutput"].get("additionalContext", "")
330
+ if ctx:
331
+ guidance_parts.append(ctx)
332
+
333
+ # Orchestrator reflection
334
+ if "hookSpecificOutput" in reflection_response:
335
+ ctx = reflection_response["hookSpecificOutput"].get("additionalContext", "")
336
+ if ctx:
337
+ guidance_parts.append(ctx)
338
+
339
+ # Task validation feedback
340
+ if "hookSpecificOutput" in validation_response:
341
+ ctx = validation_response["hookSpecificOutput"].get("additionalContext", "")
342
+ if ctx:
343
+ guidance_parts.append(ctx)
344
+
345
+ # Task validation may provide systemMessage for warnings
346
+ sys_msg = validation_response["hookSpecificOutput"].get("systemMessage", "")
347
+ if sys_msg:
348
+ system_messages.append(sys_msg)
349
+
350
+ # Debugging suggestions
351
+ if "hookSpecificOutput" in debug_suggestions:
352
+ ctx = debug_suggestions["hookSpecificOutput"].get("additionalContext", "")
353
+ if ctx:
354
+ guidance_parts.append(ctx)
355
+
356
+ # CIGS analysis (cost accounting and reinforcement)
357
+ if "hookSpecificOutput" in cigs_response:
358
+ ctx = cigs_response["hookSpecificOutput"].get("additionalContext", "")
359
+ if ctx:
360
+ guidance_parts.append(ctx)
361
+
362
+ # Build unified response
363
+ response: dict[str, Any] = {"continue": True} # PostToolUse never blocks
364
+
365
+ if guidance_parts or system_messages:
366
+ response["hookSpecificOutput"] = {
367
+ "hookEventName": "PostToolUse",
368
+ }
369
+
370
+ if guidance_parts:
371
+ response["hookSpecificOutput"]["additionalContext"] = "\n".join(
372
+ guidance_parts
373
+ )
374
+
375
+ if system_messages:
376
+ response["hookSpecificOutput"]["systemMessage"] = "\n\n".join(
377
+ system_messages
378
+ )
379
+
380
+ return response
381
+
382
+
383
+ def main() -> None:
384
+ """Hook entry point for script wrapper."""
385
+ # Check environment override
386
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
387
+ print(json.dumps({"continue": True}))
388
+ sys.exit(0)
389
+
390
+ # Determine hook type from environment
391
+ hook_type = os.environ.get("HTMLGRAPH_HOOK_TYPE", "PostToolUse")
392
+
393
+ # Read tool input from stdin
394
+ try:
395
+ hook_input = json.load(sys.stdin)
396
+ except json.JSONDecodeError:
397
+ hook_input = {}
398
+
399
+ # Run hook with parallel execution
400
+ result = asyncio.run(posttooluse_hook(hook_type, hook_input))
401
+
402
+ # Output response
403
+ print(json.dumps(result))
404
+ sys.exit(0)
405
+
406
+
407
+ if __name__ == "__main__":
408
+ main()