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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {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
|
-
|
|
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
|
|
48
|
-
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
|
-
|
|
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
|
-
|
|
245
|
+
logger.warning(f"Created debug spike: {spike.id}")
|
|
221
246
|
|
|
222
247
|
except Exception as e:
|
|
223
|
-
|
|
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)
|
htmlgraph/hooks/posttooluse.py
CHANGED
|
@@ -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
|
|
140
|
+
# Check if this is an ACTUAL error
|
|
132
141
|
has_error = False
|
|
133
|
-
tool_response = hook_input.get("
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|