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.
Files changed (155) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +114 -51
  11. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  12. htmlgraph/api/templates/dashboard.html +3 -3
  13. htmlgraph/api/templates/partials/work-items.html +613 -0
  14. htmlgraph/attribute_index.py +2 -1
  15. htmlgraph/builders/base.py +2 -1
  16. htmlgraph/builders/bug.py +2 -1
  17. htmlgraph/builders/chore.py +2 -1
  18. htmlgraph/builders/epic.py +2 -1
  19. htmlgraph/builders/feature.py +2 -1
  20. htmlgraph/builders/insight.py +2 -1
  21. htmlgraph/builders/metric.py +2 -1
  22. htmlgraph/builders/pattern.py +2 -1
  23. htmlgraph/builders/phase.py +2 -1
  24. htmlgraph/builders/spike.py +2 -1
  25. htmlgraph/builders/track.py +28 -1
  26. htmlgraph/cli/analytics.py +2 -1
  27. htmlgraph/cli/base.py +33 -8
  28. htmlgraph/cli/core.py +2 -1
  29. htmlgraph/cli/main.py +2 -1
  30. htmlgraph/cli/models.py +2 -1
  31. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  32. htmlgraph/cli/work/__init__.py +76 -1
  33. htmlgraph/cli/work/browse.py +115 -0
  34. htmlgraph/cli/work/features.py +2 -1
  35. htmlgraph/cli/work/orchestration.py +2 -1
  36. htmlgraph/cli/work/report.py +2 -1
  37. htmlgraph/cli/work/sessions.py +2 -1
  38. htmlgraph/cli/work/snapshot.py +559 -0
  39. htmlgraph/cli/work/tracks.py +2 -1
  40. htmlgraph/collections/base.py +43 -4
  41. htmlgraph/collections/bug.py +2 -1
  42. htmlgraph/collections/chore.py +2 -1
  43. htmlgraph/collections/epic.py +2 -1
  44. htmlgraph/collections/feature.py +2 -1
  45. htmlgraph/collections/insight.py +2 -1
  46. htmlgraph/collections/metric.py +2 -1
  47. htmlgraph/collections/pattern.py +2 -1
  48. htmlgraph/collections/phase.py +2 -1
  49. htmlgraph/collections/session.py +12 -7
  50. htmlgraph/collections/spike.py +6 -1
  51. htmlgraph/collections/task_delegation.py +7 -2
  52. htmlgraph/collections/todo.py +14 -1
  53. htmlgraph/collections/traces.py +15 -10
  54. htmlgraph/context_analytics.py +2 -1
  55. htmlgraph/converter.py +11 -0
  56. htmlgraph/dependency_models.py +2 -1
  57. htmlgraph/edge_index.py +2 -1
  58. htmlgraph/event_log.py +81 -66
  59. htmlgraph/event_migration.py +2 -1
  60. htmlgraph/file_watcher.py +12 -8
  61. htmlgraph/find_api.py +2 -1
  62. htmlgraph/git_events.py +6 -2
  63. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  64. htmlgraph/hooks/drift_handler.py +3 -3
  65. htmlgraph/hooks/event_tracker.py +40 -61
  66. htmlgraph/hooks/installer.py +5 -1
  67. htmlgraph/hooks/orchestrator.py +92 -14
  68. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  69. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  70. htmlgraph/hooks/posttooluse.py +4 -0
  71. htmlgraph/hooks/prompt_analyzer.py +5 -5
  72. htmlgraph/hooks/session_handler.py +5 -2
  73. htmlgraph/hooks/session_summary.py +6 -2
  74. htmlgraph/hooks/validator.py +8 -4
  75. htmlgraph/ids.py +2 -1
  76. htmlgraph/learning.py +2 -1
  77. htmlgraph/mcp_server.py +2 -1
  78. htmlgraph/models.py +18 -1
  79. htmlgraph/operations/analytics.py +2 -1
  80. htmlgraph/operations/bootstrap.py +2 -1
  81. htmlgraph/operations/events.py +2 -1
  82. htmlgraph/operations/fastapi_server.py +2 -1
  83. htmlgraph/operations/hooks.py +2 -1
  84. htmlgraph/operations/initialization.py +2 -1
  85. htmlgraph/operations/server.py +2 -1
  86. htmlgraph/orchestration/__init__.py +4 -0
  87. htmlgraph/orchestration/claude_launcher.py +23 -20
  88. htmlgraph/orchestration/command_builder.py +2 -1
  89. htmlgraph/orchestration/headless_spawner.py +6 -2
  90. htmlgraph/orchestration/model_selection.py +7 -3
  91. htmlgraph/orchestration/plugin_manager.py +25 -21
  92. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  93. htmlgraph/orchestration/spawners/claude.py +5 -2
  94. htmlgraph/orchestration/spawners/codex.py +12 -19
  95. htmlgraph/orchestration/spawners/copilot.py +13 -18
  96. htmlgraph/orchestration/spawners/gemini.py +12 -19
  97. htmlgraph/orchestration/subprocess_runner.py +6 -3
  98. htmlgraph/orchestration/task_coordination.py +16 -8
  99. htmlgraph/orchestrator.py +2 -1
  100. htmlgraph/parallel.py +2 -1
  101. htmlgraph/query_builder.py +2 -1
  102. htmlgraph/reflection.py +2 -1
  103. htmlgraph/refs.py +344 -0
  104. htmlgraph/repo_hash.py +2 -1
  105. htmlgraph/sdk/__init__.py +398 -0
  106. htmlgraph/sdk/__init__.pyi +14 -0
  107. htmlgraph/sdk/analytics/__init__.py +19 -0
  108. htmlgraph/sdk/analytics/engine.py +155 -0
  109. htmlgraph/sdk/analytics/helpers.py +178 -0
  110. htmlgraph/sdk/analytics/registry.py +109 -0
  111. htmlgraph/sdk/base.py +484 -0
  112. htmlgraph/sdk/constants.py +216 -0
  113. htmlgraph/sdk/core.pyi +308 -0
  114. htmlgraph/sdk/discovery.py +120 -0
  115. htmlgraph/sdk/help/__init__.py +12 -0
  116. htmlgraph/sdk/help/mixin.py +699 -0
  117. htmlgraph/sdk/mixins/__init__.py +15 -0
  118. htmlgraph/sdk/mixins/attribution.py +113 -0
  119. htmlgraph/sdk/mixins/mixin.py +410 -0
  120. htmlgraph/sdk/operations/__init__.py +12 -0
  121. htmlgraph/sdk/operations/mixin.py +427 -0
  122. htmlgraph/sdk/orchestration/__init__.py +17 -0
  123. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  124. htmlgraph/sdk/orchestration/spawner.py +204 -0
  125. htmlgraph/sdk/planning/__init__.py +19 -0
  126. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  127. htmlgraph/sdk/planning/mixin.py +211 -0
  128. htmlgraph/sdk/planning/parallel.py +186 -0
  129. htmlgraph/sdk/planning/queue.py +210 -0
  130. htmlgraph/sdk/planning/recommendations.py +87 -0
  131. htmlgraph/sdk/planning/smart_planning.py +319 -0
  132. htmlgraph/sdk/session/__init__.py +19 -0
  133. htmlgraph/sdk/session/continuity.py +57 -0
  134. htmlgraph/sdk/session/handoff.py +110 -0
  135. htmlgraph/sdk/session/info.py +309 -0
  136. htmlgraph/sdk/session/manager.py +103 -0
  137. htmlgraph/server.py +21 -17
  138. htmlgraph/session_manager.py +1 -7
  139. htmlgraph/session_warning.py +2 -1
  140. htmlgraph/sessions/handoff.py +10 -3
  141. htmlgraph/system_prompts.py +2 -1
  142. htmlgraph/track_builder.py +14 -1
  143. htmlgraph/transcript.py +2 -1
  144. htmlgraph/watch.py +2 -1
  145. htmlgraph/work_type_utils.py +2 -1
  146. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  147. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
  148. htmlgraph/sdk.py +0 -3430
  149. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  150. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  151. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  152. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  153. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  154. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  155. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -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
