htmlgraph 0.26.25__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 (147) 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 +58 -28
  11. htmlgraph/attribute_index.py +2 -1
  12. htmlgraph/builders/base.py +2 -1
  13. htmlgraph/builders/bug.py +2 -1
  14. htmlgraph/builders/chore.py +2 -1
  15. htmlgraph/builders/epic.py +2 -1
  16. htmlgraph/builders/feature.py +2 -1
  17. htmlgraph/builders/insight.py +2 -1
  18. htmlgraph/builders/metric.py +2 -1
  19. htmlgraph/builders/pattern.py +2 -1
  20. htmlgraph/builders/phase.py +2 -1
  21. htmlgraph/builders/spike.py +2 -1
  22. htmlgraph/builders/track.py +2 -1
  23. htmlgraph/cli/analytics.py +2 -1
  24. htmlgraph/cli/base.py +2 -1
  25. htmlgraph/cli/core.py +2 -1
  26. htmlgraph/cli/main.py +2 -1
  27. htmlgraph/cli/models.py +2 -1
  28. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  29. htmlgraph/cli/work/__init__.py +2 -1
  30. htmlgraph/cli/work/browse.py +2 -1
  31. htmlgraph/cli/work/features.py +2 -1
  32. htmlgraph/cli/work/orchestration.py +2 -1
  33. htmlgraph/cli/work/report.py +2 -1
  34. htmlgraph/cli/work/sessions.py +2 -1
  35. htmlgraph/cli/work/snapshot.py +2 -1
  36. htmlgraph/cli/work/tracks.py +2 -1
  37. htmlgraph/collections/base.py +10 -5
  38. htmlgraph/collections/bug.py +2 -1
  39. htmlgraph/collections/chore.py +2 -1
  40. htmlgraph/collections/epic.py +2 -1
  41. htmlgraph/collections/feature.py +2 -1
  42. htmlgraph/collections/insight.py +2 -1
  43. htmlgraph/collections/metric.py +2 -1
  44. htmlgraph/collections/pattern.py +2 -1
  45. htmlgraph/collections/phase.py +2 -1
  46. htmlgraph/collections/session.py +12 -7
  47. htmlgraph/collections/spike.py +6 -1
  48. htmlgraph/collections/task_delegation.py +7 -2
  49. htmlgraph/collections/todo.py +2 -1
  50. htmlgraph/collections/traces.py +15 -10
  51. htmlgraph/context_analytics.py +2 -1
  52. htmlgraph/dependency_models.py +2 -1
  53. htmlgraph/edge_index.py +2 -1
  54. htmlgraph/event_log.py +81 -66
  55. htmlgraph/event_migration.py +2 -1
  56. htmlgraph/file_watcher.py +12 -8
  57. htmlgraph/find_api.py +2 -1
  58. htmlgraph/git_events.py +6 -2
  59. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  60. htmlgraph/hooks/drift_handler.py +3 -3
  61. htmlgraph/hooks/event_tracker.py +40 -61
  62. htmlgraph/hooks/installer.py +5 -1
  63. htmlgraph/hooks/orchestrator.py +4 -0
  64. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  65. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  66. htmlgraph/hooks/posttooluse.py +4 -0
  67. htmlgraph/hooks/prompt_analyzer.py +5 -5
  68. htmlgraph/hooks/session_handler.py +2 -1
  69. htmlgraph/hooks/session_summary.py +6 -2
  70. htmlgraph/hooks/validator.py +8 -4
  71. htmlgraph/ids.py +2 -1
  72. htmlgraph/learning.py +2 -1
  73. htmlgraph/mcp_server.py +2 -1
  74. htmlgraph/operations/analytics.py +2 -1
  75. htmlgraph/operations/bootstrap.py +2 -1
  76. htmlgraph/operations/events.py +2 -1
  77. htmlgraph/operations/fastapi_server.py +2 -1
  78. htmlgraph/operations/hooks.py +2 -1
  79. htmlgraph/operations/initialization.py +2 -1
  80. htmlgraph/operations/server.py +2 -1
  81. htmlgraph/orchestration/claude_launcher.py +23 -20
  82. htmlgraph/orchestration/command_builder.py +2 -1
  83. htmlgraph/orchestration/headless_spawner.py +6 -2
  84. htmlgraph/orchestration/model_selection.py +7 -3
  85. htmlgraph/orchestration/plugin_manager.py +24 -19
  86. htmlgraph/orchestration/spawners/claude.py +5 -2
  87. htmlgraph/orchestration/spawners/codex.py +12 -19
  88. htmlgraph/orchestration/spawners/copilot.py +13 -18
  89. htmlgraph/orchestration/spawners/gemini.py +12 -19
  90. htmlgraph/orchestration/subprocess_runner.py +6 -3
  91. htmlgraph/orchestration/task_coordination.py +16 -8
  92. htmlgraph/orchestrator.py +2 -1
  93. htmlgraph/parallel.py +2 -1
  94. htmlgraph/query_builder.py +2 -1
  95. htmlgraph/reflection.py +2 -1
  96. htmlgraph/refs.py +2 -1
  97. htmlgraph/repo_hash.py +2 -1
  98. htmlgraph/sdk/__init__.py +398 -0
  99. htmlgraph/sdk/__init__.pyi +14 -0
  100. htmlgraph/sdk/analytics/__init__.py +19 -0
  101. htmlgraph/sdk/analytics/engine.py +155 -0
  102. htmlgraph/sdk/analytics/helpers.py +178 -0
  103. htmlgraph/sdk/analytics/registry.py +109 -0
  104. htmlgraph/sdk/base.py +484 -0
  105. htmlgraph/sdk/constants.py +216 -0
  106. htmlgraph/sdk/core.pyi +308 -0
  107. htmlgraph/sdk/discovery.py +120 -0
  108. htmlgraph/sdk/help/__init__.py +12 -0
  109. htmlgraph/sdk/help/mixin.py +699 -0
  110. htmlgraph/sdk/mixins/__init__.py +15 -0
  111. htmlgraph/sdk/mixins/attribution.py +113 -0
  112. htmlgraph/sdk/mixins/mixin.py +410 -0
  113. htmlgraph/sdk/operations/__init__.py +12 -0
  114. htmlgraph/sdk/operations/mixin.py +427 -0
  115. htmlgraph/sdk/orchestration/__init__.py +17 -0
  116. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  117. htmlgraph/sdk/orchestration/spawner.py +204 -0
  118. htmlgraph/sdk/planning/__init__.py +19 -0
  119. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  120. htmlgraph/sdk/planning/mixin.py +211 -0
  121. htmlgraph/sdk/planning/parallel.py +186 -0
  122. htmlgraph/sdk/planning/queue.py +210 -0
  123. htmlgraph/sdk/planning/recommendations.py +87 -0
  124. htmlgraph/sdk/planning/smart_planning.py +319 -0
  125. htmlgraph/sdk/session/__init__.py +19 -0
  126. htmlgraph/sdk/session/continuity.py +57 -0
  127. htmlgraph/sdk/session/handoff.py +110 -0
  128. htmlgraph/sdk/session/info.py +309 -0
  129. htmlgraph/sdk/session/manager.py +103 -0
  130. htmlgraph/server.py +21 -17
  131. htmlgraph/session_warning.py +2 -1
  132. htmlgraph/sessions/handoff.py +4 -3
  133. htmlgraph/system_prompts.py +2 -1
  134. htmlgraph/track_builder.py +2 -1
  135. htmlgraph/transcript.py +2 -1
  136. htmlgraph/watch.py +2 -1
  137. htmlgraph/work_type_utils.py +2 -1
  138. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  139. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +146 -114
  140. htmlgraph/sdk.py +0 -3500
  141. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  142. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  143. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  144. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  145. {htmlgraph-0.26.25.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  146. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  147. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Data models for dependency analytics.
3
5
 
@@ -9,7 +11,6 @@ Provides Pydantic models for dependency-aware analytics results including:
9
11
  - Work prioritization
10
12
  """
11
13
 
12
- from __future__ import annotations
13
14
 
14
15
  from typing import Literal
15
16
 
htmlgraph/edge_index.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Edge Index for O(1) reverse edge lookups.
3
5
 
@@ -10,7 +12,6 @@ Without this index, finding incoming edges requires scanning all nodes
10
12
  in the graph - O(V×E) complexity.
11
13
  """
12
14
 
13
- from __future__ import annotations
14
15
 
15
16
  from collections import defaultdict
16
17
  from collections.abc import Iterator
htmlgraph/event_log.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Event logging for HtmlGraph.
3
5
 
@@ -9,84 +11,94 @@ Design goals:
9
11
  - Deterministic serialization for rebuildable analytics indexes
10
12
  """
11
13
 
12
- from __future__ import annotations
13
14
 
14
15
  import json
15
- from dataclasses import dataclass
16
16
  from datetime import datetime
17
17
  from pathlib import Path
18
18
  from typing import TYPE_CHECKING, Any
19
19
 
20
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
21
+
20
22
  if TYPE_CHECKING:
21
23
  pass
22
24
 
23
25
 
24
- @dataclass(frozen=True)
25
- class EventRecord:
26
- event_id: str
27
- timestamp: datetime
28
- session_id: str
29
- agent: str
30
- tool: str
31
- summary: str
32
- success: bool
33
- feature_id: str | None
34
- drift_score: float | None
35
- start_commit: str | None
36
- continued_from: str | None
37
- work_type: str | None = None # WorkType enum value
38
- session_status: str | None = None
39
- file_paths: list[str] | None = None
40
- payload: dict[str, Any] | None = None
41
- parent_session_id: str | None = None # Link to parent session (e.g. for subagents)
26
+ class EventRecord(BaseModel):
27
+ """
28
+ Event record for HtmlGraph tracking.
29
+
30
+ Uses Pydantic for automatic validation and serialization.
31
+ Immutable via ConfigDict(frozen=True).
32
+ """
33
+
34
+ model_config = ConfigDict(frozen=True)
35
+
36
+ event_id: str = Field(..., min_length=1, description="Unique event identifier")
37
+ timestamp: datetime = Field(..., description="Event timestamp")
38
+ session_id: str = Field(..., min_length=1, description="Session identifier")
39
+ agent: str = Field(..., description="Agent name (e.g., 'claude', 'gemini')")
40
+ tool: str = Field(..., description="Tool used (e.g., 'Bash', 'Edit', 'Read')")
41
+ summary: str = Field(..., description="Human-readable event summary")
42
+ success: bool = Field(..., description="Whether the operation succeeded")
43
+ feature_id: str | None = Field(None, description="Associated feature ID")
44
+ drift_score: float | None = Field(None, description="Context drift score")
45
+ start_commit: str | None = Field(None, description="Starting git commit hash")
46
+ continued_from: str | None = Field(
47
+ None, description="Previous session ID if continued"
48
+ )
49
+ work_type: str | None = Field(None, description="WorkType enum value")
50
+ session_status: str | None = Field(None, description="Session status")
51
+ file_paths: list[str] | None = Field(None, description="Files involved in event")
52
+ payload: dict[str, Any] | None = Field(None, description="Additional event data")
53
+ parent_session_id: str | None = Field(
54
+ None, description="Parent session ID for subagents"
55
+ )
56
+
42
57
  # Phase 1: Enhanced Event Data Schema for multi-AI delegation tracking
43
- delegated_to_ai: str | None = (
44
- None # "gemini", "codex", "copilot", "claude", or None
58
+ delegated_to_ai: str | None = Field(
59
+ None, description="AI delegate: 'gemini', 'codex', 'copilot', 'claude', or None"
60
+ )
61
+ task_id: str | None = Field(
62
+ None, description="Unique task ID for parallel tracking"
63
+ )
64
+ task_status: str | None = Field(
65
+ None,
66
+ description="Task status: 'pending', 'running', 'completed', 'failed', 'timeout'",
67
+ )
68
+ model_selected: str | None = Field(
69
+ None, description="Specific model (e.g., 'gemini-2.0-flash')"
70
+ )
71
+ complexity_level: str | None = Field(
72
+ None, description="Complexity: 'low', 'medium', 'high', 'very-high'"
73
+ )
74
+ budget_mode: str | None = Field(
75
+ None, description="Budget mode: 'free', 'balanced', 'performance'"
45
76
  )
46
- task_id: str | None = None # Unique task ID for parallel tracking
47
- task_status: str | None = (
48
- None # "pending", "running", "completed", "failed", "timeout"
77
+ execution_duration_seconds: float | None = Field(
78
+ None, description="Delegation execution time"
49
79
  )
50
- model_selected: str | None = None # Specific model (e.g., "gemini-2.0-flash")
51
- complexity_level: str | None = None # "low", "medium", "high", "very-high"
52
- budget_mode: str | None = None # "free", "balanced", "performance"
53
- execution_duration_seconds: float | None = None # How long delegation took
54
- tokens_estimated: int | None = None # Estimated token usage
55
- tokens_actual: int | None = None # Actual token usage
56
- cost_usd: float | None = None # Calculated cost
57
- task_findings: str | None = None # Results from delegated task
58
-
59
- def to_json(self) -> dict[str, Any]:
60
- return {
61
- "event_id": self.event_id,
62
- "timestamp": self.timestamp.isoformat(),
63
- "session_id": self.session_id,
64
- "agent": self.agent,
65
- "tool": self.tool,
66
- "summary": self.summary,
67
- "success": self.success,
68
- "feature_id": self.feature_id,
69
- "work_type": self.work_type,
70
- "drift_score": self.drift_score,
71
- "start_commit": self.start_commit,
72
- "continued_from": self.continued_from,
73
- "session_status": self.session_status,
74
- "file_paths": self.file_paths or [],
75
- "payload": self.payload,
76
- "parent_session_id": self.parent_session_id,
77
- # Delegation fields
78
- "delegated_to_ai": self.delegated_to_ai,
79
- "task_id": self.task_id,
80
- "task_status": self.task_status,
81
- "model_selected": self.model_selected,
82
- "complexity_level": self.complexity_level,
83
- "budget_mode": self.budget_mode,
84
- "execution_duration_seconds": self.execution_duration_seconds,
85
- "tokens_estimated": self.tokens_estimated,
86
- "tokens_actual": self.tokens_actual,
87
- "cost_usd": self.cost_usd,
88
- "task_findings": self.task_findings,
89
- }
80
+ tokens_estimated: int | None = Field(None, description="Estimated token usage")
81
+ tokens_actual: int | None = Field(None, description="Actual token usage")
82
+ cost_usd: float | None = Field(None, description="Calculated cost in USD")
83
+ task_findings: str | None = Field(None, description="Results from delegated task")
84
+
85
+ @field_validator("event_id", "session_id")
86
+ @classmethod
87
+ def validate_non_empty_string(cls, v: str) -> str:
88
+ """Ensure event_id and session_id are non-empty."""
89
+ if not v or not v.strip():
90
+ raise ValueError("Field must be a non-empty string")
91
+ return v
92
+
93
+ @field_serializer("timestamp")
94
+ def serialize_timestamp(self, timestamp: datetime) -> str:
95
+ """Serialize timestamp to ISO format string."""
96
+ return timestamp.isoformat()
97
+
98
+ @field_serializer("file_paths")
99
+ def serialize_file_paths(self, file_paths: list[str] | None) -> list[str]:
100
+ """Ensure file_paths is always a list (never None) in JSON output."""
101
+ return file_paths or []
90
102
 
91
103
 
92
104
  class JsonlEventLog:
@@ -104,7 +116,10 @@ class JsonlEventLog:
104
116
 
105
117
  def append(self, record: EventRecord) -> Path:
106
118
  path = self.path_for_session(record.session_id)
107
- line = json.dumps(record.to_json(), ensure_ascii=False, default=str) + "\n"
119
+ line = (
120
+ json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
121
+ + "\n"
122
+ )
108
123
  path.parent.mkdir(parents=True, exist_ok=True)
109
124
 
110
125
  # Best-effort dedupe: some producers (e.g. git hooks) may retry or be chained.
@@ -1,8 +1,9 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Helpers to migrate legacy session HTML activity logs to JSONL event logs.
3
5
  """
4
6
 
5
- from __future__ import annotations
6
7
 
7
8
  import json
8
9
  from pathlib import Path
htmlgraph/file_watcher.py CHANGED
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  File watcher for automatic graph reloading.
3
7
 
@@ -67,7 +71,7 @@ class GraphFileHandler(FileSystemEventHandler):
67
71
 
68
72
  def _trigger_reload(self) -> None:
69
73
  """Trigger a reload after debounce delay."""
70
- print(f"[FileWatcher] Reloading collection: {self.collection}")
74
+ logger.info(f"[FileWatcher] Reloading collection: {self.collection}")
71
75
  self.reload_callback()
72
76
 
73
77
  def _debounced_reload(self) -> None:
@@ -87,7 +91,7 @@ class GraphFileHandler(FileSystemEventHandler):
87
91
  if not self._is_relevant_file(str(event.src_path)):
88
92
  return
89
93
 
90
- print(
94
+ logger.debug(
91
95
  f"[FileWatcher] {self.collection}: File created - {Path(str(event.src_path)).name}"
92
96
  )
93
97
  self._debounced_reload()
@@ -101,7 +105,7 @@ class GraphFileHandler(FileSystemEventHandler):
101
105
  if not self._is_relevant_file(str(event.src_path)):
102
106
  return
103
107
 
104
- print(
108
+ logger.debug(
105
109
  f"[FileWatcher] {self.collection}: File modified - {Path(str(event.src_path)).name}"
106
110
  )
107
111
  self._debounced_reload()
@@ -115,7 +119,7 @@ class GraphFileHandler(FileSystemEventHandler):
115
119
  if not self._is_relevant_file(str(event.src_path)):
116
120
  return
117
121
 
118
- print(
122
+ logger.debug(
119
123
  f"[FileWatcher] {self.collection}: File deleted - {Path(str(event.src_path)).name}"
120
124
  )
121
125
  self._debounced_reload()
@@ -146,7 +150,7 @@ class GraphWatcher:
146
150
 
147
151
  def start(self) -> None:
148
152
  """Start watching for file changes."""
149
- print(
153
+ logger.info(
150
154
  f"[FileWatcher] Starting file watcher for {len(self.collections)} collections..."
151
155
  )
152
156
 
@@ -160,7 +164,7 @@ class GraphWatcher:
160
164
  def reload() -> None:
161
165
  graph = self.get_graph_callback(coll)
162
166
  count = graph.reload()
163
- print(f"[FileWatcher] Reloaded {count} nodes in {coll}")
167
+ logger.info(f"[FileWatcher] Reloaded {count} nodes in {coll}")
164
168
 
165
169
  return reload
166
170
 
@@ -173,11 +177,11 @@ class GraphWatcher:
173
177
  self.observer.schedule(handler, str(collection_dir), recursive=recursive)
174
178
 
175
179
  self.observer.start()
176
- print(f"[FileWatcher] Watching {self.graph_dir} for changes...")
180
+ logger.info(f"[FileWatcher] Watching {self.graph_dir} for changes...")
177
181
 
178
182
  def stop(self) -> None:
179
183
  """Stop watching for file changes."""
180
- print("[FileWatcher] Stopping file watcher...")
184
+ logger.info("[FileWatcher] Stopping file watcher...")
181
185
  self.observer.stop()
182
186
  self.observer.join()
183
187
 
htmlgraph/find_api.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  BeautifulSoup-style Find API for HtmlGraph.
3
5
 
@@ -22,7 +24,6 @@ Example:
22
24
  nodes = graph.find_all(properties__effort__gt=8)
23
25
  """
24
26
 
25
- from __future__ import annotations
26
27
 
27
28
  import re
28
29
  from collections.abc import Callable
htmlgraph/git_events.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Git event logging for HtmlGraph.
3
5
 
@@ -10,7 +12,6 @@ Design goals:
10
12
  - Analytics-friendly: schema compatible with EventRecord/AnalyticsIndex
11
13
  """
12
14
 
13
- from __future__ import annotations
14
15
 
15
16
  import os
16
17
  import re
@@ -277,7 +278,10 @@ def _append_event(
277
278
  p.parent.mkdir(parents=True, exist_ok=True)
278
279
  with p.open("a", encoding="utf-8") as f:
279
280
  f.write(
280
- json.dumps(record.to_json(), ensure_ascii=False, default=str) + "\n"
281
+ json.dumps(
282
+ record.model_dump(mode="json"), ensure_ascii=False, default=str
283
+ )
284
+ + "\n"
281
285
  )
282
286
  return
283
287
 
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  CIGS PreToolUse Enforcer - Enhanced Orchestrator Enforcement with Escalation
3
7
 
@@ -309,7 +313,7 @@ def enforce_cigs_pretool(tool_input: dict[str, Any]) -> dict[str, Any]:
309
313
  return enforcer.enforce(tool, params)
310
314
  except Exception as e:
311
315
  # Graceful degradation - allow on error
312
- print(f"Warning: CIGS enforcement error: {e}", file=sys.stderr)
316
+ logger.warning(f"Warning: CIGS enforcement error: {e}")
313
317
  return {
314
318
  "hookSpecificOutput": {
315
319
  "hookEventName": "PreToolUse",
@@ -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
 
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Orchestrator Reflection Module
3
7