htmlgraph 0.26.25__py3-none-any.whl → 0.27.1__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 (175) 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/cost_monitor.py +664 -0
  7. htmlgraph/analytics/cross_session.py +13 -9
  8. htmlgraph/analytics/dependency.py +10 -6
  9. htmlgraph/analytics/strategic/__init__.py +80 -0
  10. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  11. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  12. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  13. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  14. htmlgraph/analytics/work_type.py +15 -11
  15. htmlgraph/analytics_index.py +2 -1
  16. htmlgraph/api/cost_alerts_websocket.py +416 -0
  17. htmlgraph/api/main.py +167 -62
  18. htmlgraph/api/websocket.py +538 -0
  19. htmlgraph/attribute_index.py +2 -1
  20. htmlgraph/builders/base.py +2 -1
  21. htmlgraph/builders/bug.py +2 -1
  22. htmlgraph/builders/chore.py +2 -1
  23. htmlgraph/builders/epic.py +2 -1
  24. htmlgraph/builders/feature.py +2 -1
  25. htmlgraph/builders/insight.py +2 -1
  26. htmlgraph/builders/metric.py +2 -1
  27. htmlgraph/builders/pattern.py +2 -1
  28. htmlgraph/builders/phase.py +2 -1
  29. htmlgraph/builders/spike.py +2 -1
  30. htmlgraph/builders/track.py +2 -1
  31. htmlgraph/cli/analytics.py +2 -1
  32. htmlgraph/cli/base.py +2 -1
  33. htmlgraph/cli/core.py +2 -1
  34. htmlgraph/cli/main.py +2 -1
  35. htmlgraph/cli/models.py +2 -1
  36. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  37. htmlgraph/cli/work/__init__.py +2 -1
  38. htmlgraph/cli/work/browse.py +2 -1
  39. htmlgraph/cli/work/features.py +2 -1
  40. htmlgraph/cli/work/orchestration.py +2 -1
  41. htmlgraph/cli/work/report.py +2 -1
  42. htmlgraph/cli/work/sessions.py +2 -1
  43. htmlgraph/cli/work/snapshot.py +2 -1
  44. htmlgraph/cli/work/tracks.py +2 -1
  45. htmlgraph/collections/base.py +10 -5
  46. htmlgraph/collections/bug.py +2 -1
  47. htmlgraph/collections/chore.py +2 -1
  48. htmlgraph/collections/epic.py +2 -1
  49. htmlgraph/collections/feature.py +2 -1
  50. htmlgraph/collections/insight.py +2 -1
  51. htmlgraph/collections/metric.py +2 -1
  52. htmlgraph/collections/pattern.py +2 -1
  53. htmlgraph/collections/phase.py +2 -1
  54. htmlgraph/collections/session.py +12 -7
  55. htmlgraph/collections/spike.py +6 -1
  56. htmlgraph/collections/task_delegation.py +7 -2
  57. htmlgraph/collections/todo.py +2 -1
  58. htmlgraph/collections/traces.py +15 -10
  59. htmlgraph/config/cost_models.json +56 -0
  60. htmlgraph/context_analytics.py +2 -1
  61. htmlgraph/db/schema.py +67 -6
  62. htmlgraph/dependency_models.py +2 -1
  63. htmlgraph/edge_index.py +2 -1
  64. htmlgraph/event_log.py +83 -64
  65. htmlgraph/event_migration.py +2 -1
  66. htmlgraph/file_watcher.py +12 -8
  67. htmlgraph/find_api.py +2 -1
  68. htmlgraph/git_events.py +6 -2
  69. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  70. htmlgraph/hooks/drift_handler.py +3 -3
  71. htmlgraph/hooks/event_tracker.py +40 -61
  72. htmlgraph/hooks/installer.py +5 -1
  73. htmlgraph/hooks/orchestrator.py +4 -0
  74. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  75. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  76. htmlgraph/hooks/posttooluse.py +4 -0
  77. htmlgraph/hooks/prompt_analyzer.py +5 -5
  78. htmlgraph/hooks/session_handler.py +2 -1
  79. htmlgraph/hooks/session_summary.py +6 -2
  80. htmlgraph/hooks/validator.py +8 -4
  81. htmlgraph/ids.py +2 -1
  82. htmlgraph/learning.py +2 -1
  83. htmlgraph/mcp_server.py +2 -1
  84. htmlgraph/operations/analytics.py +2 -1
  85. htmlgraph/operations/bootstrap.py +2 -1
  86. htmlgraph/operations/events.py +2 -1
  87. htmlgraph/operations/fastapi_server.py +2 -1
  88. htmlgraph/operations/hooks.py +2 -1
  89. htmlgraph/operations/initialization.py +2 -1
  90. htmlgraph/operations/server.py +2 -1
  91. htmlgraph/orchestration/claude_launcher.py +23 -20
  92. htmlgraph/orchestration/command_builder.py +2 -1
  93. htmlgraph/orchestration/headless_spawner.py +6 -2
  94. htmlgraph/orchestration/model_selection.py +7 -3
  95. htmlgraph/orchestration/plugin_manager.py +24 -19
  96. htmlgraph/orchestration/spawners/claude.py +5 -2
  97. htmlgraph/orchestration/spawners/codex.py +12 -19
  98. htmlgraph/orchestration/spawners/copilot.py +13 -18
  99. htmlgraph/orchestration/spawners/gemini.py +12 -19
  100. htmlgraph/orchestration/subprocess_runner.py +6 -3
  101. htmlgraph/orchestration/task_coordination.py +16 -8
  102. htmlgraph/orchestrator.py +2 -1
  103. htmlgraph/parallel.py +2 -1
  104. htmlgraph/query_builder.py +2 -1
  105. htmlgraph/reflection.py +2 -1
  106. htmlgraph/refs.py +2 -1
  107. htmlgraph/repo_hash.py +2 -1
  108. htmlgraph/repositories/__init__.py +292 -0
  109. htmlgraph/repositories/analytics_repository.py +455 -0
  110. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  111. htmlgraph/repositories/feature_repository.py +581 -0
  112. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  113. htmlgraph/repositories/feature_repository_memory.py +607 -0
  114. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  115. htmlgraph/repositories/filter_service.py +620 -0
  116. htmlgraph/repositories/filter_service_standard.py +445 -0
  117. htmlgraph/repositories/shared_cache.py +621 -0
  118. htmlgraph/repositories/shared_cache_memory.py +395 -0
  119. htmlgraph/repositories/track_repository.py +552 -0
  120. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  121. htmlgraph/repositories/track_repository_memory.py +508 -0
  122. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  123. htmlgraph/sdk/__init__.py +398 -0
  124. htmlgraph/sdk/__init__.pyi +14 -0
  125. htmlgraph/sdk/analytics/__init__.py +19 -0
  126. htmlgraph/sdk/analytics/engine.py +155 -0
  127. htmlgraph/sdk/analytics/helpers.py +178 -0
  128. htmlgraph/sdk/analytics/registry.py +109 -0
  129. htmlgraph/sdk/base.py +484 -0
  130. htmlgraph/sdk/constants.py +216 -0
  131. htmlgraph/sdk/core.pyi +308 -0
  132. htmlgraph/sdk/discovery.py +120 -0
  133. htmlgraph/sdk/help/__init__.py +12 -0
  134. htmlgraph/sdk/help/mixin.py +699 -0
  135. htmlgraph/sdk/mixins/__init__.py +15 -0
  136. htmlgraph/sdk/mixins/attribution.py +113 -0
  137. htmlgraph/sdk/mixins/mixin.py +410 -0
  138. htmlgraph/sdk/operations/__init__.py +12 -0
  139. htmlgraph/sdk/operations/mixin.py +427 -0
  140. htmlgraph/sdk/orchestration/__init__.py +17 -0
  141. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  142. htmlgraph/sdk/orchestration/spawner.py +204 -0
  143. htmlgraph/sdk/planning/__init__.py +19 -0
  144. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  145. htmlgraph/sdk/planning/mixin.py +211 -0
  146. htmlgraph/sdk/planning/parallel.py +186 -0
  147. htmlgraph/sdk/planning/queue.py +210 -0
  148. htmlgraph/sdk/planning/recommendations.py +87 -0
  149. htmlgraph/sdk/planning/smart_planning.py +319 -0
  150. htmlgraph/sdk/session/__init__.py +19 -0
  151. htmlgraph/sdk/session/continuity.py +57 -0
  152. htmlgraph/sdk/session/handoff.py +110 -0
  153. htmlgraph/sdk/session/info.py +309 -0
  154. htmlgraph/sdk/session/manager.py +103 -0
  155. htmlgraph/sdk/strategic/__init__.py +26 -0
  156. htmlgraph/sdk/strategic/mixin.py +563 -0
  157. htmlgraph/server.py +21 -17
  158. htmlgraph/session_warning.py +2 -1
  159. htmlgraph/sessions/handoff.py +4 -3
  160. htmlgraph/system_prompts.py +2 -1
  161. htmlgraph/track_builder.py +2 -1
  162. htmlgraph/transcript.py +2 -1
  163. htmlgraph/watch.py +2 -1
  164. htmlgraph/work_type_utils.py +2 -1
  165. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/METADATA +1 -1
  166. htmlgraph-0.27.1.dist-info/RECORD +332 -0
  167. htmlgraph/sdk.py +0 -3500
  168. htmlgraph-0.26.25.dist-info/RECORD +0 -274
  169. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/dashboard.html +0 -0
  170. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/styles.css +0 -0
  171. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  172. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  173. {htmlgraph-0.26.25.data → htmlgraph-0.27.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  174. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/WHEEL +0 -0
  175. {htmlgraph-0.26.25.dist-info → htmlgraph-0.27.1.dist-info}/entry_points.txt +0 -0
htmlgraph/db/schema.py CHANGED
@@ -141,8 +141,15 @@ class HtmlGraphDB:
141
141
 
142
142
  # Migration: rename 'agent' to 'agent_assigned' if needed
143
143
  if "agent" in columns and "agent_assigned" not in columns:
144
- cursor.execute("ALTER TABLE sessions RENAME COLUMN agent TO agent_assigned")
145
- logger.info("Migrated sessions.agent -> sessions.agent_assigned")
144
+ try:
145
+ cursor.execute(
146
+ "ALTER TABLE sessions RENAME COLUMN agent TO agent_assigned"
147
+ )
148
+ logger.info("Migrated sessions.agent -> sessions.agent_assigned")
149
+ except sqlite3.OperationalError as e:
150
+ logger.debug(f"Could not rename column: {e}")
151
+ # Column may already exist
152
+ pass
146
153
 
147
154
  # Add missing columns with defaults
148
155
  # Note: SQLite doesn't allow CURRENT_TIMESTAMP in ALTER TABLE, so we use NULL
@@ -169,6 +176,10 @@ class HtmlGraphDB:
169
176
  ("blockers", "TEXT"), # JSON array of blocker strings
170
177
  ("recommended_context", "TEXT"), # JSON array of file paths
171
178
  ("continued_from", "TEXT"), # Previous session ID
179
+ # Phase 3.1: Real-time cost monitoring
180
+ ("cost_budget", "REAL"), # Budget in USD for this session
181
+ ("cost_threshold_breached", "INTEGER DEFAULT 0"), # Whether budget exceeded
182
+ ("predicted_cost", "REAL DEFAULT 0.0"), # Predicted final cost
172
183
  ]
