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
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Feature collection for managing feature work items.
3
5
 
4
6
  Extends BaseCollection with feature-specific builder support.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  SessionInsight collection for managing session insights.
3
5
 
4
6
  Extends BaseCollection with insight-specific query methods.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  AggregatedMetric collection for managing aggregated metrics.
3
5
 
4
6
  Extends BaseCollection with metric-specific query methods.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Pattern collection for managing workflow patterns.
3
5
 
4
6
  Extends BaseCollection with pattern-specific query methods.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Phase collection for managing project phase work items.
3
5
 
4
6
  Extends BaseCollection with phase-specific builder support.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11
 
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  SessionCollection - Session state and management interface.
3
9
 
@@ -13,7 +19,6 @@ Integration with SessionStart hook:
13
19
  sdk.sessions.setup_environment_variables(state)
14
20
  """
15
21
 
16
- from __future__ import annotations
17
22
 
18
23
  from typing import TYPE_CHECKING
19
24
 
@@ -82,9 +87,9 @@ class SessionCollection(BaseCollection):
82
87
  Example:
83
88
  >>> sdk = SDK()
84
89
  >>> state = sdk.sessions.get_current_state()
85
- >>> print(f"Session: {state['session_id']}")
86
- >>> print(f"Post-compact: {state['is_post_compact']}")
87
- >>> print(f"Delegation enabled: {state['delegation_enabled']}")
90
+ >>> logger.info(f"Session: {state['session_id']}")
91
+ >>> logger.info(f"Post-compact: {state['is_post_compact']}")
92
+ >>> logger.info(f"Delegation enabled: {state['delegation_enabled']}")
88
93
  """
89
94
  return self._state_manager.get_current_state()
90
95
 
@@ -116,8 +121,8 @@ class SessionCollection(BaseCollection):
116
121
  >>> sdk = SDK()
117
122
  >>> state = sdk.sessions.get_current_state()
118
123
  >>> env_vars = sdk.sessions.setup_environment_variables(state)
119
- >>> print(f"CLAUDE_SESSION_ID: {env_vars['CLAUDE_SESSION_ID']}")
120
- >>> print(f"CLAUDE_DELEGATION_ENABLED: {env_vars['CLAUDE_DELEGATION_ENABLED']}")
124
+ >>> logger.info(f"CLAUDE_SESSION_ID: {env_vars['CLAUDE_SESSION_ID']}")
125
+ >>> logger.info(f"CLAUDE_DELEGATION_ENABLED: {env_vars['CLAUDE_DELEGATION_ENABLED']}")
121
126
  """
122
127
  return self._state_manager.setup_environment_variables(
123
128
  session_state=session_state, auto_detect_compact=auto_detect_compact
@@ -168,7 +173,7 @@ class SessionCollection(BaseCollection):
168
173
  Example:
169
174
  >>> sdk = SDK()
170
175
  >>> if sdk.sessions.detect_compact_automatically():
171
- ... print("This is a post-compact session")
176
+ ... logger.info("This is a post-compact session")
172
177
  """
173
178
  return self._state_manager.detect_compact_automatically()
174
179
 
@@ -1,10 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Spike collection for managing investigation and research spikes.
3
9
 
4
10
  Extends BaseCollection with spike-specific builder support.
5
11
  """
6
12
 
7
- from __future__ import annotations
8
13
 
9
14
  from datetime import datetime
10
15
  from typing import TYPE_CHECKING
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Task delegation collection for tracking spawned agent work.
3
9
 
@@ -12,7 +18,6 @@ Captures observability data for Task() calls:
12
18
  This data proves multi-agent orchestration works and enables dashboard attribution.
13
19
  """
14
20
 
15
- from __future__ import annotations
16
21
 
17
22
  from datetime import datetime
18
23
  from typing import TYPE_CHECKING, Any
@@ -41,7 +46,7 @@ class TaskDelegationCollection(BaseCollection["TaskDelegationCollection"]):
41
46
  >>> sdk = SDK(agent="orchestrator")
42
47
  >>> delegations = sdk.task_delegations.where(agent_type="codex-spawner")
43
48
  >>> for d in delegations:
44
- ... print(f"{d.agent_type}: {d.task_description} ({d.duration_seconds}s)")
49
+ ... logger.info(f"{d.agent_type}: {d.task_description} ({d.duration_seconds}s)")
45
50
  >>>
46
51
  >>> # Get all delegations for a specific agent
47
52
  >>> gemini_work = sdk.task_delegations.where(agent_type="gemini-spawner")
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Todo collection for managing persistent todo items.
3
5
 
@@ -8,7 +10,6 @@ Unlike other collections, TodoCollection provides:
8
10
  - Bulk sync from TodoWrite format
9
11
  """
10
12
 
11
- from __future__ import annotations
12
13
 
13
14
  from datetime import datetime
14
15
  from typing import TYPE_CHECKING, Any
@@ -64,6 +65,18 @@ class TodoCollection:
64
65
 
65
66
  # Cache for loaded todos (lazy-loaded)
66
67
  self._todos: dict[str, Todo] | None = None
68
+ self._ref_manager: Any = None # Set by SDK during initialization
69
+
70
+ def set_ref_manager(self, ref_manager: Any) -> None:
71
+ """
72
+ Set the ref manager for this collection.
73
+
74
+ Called by SDK during initialization to enable short ref support.
75
+
76
+ Args:
77
+ ref_manager: RefManager instance from SDK
78
+ """
79
+ self._ref_manager = ref_manager
67
80
 
68
81
  def _ensure_loaded(self) -> dict[str, Todo]:
69
82
  """Load todos from disk if not cached."""
@@ -1,3 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
1
7
  """
