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,4 +1,8 @@
1
1
  #!/usr/bin/env python3
2
+ import logging
3
+
4
+ logger = logging.getLogger(__name__)
5
+
2
6
  """
3
7
  PostToolUseFailure Hook - Automatic Error Tracking and Debug Spike Creation
4
8
 
@@ -37,27 +41,48 @@ def run(hook_input: dict[str, Any]) -> dict[str, Any]:
37
41
  Standard hook response: {"continue": True}
38
42
  """
39
43
  try:
44
+ # DEBUG: Log raw hook input to understand structure
45
+ debug_log = Path(".htmlgraph/hook-debug.jsonl")
46
+ debug_log.parent.mkdir(parents=True, exist_ok=True)
47
+ with open(debug_log, "a") as f:
48
+ f.write(
49
+ json.dumps(
50
+ {
51
+ "raw_input": hook_input,
52
+ "keys": list(hook_input.keys()),
53
+ "ts": datetime.now().isoformat(),
54
+ }
55
+ )
56
+ + "\n"
57
+ )
58
+
40
59
  # Extract error information from PostToolUse hook format
41
- tool_name = hook_input.get("name", "unknown")
60
+ # Official PostToolUse uses: tool_name, tool_response
61
+ # Custom hooks may use: name, result
62
+ tool_name = hook_input.get("tool_name") or hook_input.get("name", "unknown")
42
63
  session_id = hook_input.get("session_id", "unknown")
43
64
 
44
65
  # Error message can be in different places depending on tool
45
66
  error_msg = "No error message"
46
67
 
47
- # Check result field first (Bash, Read, etc.)
48
- result = hook_input.get("result", {})
68
+ # Check tool_response field first (official PostToolUse format)
69
+ # Then check result field (custom hook format)
70
+ result = hook_input.get("tool_response") or hook_input.get("result", {})
49
71
  if isinstance(result, dict):
50
72
  if "error" in result:
51
73
  error_msg = result["error"]
52
74
  elif "message" in result:
53
75
  error_msg = result["message"]
76
+ elif isinstance(result, str):
77
+ # Sometimes the error is directly in the result as a string
78
+ error_msg = result
54
79
 
55
80
  # Fallback: check top-level error field
56
81
  if error_msg == "No error message" and "error" in hook_input:
57
82
  error_msg = hook_input["error"]
58
83
 
59
84
  # Last resort: stringify the result if it contains error indicators
60
- if error_msg == "No error message":
85
+ if error_msg == "No error message" and result:
61
86
  result_str = str(result).lower()
