htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
@@ -21,7 +21,7 @@ Enforcement Levels:
21
21
  - guidance: ALLOWS but provides warnings and suggestions
22
22
 
23
23
  Public API:
24
- - enforce_orchestrator_mode(tool: str, params: dict) -> dict
24
+ - enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
25
25
  Main entry point for hook scripts. Returns hook response dict.
26
26
  """
27
27
 
@@ -93,7 +93,9 @@ def add_to_tool_history(tool: str) -> None:
93
93
  save_tool_history(history)
94
94
 
95
95
 
96
- def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, str, str]:
96
+ def is_allowed_orchestrator_operation(
97
+ tool: str, params: dict[str, Any]
98
+ ) -> tuple[bool, str, str]:
97
99
  """
98
100
  Check if operation is allowed for orchestrators.
99
101
 
@@ -268,7 +270,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
268
270
  return True, "Allowed in guidance mode", "guidance-allowed"
269
271
 
270
272
 
271
- def create_task_suggestion(tool: str, params: dict) -> str:
273
+ def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
272
274
  """
273
275
  Create Task tool suggestion based on blocked operation.
274
276
 
@@ -370,7 +372,7 @@ def create_task_suggestion(tool: str, params: dict) -> str:
370
372
  )
371
373
 
372
374
 
373
- def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
375
+ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, Any]:
374
376
  """
375
377
  Enforce orchestrator mode rules.
376
378
 
@@ -22,7 +22,7 @@ Usage:
22
22
  """
23
23
 
24
24
  import re
25
- from typing import TypedDict
25
+ from typing import Any, TypedDict
26
26
 
27
27
 
28
28
  class HookSpecificOutput(TypedDict):
@@ -93,7 +93,7 @@ def is_python_execution(command: str) -> bool:
93
93
  return False
94
94
 
95
95
 
96
- def should_reflect(hook_input: dict) -> tuple[bool, str]:
96
+ def should_reflect(hook_input: dict[str, Any]) -> tuple[bool, str]:
97
97
  """
98
98
  Check if we should show reflection prompt.
99
99
 
@@ -156,7 +156,7 @@ Ask yourself:
156
156
  Continue, but consider delegation for similar future tasks."""
157
157
 
158
158
 
159
- def orchestrator_reflect(tool_input: dict) -> dict:
159
+ def orchestrator_reflect(tool_input: dict[str, Any]) -> dict[str, Any]:
160
160
  """
161
161
  Main API function for orchestrator reflection.
162
162
 
@@ -184,7 +184,7 @@ def orchestrator_reflect(tool_input: dict) -> dict:
184
184
  should_show, command_preview = should_reflect(tool_input)
185
185
 
186
186
  # Build response
187
- response: dict = {"continue": True}
187
+ response: dict[str, Any] = {"continue": True}
188
188
 
189
189
  if should_show:
190
190
  reflection = build_reflection_message(command_preview)
@@ -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)