2
8
  Tool Execution Traces Collection
3
9
 
@@ -11,21 +17,20 @@ Example:
11
17
  >>> # Get traces for current session
12
18
  >>> traces = sdk.traces.get_traces(session_id="sess-abc123")
13
19
  >>> for trace in traces:
14
- ... print(f"{trace.tool_name}: {trace.duration_ms}ms")
20
+ ... logger.info(f"{trace.tool_name}: {trace.duration_ms}ms")
15
21
  >>>
16
22
  >>> # Find slow tool calls
17
23
  >>> slow = sdk.traces.get_slow_traces(threshold_ms=1000)
18
24
  >>>
19
25
  >>> # Get hierarchical view (parent-child relationships)
20
26
  >>> tree = sdk.traces.get_trace_tree(trace_id="trace-xyz")
21
- >>> print(f"Root: {tree.root.tool_name}")
22
- >>> print(f"Children: {len(tree.children)}")
27
+ >>> logger.info(f"Root: {tree.root.tool_name}")
28
+ >>> logger.info(f"Children: {len(tree.children)}")
23
29
  >>>
24
30
  >>> # Get error traces for debugging
25
31
  >>> errors = sdk.traces.get_error_traces(session_id="sess-abc123")
26
32
  """
27
33
 
28
- from __future__ import annotations
29
34
 
30
35
  from dataclasses import dataclass
31
36
  from datetime import datetime, timezone
@@ -245,7 +250,7 @@ class TraceCollection:
245
250
 
246
251
  return None
247
252
  except Exception as e:
248
- print(f"Error getting trace {tool_use_id}: {e}")
253
+ logger.info(f"Error getting trace {tool_use_id}: {e}")
249
254
  return None
250
255
 
251
256
  def get_traces(
@@ -302,7 +307,7 @@ class TraceCollection:
302
307
  rows = cursor.fetchall()
303
308
  return [self._row_to_trace(row) for row in rows]
304
309
  except Exception as e:
305
- print(f"Error getting traces for session {session_id}: {e}")
310
+ logger.info(f"Error getting traces for session {session_id}: {e}")
306
311
  return []
307
312
 
308
313
  def get_traces_by_tool(self, tool_name: str, limit: int = 100) -> list[TraceRecord]:
@@ -337,7 +342,7 @@ class TraceCollection:
337
342
  rows = cursor.fetchall()
338
343
  return [self._row_to_trace(row) for row in rows]
339
344
  except Exception as e:
340
- print(f"Error getting traces for tool {tool_name}: {e}")
345
+ logger.info(f"Error getting traces for tool {tool_name}: {e}")
341
346
  return []
342
347
 
343
348
  def get_trace_tree(self, trace_id: str) -> TraceTree | None:
@@ -403,7 +408,7 @@ class TraceCollection:
403
408
 
404
409
  return build_tree(root)
405
410
  except Exception as e:
406
- print(f"Error getting trace tree for {trace_id}: {e}")
411
+ logger.info(f"Error getting trace tree for {trace_id}: {e}")
407
412
  return None
408
413
 
409
414
  def get_slow_traces(self, threshold_ms: int, limit: int = 100) -> list[TraceRecord]:
@@ -440,7 +445,7 @@ class TraceCollection:
440
445
  rows = cursor.fetchall()
441
446
  return [self._row_to_trace(row) for row in rows]
442
447
  except Exception as e:
443
- print(f"Error getting slow traces: {e}")
448
+ logger.info(f"Error getting slow traces: {e}")
444
449
  return []
445
450
 
446
451
  def get_error_traces(self, session_id: str, limit: int = 100) -> list[TraceRecord]:
@@ -478,5 +483,5 @@ class TraceCollection:
478
483
  rows = cursor.fetchall()
479
484
  return [self._row_to_trace(row) for row in rows]
480
485
  except Exception as e:
481
- print(f"Error getting error traces: {e}")
486
+ logger.info(f"Error getting error traces: {e}")
482
487
  return []
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Context Analytics for HtmlGraph
3
5
 
@@ -7,7 +9,6 @@ Provides hierarchical context usage tracking and analytics:
7
9
  Enables drill-down analysis of where context was consumed.
8
10
  """
9
11
 
10
- from __future__ import annotations
11
12
 
12
13
  from dataclasses import dataclass, field
13
14
  from typing import TYPE_CHECKING, Any
htmlgraph/converter.py CHANGED
@@ -520,6 +520,17 @@ def html_to_session(filepath: Path | str) -> Session:
520
520
  if blockers:
521
521
  data["blockers"] = blockers
522
522
 
523
+ # Parse recommended context files
524
+ recommended_context = []
525
+ for li in parser.query(
526
+ "section[data-handoff] div[data-recommended-context] li"
527
+ ):
528
+ file_path = li.to_text().strip()
529
+ if file_path:
530
+ recommended_context.append(file_path)
531
+ if recommended_context:
532
+ data["recommended_context"] = recommended_context
533
+
523
534
  # Parse activity log
524
535
  activity_log = []
525
536
  for li in parser.query("section[data-activity-log] ol li"):
@@ -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",