173
184
 
174
185
  # Refresh columns after potential rename
@@ -252,7 +263,7 @@ class HtmlGraphDB:
252
263
  title TEXT NOT NULL,
253
264
  description TEXT,
254
265
  status TEXT NOT NULL DEFAULT 'todo' CHECK(
255
- status IN ('todo', 'in_progress', 'blocked', 'done', 'cancelled')
266
+ status IN ('todo', 'in-progress', 'blocked', 'done', 'active', 'ended', 'stale')
256
267
  ),
257
268
  priority TEXT DEFAULT 'medium' CHECK(
258
269
  priority IN ('low', 'medium', 'high', 'critical')
@@ -267,7 +278,7 @@ class HtmlGraphDB:
267
278
  parent_feature_id TEXT,
268
279
  tags JSON,
269
280
  metadata JSON,
270
- FOREIGN KEY (track_id) REFERENCES tracks(track_id),
281
+ FOREIGN KEY (track_id) REFERENCES tracks(id),
271
282
  FOREIGN KEY (parent_feature_id) REFERENCES features(id)
272
283
  )
273
284
  """)
@@ -302,6 +313,9 @@ class HtmlGraphDB:
302
313
  blockers JSON,
303
314
  recommended_context JSON,
304
315
  continued_from TEXT,
316
+ cost_budget REAL,
317
+ cost_threshold_breached INTEGER DEFAULT 0,
318
+ predicted_cost REAL DEFAULT 0.0,
305
319
  FOREIGN KEY (parent_session_id) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE,
306
320
  FOREIGN KEY (parent_event_id) REFERENCES agent_events(event_id) ON DELETE SET NULL ON UPDATE CASCADE,
307
321
  FOREIGN KEY (continued_from) REFERENCES sessions(session_id) ON DELETE SET NULL ON UPDATE CASCADE
@@ -311,14 +325,15 @@ class HtmlGraphDB:
311
325
  # 4. TRACKS TABLE - Multi-feature initiatives
312
326
  cursor.execute("""
