htmlgraph 0.20.1__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 (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -1,27 +1,38 @@
1
1
  """
2
- Unified PreToolUse Hook - Parallel Orchestrator + Validator
2
+ Unified PreToolUse Hook - Parallel Orchestrator + Validator + Event Tracing
3
3
 
4
- This module provides a unified PreToolUse hook that runs both orchestrator
5
- enforcement and work validation checks in parallel using asyncio.
4
+ This module provides a unified PreToolUse hook that runs orchestrator
5
+ enforcement, work validation checks, and event tracing in parallel using asyncio.
6
6
 
7
7
  Architecture:
8
- - Runs orchestrator check and validator check simultaneously
8
+ - Runs orchestrator check, validator check, and event tracing simultaneously
9
9
  - Combines results into Claude Code standard format
10
10
  - Returns blocking response only if both checks agree
11
11
  - Provides combined guidance from both systems
12
+ - Generates tool_use_id and initiates event tracing for correlation
12
13
 
13
14
  Performance:
14
15
  - ~40-50% faster than sequential subprocess execution
15
16
  - Single Python process (no subprocess overhead)
16
17
  - Parallel execution via asyncio.gather()
18
+
19
+ Event Tracing:
20
+ - Generates UUID v4 for tool_use_id
21
+ - Captures tool name, input, start time (ISO8601 UTC), session_id
22
+ - Inserts start event into tool_traces table for PostToolUse correlation
23
+ - Non-blocking - errors gracefully degrade to allow tool execution
17
24
  """
18
25
 
19
26
  import asyncio
20
27
  import json
28
+ import logging
21
29
  import os
22
30
  import sys
31
+ import uuid
32
+ from datetime import datetime, timezone
23
33
  from typing import Any
24
34
 
35
+ from htmlgraph.db.schema import HtmlGraphDB
25
36
  from htmlgraph.hooks.orchestrator import enforce_orchestrator_mode
26
37
  from htmlgraph.hooks.task_enforcer import enforce_task_saving
27
38
  from htmlgraph.hooks.validator import (
@@ -32,6 +43,497 @@ from htmlgraph.hooks.validator import (
32
43
  validate_tool_call,
33
44
  )
34
45
 
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ def generate_tool_use_id() -> str:
50
+ """
51
+ Generate UUID v4 for tool_use_id.
52
+
53
+ Used for trace correlation between PreToolUse and PostToolUse hooks.
54
+
55
+ Returns:
56
+ UUID v4 string (36 chars)
57
+ """
58
+ return str(uuid.uuid4())
59
+
60
+
61
+ def get_current_session_id() -> str | None:
62
+ """
63
+ Query current session_id from environment or session files.
64
+
65
+ Reads from:
66
+ 1. Environment variable HTMLGRAPH_SESSION_ID (set by SessionStart hook)
67
+ 2. Latest session HTML file (fallback if env var not set)
68
+ 3. Session registry file (fallback if HTML file not found)
69
+
70
+ Returns:
71
+ Session ID string or None if not found
72
+ """
73
+ # First try environment variable
74
+ session_id = os.environ.get("HTMLGRAPH_SESSION_ID")
75
+ if session_id:
76
+ logger.debug(f"Session ID from environment: {session_id}")
77
+ return session_id
78
+
79
+ # Fallback: Read from latest session HTML file
80
+ try:
81
+ import re
82
+ from pathlib import Path
83
+
84
+ graph_dir = Path.cwd() / ".htmlgraph"
85
+ sessions_dir = graph_dir / "sessions"
86
+
87
+ logger.debug(f"Looking for session files in: {sessions_dir}")
88
+
89
+ if sessions_dir.exists():
90
+ # Get the most recent session HTML file
91
+ session_files = sorted(
92
+ sessions_dir.glob("sess-*.html"),
93
+ key=lambda p: p.stat().st_mtime,
94
+ reverse=True,
95
+ )
96
+ logger.debug(f"Found {len(session_files)} session files")
97
+
98
+ for session_file in session_files:
99
+ try:
100
+ # Extract session_id from filename (sess-XXXXX.html)
101
+ match = re.search(r"sess-([a-f0-9]+)", session_file.name)
102
+ if match:
103
+ session_id = f"sess-{match.group(1)}"
104
+ logger.debug(f"Found session ID from file: {session_id}")
105
+ return session_id
106
+ except Exception as e:
107
+ logger.debug(f"Error reading session file {session_file}: {e}")
108
+ continue
109
+ logger.debug("No valid session files found")
110
+ else:
111
+ logger.debug(f"Sessions directory not found: {sessions_dir}")
112
+ except Exception as e:
113
+ logger.debug(f"Could not read from session files: {e}")
114
+
115
+ # Fallback: Read from session registry
116
+ try:
117
+ import json
118
+ from pathlib import Path
119
+
120
+ graph_dir = Path.cwd() / ".htmlgraph"
121
+ registry_dir = graph_dir / "sessions" / "registry" / "active"
122
+
123
+ if registry_dir.exists():
124
+ # Get the most recent session file
125
+ session_files = sorted(
126
+ registry_dir.glob("*.json"),
127
+ key=lambda p: p.stat().st_mtime,
128
+ reverse=True,
129
+ )
130
+
131
+ for session_file in session_files:
132
+ try:
133
+ with open(session_file) as f:
134
+ data = json.load(f)
135
+ if data.get("status") == "active":
136
+ session_id = data.get("session_id")
137
+ if isinstance(session_id, str):
138
+ return session_id
139
+ except Exception:
140
+ continue
141
+ except Exception as e:
142
+ logger.debug(f"Could not read from session registry: {e}")
143
+
144
+ return None
145
+
146
+
147
+ def sanitize_tool_input(tool_input: dict[str, Any]) -> dict[str, Any]:
148
+ """
149
+ Sanitize tool input to remove sensitive data before storage.
150
+
151
+ Removes or truncates:
152
+ - Passwords and tokens (any field with 'password', 'token', 'secret', 'key')
153
+ - Large binary data
154
+ - Deeply nested structures
155
+
156
+ Args:
157
+ tool_input: Raw tool input to sanitize
158
+
159
+ Returns:
160
+ Sanitized copy of tool_input
161
+ """
162
+ try:
163
+ sanitized = {}
164
+ sensitive_keys = {"password", "token", "secret", "key", "auth", "api_key"}
165
+
166
+ for key, value in tool_input.items():
167
+ # Remove sensitive fields
168
+ if any(sens in key.lower() for sens in sensitive_keys):
169
+ sanitized[key] = "[REDACTED]"
170
+ # Truncate very large values
171
+ elif isinstance(value, str) and len(value) > 10000:
172
+ sanitized[key] = f"{value[:10000]}... [TRUNCATED]"
173
+ # Keep other values
174
+ else:
175
+ sanitized[key] = value
176
+
177
+ return sanitized
178
+ except Exception as e:
179
+ logger.warning(f"Error sanitizing tool input: {e}")
180
+ return tool_input
181
+
182
+
183
+ def extract_subagent_type(tool_input: dict[str, Any]) -> str | None:
184
+ """
185
+ Extract subagent_type from Task() tool input.
186
+
187
+ Looks for patterns like:
188
+ - "subagent_type": "gemini-spawner"
189
+ - Task with specific naming patterns
190
+
191
+ Args:
192
+ tool_input: Task() tool input parameters
193
+
194
+ Returns:
195
+ Subagent type string or None if not found
196
+ """
197
+ try:
198
+ # Check for explicit subagent_type parameter
199
+ if "subagent_type" in tool_input:
200
+ return str(tool_input.get("subagent_type"))
201
+
202
+ # Check in prompt for agent references
203
+ prompt = str(tool_input.get("prompt", "")).lower()
204
+ if "gemini" in prompt:
205
+ return "gemini-spawner"
206
+ if "codex" in prompt:
207
+ return "codex-spawner"
208
+ if "researcher" in prompt:
209
+ return "researcher"
210
+ if "debugger" in prompt:
211
+ return "debugger"
212
+
213
+ return None
214
+ except Exception:
215
+ return None
216
+
217
+
218
+ def create_task_parent_event(
219
+ db: HtmlGraphDB,
220
+ tool_input: dict[str, Any],
221
+ session_id: str,
222
+ start_time: str,
223
+ ) -> str | None:
224
+ """
225
+ Create a parent event for Task() delegations.
226
+
227
+ Inserts into agent_events with:
228
+ - event_type: 'task_delegation'
229
+ - subagent_type: Extracted from tool input
230
+ - status: 'started'
231
+ - parent_event_id: UserQuery event ID (links back to conversation root)
232
+
233
+ This event will be linked to child events created by the subagent
234
+ and updated when SubagentStop fires.
235
+
236
+ Args:
237
+ db: Database connection
238
+ tool_input: Task() tool input parameters
239
+ session_id: Current session ID
240
+ start_time: ISO8601 UTC timestamp
241
+
242
+ Returns:
243
+ Parent event_id if successful, None otherwise
244
+ """
245
+ try:
246
+ if not db.connection:
247
+ db.connect()
248
+
249
+ parent_event_id = f"evt-{str(uuid.uuid4())[:8]}"
250
+ subagent_type = extract_subagent_type(tool_input)
251
+ prompt = str(tool_input.get("prompt", ""))[:200]
252
+
253
+ # Load UserQuery event ID for parent-child linking from database
254
+ user_query_event_id = None
255
+ try:
256
+ from htmlgraph.hooks.event_tracker import get_parent_user_query
257
+
258
+ user_query_event_id = get_parent_user_query(db, session_id)
259
+ except Exception:
260
+ pass
261
+
262
+ # Build input summary
263
+ input_summary = json.dumps(
264
+ {
265
+ "subagent_type": subagent_type or "general-purpose",
266
+ "prompt": prompt,
267
+ }
268
+ )[:500]
269
+
270
+ cursor = db.connection.cursor() # type: ignore[union-attr]
271
+
272
+ # Insert parent event
273
+ cursor.execute(
274
+ """
275
+ INSERT INTO agent_events
276
+ (event_id, agent_id, event_type, timestamp, tool_name,
277
+ input_summary, session_id, status, subagent_type, parent_event_id)
278
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
279
+ """,
280
+ (
281
+ parent_event_id,
282
+ "claude-code", # Main orchestrator agent
283
+ "task_delegation",
284
+ start_time,
285
+ "Task",
286
+ input_summary,
287
+ session_id,
288
+ "started",
289
+ subagent_type or "general-purpose",
290
+ user_query_event_id, # Link to UserQuery event
291
+ ),
292
+ )
293
+
294
+ db.connection.commit() # type: ignore[union-attr]
295
+
296
+ # Export to environment for subagent reference
297
+ os.environ["HTMLGRAPH_PARENT_EVENT"] = parent_event_id
298
+ os.environ["HTMLGRAPH_PARENT_QUERY_EVENT"] = (
299
+ user_query_event_id or ""
300
+ ) # For spawners to use
301
+ os.environ["HTMLGRAPH_SUBAGENT_TYPE"] = subagent_type or "general-purpose"
302
+
303
+ logger.debug(
304
+ f"Created parent event for Task delegation: "
305
+ f"event_id={parent_event_id}, subagent_type={subagent_type}, "
306
+ f"parent_query_event={user_query_event_id}"
307
+ )
308
+
309
+ return parent_event_id
310
+
311
+ except Exception as e:
312
+ logger.warning(f"Error creating parent event: {e}")
313
+ return None
314
+
315
+
316
+ def create_start_event(
317
+ tool_name: str, tool_input: dict[str, Any], session_id: str
318
+ ) -> str | None:
319
+ """
320
+ Capture and store tool execution start event.
321
+
322
+ Inserts into tool_traces table with:
323
+ - tool_use_id: UUID v4 for correlation
324
+ - trace_id: Parent trace ID (from context)
325
+ - session_id: Current session
326
+ - tool_name: Tool being executed
327
+ - tool_input: Sanitized input parameters
328
+ - start_time: ISO8601 UTC timestamp
329
+ - status: 'started'
330
+
331
+ For Task() calls, also creates a parent event for event nesting.
332
+
333
+ Args:
334
+ tool_name: Name of tool being executed
335
+ tool_input: Tool input parameters (will be sanitized)
336
+ session_id: Current session ID
337
+
338
+ Returns:
339
+ tool_use_id on success, None on error
340
+ """
341
+ tool_use_id = None
342
+ try:
343
+ tool_use_id = generate_tool_use_id()
344
+ trace_id = os.environ.get("HTMLGRAPH_TRACE_ID", tool_use_id)
345
+ start_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
346
+
347
+ # Sanitize input before storing
348
+ sanitized_input = sanitize_tool_input(tool_input)
349
+
350
+ # Connect to database (use project's .htmlgraph/htmlgraph.db, not home directory)
351
+ from htmlgraph.config import get_database_path
352
+
353
+ db_path = str(get_database_path())
354
+ db = HtmlGraphDB(db_path)
355
+
356
+ # Ensure session exists (create placeholder if needed)
357
+ if not db._ensure_session_exists(session_id, "system"):
358
+ logger.warning(f"Could not ensure session {session_id} exists in database")
359
+
360
+ # Insert start event into tool_traces
361
+ if not db.connection:
362
+ db.connect()
363
+
364
+ cursor = db.connection.cursor() # type: ignore[union-attr]
365
+
366
+ # Determine parent event ID with proper hierarchy:
367
+ # 1. FIRST check HTMLGRAPH_PARENT_EVENT env var (set by Task delegation for subagents)
368
+ # 2. For Task() tool, create a new task_delegation event
369
+ # 3. Fall back to UserQuery only if no parent context available
370
+ #
371
+ # This ensures tool events executed within Task() subagents are properly
372
+ # nested under the Task delegation event, not flattened to UserQuery.
373
+ env_parent_event = os.environ.get("HTMLGRAPH_PARENT_EVENT")
374
+
375
+ # Get UserQuery event ID as fallback (for top-level tool calls)
376
+ user_query_event_id = None
377
+ try:
378
+ from htmlgraph.hooks.event_tracker import get_parent_user_query
379
+
380
+ user_query_event_id = get_parent_user_query(db, session_id)
381
+ except Exception:
382
+ pass
383
+
384
+ # Check if this is a Task() call for parent event creation
385
+ task_parent_event_id = None
386
+ if tool_name == "Task":
387
+ task_parent_event_id = create_task_parent_event(
388
+ db, tool_input, session_id, start_time
389
+ )
390
+
391
+ # Insert into agent_events table (for dashboard display)
392
+ import uuid
393
+
394
+ event_id = f"evt-{str(uuid.uuid4())[:8]}"
395
+
396
+ # Determine parent with proper hierarchy:
397
+ # - Task() tools: Use the newly created task_delegation event
398
+ # - Tools in subagent context: Use HTMLGRAPH_PARENT_EVENT (Task delegation)
399
+ # - Top-level tools: Fall back to UserQuery
400
+ if tool_name == "Task":
401
+ parent_event_id = task_parent_event_id
402
+ elif env_parent_event:
403
+ # Subagent context: tools should be children of Task delegation
404
+ parent_event_id = env_parent_event
405
+ logger.debug(
406
+ f"Using parent from environment: {env_parent_event} for {tool_name}"
407
+ )
408
+ else:
409
+ # Top-level context: tools are children of UserQuery
410
+ parent_event_id = user_query_event_id
411
+
412
+ cursor.execute(
413
+ """
414
+ INSERT INTO agent_events
415
+ (event_id, agent_id, event_type, timestamp, tool_name,
416
+ input_summary, session_id, status, parent_event_id)
417
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
418
+ """,
419
+ (
420
+ event_id,
421
+ "claude-code", # Agent executing the tool
422
+ "tool_call",
423
+ start_time,
424
+ tool_name,
425
+ json.dumps(sanitized_input)[:500], # Truncate for summary
426
+ session_id,
427
+ "recorded",
428
+ parent_event_id, # Link to UserQuery or Task parent
429
+ ),
430
+ )
431
+
432
+ # Export Bash event as parent for child processes (e.g., spawner executables)
433
+ if tool_name == "Bash":
434
+ os.environ["HTMLGRAPH_PARENT_EVENT"] = event_id
435
+ logger.debug(
436
+ f"Exported HTMLGRAPH_PARENT_EVENT={event_id} for Bash tool call"
437
+ )
438
+
439
+ # Also insert into tool_traces for correlation (if table exists)
440
+ try:
441
+ cursor.execute(
442
+ """
443
+ INSERT INTO tool_traces
444
+ (tool_use_id, trace_id, session_id, tool_name, tool_input,
445
+ start_time, status, parent_tool_use_id)
446
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
447
+ """,
448
+ (
449
+ tool_use_id,
450
+ trace_id,
451
+ session_id,
452
+ tool_name,
453
+ json.dumps(sanitized_input),
454
+ start_time,
455
+ "started",
456
+ None, # Will be set by SubagentStop hook
457
+ ),
458
+ )
459
+ except Exception as e:
460
+ logger.debug(f"Could not insert into tool_traces: {e}")
461
+
462
+ db.connection.commit() # type: ignore[union-attr]
463
+ db.disconnect()
464
+
465
+ logger.debug(
466
+ f"Created start event: tool_use_id={tool_use_id}, "
467
+ f"tool={tool_name}, session={session_id}, parent_event={parent_event_id}"
468
+ )
469
+ return tool_use_id
470
+
471
+ except Exception as e:
472
+ logger.warning(f"Error creating start event: {e}")
473
+ # Graceful degradation - return None but don't block tool
474
+ return None
475
+
476
+
477
+ async def run_event_tracing(
478
+ tool_input: dict[str, Any],
479
+ ) -> dict[str, Any]:
480
+ """
481
+ Run event tracing (async wrapper).
482
+
483
+ Generates tool_use_id and creates start event in database.
484
+ Non-blocking - errors don't prevent tool execution.
485
+
486
+ Args:
487
+ tool_input: Hook input with tool name and parameters
488
+
489
+ Returns:
490
+ Event tracing response: {"hookSpecificOutput": {"tool_use_id": "...", ...}}
491
+ """
492
+ try:
493
+ from htmlgraph.hooks.context import HookContext
494
+
495
+ loop = asyncio.get_event_loop()
496
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
497
+
498
+ # Use HookContext to properly extract session_id (same as UserPromptSubmit)
499
+ context = HookContext.from_input(tool_input)
500
+
501
+ try:
502
+ session_id = context.session_id
503
+
504
+ # Skip if no session ID
505
+ if not session_id or session_id == "unknown":
506
+ logger.debug("No session ID found, skipping event tracing")
507
+ return {}
508
+
509
+ # Run in thread pool since it involves I/O
510
+ tool_use_id = await loop.run_in_executor(
511
+ None,
512
+ create_start_event,
513
+ tool_name,
514
+ tool_input,
515
+ session_id,
516
+ )
517
+
518
+ if tool_use_id:
519
+ # Store in environment for PostToolUse correlation
520
+ os.environ["HTMLGRAPH_TOOL_USE_ID"] = tool_use_id
521
+
522
+ return {
523
+ "hookSpecificOutput": {
524
+ "tool_use_id": tool_use_id,
525
+ "additionalContext": f"Event tracing started: {tool_use_id}",
526
+ }
527
+ }
528
+
529
+ return {}
530
+ finally:
531
+ # Ensure context resources are properly closed
532
+ context.close()
533
+ except Exception:
534
+ # Graceful degradation - allow on error
535
+ return {}
536
+
35
537
 
36
538
  async def run_orchestrator_check(tool_input: dict[str, Any]) -> dict[str, Any]:
37
539
  """
@@ -75,10 +577,13 @@ async def run_validation_check(tool_input: dict[str, Any]) -> dict[str, Any]:
75
577
 
76
578
  tool_name = tool_input.get("name", "") or tool_input.get("tool", "")
77
579
  tool_params = tool_input.get("input", {}) or tool_input.get("params", {})
580
+ session_id = tool_input.get("session_id", "unknown")
78
581
 
79
582
  # Load config and history in thread pool
80
583
  config = await loop.run_in_executor(None, load_validation_config)
81
- history = await loop.run_in_executor(None, validator_load_history)
584
+ history = await loop.run_in_executor(
585
+ None, lambda: validator_load_history(session_id)
586
+ )
82
587
 
83
588
  # Run validation
84
589
  return await loop.run_in_executor(
@@ -183,17 +688,20 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
183
688
  "hookSpecificOutput": {
184
689
  "hookEventName": "PreToolUse",
185
690
  "updatedInput": {...}, # If task enforcer modified input
186
- "additionalContext": "Combined guidance"
691
+ "additionalContext": "Combined guidance",
692
+ "tool_use_id": "..." # For PostToolUse correlation
187
693
  }
188
694
  }
189
695
  """
190
- # Run all four checks in parallel using asyncio.gather
696
+ # Run all five checks in parallel using asyncio.gather
191
697
  (
698
+ event_tracing_response,
192
699
  orch_response,
193
700
  validate_response,
194
701
  task_response,
195
702
  debug_guidance,
196
703
  ) = await asyncio.gather(
704
+ run_event_tracing(tool_input),
197
705
  run_orchestrator_check(tool_input),
198
706
  run_validation_check(tool_input),
199
707
  run_task_enforcement(tool_input),
@@ -209,6 +717,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
209
717
  # Collect guidance from all systems
210
718
  guidance_parts = []
211
719
 
720
+ # Event tracing guidance
721
+ if "hookSpecificOutput" in event_tracing_response:
722
+ ctx = event_tracing_response["hookSpecificOutput"].get("additionalContext", "")
723
+ if ctx:
724
+ guidance_parts.append(f"[EventTrace] {ctx}")
725
+
212
726
  # Orchestrator guidance
213
727
  if "hookSpecificOutput" in orch_response:
214
728
  ctx = orch_response["hookSpecificOutput"].get("additionalContext", "")
@@ -245,6 +759,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
245
759
  }
246
760
  }
247
761
 
762
+ # Add tool_use_id for PostToolUse correlation if available
763
+ if "hookSpecificOutput" in event_tracing_response:
764
+ tool_use_id = event_tracing_response["hookSpecificOutput"].get("tool_use_id")
765
+ if tool_use_id:
766
+ response["hookSpecificOutput"]["tool_use_id"] = tool_use_id
767
+
248
768
  # Check if task enforcer provided updatedInput
249
769
  updated_input = None
250
770
  if "hookSpecificOutput" in task_response: