htmlgraph 0.24.1__py3-none-any.whl → 0.25.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) 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 +2115 -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 +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -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 +3315 -492
  51. htmlgraph-0.24.1.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 +1334 -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/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.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,450 @@ 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
+ from pathlib import Path
247
+
248
+ if not db.connection:
249
+ db.connect()
250
+
251
+ parent_event_id = f"evt-{str(uuid.uuid4())[:8]}"
252
+ subagent_type = extract_subagent_type(tool_input)
253
+ prompt = str(tool_input.get("prompt", ""))[:200]
254
+
255
+ # Load UserQuery event ID for parent-child linking
256
+ graph_dir = Path.cwd() / ".htmlgraph"
257
+ user_query_event_id = None
258
+ try:
259
+ from htmlgraph.hooks.event_tracker import load_user_query_event
260
+
261
+ user_query_event_id = load_user_query_event(graph_dir, session_id)
262
+ except Exception:
263
+ pass
264
+
265
+ # Build input summary
266
+ input_summary = json.dumps(
267
+ {
268
+ "subagent_type": subagent_type or "general-purpose",
269
+ "prompt": prompt,
270
+ }
271
+ )[:500]
272
+
273
+ cursor = db.connection.cursor() # type: ignore[union-attr]
274
+
275
+ # Insert parent event
276
+ cursor.execute(
277
+ """
278
+ INSERT INTO agent_events
279
+ (event_id, agent_id, event_type, timestamp, tool_name,
280
+ input_summary, session_id, status, subagent_type, parent_event_id)
281
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
282
+ """,
283
+ (
284
+ parent_event_id,
285
+ "claude-code", # Main orchestrator agent
286
+ "task_delegation",
287
+ start_time,
288
+ "Task",
289
+ input_summary,
290
+ session_id,
291
+ "started",
292
+ subagent_type or "general-purpose",
293
+ user_query_event_id, # Link to UserQuery event
294
+ ),
295
+ )
296
+
297
+ db.connection.commit() # type: ignore[union-attr]
298
+
299
+ # Export to environment for subagent reference
300
+ os.environ["HTMLGRAPH_PARENT_EVENT"] = parent_event_id
301
+ os.environ["HTMLGRAPH_PARENT_QUERY_EVENT"] = (
302
+ user_query_event_id or ""
303
+ ) # For spawners to use
304
+ os.environ["HTMLGRAPH_SUBAGENT_TYPE"] = subagent_type or "general-purpose"
305
+
306
+ logger.debug(
307
+ f"Created parent event for Task delegation: "
308
+ f"event_id={parent_event_id}, subagent_type={subagent_type}, "
309
+ f"parent_query_event={user_query_event_id}"
310
+ )
311
+
312
+ return parent_event_id
313
+
314
+ except Exception as e:
315
+ logger.warning(f"Error creating parent event: {e}")
316
+ return None
317
+
318
+
319
+ def create_start_event(
320
+ tool_name: str, tool_input: dict[str, Any], session_id: str
321
+ ) -> str | None:
322
+ """
323
+ Capture and store tool execution start event.
324
+
325
+ Inserts into tool_traces table with:
326
+ - tool_use_id: UUID v4 for correlation
327
+ - trace_id: Parent trace ID (from context)
328
+ - session_id: Current session
329
+ - tool_name: Tool being executed
330
+ - tool_input: Sanitized input parameters
331
+ - start_time: ISO8601 UTC timestamp
332
+ - status: 'started'
333
+
334
+ For Task() calls, also creates a parent event for event nesting.
335
+
336
+ Args:
337
+ tool_name: Name of tool being executed
338
+ tool_input: Tool input parameters (will be sanitized)
339
+ session_id: Current session ID
340
+
341
+ Returns:
342
+ tool_use_id on success, None on error
343
+ """
344
+ tool_use_id = None
345
+ try:
346
+ from pathlib import Path
347
+
348
+ tool_use_id = generate_tool_use_id()
349
+ trace_id = os.environ.get("HTMLGRAPH_TRACE_ID", tool_use_id)
350
+ start_time = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
351
+
352
+ # Sanitize input before storing
353
+ sanitized_input = sanitize_tool_input(tool_input)
354
+
355
+ # Connect to database (use project's .htmlgraph/index.sqlite, not home directory)
356
+ graph_dir = Path.cwd() / ".htmlgraph"
357
+ db_path = str(graph_dir / "index.sqlite")
358
+ db = HtmlGraphDB(db_path)
359
+
360
+ # Ensure session exists (create placeholder if needed)
361
+ if not db._ensure_session_exists(session_id, "system"):
362
+ logger.warning(f"Could not ensure session {session_id} exists in database")
363
+
364
+ # Insert start event into tool_traces
365
+ if not db.connection:
366
+ db.connect()
367
+
368
+ cursor = db.connection.cursor() # type: ignore[union-attr]
369
+
370
+ # Check if this is a Task() call for parent event creation
371
+ parent_event_id = None
372
+ if tool_name == "Task":
373
+ parent_event_id = create_task_parent_event(
374
+ db, tool_input, session_id, start_time
375
+ )
376
+
377
+ # Insert into agent_events table (for dashboard display)
378
+ import uuid
379
+
380
+ event_id = f"evt-{str(uuid.uuid4())[:8]}"
381
+
382
+ cursor.execute(
383
+ """
384
+ INSERT INTO agent_events
385
+ (event_id, agent_id, event_type, timestamp, tool_name,
386
+ input_summary, session_id, status, parent_event_id)
387
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
388
+ """,
389
+ (
390
+ event_id,
391
+ "claude-code", # Agent executing the tool
392
+ "tool_call",
393
+ start_time,
394
+ tool_name,
395
+ json.dumps(sanitized_input)[:500], # Truncate for summary
396
+ session_id,
397
+ "recorded",
398
+ parent_event_id, # Link to parent if this is Task()
399
+ ),
400
+ )
401
+
402
+ # Also insert into tool_traces for correlation (if table exists)
403
+ try:
404
+ cursor.execute(
405
+ """
406
+ INSERT INTO tool_traces
407
+ (tool_use_id, trace_id, session_id, tool_name, tool_input,
408
+ start_time, status, parent_tool_use_id)
409
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
410
+ """,
411
+ (
412
+ tool_use_id,
413
+ trace_id,
414
+ session_id,
415
+ tool_name,
416
+ json.dumps(sanitized_input),
417
+ start_time,
418
+ "started",
419
+ None, # Will be set by SubagentStop hook
420
+ ),
421
+ )
422
+ except Exception as e:
423
+ logger.debug(f"Could not insert into tool_traces: {e}")
424
+
425
+ db.connection.commit() # type: ignore[union-attr]
426
+ db.disconnect()
427
+
428
+ logger.debug(
429
+ f"Created start event: tool_use_id={tool_use_id}, "
430
+ f"tool={tool_name}, session={session_id}, parent_event={parent_event_id}"
431
+ )
432
+ return tool_use_id
433
+
434
+ except Exception as e:
435
+ logger.warning(f"Error creating start event: {e}")
436
+ # Graceful degradation - return None but don't block tool
437
+ return None
438
+
439
+
440
+ async def run_event_tracing(
441
+ tool_input: dict[str, Any],
442
+ ) -> dict[str, Any]:
443
+ """
444
+ Run event tracing (async wrapper).
445
+
446
+ Generates tool_use_id and creates start event in database.
447
+ Non-blocking - errors don't prevent tool execution.
448
+
449
+ Args:
450
+ tool_input: Hook input with tool name and parameters
451
+
452
+ Returns:
453
+ Event tracing response: {"hookSpecificOutput": {"tool_use_id": "...", ...}}
454
+ """
455
+ try:
456
+ loop = asyncio.get_event_loop()
457
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
458
+ session_id = get_current_session_id()
459
+
460
+ # Skip if no session ID
461
+ if not session_id:
462
+ logger.debug("No session ID found, skipping event tracing")
463
+ return {}
464
+
465
+ # Run in thread pool since it involves I/O
466
+ tool_use_id = await loop.run_in_executor(
467
+ None,
468
+ create_start_event,
469
+ tool_name,
470
+ tool_input,
471
+ session_id,
472
+ )
473
+
474
+ if tool_use_id:
475
+ # Store in environment for PostToolUse correlation
476
+ os.environ["HTMLGRAPH_TOOL_USE_ID"] = tool_use_id
477
+
478
+ return {
479
+ "hookSpecificOutput": {
480
+ "tool_use_id": tool_use_id,
481
+ "additionalContext": f"Event tracing started: {tool_use_id}",
482
+ }
483
+ }
484
+
485
+ return {}
486
+ except Exception:
487
+ # Graceful degradation - allow on error
488
+ return {}
489
+
35
490
 
36
491
  async def run_orchestrator_check(tool_input: dict[str, Any]) -> dict[str, Any]:
37
492
  """
@@ -183,17 +638,20 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
183
638
  "hookSpecificOutput": {
184
639
  "hookEventName": "PreToolUse",
185
640
  "updatedInput": {...}, # If task enforcer modified input
186
- "additionalContext": "Combined guidance"
641
+ "additionalContext": "Combined guidance",
642
+ "tool_use_id": "..." # For PostToolUse correlation
187
643
  }
188
644
  }
189
645
  """
190
- # Run all four checks in parallel using asyncio.gather
646
+ # Run all five checks in parallel using asyncio.gather
191
647
  (
648
+ event_tracing_response,
192
649
  orch_response,
193
650
  validate_response,
194
651
  task_response,
195
652
  debug_guidance,
196
653
  ) = await asyncio.gather(
654
+ run_event_tracing(tool_input),
197
655
  run_orchestrator_check(tool_input),
198
656
  run_validation_check(tool_input),
199
657
  run_task_enforcement(tool_input),
@@ -209,6 +667,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
209
667
  # Collect guidance from all systems
210
668
  guidance_parts = []
211
669
 
670
+ # Event tracing guidance
671
+ if "hookSpecificOutput" in event_tracing_response:
672
+ ctx = event_tracing_response["hookSpecificOutput"].get("additionalContext", "")
673
+ if ctx:
674
+ guidance_parts.append(f"[EventTrace] {ctx}")
675
+
212
676
  # Orchestrator guidance
213
677
  if "hookSpecificOutput" in orch_response:
214
678
  ctx = orch_response["hookSpecificOutput"].get("additionalContext", "")
@@ -245,6 +709,12 @@ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
245
709
  }
246
710
  }
247
711
 
712
+ # Add tool_use_id for PostToolUse correlation if available
713
+ if "hookSpecificOutput" in event_tracing_response:
714
+ tool_use_id = event_tracing_response["hookSpecificOutput"].get("tool_use_id")
715
+ if tool_use_id:
716
+ response["hookSpecificOutput"]["tool_use_id"] = tool_use_id
717
+
248
718
  # Check if task enforcer provided updatedInput
249
719
  updated_input = None
250
720
  if "hookSpecificOutput" in task_response: