htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__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/__init__.py +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/main.py +114 -51
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +28 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +33 -8
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +76 -1
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +43 -4
- 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 +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +11 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +81 -66
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +92 -14
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +5 -2
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +18 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +25 -21
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +2 -1
- 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/server.py +21 -17
- htmlgraph/session_manager.py +1 -7
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +10 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +14 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
- htmlgraph/sdk.py +0 -3430
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
htmlgraph/hooks/drift_handler.py
CHANGED
|
@@ -133,7 +133,7 @@ def load_drift_config(graph_dir: Path) -> dict[str, Any]:
|
|
|
133
133
|
Example:
|
|
134
134
|
```python
|
|
135
135
|
config = load_drift_config(Path(".htmlgraph"))
|
|
136
|
-
|
|
136
|
+
logger.info(f"Auto-classify threshold: {config['drift_detection']['auto_classify_threshold']}")
|
|
137
137
|
```
|
|
138
138
|
"""
|
|
139
139
|
graph_dir = Path(graph_dir)
|
|
@@ -196,7 +196,7 @@ def detect_drift(
|
|
|
196
196
|
```python
|
|
197
197
|
score, feature_id = detect_drift(activity_result, config)
|
|
198
198
|
if score > config['drift_detection']['auto_classify_threshold']:
|
|
199
|
-
|
|
199
|
+
logger.info(f"HIGH DRIFT: {score:.2f}")
|
|
200
200
|
```
|
|
201
201
|
"""
|
|
202
202
|
drift_score = getattr(activity_result, "drift_score", 0.0) or 0.0
|
|
@@ -242,7 +242,7 @@ def handle_high_drift(
|
|
|
242
242
|
```python
|
|
243
243
|
nudge = handle_high_drift(context, 0.87, queue, config)
|
|
244
244
|
if nudge:
|
|
245
|
-
|
|
245
|
+
logger.info("%s", nudge) # "HIGH DRIFT (0.87): Activity queued for classification..."
|
|
246
246
|
```
|
|
247
247
|
"""
|
|
248
248
|
drift_config = config.get("drift_detection", {})
|
htmlgraph/hooks/event_tracker.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
HtmlGraph Event Tracker Module
|
|
3
7
|
|
|
@@ -22,7 +26,6 @@ import json
|
|
|
22
26
|
import os
|
|
23
27
|
import re
|
|
24
28
|
import subprocess
|
|
25
|
-
import sys
|
|
26
29
|
from datetime import datetime, timedelta, timezone
|
|
27
30
|
from pathlib import Path
|
|
28
31
|
from typing import Any, cast # noqa: F401
|
|
@@ -149,10 +152,7 @@ def get_parent_user_query(db: HtmlGraphDB, session_id: str) -> str | None:
|
|
|
149
152
|
return str(row[0])
|
|
150
153
|
return None
|
|
151
154
|
except Exception as e:
|
|
152
|
-
|
|
153
|
-
f"Debug: Database query for UserQuery failed: {e}",
|
|
154
|
-
file=sys.stderr,
|
|
155
|
-
)
|
|
155
|
+
logger.warning(f"Debug: Database query for UserQuery failed: {e}")
|
|
156
156
|
return None
|
|
157
157
|
|
|
158
158
|
|
|
@@ -194,9 +194,8 @@ def load_drift_queue(graph_dir: Path, max_age_hours: int = 48) -> dict[str, Any]
|
|
|
194
194
|
queue["activities"] = fresh_activities
|
|
195
195
|
save_drift_queue(graph_dir, queue)
|
|
196
196
|
removed = original_count - len(fresh_activities)
|
|
197
|
-
|
|
198
|
-
f"Cleaned {removed} stale drift queue entries (older than {max_age_hours}h)"
|
|
199
|
-
file=sys.stderr,
|
|
197
|
+
logger.warning(
|
|
198
|
+
f"Cleaned {removed} stale drift queue entries (older than {max_age_hours}h)"
|
|
200
199
|
)
|
|
201
200
|
|
|
202
201
|
return cast(dict[Any, Any], queue)
|
|
@@ -212,7 +211,7 @@ def save_drift_queue(graph_dir: Path, queue: dict[str, Any]) -> None:
|
|
|
212
211
|
with open(queue_path, "w") as f:
|
|
213
212
|
json.dump(queue, f, indent=2, default=str)
|
|
214
213
|
except Exception as e:
|
|
215
|
-
|
|
214
|
+
logger.warning(f"Warning: Could not save drift queue: {e}")
|
|
216
215
|
|
|
217
216
|
|
|
218
217
|
def clear_drift_queue_activities(graph_dir: Path) -> None:
|
|
@@ -236,7 +235,7 @@ def clear_drift_queue_activities(graph_dir: Path) -> None:
|
|
|
236
235
|
with open(queue_path, "w") as f:
|
|
237
236
|
json.dump(queue, f, indent=2)
|
|
238
237
|
except Exception as e:
|
|
239
|
-
|
|
238
|
+
logger.warning(f"Warning: Could not clear drift queue: {e}")
|
|
240
239
|
|
|
241
240
|
|
|
242
241
|
def add_to_drift_queue(
|
|
@@ -624,7 +623,7 @@ def record_event_to_sqlite(
|
|
|
624
623
|
return None
|
|
625
624
|
|
|
626
625
|
except Exception as e:
|
|
627
|
-
|
|
626
|
+
logger.warning(f"Warning: Could not record event to SQLite: {e}")
|
|
628
627
|
return None
|
|
629
628
|
|
|
630
629
|
|
|
@@ -676,7 +675,7 @@ def record_delegation_to_sqlite(
|
|
|
676
675
|
return None
|
|
677
676
|
|
|
678
677
|
except Exception as e:
|
|
679
|
-
|
|
678
|
+
logger.warning(f"Warning: Could not record delegation to SQLite: {e}")
|
|
680
679
|
return None
|
|
681
680
|
|
|
682
681
|
|
|
@@ -702,7 +701,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
702
701
|
try:
|
|
703
702
|
manager = SessionManager(graph_dir)
|
|
704
703
|
except Exception as e:
|
|
705
|
-
|
|
704
|
+
logger.warning(f"Warning: Could not initialize SessionManager: {e}")
|
|
706
705
|
return {"continue": True}
|
|
707
706
|
|
|
708
707
|
# Initialize SQLite database for event recording
|
|
@@ -713,7 +712,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
713
712
|
|
|
714
713
|
db = HtmlGraphDB(str(get_database_path()))
|
|
715
714
|
except Exception as e:
|
|
716
|
-
|
|
715
|
+
logger.warning(f"Warning: Could not initialize SQLite database: {e}")
|
|
717
716
|
# Continue without SQLite (graceful degradation)
|
|
718
717
|
|
|
719
718
|
# Detect agent and model from environment
|
|
@@ -804,36 +803,28 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
804
803
|
task_row = cursor.fetchone()
|
|
805
804
|
if task_row:
|
|
806
805
|
task_event_id_from_db = task_row[0]
|
|
807
|
-
|
|
808
|
-
f"DEBUG Method 1 fallback: Found task_delegation={task_event_id_from_db} for {subagent_type}"
|
|
809
|
-
file=sys.stderr,
|
|
806
|
+
logger.warning(
|
|
807
|
+
f"DEBUG Method 1 fallback: Found task_delegation={task_event_id_from_db} for {subagent_type}"
|
|
810
808
|
)
|
|
811
809
|
else:
|
|
812
|
-
|
|
813
|
-
f"DEBUG Method 1: No task_delegation found for subagent_type={subagent_type}"
|
|
814
|
-
file=sys.stderr,
|
|
810
|
+
logger.warning(
|
|
811
|
+
f"DEBUG Method 1: No task_delegation found for subagent_type={subagent_type}"
|
|
815
812
|
)
|
|
816
813
|
else:
|
|
817
|
-
|
|
818
|
-
f"DEBUG Method 1: Found task_delegation={task_event_id_from_db} for subagent {subagent_type}"
|
|
819
|
-
file=sys.stderr,
|
|
814
|
+
logger.warning(
|
|
815
|
+
f"DEBUG Method 1: Found task_delegation={task_event_id_from_db} for subagent {subagent_type}"
|
|
820
816
|
)
|
|
821
817
|
except Exception as e:
|
|
822
|
-
|
|
823
|
-
f"DEBUG: Error finding task_delegation for Method 1: {e}"
|
|
824
|
-
file=sys.stderr,
|
|
818
|
+
logger.warning(
|
|
819
|
+
f"DEBUG: Error finding task_delegation for Method 1: {e}"
|
|
825
820
|
)
|
|
826
821
|
|
|
827
|
-
|
|
822
|
+
logger.debug(
|
|
828
823
|
f"DEBUG subagent persistence: Found current session as subagent in sessions table: "
|
|
829
824
|
f"type={subagent_type}, parent_session={parent_session_id}, task_event={task_event_id_from_db}",
|
|
830
|
-
file=sys.stderr,
|
|
831
825
|
)
|
|
832
826
|
except Exception as e:
|
|
833
|
-
|
|
834
|
-
f"DEBUG: Error checking sessions table for subagent: {e}",
|
|
835
|
-
file=sys.stderr,
|
|
836
|
-
)
|
|
827
|
+
logger.warning(f"DEBUG: Error checking sessions table for subagent: {e}")
|
|
837
828
|
|
|
838
829
|
# Method 2: Environment variables (for first tool call before session table is populated)
|
|
839
830
|
if not subagent_type:
|
|
@@ -878,17 +869,13 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
878
869
|
task_event_id_from_db = (
|
|
879
870
|
task_event_id # Store for later use as parent_event_id
|
|
880
871
|
)
|
|
881
|
-
|
|
872
|
+
logger.debug(
|
|
882
873
|
f"DEBUG subagent detection (database): Detected active task_delegation "
|
|
883
874
|
f"type={subagent_type}, parent_session={parent_session_id}, "
|
|
884
|
-
f"parent_event={task_event_id}"
|
|
885
|
-
file=sys.stderr,
|
|
875
|
+
f"parent_event={task_event_id}"
|
|
886
876
|
)
|
|
887
877
|
except Exception as e:
|
|
888
|
-
|
|
889
|
-
f"DEBUG: Error detecting subagent from database: {e}",
|
|
890
|
-
file=sys.stderr,
|
|
891
|
-
)
|
|
878
|
+
logger.warning(f"DEBUG: Error detecting subagent from database: {e}")
|
|
892
879
|
|
|
893
880
|
if subagent_type and parent_session_id:
|
|
894
881
|
# We're in a subagent - create or get subagent session
|
|
@@ -899,9 +886,8 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
899
886
|
existing = manager.session_converter.load(subagent_session_id)
|
|
900
887
|
if existing:
|
|
901
888
|
active_session = existing
|
|
902
|
-
|
|
903
|
-
f"Debug: Using existing subagent session: {subagent_session_id}"
|
|
904
|
-
file=sys.stderr,
|
|
889
|
+
logger.warning(
|
|
890
|
+
f"Debug: Using existing subagent session: {subagent_session_id}"
|
|
905
891
|
)
|
|
906
892
|
else:
|
|
907
893
|
# Create new subagent session with parent link
|
|
@@ -913,16 +899,12 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
913
899
|
parent_session_id=parent_session_id,
|
|
914
900
|
title=f"{subagent_type.capitalize()} Subagent",
|
|
915
901
|
)
|
|
916
|
-
|
|
902
|
+
logger.debug(
|
|
917
903
|
f"Debug: Created subagent session: {subagent_session_id} "
|
|
918
|
-
f"(parent: {parent_session_id})"
|
|
919
|
-
file=sys.stderr,
|
|
904
|
+
f"(parent: {parent_session_id})"
|
|
920
905
|
)
|
|
921
906
|
except Exception as e:
|
|
922
|
-
|
|
923
|
-
f"Warning: Could not create subagent session: {e}",
|
|
924
|
-
file=sys.stderr,
|
|
925
|
-
)
|
|
907
|
+
logger.warning(f"Warning: Could not create subagent session: {e}")
|
|
926
908
|
return {"continue": True}
|
|
927
909
|
|
|
928
910
|
# Override detected agent for subagent context
|
|
@@ -1004,9 +986,8 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1004
986
|
)
|
|
1005
987
|
except Exception as e:
|
|
1006
988
|
# Session may already exist, that's OK - continue
|
|
1007
|
-
|
|
1008
|
-
f"Debug: Could not insert session to SQLite (may already exist): {e}"
|
|
1009
|
-
file=sys.stderr,
|
|
989
|
+
logger.warning(
|
|
990
|
+
f"Debug: Could not insert session to SQLite (may already exist): {e}"
|
|
1010
991
|
)
|
|
1011
992
|
|
|
1012
993
|
# Handle different hook types
|
|
@@ -1031,7 +1012,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1031
1012
|
feature_id=result.feature_id if result else None,
|
|
1032
1013
|
)
|
|
1033
1014
|
except Exception as e:
|
|
1034
|
-
|
|
1015
|
+
logger.warning(f"Warning: Could not track stop: {e}")
|
|
1035
1016
|
return {"continue": True}
|
|
1036
1017
|
|
|
1037
1018
|
elif hook_type == "UserPromptSubmit":
|
|
@@ -1063,7 +1044,7 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1063
1044
|
)
|
|
1064
1045
|
|
|
1065
1046
|
except Exception as e:
|
|
1066
|
-
|
|
1047
|
+
logger.warning(f"Warning: Could not track query: {e}")
|
|
1067
1048
|
return {"continue": True}
|
|
1068
1049
|
|
|
1069
1050
|
elif hook_type == "PostToolUse":
|
|
@@ -1160,14 +1141,12 @@ def track_event(hook_type: str, hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
1160
1141
|
task_row = cursor.fetchone()
|
|
1161
1142
|
if task_row:
|
|
1162
1143
|
parent_activity_id = task_row[0]
|
|
1163
|
-
|
|
1164
|
-
f"DEBUG: Found active task_delegation={parent_activity_id} in parent_activity_id fallback"
|
|
1165
|
-
file=sys.stderr,
|
|
1144
|
+
logger.warning(
|
|
1145
|
+
f"DEBUG: Found active task_delegation={parent_activity_id} in parent_activity_id fallback"
|
|
1166
1146
|
)
|
|
1167
1147
|
except Exception as e:
|
|
1168
|
-
|
|
1169
|
-
f"DEBUG: Error finding task_delegation in parent_activity_id: {e}"
|
|
1170
|
-
file=sys.stderr,
|
|
1148
|
+
logger.warning(
|
|
1149
|
+
f"DEBUG: Error finding task_delegation in parent_activity_id: {e}"
|
|
1171
1150
|
)
|
|
1172
1151
|
|
|
1173
1152
|
# Only if no active task found, fall back to UserQuery
|
|
@@ -1320,7 +1299,7 @@ Or manually create a work item in .htmlgraph/ (bug, feature, spike, or chore).""
|
|
|
1320
1299
|
nudge = f"Drift detected ({drift_score:.2f}): Activity may not align with {feature_id}. Consider refocusing or updating the feature."
|
|
1321
1300
|
|
|
1322
1301
|
except Exception as e:
|
|
1323
|
-
|
|
1302
|
+
logger.warning(f"Warning: Could not track activity: {e}")
|
|
1324
1303
|
|
|
1325
1304
|
# Build response
|
|
1326
1305
|
response: dict[str, Any] = {"continue": True}
|
htmlgraph/hooks/installer.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Git hooks installation and configuration management.
|
|
3
7
|
"""
|
|
@@ -52,7 +56,7 @@ class HookConfig:
|
|
|
52
56
|
user_config = json.load(f)
|
|
53
57
|
self.config.update(user_config)
|
|
54
58
|
except Exception as e:
|
|
55
|
-
|
|
59
|
+
logger.info(f"Warning: Failed to load hook config: {e}")
|
|
56
60
|
|
|
57
61
|
def save(self) -> None:
|
|
58
62
|
"""Save configuration to file."""
|
htmlgraph/hooks/orchestrator.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Orchestrator Enforcement Module
|
|
3
7
|
|
|
@@ -94,6 +98,73 @@ def load_tool_history(session_id: str) -> list[dict]:
|
|
|
94
98
|
return []
|
|
95
99
|
|
|
96
100
|
|
|
101
|
+
def record_tool_event(tool_name: str, session_id: str) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Record a tool event to the database for history tracking.
|
|
104
|
+
|
|
105
|
+
This is called at the end of PreToolUse hook execution to track
|
|
106
|
+
tool usage patterns for sequence detection.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
tool_name: Name of the tool being called
|
|
110
|
+
session_id: Session identifier for isolation
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
import datetime
|
|
114
|
+
import uuid
|
|
115
|
+
|
|
116
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
117
|
+
|
|
118
|
+
# Find database path
|
|
119
|
+
cwd = Path.cwd()
|
|
120
|
+
graph_dir = cwd / ".htmlgraph"
|
|
121
|
+
if not graph_dir.exists():
|
|
122
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
123
|
+
candidate = parent / ".htmlgraph"
|
|
124
|
+
if candidate.exists():
|
|
125
|
+
graph_dir = candidate
|
|
126
|
+
break
|
|
127
|
+
|
|
128
|
+
if not graph_dir.exists():
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
132
|
+
db = HtmlGraphDB(str(db_path))
|
|
133
|
+
if db.connection is None:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
cursor = db.connection.cursor()
|
|
137
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
138
|
+
|
|
139
|
+
# Ensure session exists (required by FK constraint)
|
|
140
|
+
cursor.execute(
|
|
141
|
+
"""
|
|
142
|
+
INSERT OR IGNORE INTO sessions (session_id, agent_assigned, created_at, status)
|
|
143
|
+
VALUES (?, ?, ?, ?)
|
|
144
|
+
""",
|
|
145
|
+
(session_id, "orchestrator-hook", timestamp, "active"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Record the tool event using the actual schema
|
|
149
|
+
# Schema has: event_id, agent_id, event_type, timestamp, tool_name, session_id, etc.
|
|
150
|
+
event_id = str(uuid.uuid4())
|
|
151
|
+
agent_id = "orchestrator-hook" # Identifier for the hook
|
|
152
|
+
|
|
153
|
+
cursor.execute(
|
|
154
|
+
"""
|
|
155
|
+
INSERT INTO agent_events (event_id, agent_id, event_type, timestamp, tool_name, session_id)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
157
|
+
""",
|
|
158
|
+
(event_id, agent_id, "tool_call", timestamp, tool_name, session_id),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
db.connection.commit()
|
|
162
|
+
db.disconnect()
|
|
163
|
+
except Exception:
|
|
164
|
+
# Graceful degradation - don't fail hook on recording error
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
|
|
97
168
|
def is_allowed_orchestrator_operation(
|
|
98
169
|
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
99
170
|
) -> tuple[bool, str, str]:
|
|
@@ -402,6 +473,7 @@ def enforce_orchestrator_mode(
|
|
|
402
473
|
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
403
474
|
if is_subagent_context():
|
|
404
475
|
return {
|
|
476
|
+
"continue": True,
|
|
405
477
|
"hookSpecificOutput": {
|
|
406
478
|
"hookEventName": "PreToolUse",
|
|
407
479
|
"permissionDecision": "allow",
|
|
@@ -425,18 +497,14 @@ def enforce_orchestrator_mode(
|
|
|
425
497
|
manager = OrchestratorModeManager(graph_dir)
|
|
426
498
|
|
|
427
499
|
if not manager.is_enabled():
|
|
428
|
-
# Mode not active, allow everything
|
|
429
|
-
return {
|
|
430
|
-
"hookSpecificOutput": {
|
|
431
|
-
"hookEventName": "PreToolUse",
|
|
432
|
-
"permissionDecision": "allow",
|
|
433
|
-
},
|
|
434
|
-
}
|
|
500
|
+
# Mode not active, allow everything with no additional output
|
|
501
|
+
return {"continue": True}
|
|
435
502
|
|
|
436
503
|
enforcement_level = manager.get_enforcement_level()
|
|
437
504
|
except Exception:
|
|
438
505
|
# If we can't check mode, fail open (allow)
|
|
439
506
|
return {
|
|
507
|
+
"continue": True,
|
|
440
508
|
"hookSpecificOutput": {
|
|
441
509
|
"hookEventName": "PreToolUse",
|
|
442
510
|
"permissionDecision": "allow",
|
|
@@ -467,6 +535,7 @@ def enforce_orchestrator_mode(
|
|
|
467
535
|
)
|
|
468
536
|
|
|
469
537
|
return {
|
|
538
|
+
"continue": False,
|
|
470
539
|
"hookSpecificOutput": {
|
|
471
540
|
"hookEventName": "PreToolUse",
|
|
472
541
|
"permissionDecision": "deny",
|
|
@@ -491,6 +560,7 @@ def enforce_orchestrator_mode(
|
|
|
491
560
|
):
|
|
492
561
|
# Provide guidance even when allowing
|
|
493
562
|
return {
|
|
563
|
+
"continue": True,
|
|
494
564
|
"hookSpecificOutput": {
|
|
495
565
|
"hookEventName": "PreToolUse",
|
|
496
566
|
"permissionDecision": "allow",
|
|
@@ -498,6 +568,7 @@ def enforce_orchestrator_mode(
|
|
|
498
568
|
},
|
|
499
569
|
}
|
|
500
570
|
return {
|
|
571
|
+
"continue": True,
|
|
501
572
|
"hookSpecificOutput": {
|
|
502
573
|
"hookEventName": "PreToolUse",
|
|
503
574
|
"permissionDecision": "allow",
|
|
@@ -513,8 +584,8 @@ def enforce_orchestrator_mode(
|
|
|
513
584
|
suggestion = create_task_suggestion(tool, params)
|
|
514
585
|
|
|
515
586
|
if enforcement_level == "strict":
|
|
516
|
-
# STRICT mode -
|
|
517
|
-
|
|
587
|
+
# STRICT mode - advisory warning with violation count (does not block)
|
|
588
|
+
warning_message = (
|
|
518
589
|
f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
|
|
519
590
|
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
520
591
|
f"Suggested delegation:\n"
|
|
@@ -523,23 +594,25 @@ def enforce_orchestrator_mode(
|
|
|
523
594
|
|
|
524
595
|
# Add circuit breaker warning if approaching threshold
|
|
525
596
|
if violations >= circuit_breaker_threshold:
|
|
526
|
-
|
|
597
|
+
warning_message += (
|
|
527
598
|
"🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
|
|
528
599
|
"Reset with: uv run htmlgraph orchestrator reset-violations\n"
|
|
529
600
|
)
|
|
530
601
|
elif violations == circuit_breaker_threshold - 1:
|
|
531
|
-
|
|
602
|
+
warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
|
|
532
603
|
|
|
533
|
-
|
|
604
|
+
warning_message += (
|
|
534
605
|
"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
|
|
535
606
|
"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
|
|
536
607
|
)
|
|
537
608
|
|
|
609
|
+
# Advisory-only: allow operation but provide warning
|
|
538
610
|
return {
|
|
611
|
+
"continue": True,
|
|
539
612
|
"hookSpecificOutput": {
|
|
540
613
|
"hookEventName": "PreToolUse",
|
|
541
|
-
"permissionDecision": "
|
|
542
|
-
"
|
|
614
|
+
"permissionDecision": "allow",
|
|
615
|
+
"additionalContext": warning_message,
|
|
543
616
|
},
|
|
544
617
|
}
|
|
545
618
|
else:
|
|
@@ -549,6 +622,7 @@ def enforce_orchestrator_mode(
|
|
|
549
622
|
)
|
|
550
623
|
|
|
551
624
|
return {
|
|
625
|
+
"continue": True,
|
|
552
626
|
"hookSpecificOutput": {
|
|
553
627
|
"hookEventName": "PreToolUse",
|
|
554
628
|
"permissionDecision": "allow",
|
|
@@ -592,5 +666,9 @@ def main() -> None:
|
|
|
592
666
|
# Enforce orchestrator mode with session_id for history lookup
|
|
593
667
|
response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
|
|
594
668
|
|
|
669
|
+
# Record tool event to database for history tracking
|
|
670
|
+
# This allows subsequent calls to detect patterns (e.g., multiple Reads)
|
|
671
|
+
record_tool_event(tool_name, session_id)
|
|
672
|
+
|
|
595
673
|
# Output JSON response
|
|
596
674
|
print(json.dumps(response))
|
|
@@ -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
|
|
|
@@ -109,7 +113,7 @@ def run(hook_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
109
113
|
|
|
110
114
|
except Exception as e:
|
|
111
115
|
# Never raise - log and continue
|
|
112
|
-
|
|
116
|
+
logger.warning(f"PostToolUseFailure hook error: {e}")
|
|
113
117
|
return {"continue": True}
|
|
114
118
|
|
|
115
119
|
|
|
@@ -238,10 +242,10 @@ def create_debug_spike(tool: str, error: str, log_path: Path) -> None:
|
|
|
238
242
|
with open(spike_marker, "w") as f:
|
|
239
243
|
json.dump(existing_spikes, f, indent=2)
|
|
240
244
|
|
|
241
|
-
|
|
245
|
+
logger.warning(f"Created debug spike: {spike.id}")
|
|
242
246
|
|
|
243
247
|
except Exception as e:
|
|
244
|
-
|
|
248
|
+
logger.warning(f"Failed to create debug spike: {e}")
|
|
245
249
|
|
|
246
250
|
|
|
247
251
|
def main() -> None:
|
htmlgraph/hooks/posttooluse.py
CHANGED
|
@@ -259,7 +259,7 @@ def get_session_violation_count(context: HookContext) -> tuple[int, int]:
|
|
|
259
259
|
Example:
|
|
260
260
|
>>> violation_count, waste_tokens = get_session_violation_count(context)
|
|
261
261
|
>>> if violation_count > 0:
|
|
262
|
-
...
|
|
262
|
+
... logger.info(f"Violations this session: {violation_count}")
|
|
263
263
|
"""
|
|
264
264
|
try:
|
|
265
265
|
from htmlgraph.cigs import ViolationTracker
|
|
@@ -289,7 +289,7 @@ def get_active_work_item(context: HookContext) -> dict[str, Any] | None:
|
|
|
289
289
|
Example:
|
|
290
290
|
>>> active = get_active_work_item(context)
|
|
291
291
|
>>> if active and active['type'] == 'feature':
|
|
292
|
-
...
|
|
292
|
+
... logger.info(f"Active feature: {active['title']}")
|
|
293
293
|
"""
|
|
294
294
|
try:
|
|
295
295
|
from htmlgraph import SDK
|
|
@@ -326,7 +326,7 @@ def generate_guidance(
|
|
|
326
326
|
>>> classification = classify_prompt("Implement new API endpoint")
|
|
327
327
|
>>> guidance = generate_guidance(classification, None, prompt)
|
|
328
328
|
>>> if guidance:
|
|
329
|
-
...
|
|
329
|
+
... logger.info("%s", guidance)
|
|
330
330
|
"""
|
|
331
331
|
|
|
332
332
|
# If continuing and has active work, no guidance needed
|
|
@@ -467,7 +467,7 @@ def generate_cigs_guidance(
|
|
|
467
467
|
>>> cigs = classify_cigs_intent("Search for all error handling")
|
|
468
468
|
>>> guidance = generate_cigs_guidance(cigs, 0, 0)
|
|
469
469
|
>>> if guidance:
|
|
470
|
-
...
|
|
470
|
+
... logger.info("%s", guidance)
|
|
471
471
|
"""
|
|
472
472
|
imperatives = []
|
|
473
473
|
|
|
@@ -544,7 +544,7 @@ def create_user_query_event(context: HookContext, prompt: str) -> str | None:
|
|
|
544
544
|
>>> context = HookContext.from_input(hook_input)
|
|
545
545
|
>>> event_id = create_user_query_event(context, "Implement feature X")
|
|
546
546
|
>>> if event_id:
|
|
547
|
-
...
|
|
547
|
+
... logger.info(f"Created event: {event_id}")
|
|
548
548
|
"""
|
|
549
549
|
try:
|
|
550
550
|
session_id = context.session_id
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
HtmlGraph Session Handler Module
|
|
3
5
|
|
|
@@ -24,7 +26,6 @@ Public API:
|
|
|
24
26
|
Check if HtmlGraph has updates available
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
|
-
from __future__ import annotations
|
|
28
29
|
|
|
29
30
|
import json
|
|
30
31
|
import logging
|
|
@@ -185,7 +186,9 @@ def handle_session_start(context: HookContext, session: Any | None) -> dict[str,
|
|
|
185
186
|
|
|
186
187
|
{feature_list}
|
|
187
188
|
|
|
188
|
-
Activity will be attributed to these features based on file patterns and keywords.
|
|
189
|
+
Activity will be attributed to these features based on file patterns and keywords.
|
|
190
|
+
|
|
191
|
+
**To view all work and progress:** `htmlgraph snapshot --summary`"""
|
|
189
192
|
output["hookSpecificOutput"]["sessionFeatureContext"] = context_str
|
|
190
193
|
context.log("info", f"Loaded {len(active_features)} active features")
|
|
191
194
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Session Summary Module - CIGS Integration
|
|
3
7
|
|
|
@@ -353,7 +357,7 @@ class CIGSSessionSummarizer:
|
|
|
353
357
|
with open(summary_file, "w") as f:
|
|
354
358
|
json.dump(summary_data, f, indent=2, default=str)
|
|
355
359
|
except Exception as e:
|
|
356
|
-
|
|
360
|
+
logger.warning(f"Warning: Failed to persist summary: {e}")
|
|
357
361
|
|
|
358
362
|
|
|
359
363
|
def main() -> None:
|
|
@@ -387,5 +391,5 @@ def main() -> None:
|
|
|
387
391
|
result = summarizer.summarize(session_id)
|
|
388
392
|
print(json.dumps(result))
|
|
389
393
|
except Exception as e:
|
|
390
|
-
|
|
394
|
+
logger.warning(f"Warning: Could not generate CIGS summary: {e}")
|
|
391
395
|
print(json.dumps({"continue": True}))
|
htmlgraph/hooks/validator.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Work Validation Module for HtmlGraph Hooks
|
|
3
7
|
|
|
@@ -22,9 +26,9 @@ Example:
|
|
|
22
26
|
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
23
27
|
|
|
24
28
|
if result["decision"] == "block":
|
|
25
|
-
|
|
29
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
26
30
|
elif "guidance" in result:
|
|
27
|
-
|
|
31
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
28
32
|
"""
|
|
29
33
|
|
|
30
34
|
import json
|
|
@@ -448,9 +452,9 @@ def validate_tool_call(
|
|
|
448
452
|
history = load_tool_history(session_id)
|
|
449
453
|
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
450
454
|
if result["decision"] == "block":
|
|
451
|
-
|
|
455
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
452
456
|
elif "guidance" in result:
|
|
453
|
-
|
|
457
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
454
458
|
"""
|
|
455
459
|
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
456
460
|
if is_subagent_context():
|
htmlgraph/ids.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Hash-based ID generation for HtmlGraph.
|
|
3
5
|
|
|
@@ -15,7 +17,6 @@ collision probability is effectively zero even with thousands
|
|
|
15
17
|
of concurrent agents creating tasks simultaneously.
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
from __future__ import annotations
|
|
19
20
|
|
|
20
21
|
import hashlib
|
|
21
22
|
import os
|