- print(f"Auto-classify threshold: {config['drift_detection']['auto_classify_threshold']}")
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
- print(f"HIGH DRIFT: {score:.2f}")
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
- print(nudge) # "HIGH DRIFT (0.87): Activity queued for classification..."
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", {})
@@ -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
- print(
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
- print(
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
- print(f"Warning: Could not save drift queue: {e}", file=sys.stderr)
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
- print(f"Warning: Could not clear drift queue: {e}", file=sys.stderr)
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
- print(f"Warning: Could not record event to SQLite: {e}", file=sys.stderr)
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
- print(f"Warning: Could not record delegation to SQLite: {e}", file=sys.stderr)
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
- print(f"Warning: Could not initialize SessionManager: {e}", file=sys.stderr)
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
- print(f"Warning: Could not initialize SQLite database: {e}", file=sys.stderr)
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(
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
- print(f"Warning: Could not track stop: {e}", file=sys.stderr)
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
- print(f"Warning: Could not track query: {e}", file=sys.stderr)
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
- print(
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
- print(
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
- print(f"Warning: Could not track activity: {e}", file=sys.stderr)
1302
+ logger.warning(f"Warning: Could not track activity: {e}")
1324
1303
 
1325
1304
  # Build response
1326
1305
  response: dict[str, Any] = {"continue": True}
@@ -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
- print(f"Warning: Failed to load hook config: {e}")
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."""
@@ -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 - loud warning with violation count
517
- error_message = (
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
- error_message += (
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
- error_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
602
+ warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
532
603
 
533
- error_message += (
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": "deny",
542
- "permissionDecisionReason": error_message,
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,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Orchestrator Reflection Module
3
7
 
@@ -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
- print(f"PostToolUseFailure hook error: {e}", file=sys.stderr)
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
- print(f"Created debug spike: {spike.id}", file=sys.stderr)
245
+ logger.warning(f"Created debug spike: {spike.id}")
242
246
 
243
247
  except Exception as e:
244
- print(f"Failed to create debug spike: {e}", file=sys.stderr)
248
+ logger.warning(f"Failed to create debug spike: {e}")
245
249
 
246
250
 
247
251
  def main() -> None:
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Unified PostToolUse Hook - Parallel Execution of Multiple Tasks
3
7
 
@@ -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
- ... print(f"Violations this session: {violation_count}")
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
- ... print(f"Active feature: {active['title']}")
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
- ... print(guidance)
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
- ... print(guidance)
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
- ... print(f"Created event: {event_id}")
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
- print(f"Warning: Failed to persist summary: {e}", file=sys.stderr)
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
- print(f"Warning: Could not generate CIGS summary: {e}", file=sys.stderr)
394
+ logger.warning(f"Warning: Could not generate CIGS summary: {e}")
391
395
  print(json.dumps({"continue": True}))
@@ -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
- print(result["reason"])
29
+ logger.debug("Validation reason: %s", result["reason"])
26
30
  elif "guidance" in result:
27
- print(result["guidance"])
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
- print(result["reason"])
455
+ logger.debug("Validation reason: %s", result["reason"])
452
456
  elif "guidance" in result:
453
- print(result["guidance"])
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