313
327
  CREATE TABLE IF NOT EXISTS tracks (
314
- track_id TEXT PRIMARY KEY,
328
+ id TEXT PRIMARY KEY,
329
+ type TEXT DEFAULT 'track',
315
330
  title TEXT NOT NULL,
316
331
  description TEXT,
317
332
  priority TEXT DEFAULT 'medium' CHECK(
318
333
  priority IN ('low', 'medium', 'high', 'critical')
319
334
  ),
320
335
  status TEXT NOT NULL DEFAULT 'todo' CHECK(
321
- status IN ('todo', 'in_progress', 'blocked', 'done', 'cancelled')
336
+ status IN ('todo', 'in-progress', 'blocked', 'done', 'active', 'ended', 'stale')
322
337
  ),
323
338
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
324
339
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -436,6 +451,39 @@ class HtmlGraphDB:
436
451
  )
437
452
  """)
438
453
 
454
+ # 11. COST_EVENTS TABLE - Phase 3.1: Real-time cost monitoring & alerts
455
+ cursor.execute("""
456
+ CREATE TABLE IF NOT EXISTS cost_events (
457
+ event_id TEXT PRIMARY KEY,
458
+ session_id TEXT NOT NULL,
459
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
460
+
461
+ -- Token tracking
462
+ tool_name TEXT,
463
+ model TEXT,
464
+ input_tokens INTEGER DEFAULT 0,
465
+ output_tokens INTEGER DEFAULT 0,
466
+ total_tokens INTEGER DEFAULT 0,
467
+ cost_usd REAL DEFAULT 0.0,
468
+
469
+ -- Agent tracking
470
+ agent_id TEXT,
471
+ subagent_type TEXT,
472
+
473
+ -- Alert tracking
474
+ alert_type TEXT,
475
+ message TEXT,
476
+ current_cost_usd REAL,
477
+ budget_usd REAL,
478
+ predicted_cost_usd REAL,
479
+ severity TEXT,
480
+ acknowledged BOOLEAN DEFAULT 0,
481
+
482
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
483
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
484
+ )
485
+ """)
486
+
439
487
  # 9. Create indexes for performance
440
488
  self._create_indexes(cursor)
441
489
 
@@ -529,6 +577,19 @@ class HtmlGraphDB:
529
577
  "CREATE INDEX IF NOT EXISTS idx_handoff_from_session ON handoff_tracking(from_session_id, created_at DESC)",
530
578
  "CREATE INDEX IF NOT EXISTS idx_handoff_to_session ON handoff_tracking(to_session_id, resumed_at DESC)",
531
579
  "CREATE INDEX IF NOT EXISTS idx_handoff_rating ON handoff_tracking(user_rating, created_at DESC)",
580
+ # cost_events indexes - optimized for real-time cost monitoring & alerts
581
+ # Pattern: WHERE session_id ORDER BY timestamp DESC (cost timeline)
582
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_session_ts ON cost_events(session_id, timestamp DESC)",
583
+ # Pattern: WHERE alert_type (alert filtering)
584
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_alert_type ON cost_events(alert_type, timestamp DESC)",
585
+ # Pattern: WHERE model GROUP BY (cost breakdown)
586
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_model ON cost_events(model, session_id)",
587
+ # Pattern: WHERE tool_name GROUP BY (tool cost analysis)
588
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_tool ON cost_events(tool_name, session_id)",
589
+ # Pattern: WHERE severity (alert severity filtering)
590
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_severity ON cost_events(severity, timestamp DESC)",
591
+ # Pattern: Timestamp range queries for predictions
592
+ "CREATE INDEX IF NOT EXISTS idx_cost_events_timestamp ON cost_events(timestamp DESC)",
532
593
  ]
533
594
 
534
595
  for index_sql in indexes:
@@ -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,98 @@ 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
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 []
58
102
 
59
103
  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
- }
104
+ """Convert EventRecord to JSON-serializable dictionary."""
105
+ return self.model_dump(mode="json")
90
106
 
91
107
 
92
108
  class JsonlEventLog:
@@ -104,7 +120,10 @@ class JsonlEventLog:
104
120
 
105
121
  def append(self, record: EventRecord) -> Path:
106
122
  path = self.path_for_session(record.session_id)
107
- line = json.dumps(record.to_json(), ensure_ascii=False, default=str) + "\n"
123
+ line = (
124
+ json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
125
+ + "\n"
126
+ )
108
127
  path.parent.mkdir(parents=True, exist_ok=True)
109
128
 
110
129
  # 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", {})