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,819 @@
1
+ """
2
+ Unified PreToolUse Hook - Parallel Orchestrator + Validator + Event Tracing
3
+
4
+ This module provides a unified PreToolUse hook that runs orchestrator
5
+ enforcement, work validation checks, and event tracing in parallel using asyncio.
6
+
7
+ Architecture:
8
+ - Runs orchestrator check, validator check, and event tracing simultaneously
9
+ - Combines results into Claude Code standard format
10
+ - Returns blocking response only if both checks agree
11
+ - Provides combined guidance from both systems
12
+ - Generates tool_use_id and initiates event tracing for correlation
13
+
14
+ Performance:
15
+ - ~40-50% faster than sequential subprocess execution
16
+ - Single Python process (no subprocess overhead)
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
24
+ """
25
+
26
+ import asyncio
27
+ import json
28
+ import logging
29
+ import os
30
+ import sys
31
+ import uuid
32
+ from datetime import datetime, timezone
33
+ from typing import Any
34
+
35
+ from htmlgraph.db.schema import HtmlGraphDB
36
+ from htmlgraph.hooks.orchestrator import enforce_orchestrator_mode
37
+ from htmlgraph.hooks.task_enforcer import enforce_task_saving
38
+ from htmlgraph.hooks.validator import (
39
+ load_tool_history as validator_load_history,
40
+ )
41
+ from htmlgraph.hooks.validator import (
42
+ load_validation_config,
43
+ validate_tool_call,
44
+ )
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
+
537
+
538
+ async def run_orchestrator_check(tool_input: dict[str, Any]) -> dict[str, Any]:
539
+ """
540
+ Run orchestrator enforcement check (async wrapper).
541
+
542
+ Args:
543
+ tool_input: Hook input with tool name and parameters
544
+
545
+ Returns:
546
+ Orchestrator response: {"continue": bool, "hookSpecificOutput": {...}}
547
+ """
548
+ try:
549
+ loop = asyncio.get_event_loop()
550
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
551
+ tool_params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
552
+
553
+ # Run in thread pool since it's CPU-bound
554
+ return await loop.run_in_executor(
555
+ None,
556
+ enforce_orchestrator_mode,
557
+ tool_name,
558
+ tool_params,
559
+ )
560
+ except Exception:
561
+ # Graceful degradation - allow on error
562
+ return {"continue": True}
563
+
564
+
565
+ async def run_validation_check(tool_input: dict[str, Any]) -> dict[str, Any]:
566
+ """
567
+ Run work validation check (async wrapper).
568
+
569
+ Args:
570
+ tool_input: Hook input with tool name and parameters
571
+
572
+ Returns:
573
+ Validator response: {"decision": "allow"|"deny", "guidance": "...", ...}
574
+ """
575
+ try:
576
+ loop = asyncio.get_event_loop()
577
+
578
+ tool_name = tool_input.get("name", "") or tool_input.get("tool", "")
579
+ tool_params = tool_input.get("input", {}) or tool_input.get("params", {})
580
+ session_id = tool_input.get("session_id", "unknown")
581
+
582
+ # Load config and history in thread pool
583
+ config = await loop.run_in_executor(None, load_validation_config)
584
+ history = await loop.run_in_executor(
585
+ None, lambda: validator_load_history(session_id)
586
+ )
587
+
588
+ # Run validation
589
+ return await loop.run_in_executor(
590
+ None,
591
+ validate_tool_call,
592
+ tool_name,
593
+ tool_params,
594
+ config,
595
+ history,
596
+ )
597
+ except Exception:
598
+ # Graceful degradation - allow on error
599
+ return {"decision": "allow"}
600
+
601
+
602
+ async def run_task_enforcement(tool_input: dict[str, Any]) -> dict[str, Any]:
603
+ """
604
+ Run task save enforcement check (async wrapper).
605
+
606
+ Args:
607
+ tool_input: Hook input with tool name and parameters
608
+
609
+ Returns:
610
+ Task enforcer response: {"continue": bool, "hookSpecificOutput": {...}}
611
+ """
612
+ try:
613
+ loop = asyncio.get_event_loop()
614
+
615
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
616
+ tool_params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
617
+
618
+ # Run task enforcement
619
+ return await loop.run_in_executor(
620
+ None,
621
+ enforce_task_saving,
622
+ tool_name,
623
+ tool_params,
624
+ )
625
+ except Exception:
626
+ # Graceful degradation - allow on error
627
+ return {"continue": True}
628
+
629
+
630
+ async def provide_debugging_guidance(tool_input: dict[str, Any]) -> dict[str, Any]:
631
+ """
632
+ Provide debugging guidance based on tool patterns and context.
633
+
634
+ Args:
635
+ tool_input: Hook input with tool name and parameters
636
+
637
+ Returns:
638
+ Guidance response: {"hookSpecificOutput": {"additionalContext": "..."}}
639
+ """
640
+ try:
641
+ tool_name = tool_input.get("name", "") or tool_input.get("tool_name", "")
642
+ tool_params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
643
+
644
+ # High-risk tools that often indicate debugging scenarios
645
+ high_risk_tools = ["Edit", "Write", "Bash", "Read"]
646
+ if tool_name not in high_risk_tools:
647
+ return {}
648
+
649
+ guidance = []
650
+
651
+ # Check for debugging keywords in tool parameters
652
+ params_text = str(tool_params).lower()
653
+ debug_keywords = ["error", "fix", "broken", "failed", "bug", "issue", "problem"]
654
+
655
+ if any(kw in params_text for kw in debug_keywords):
656
+ guidance.append("🔍 Debugging task detected")
657
+ guidance.append("Consider:")
658
+ guidance.append(" - Review DEBUGGING.md for systematic approach")
659
+ guidance.append(" - Use researcher agent for unfamiliar errors")
660
+ guidance.append(" - Use debugger agent for systematic analysis")
661
+ guidance.append(" - Run /doctor or /hooks for diagnostics")
662
+
663
+ if guidance:
664
+ return {
665
+ "hookSpecificOutput": {
666
+ "hookEventName": "PreToolUse",
667
+ "additionalContext": "\n".join(guidance),
668
+ }
669
+ }
670
+
671
+ return {}
672
+ except Exception:
673
+ # Graceful degradation - no guidance on error
674
+ return {}
675
+
676
+
677
+ async def pretooluse_hook(tool_input: dict[str, Any]) -> dict[str, Any]:
678
+ """
679
+ Unified PreToolUse hook - runs all checks in parallel.
680
+
681
+ Args:
682
+ tool_input: Hook input with tool name and parameters
683
+
684
+ Returns:
685
+ Claude Code standard format:
686
+ {
687
+ "continue": bool,
688
+ "hookSpecificOutput": {
689
+ "hookEventName": "PreToolUse",
690
+ "updatedInput": {...}, # If task enforcer modified input
691
+ "additionalContext": "Combined guidance",
692
+ "tool_use_id": "..." # For PostToolUse correlation
693
+ }
694
+ }
695
+ """
696
+ # Run all five checks in parallel using asyncio.gather
697
+ (
698
+ event_tracing_response,
699
+ orch_response,
700
+ validate_response,
701
+ task_response,
702
+ debug_guidance,
703
+ ) = await asyncio.gather(
704
+ run_event_tracing(tool_input),
705
+ run_orchestrator_check(tool_input),
706
+ run_validation_check(tool_input),
707
+ run_task_enforcement(tool_input),
708
+ provide_debugging_guidance(tool_input),
709
+ )
710
+
711
+ # Integrate responses
712
+ orch_continues = orch_response.get("continue", True)
713
+ validate_allows = validate_response.get("decision", "allow") == "allow"
714
+ task_continues = task_response.get("continue", True)
715
+ should_continue = orch_continues and validate_allows and task_continues
716
+
717
+ # Collect guidance from all systems
718
+ guidance_parts = []
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
+
726
+ # Orchestrator guidance
727
+ if "hookSpecificOutput" in orch_response:
728
+ ctx = orch_response["hookSpecificOutput"].get("additionalContext", "")
729
+ if ctx:
730
+ guidance_parts.append(f"[Orchestrator] {ctx}")
731
+
732
+ # Validator guidance
733
+ if "guidance" in validate_response:
734
+ guidance_parts.append(f"[Validator] {validate_response['guidance']}")
735
+
736
+ if "imperative" in validate_response:
737
+ guidance_parts.append(f"[Validator] {validate_response['imperative']}")
738
+
739
+ if "suggestion" in validate_response:
740
+ guidance_parts.append(f"[Validator] {validate_response['suggestion']}")
741
+
742
+ # Task enforcer guidance
743
+ if "hookSpecificOutput" in task_response:
744
+ ctx = task_response["hookSpecificOutput"].get("additionalContext", "")
745
+ if ctx:
746
+ guidance_parts.append(f"[TaskEnforcer] {ctx}")
747
+
748
+ # Debugging guidance
749
+ if "hookSpecificOutput" in debug_guidance:
750
+ ctx = debug_guidance["hookSpecificOutput"].get("additionalContext", "")
751
+ if ctx:
752
+ guidance_parts.append(f"[Debugging] {ctx}")
753
+
754
+ # Build unified response in Claude Code format
755
+ response = {
756
+ "hookSpecificOutput": {
757
+ "hookEventName": "PreToolUse",
758
+ "permissionDecision": "allow" if should_continue else "deny",
759
+ }
760
+ }
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
+
768
+ # Check if task enforcer provided updatedInput
769
+ updated_input = None
770
+ if "hookSpecificOutput" in task_response:
771
+ updated_input = task_response["hookSpecificOutput"].get("updatedInput")
772
+
773
+ if updated_input:
774
+ response["hookSpecificOutput"]["updatedInput"] = updated_input
775
+
776
+ if guidance_parts:
777
+ combined_guidance = "\n".join(guidance_parts)
778
+ if should_continue:
779
+ # Allow with context
780
+ response["hookSpecificOutput"]["additionalContext"] = combined_guidance
781
+ else:
782
+ # Deny with reason
783
+ response["hookSpecificOutput"]["permissionDecisionReason"] = (
784
+ combined_guidance
785
+ )
786
+
787
+ return response
788
+
789
+
790
+ def main() -> None:
791
+ """Hook entry point for script wrapper."""
792
+ # Check environment overrides
793
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
794
+ print(json.dumps({"continue": True}))
795
+ sys.exit(0)
796
+
797
+ if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
798
+ print(json.dumps({"continue": True}))
799
+ sys.exit(0)
800
+
801
+ # Read tool input from stdin
802
+ try:
803
+ tool_input = json.load(sys.stdin)
804
+ except json.JSONDecodeError:
805
+ tool_input = {}
806
+
807
+ # Run hook with parallel execution
808
+ result = asyncio.run(pretooluse_hook(tool_input))
809
+
810
+ # Output response
811
+ print(json.dumps(result))
812
+
813
+ # Exit code based on permission decision
814
+ permission = result.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
815
+ sys.exit(0 if permission == "allow" else 1)
816
+
817
+
818
+ if __name__ == "__main__":
819
+ main()