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.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- 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 +192 -100
- 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 +190 -14
- htmlgraph/analytics_index.py +135 -51
- 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 +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- 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 +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- 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 +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- 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 +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- 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 +45 -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 +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- 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 +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- 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/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- 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/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- 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 +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -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)
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Unified PostToolUse Hook - Parallel Execution of Multiple Tasks
|
|
7
|
+
|
|
8
|
+
This module provides a unified PostToolUse hook that runs multiple tasks
|
|
9
|
+
in parallel using asyncio:
|
|
10
|
+
1. Event tracking - logs tool usage to session events
|
|
11
|
+
2. Orchestrator reflection - provides delegation suggestions
|
|
12
|
+
3. Task validation - validates task results
|
|
13
|
+
4. Error tracking - logs errors and auto-creates debug spikes
|
|
14
|
+
5. Debugging suggestions - suggests resources when errors detected
|
|
15
|
+
6. CIGS analysis - cost accounting and reinforcement for delegation
|
|
16
|
+
|
|
17
|
+
Architecture:
|
|
18
|
+
- All tasks run simultaneously via asyncio.gather()
|
|
19
|
+
- Error tracking logs to .htmlgraph/errors.jsonl
|
|
20
|
+
- Auto-creates debug spikes after 3+ similar errors
|
|
21
|
+
- Returns combined response with all feedback
|
|
22
|
+
|
|
23
|
+
Performance:
|
|
24
|
+
- ~40-50% faster than sequential execution
|
|
25
|
+
- Single Python process (no subprocess overhead)
|
|
26
|
+
- Parallel execution maximizes throughput
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from htmlgraph.cigs import CIGSPostToolAnalyzer
|
|
37
|
+
from htmlgraph.hooks.event_tracker import track_event
|
|
38
|
+
from htmlgraph.hooks.orchestrator_reflector import orchestrator_reflect
|
|
39
|
+
from htmlgraph.hooks.post_tool_use_failure import run as track_error
|
|
40
|
+
from htmlgraph.hooks.task_validator import validate_task_results
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def run_event_tracking(
|
|
44
|
+
hook_type: str, hook_input: dict[str, Any]
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Run event tracking (async wrapper).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
hook_type: "PostToolUse" or "Stop"
|
|
51
|
+
hook_input: Hook input with tool execution details
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Event tracking response: {"continue": True, "hookSpecificOutput": {...}}
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
loop = asyncio.get_event_loop()
|
|
58
|
+
|
|
59
|
+
# Run in thread pool since it involves I/O
|
|
60
|
+
return await loop.run_in_executor(
|
|
61
|
+
None,
|
|
62
|
+
track_event,
|
|
63
|
+
hook_type,
|
|
64
|
+
hook_input,
|
|
65
|
+
)
|
|
66
|
+
except Exception:
|
|
67
|
+
# Graceful degradation - allow on error
|
|
68
|
+
return {"continue": True}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def run_orchestrator_reflection(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Run orchestrator reflection (async wrapper).
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
hook_input: Hook input with tool execution details
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Reflection response: {"continue": True, "hookSpecificOutput": {...}}
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
loop = asyncio.get_event_loop()
|
|
83
|
+
|
|
84
|
+
# Run in thread pool
|
|
85
|
+
return await loop.run_in_executor(
|
|
86
|
+
None,
|
|
87
|
+
orchestrator_reflect,
|
|
88
|
+
hook_input,
|
|
89
|
+
)
|
|
90
|
+
except Exception:
|
|
91
|
+
# Graceful degradation - allow on error
|
|
92
|
+
return {"continue": True}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def run_task_validation(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
"""
|
|
97
|
+
Run task result validation (async wrapper).
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
hook_input: Hook input with tool execution details
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Validation response: {"continue": True, "hookSpecificOutput": {...}}
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
loop = asyncio.get_event_loop()
|
|
107
|
+
|
|
108
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
109
|
+
tool_response = hook_input.get("result", {}) or hook_input.get(
|
|
110
|
+
"tool_response", {}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Run task validation
|
|
114
|
+
return await loop.run_in_executor(
|
|
115
|
+
None,
|
|
116
|
+
validate_task_results,
|
|
117
|
+
tool_name,
|
|
118
|
+
tool_response,
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
# Graceful degradation - allow on error
|
|
122
|
+
return {"continue": True}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def run_error_tracking(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
Track errors to .htmlgraph/errors.jsonl and auto-create debug spikes.
|
|
128
|
+
|
|
129
|
+
Only tracks ACTUAL errors, not responses containing the word "error".
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
hook_input: Hook input with tool execution details
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Error tracking response: {"continue": True}
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
loop = asyncio.get_event_loop()
|
|
139
|
+
|
|
140
|
+
# Check if this is an ACTUAL error
|
|
141
|
+
has_error = False
|
|
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
|
|
149
|
+
|
|
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
|
|
154
|
+
|
|
155
|
+
# success=false flag
|
|
156
|
+
if tool_response.get("success") is False:
|
|
157
|
+
has_error = True
|
|
158
|
+
|
|
159
|
+
# Only track if there's an actual error
|
|
160
|
+
if has_error:
|
|
161
|
+
return await loop.run_in_executor(
|
|
162
|
+
None,
|
|
163
|
+
track_error,
|
|
164
|
+
hook_input,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return {"continue": True}
|
|
168
|
+
except Exception:
|
|
169
|
+
# Graceful degradation - allow on error
|
|
170
|
+
return {"continue": True}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def suggest_debugging_resources(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Suggest debugging resources based on tool results.
|
|
176
|
+
|
|
177
|
+
Only triggers on ACTUAL errors, not on responses that happen to contain
|
|
178
|
+
the word "error" in their content.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
hook_input: Hook input with tool execution details
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Suggestion response: {"hookSpecificOutput": {"additionalContext": "..."}}
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
188
|
+
tool_response = hook_input.get("result", {}) or hook_input.get(
|
|
189
|
+
"tool_response", {}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
suggestions = []
|
|
193
|
+
|
|
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
|
|
206
|
+
|
|
207
|
+
# success=false flag
|
|
208
|
+
if tool_response.get("success") is False:
|
|
209
|
+
has_actual_error = True
|
|
210
|
+
|
|
211
|
+
if has_actual_error:
|
|
212
|
+
suggestions.append("⚠️ Error detected in tool response")
|
|
213
|
+
suggestions.append("Debugging resources:")
|
|
214
|
+
suggestions.append(" 📚 DEBUGGING.md - Systematic debugging guide")
|
|
215
|
+
suggestions.append(" 🔬 Researcher agent - Research error patterns")
|
|
216
|
+
suggestions.append(" 🐛 Debugger agent - Root cause analysis")
|
|
217
|
+
suggestions.append(" Built-in: /doctor, /hooks, claude --debug")
|
|
218
|
+
|
|
219
|
+
# Check for Task tool without save evidence
|
|
220
|
+
if tool_name == "Task":
|
|
221
|
+
result_text = str(tool_response).lower()
|
|
222
|
+
save_indicators = [".save()", "spike", "htmlgraph", ".create("]
|
|
223
|
+
if not any(ind in result_text for ind in save_indicators):
|
|
224
|
+
suggestions.append("💡 Task completed - remember to document findings")
|
|
225
|
+
suggestions.append(
|
|
226
|
+
" See DEBUGGING.md for research documentation patterns"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if suggestions:
|
|
230
|
+
return {
|
|
231
|
+
"hookSpecificOutput": {
|
|
232
|
+
"hookEventName": "PostToolUse",
|
|
233
|
+
"additionalContext": "\n".join(suggestions),
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {}
|
|
238
|
+
except Exception:
|
|
239
|
+
# Graceful degradation - no suggestions on error
|
|
240
|
+
return {}
|
|
241
|
+
|
|
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
|
+
|
|
280
|
+
async def posttooluse_hook(
|
|
281
|
+
hook_type: str, hook_input: dict[str, Any]
|
|
282
|
+
) -> dict[str, Any]:
|
|
283
|
+
"""
|
|
284
|
+
Unified PostToolUse hook - runs tracking, reflection, validation, error tracking, debugging suggestions, and CIGS analysis in parallel.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
hook_type: "PostToolUse" or "Stop"
|
|
288
|
+
hook_input: Hook input with tool execution details
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Claude Code standard format:
|
|
292
|
+
{
|
|
293
|
+
"continue": True,
|
|
294
|
+
"hookSpecificOutput": {
|
|
295
|
+
"hookEventName": "PostToolUse",
|
|
296
|
+
"additionalContext": "Combined feedback",
|
|
297
|
+
"systemMessage": "Warnings/alerts"
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
"""
|
|
301
|
+
# Run all six in parallel using asyncio.gather
|
|
302
|
+
(
|
|
303
|
+
event_response,
|
|
304
|
+
reflection_response,
|
|
305
|
+
validation_response,
|
|
306
|
+
error_tracking_response,
|
|
307
|
+
debug_suggestions,
|
|
308
|
+
cigs_response,
|
|
309
|
+
) = await asyncio.gather(
|
|
310
|
+
run_event_tracking(hook_type, hook_input),
|
|
311
|
+
run_orchestrator_reflection(hook_input),
|
|
312
|
+
run_task_validation(hook_input),
|
|
313
|
+
run_error_tracking(hook_input),
|
|
314
|
+
suggest_debugging_resources(hook_input),
|
|
315
|
+
run_cigs_analysis(hook_input),
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Combine responses (all should return continue=True)
|
|
319
|
+
# Event tracking is async and shouldn't block
|
|
320
|
+
# Reflection provides optional guidance
|
|
321
|
+
# Validation provides warnings but doesn't block
|
|
322
|
+
|
|
323
|
+
# Collect all guidance and messages
|
|
324
|
+
guidance_parts = []
|
|
325
|
+
system_messages = []
|
|
326
|
+
|
|
327
|
+
# Event tracking guidance (e.g., drift warnings)
|
|
328
|
+
if "hookSpecificOutput" in event_response:
|
|
329
|
+
ctx = event_response["hookSpecificOutput"].get("additionalContext", "")
|
|
330
|
+
if ctx:
|
|
331
|
+
guidance_parts.append(ctx)
|
|
332
|
+
|
|
333
|
+
# Orchestrator reflection
|
|
334
|
+
if "hookSpecificOutput" in reflection_response:
|
|
335
|
+
ctx = reflection_response["hookSpecificOutput"].get("additionalContext", "")
|
|
336
|
+
if ctx:
|
|
337
|
+
guidance_parts.append(ctx)
|
|
338
|
+
|
|
339
|
+
# Task validation feedback
|
|
340
|
+
if "hookSpecificOutput" in validation_response:
|
|
341
|
+
ctx = validation_response["hookSpecificOutput"].get("additionalContext", "")
|
|
342
|
+
if ctx:
|
|
343
|
+
guidance_parts.append(ctx)
|
|
344
|
+
|
|
345
|
+
# Task validation may provide systemMessage for warnings
|
|
346
|
+
sys_msg = validation_response["hookSpecificOutput"].get("systemMessage", "")
|
|
347
|
+
if sys_msg:
|
|
348
|
+
system_messages.append(sys_msg)
|
|
349
|
+
|
|
350
|
+
# Debugging suggestions
|
|
351
|
+
if "hookSpecificOutput" in debug_suggestions:
|
|
352
|
+
ctx = debug_suggestions["hookSpecificOutput"].get("additionalContext", "")
|
|
353
|
+
if ctx:
|
|
354
|
+
guidance_parts.append(ctx)
|
|
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
|
+
|
|
362
|
+
# Build unified response
|
|
363
|
+
response: dict[str, Any] = {"continue": True} # PostToolUse never blocks
|
|
364
|
+
|
|
365
|
+
if guidance_parts or system_messages:
|
|
366
|
+
response["hookSpecificOutput"] = {
|
|
367
|
+
"hookEventName": "PostToolUse",
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if guidance_parts:
|
|
371
|
+
response["hookSpecificOutput"]["additionalContext"] = "\n".join(
|
|
372
|
+
guidance_parts
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if system_messages:
|
|
376
|
+
response["hookSpecificOutput"]["systemMessage"] = "\n\n".join(
|
|
377
|
+
system_messages
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return response
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def main() -> None:
|
|
384
|
+
"""Hook entry point for script wrapper."""
|
|
385
|
+
# Check environment override
|
|
386
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
387
|
+
print(json.dumps({"continue": True}))
|
|
388
|
+
sys.exit(0)
|
|
389
|
+
|
|
390
|
+
# Determine hook type from environment
|
|
391
|
+
hook_type = os.environ.get("HTMLGRAPH_HOOK_TYPE", "PostToolUse")
|
|
392
|
+
|
|
393
|
+
# Read tool input from stdin
|
|
394
|
+
try:
|
|
395
|
+
hook_input = json.load(sys.stdin)
|
|
396
|
+
except json.JSONDecodeError:
|
|
397
|
+
hook_input = {}
|
|
398
|
+
|
|
399
|
+
# Run hook with parallel execution
|
|
400
|
+
result = asyncio.run(posttooluse_hook(hook_type, hook_input))
|
|
401
|
+
|
|
402
|
+
# Output response
|
|
403
|
+
print(json.dumps(result))
|
|
404
|
+
sys.exit(0)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
if __name__ == "__main__":
|
|
408
|
+
main()
|