62
87
  if any(
63
88
  indicator in result_str
@@ -88,7 +113,7 @@ def run(hook_input: dict[str, Any]) -> dict[str, Any]:
88
113
 
89
114
  except Exception as e:
90
115
  # Never raise - log and continue
91
- print(f"PostToolUseFailure hook error: {e}", file=sys.stderr)
116
+ logger.warning(f"PostToolUseFailure hook error: {e}")
92
117
  return {"continue": True}
93
118
 
94
119
 
@@ -217,10 +242,10 @@ def create_debug_spike(tool: str, error: str, log_path: Path) -> None:
217
242
  with open(spike_marker, "w") as f:
218
243
  json.dump(existing_spikes, f, indent=2)
219
244
 
220
- print(f"Created debug spike: {spike.id}", file=sys.stderr)
245
+ logger.warning(f"Created debug spike: {spike.id}")
221
246
 
222
247
  except Exception as e:
223
- print(f"Failed to create debug spike: {e}", file=sys.stderr)
248
+ logger.warning(f"Failed to create debug spike: {e}")
224
249
 
225
250
 
226
251
  def main() -> None:
@@ -0,0 +1,257 @@
1
+ """
2
+ PostToolUse Enhancement - Duration Calculation and Tool Trace Updates
3
+
4
+ This module handles the PostToolUse hook event and updates tool traces with:
5
+ 1. Execution end time (when the tool completed)
6
+ 2. Duration in milliseconds (end_time - start_time)
7
+ 3. Tool output (result of the tool execution)
8
+ 4. Status (Ok or Error)
9
+ 5. Error message (if status is Error)
10
+
11
+ The module correlates with PreToolUse via tool_use_id environment variable
12
+ and gracefully handles missing pre-events (logs warning, continues).
13
+
14
+ Design:
15
+ - Query tool_traces for matching tool_use_id
16
+ - Get start_time from pre-event
17
+ - Calculate duration_ms (end_time - start_time)
18
+ - Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
19
+ - Handle missing pre-event gracefully (log warning, continue)
20
+ - Non-blocking - errors don't prevent tool execution continuation
21
+ """
22
+
23
+ import json
24
+ import logging
25
+ import os
26
+ from datetime import datetime, timezone
27
+ from typing import Any
28
+
29
+ from htmlgraph.db.schema import HtmlGraphDB
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def calculate_duration(start_time_iso: str, end_time_iso: str) -> int:
35
+ """
36
+ Calculate duration in milliseconds between two ISO8601 UTC timestamps.
37
+
38
+ Args:
39
+ start_time_iso: ISO8601 UTC timestamp from PreToolUse (e.g., "2025-01-07T12:34:56.789000+00:00")
40
+ end_time_iso: ISO8601 UTC timestamp (now, e.g., "2025-01-07T12:34:57.123000+00:00")
41
+
42
+ Returns:
43
+ duration_ms: Integer milliseconds between timestamps (accurate within 1ms)
44
+
45
+ Raises:
46
+ ValueError: If timestamps cannot be parsed
47
+ TypeError: If inputs are not strings
48
+ """
49
+ try:
50
+ # Parse ISO8601 timestamps (handles timezone-aware datetimes)
51
+ start_dt = datetime.fromisoformat(start_time_iso.replace("Z", "+00:00"))
52
+ end_dt = datetime.fromisoformat(end_time_iso.replace("Z", "+00:00"))
53
+
54
+ # Calculate difference and convert to milliseconds
55
+ delta = end_dt - start_dt
56
+ duration_ms = int(delta.total_seconds() * 1000)
57
+
58
+ return duration_ms
59
+ except (ValueError, AttributeError, TypeError) as e:
60
+ logger.warning(f"Error calculating duration: {e}")
61
+ raise
62
+
63
+
64
+ def update_tool_trace(
65
+ tool_use_id: str,
66
+ tool_output: dict[str, Any] | None,
67
+ status: str,
68
+ error_message: str | None = None,
69
+ ) -> bool:
70
+ """
71
+ Update tool_traces table with execution end event.
72
+
73
+ Updates an existing tool trace (created by PreToolUse) with:
74
+ - end_time: Current UTC timestamp
75
+ - duration_ms: Milliseconds between start and end
76
+ - tool_output: Result of tool execution (JSON)
77
+ - status: 'Ok' or 'Error'
78
+ - error_message: Error details if status='Error'
79
+
80
+ Args:
81
+ tool_use_id: Correlation ID from PreToolUse event (from environment)
82
+ tool_output: Tool execution result (dict, will be JSON serialized)
83
+ status: 'Ok' or 'Error'
84
+ error_message: Error details if status='Error'
85
+
86
+ Returns:
87
+ True if update successful, False otherwise
88
+
89
+ Workflow:
90
+ 1. Query tool_traces for matching tool_use_id
91
+ 2. Get start_time from pre-event
92
+ 3. Calculate duration_ms (end_time - start_time)
93
+ 4. Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
94
+ 5. Handle missing pre-event gracefully (log warning, continue)
95
+ """
96
+ try:
97
+ # Connect to database
98
+ db = HtmlGraphDB()
99
+
100
+ if not db.connection:
101
+ db.connect()
102
+
103
+ cursor = db.connection.cursor() # type: ignore[union-attr]
104
+
105
+ # Query tool_traces for matching tool_use_id
106
+ cursor.execute(
107
+ """
108
+ SELECT tool_use_id, start_time FROM tool_traces
109
+ WHERE tool_use_id = ?
110
+ """,
111
+ (tool_use_id,),
112
+ )
113
+
114
+ row = cursor.fetchone()
115
+
116
+ if not row:
117
+ # Missing pre-event - log warning but continue (graceful degradation)
118
+ logger.warning(
119
+ f"Could not find start event for tool_use_id={tool_use_id}. "
120
+ f"PreToolUse event may not have completed. Skipping duration update."
121
+ )
122
+ db.disconnect()
123
+ return False
124
+
125
+ # Get start_time from pre-event
126
+ start_time_iso = row[1]
127
+
128
+ # Calculate end_time (now in UTC)
129
+ end_time_iso = datetime.now(timezone.utc).isoformat()
130
+
131
+ # Calculate duration_ms
132
+ try:
133
+ duration_ms = calculate_duration(start_time_iso, end_time_iso)
134
+ except (ValueError, TypeError) as e:
135
+ logger.warning(
136
+ f"Could not calculate duration for tool_use_id={tool_use_id}: {e}. "
137
+ f"Using None for duration."
138
+ )
139
+ duration_ms = None
140
+
141
+ # Validate status
142
+ valid_statuses = {"Ok", "Error", "completed", "failed", "timeout"}
143
+ if status not in valid_statuses:
144
+ logger.warning(
145
+ f"Invalid status '{status}' for tool_use_id={tool_use_id}. "
146
+ f"Using 'Ok' as default."
147
+ )
148
+ status = "Ok"
149
+
150
+ # JSON serialize tool_output
151
+ tool_output_json = None
152
+ if tool_output:
153
+ try:
154
+ tool_output_json = json.dumps(tool_output)
155
+ except (TypeError, ValueError) as e:
156
+ logger.warning(
157
+ f"Could not JSON serialize tool_output for "
158
+ f"tool_use_id={tool_use_id}: {e}"
159
+ )
160
+ tool_output_json = json.dumps(
161
+ {"error": str(e), "output": str(tool_output)}
162
+ )
163
+
164
+ # Update tool_traces with: end_time, duration_ms, tool_output, status, error_message
165
+ cursor.execute(
166
+ """
167
+ UPDATE tool_traces
168
+ SET end_time = ?, duration_ms = ?, tool_output = ?,
169
+ status = ?, error_message = ?
170
+ WHERE tool_use_id = ?
171
+ """,
172
+ (
173
+ end_time_iso,
174
+ duration_ms,
175
+ tool_output_json,
176
+ status,
177
+ error_message,
178
+ tool_use_id,
179
+ ),
180
+ )
181
+
182
+ if not db.connection:
183
+ db.connect()
184
+
185
+ db.connection.commit() # type: ignore[union-attr]
186
+
187
+ logger.debug(
188
+ f"Updated tool trace: tool_use_id={tool_use_id}, "
189
+ f"duration_ms={duration_ms}, status={status}"
190
+ )
191
+
192
+ db.disconnect()
193
+ return True
194
+
195
+ except Exception as e:
196
+ # Log error but don't block
197
+ logger.error(f"Error updating tool trace for tool_use_id={tool_use_id}: {e}")
198
+ return False
199
+
200
+
201
+ def get_tool_use_id_from_context() -> str | None:
202
+ """
203
+ Get tool_use_id from environment (set by PreToolUse hook).
204
+
205
+ Returns:
206
+ tool_use_id string or None if not set
207
+ """
208
+ return os.environ.get("HTMLGRAPH_TOOL_USE_ID")
209
+
210
+
211
+ def determine_status_from_response(
212
+ tool_response: dict[str, Any] | None,
213
+ ) -> tuple[str, str | None]:
214
+ """
215
+ Determine status (Ok/Error) and error message from tool response.
216
+
217
+ Analyzes tool response to determine if execution was successful.
218
+ Returns (status, error_message) tuple.
219
+
220
+ Args:
221
+ tool_response: Tool execution response (dict)
222
+
223
+ Returns:
224
+ (status, error_message) where:
225
+ - status: 'Ok' or 'Error'
226
+ - error_message: Error details if Error, else None
227
+ """
228
+ if not tool_response:
229
+ return ("Ok", None)
230
+
231
+ if not isinstance(tool_response, dict):
232
+ return ("Ok", None)
233
+
234
+ # Check for explicit error indicators
235
+ # Bash tool: non-empty stderr
236
+ stderr = tool_response.get("stderr", "")
237
+ if stderr and isinstance(stderr, str) and stderr.strip():
238
+ return ("Error", f"stderr: {stderr[:500]}")
239
+
240
+ # Explicit error field
241
+ error_field = tool_response.get("error")
242
+ if error_field and str(error_field).strip():
243
+ return ("Error", str(error_field)[:500])
244
+
245
+ # success=false flag
246
+ if tool_response.get("success") is False:
247
+ reason = tool_response.get("reason", "Unknown error")
248
+ return ("Error", str(reason)[:500])
249
+
250
+ # status field indicating failure
251
+ status_field = tool_response.get("status")
252
+ if status_field and status_field.lower() in {"error", "failed", "failed"}:
253
+ reason = tool_response.get("message", "Unknown error")
254
+ return ("Error", str(reason)[:500])
255
+
256
+ # Default to success
257
+ return ("Ok", None)
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Unified PostToolUse Hook - Parallel Execution of Multiple Tasks
3
7
 
@@ -8,6 +12,7 @@ in parallel using asyncio:
8
12
  3. Task validation - validates task results
9
13
  4. Error tracking - logs errors and auto-creates debug spikes
10
14
  5. Debugging suggestions - suggests resources when errors detected
15
+ 6. CIGS analysis - cost accounting and reinforcement for delegation
11
16
 
12
17
  Architecture:
13
18
  - All tasks run simultaneously via asyncio.gather()
@@ -25,8 +30,10 @@ import asyncio
25
30
  import json
26
31
  import os
27
32
  import sys
33
+ from pathlib import Path
28
34
  from typing import Any
29
35
 
36
+ from htmlgraph.cigs import CIGSPostToolAnalyzer
30
37
  from htmlgraph.hooks.event_tracker import track_event
31
38
  from htmlgraph.hooks.orchestrator_reflector import orchestrator_reflect
32
39
  from htmlgraph.hooks.post_tool_use_failure import run as track_error
@@ -119,6 +126,8 @@ async def run_error_tracking(hook_input: dict[str, Any]) -> dict[str, Any]:
119
126
  """
120
127
  Track errors to .htmlgraph/errors.jsonl and auto-create debug spikes.
121
128
 
129
+ Only tracks ACTUAL errors, not responses containing the word "error".
130
+
122
131
  Args:
123
132
  hook_input: Hook input with tool execution details
124
133
 
@@ -128,23 +137,26 @@ async def run_error_tracking(hook_input: dict[str, Any]) -> dict[str, Any]:
128
137
  try:
129
138
  loop = asyncio.get_event_loop()
130
139
 
131
- # Check if this is an error (check for error field or non-zero exit code)
140
+ # Check if this is an ACTUAL error
132
141
  has_error = False
133
- tool_response = hook_input.get("result", {}) or hook_input.get(
134
- "tool_response", {}
135
- )
142
+ tool_response = hook_input.get("tool_response") or hook_input.get("result", {})
143
+
144
+ if isinstance(tool_response, dict):
145
+ # Bash: non-empty stderr indicates error
146
+ stderr = tool_response.get("stderr", "")
147
+ if stderr and isinstance(stderr, str) and stderr.strip():
148
+ has_error = True
136
149
 
137
- # Check for explicit error field
138
- if "error" in tool_response or hook_input.get("error"):
139
- has_error = True
150
+ # Explicit error field with content
151
+ error_field = tool_response.get("error")
152
+ if error_field and str(error_field).strip():
153
+ has_error = True
140
154
 
141
- # Check for error indicators in response text
142
- response_text = str(tool_response).lower()
143
- error_indicators = ["error", "failed", "exception", "traceback", "errno"]
144
- if any(indicator in response_text for indicator in error_indicators):
145
- has_error = True
155
+ # success=false flag
156
+ if tool_response.get("success") is False:
157
+ has_error = True
146
158
 
147
- # Only track if there's an error
159
+ # Only track if there's an actual error
148
160
  if has_error:
149
161
  return await loop.run_in_executor(
150
162
  None,
@@ -162,6 +174,9 @@ async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, A
162
174
  """
163
175
  Suggest debugging resources based on tool results.
164
176
 
177
+ Only triggers on ACTUAL errors, not on responses that happen to contain
178
+ the word "error" in their content.
179
+
165
180
  Args:
166
181
  hook_input: Hook input with tool execution details
167
182
 
@@ -176,11 +191,24 @@ async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, A
176
191
 
177
192
  suggestions = []
178
193
 
179
- # Check for error indicators in response
180
- response_text = str(tool_response).lower()
181
- error_indicators = ["error", "failed", "exception", "traceback", "errno"]
194
+ # Check for ACTUAL errors (not just text containing "error")
195
+ has_actual_error = False
196
+
197
+ if isinstance(tool_response, dict):
198
+ # Bash: non-empty stderr indicates error
199
+ stderr = tool_response.get("stderr", "")
200
+ if stderr and isinstance(stderr, str) and stderr.strip():
201
+ has_actual_error = True
202
+
203
+ # Explicit error field
204
+ if tool_response.get("error"):
205
+ has_actual_error = True
182
206
 
183
- if any(indicator in response_text for indicator in error_indicators):
207
+ # success=false flag
208
+ if tool_response.get("success") is False:
209
+ has_actual_error = True
210
+
211
+ if has_actual_error:
184
212
  suggestions.append("⚠️ Error detected in tool response")
185
213
  suggestions.append("Debugging resources:")
186
214
  suggestions.append(" 📚 DEBUGGING.md - Systematic debugging guide")
@@ -212,11 +240,48 @@ async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, A
212
240
  return {}
213
241
 
214
242
 
243
+ async def run_cigs_analysis(hook_input: dict[str, Any]) -> dict[str, Any]:
244
+ """
245
+ Run CIGS cost accounting and reinforcement analysis.
246
+
247
+ Args:
248
+ hook_input: Hook input with tool execution details
249
+
250
+ Returns:
251
+ CIGS analysis response: {"hookSpecificOutput": {...}}
252
+ """
253
+ try:
254
+ loop = asyncio.get_event_loop()
255
+
256
+ # Extract tool info
257
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
258
+ tool_params = hook_input.get("input", {}) or hook_input.get("tool_input", {})
259
+ tool_response = hook_input.get("result", {}) or hook_input.get(
260
+ "tool_response", {}
261
+ )
262
+
263
+ # Initialize CIGS analyzer
264
+ graph_dir = Path.cwd() / ".htmlgraph"
265
+ analyzer = CIGSPostToolAnalyzer(graph_dir)
266
+
267
+ # Run analysis in executor (may involve I/O)
268
+ return await loop.run_in_executor(
269
+ None,
270
+ analyzer.analyze,
271
+ tool_name,
272
+ tool_params,
273
+ tool_response,
274
+ )
275
+ except Exception:
276
+ # Graceful degradation - allow on error
277
+ return {}
278
+
279
+
215
280
  async def posttooluse_hook(
216
281
  hook_type: str, hook_input: dict[str, Any]
217
282
  ) -> dict[str, Any]:
218
283
  """
219
- Unified PostToolUse hook - runs tracking, reflection, validation, error tracking, and debugging suggestions in parallel.
284
+ Unified PostToolUse hook - runs tracking, reflection, validation, error tracking, debugging suggestions, and CIGS analysis in parallel.
220
285
 
221
286
  Args:
222
287
  hook_type: "PostToolUse" or "Stop"
@@ -233,19 +298,21 @@ async def posttooluse_hook(
233
298
  }
234
299
  }
235
300
  """
236
- # Run all five in parallel using asyncio.gather
301
+ # Run all six in parallel using asyncio.gather
237
302
  (
238
303
  event_response,
239
304
  reflection_response,
240
305
  validation_response,
241
306
  error_tracking_response,
242
307
  debug_suggestions,
308
+ cigs_response,
243
309
  ) = await asyncio.gather(
244
310
  run_event_tracking(hook_type, hook_input),
245
311
  run_orchestrator_reflection(hook_input),
246
312
  run_task_validation(hook_input),
247
313
  run_error_tracking(hook_input),
248
314
  suggest_debugging_resources(hook_input),
315
+ run_cigs_analysis(hook_input),
249
316
  )
250
317
 
251
318
  # Combine responses (all should return continue=True)
@@ -286,6 +353,12 @@ async def posttooluse_hook(
286
353
  if ctx:
287
354
  guidance_parts.append(ctx)
288
355
 
356
+ # CIGS analysis (cost accounting and reinforcement)
357
+ if "hookSpecificOutput" in cigs_response:
358
+ ctx = cigs_response["hookSpecificOutput"].get("additionalContext", "")
359
+ if ctx:
360
+ guidance_parts.append(ctx)
361
+
289
362
  # Build unified response
290
363
  response: dict[str, Any] = {"continue": True} # PostToolUse never blocks
291
364