htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__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 (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +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,447 @@ 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
+ from pathlib import Path
344
+
345
+ tool_use_id = generate_tool_use_id()
346
+ trace_id = os.environ.get("HTMLGRAPH_TRACE_ID", tool_use_id)
347
+ start_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
348
+
349
+ # Sanitize input before storing
350
+ sanitized_input = sanitize_tool_input(tool_input)
351
+
352
+ # Connect to database (use project's .htmlgraph/index.sqlite, not home directory)
353
+ graph_dir = Path.cwd() / ".htmlgraph"
354
+ db_path = str(graph_dir / "index.sqlite")
355
+ db = HtmlGraphDB(db_path)
356
+
357
+ # Ensure session exists (create placeholder if needed)
358
+ if not db._ensure_session_exists(session_id, "system"):
359
+ logger.warning(f"Could not ensure session {session_id} exists in database")
360
+
361
+ # Insert start event into tool_traces
362
+ if not db.connection:
363
+ db.connect()
364
+
365
+ cursor = db.connection.cursor() # type: ignore[union-attr]
366
+
367
+ # Check if this is a Task() call for parent event creation
368
+ parent_event_id = None
369
+ if tool_name == "Task":
370
+ parent_event_id = create_task_parent_event(
371
+ db, tool_input, session_id, start_time
372
+ )
373
+
374
+ # Insert into agent_events table (for dashboard display)
375
+ import uuid
376
+
377
+ event_id = f"evt-{str(uuid.uuid4())[:8]}"
378
+
379
+ cursor.execute(
380
+ """
381
+ INSERT INTO agent_events
382
+ (event_id, agent_id, event_type, timestamp, tool_name,
383
+ input_summary, session_id, status, parent_event_id)
384
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
385
+ """,
386
+ (
387
+ event_id,
388
+ "claude-code", # Agent executing the tool
389
+ "tool_call",
390
+ start_time,
391
+ tool_name,
392
+ json.dumps(sanitized_input)[:500], # Truncate for summary
393
+ session_id,
394
+ "recorded",
395
+ parent_event_id, # Link to parent if this is Task()
396
+ ),
397
+ )
398
+
399
+ # Also insert into tool_traces for correlation (if table exists)
400
+ try:
401
+ cursor.execute(
402
+ """
403
+ INSERT INTO tool_traces
404
+ (tool_use_id, trace_id, session_id, tool_name, tool_input,
405
+ start_time, status, parent_tool_use_id)
406
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
407
+ """,
408
+ (
409
+ tool_use_id,
410
+ trace_id,
411
+ session_id,
412
+ tool_name,
413
+ json.dumps(sanitized_input),
414
+ start_time,
415
+ "started",
416
+ None, # Will be set by SubagentStop hook
417
+ ),
418
+ )
419
+ except Exception as e:
420
+ logger.debug(f"Could not insert into tool_traces: {e}")
421
+
422
+ db.connection.commit() # type: ignore[union-attr]
423
+ db.disconnect()
424
+
425
+ logger.debug(
426
+ f"Created start event: tool_use_id={tool_use_id}, "
427
+ f"tool={tool_name}, session={session_id}, parent_event={parent_event_id}"
428
+ )
429
+ return tool_use_id
430
+
431
+ except Exception as e:
432
+ logger.warning(f"Error creating start event: {e}")
433
+ # Graceful degradation - return None but don't block tool
434
+ return None
435
+
436
+
437
+ async def run_event_tracing(
438
+ tool_input: dict[str, Any],
439
+ ) -> dict[str, Any]:
440
+ """
441
+ Run event tracing (async wrapper).
442
+
443
+ Generates tool_use_id and creates start event in database.
444
+ Non-blocking - errors don't prevent tool execution.
445
+
446
+ Args:
447
+ tool_input: Hook input with tool name and parameters
448
+
449
+ Returns:
450
+ Event tracing response: {"hookSpecificOutput": {"tool_use_id": "...", ...}}
451
+ """
452
+ try:
453
+ loop = asyncio.get_event_loop()
454
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
455
+ session_id = get_current_session_id()
456
+
457
+ # Skip if no session ID
458
+ if not session_id:
459
+ logger.debug("No session ID found, skipping event tracing")
460
+ return {}
461
+
462
+ # Run in thread pool since it involves I/O
463
+ tool_use_id = await loop.run_in_executor(
464
+ None,
465
+ create_start_event,
466
+ tool_name,
467
+ tool_input,
468
+ session_id,
469
+ )
470
+
471
+ if tool_use_id:
472
+ # Store in environment for PostToolUse correlation
473
+ os.environ["HTMLGRAPH_TOOL_USE_ID"] = tool_use_id
474
+
475
+ return {
476
+ "hookSpecificOutput": {
477
+ "tool_use_id": tool_use_id,
478
+ "additionalContext": f"Event tracing started: {tool_use_id}",
479
+ }
480
+ }
481
+
482
+ return {}
483
+ except Exception:
484
+ # Graceful degradation - allow on error
485
+ return {}
486
+
35
487
 
36
488
  async def run_orchestrator_check(tool_input: dict[str, Any]) -> dict[str, Any]:
37
489
  """
@@ -183,17 +635,20 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
183
635
  "hookSpecificOutput": {
184
636
  "hookEventName": "PreToolUse",
185
637
  "updatedInput": {...}, # If task enforcer modified input
186
- "additionalContext": "Combined guidance"
638
+ "additionalContext": "Combined guidance",
639
+ "tool_use_id": "..." # For PostToolUse correlation
187
640
  }
188
641
  }
189
642
  """
190
- # Run all four checks in parallel using asyncio.gather
643
+ # Run all five checks in parallel using asyncio.gather
191
644
  (
645
+ event_tracing_response,
192
646
  orch_response,
193
647
  validate_response,
194
648
  task_response,
195
649
  debug_guidance,
196
650
  ) = await asyncio.gather(
651
+ run_event_tracing(tool_input),
197
652
  run_orchestrator_check(tool_input),
198
653
  run_validation_check(tool_input),
199
654
  run_task_enforcement(tool_input),
@@ -209,6 +664,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
209
664
  # Collect guidance from all systems
210
665
  guidance_parts = []
211
666
 
667
+ # Event tracing guidance
668
+ if "hookSpecificOutput" in event_tracing_response:
669
+ ctx = event_tracing_response["hookSpecificOutput"].get("additionalContext", "")
670
+ if ctx:
671
+ guidance_parts.append(f"[EventTrace] {ctx}")
672
+
212
673
  # Orchestrator guidance
213
674
  if "hookSpecificOutput" in orch_response:
214
675
  ctx = orch_response["hookSpecificOutput"].get("additionalContext", "")
@@ -245,6 +706,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
245
706
  }
246
707
  }
247
708
 
709
+ # Add tool_use_id for PostToolUse correlation if available
710
+ if "hookSpecificOutput" in event_tracing_response:
711
+ tool_use_id = event_tracing_response["hookSpecificOutput"].get("tool_use_id")
712
+ if tool_use_id:
713
+ response["hookSpecificOutput"]["tool_use_id"] = tool_use_id
714
+
248
715
  # Check if task enforcer provided updatedInput
249
716
  updated_input = None
250
717
  if "hookSpecificOutput" in task_response: