htmlgraph 0.9.3__py3-none-any.whl → 0.27.5__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 (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
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,54 +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
- from typing import Any, TYPE_CHECKING
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
19
21
 
20
22
  if TYPE_CHECKING:
21
- from htmlgraph.models import WorkType
22
-
23
-
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
23
+ pass
24
+
25
+
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
+
57
+ # Phase 1: Enhanced Event Data Schema for multi-AI delegation tracking
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'"
76
+ )
77
+ execution_duration_seconds: float | None = Field(
78
+ None, description="Delegation execution time"
79
+ )
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 []
41
102
 
42
103
  def to_json(self) -> dict[str, Any]:
43
- return {
44
- "event_id": self.event_id,
45
- "timestamp": self.timestamp.isoformat(),
46
- "session_id": self.session_id,
47
- "agent": self.agent,
48
- "tool": self.tool,
49
- "summary": self.summary,
50
- "success": self.success,
51
- "feature_id": self.feature_id,
52
- "work_type": self.work_type,
53
- "drift_score": self.drift_score,
54
- "start_commit": self.start_commit,
55
- "continued_from": self.continued_from,
56
- "session_status": self.session_status,
57
- "file_paths": self.file_paths or [],
58
- "payload": self.payload,
59
- }
104
+ """Convert EventRecord to JSON-serializable dictionary."""
105
+ return self.model_dump(mode="json")
60
106
 
61
107
 
62
108
  class JsonlEventLog:
@@ -74,7 +120,10 @@ class JsonlEventLog:
74
120
 
75
121
  def append(self, record: EventRecord) -> Path:
76
122
  path = self.path_for_session(record.session_id)
77
- 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
+ )
78
127
  path.parent.mkdir(parents=True, exist_ok=True)
79
128
 
80
129
  # Best-effort dedupe: some producers (e.g. git hooks) may retry or be chained.
@@ -106,7 +155,7 @@ class JsonlEventLog:
106
155
  f.write(line)
107
156
  return path
108
157
 
109
- def iter_events(self):
158
+ def iter_events(self) -> Any:
110
159
  """
111
160
  Yield (path, event_dict) for all events across all JSONL files.
112
161
  Skips malformed lines.
@@ -129,10 +178,7 @@ class JsonlEventLog:
129
178
  continue
130
179
 
131
180
  def get_session_events(
132
- self,
133
- session_id: str,
134
- limit: int | None = None,
135
- offset: int = 0
181
+ self, session_id: str, limit: int | None = None, offset: int = 0
136
182
  ) -> list[dict[str, Any]]:
137
183
  """
138
184
  Get events for a specific session with pagination.
@@ -177,7 +223,7 @@ class JsonlEventLog:
177
223
  tool: str | None = None,
178
224
  feature_id: str | None = None,
179
225
  since: Any = None, # datetime or ISO string
180
- limit: int | None = None
226
+ limit: int | None = None,
181
227
  ) -> list[dict[str, Any]]:
182
228
  """
183
229
  Query events with filters.
@@ -198,7 +244,7 @@ class JsonlEventLog:
198
244
  since_dt = None
199
245
  if since:
200
246
  if isinstance(since, str):
201
- since_dt = datetime.fromisoformat(since.replace('Z', '+00:00'))
247
+ since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
202
248
  else:
203
249
  since_dt = since
204
250
 
@@ -213,19 +259,21 @@ class JsonlEventLog:
213
259
  filtered: list[dict[str, Any]] = []
214
260
  for evt in events:
215
261
  # Tool filter
216
- if tool and evt.get('tool') != tool:
262
+ if tool and evt.get("tool") != tool:
217
263
  continue
218
264
 
219
265
  # Feature filter
220
- if feature_id and evt.get('feature_id') != feature_id:
266
+ if feature_id and evt.get("feature_id") != feature_id:
221
267
  continue
222
268
 
223
269
  # Timestamp filter
224
270
  if since_dt:
225
- evt_time_str = evt.get('timestamp')
271
+ evt_time_str = evt.get("timestamp")
226
272
  if evt_time_str and isinstance(evt_time_str, str):
227
273
  try:
228
- evt_time = datetime.fromisoformat(evt_time_str.replace('Z', '+00:00'))
274
+ evt_time = datetime.fromisoformat(
275
+ evt_time_str.replace("Z", "+00:00")
276
+ )
229
277
  if evt_time < since_dt:
230
278
  continue
231
279
  except (ValueError, AttributeError):
@@ -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
@@ -43,9 +44,15 @@ def export_sessions_to_jsonl(
43
44
  # Serialize as one JSON object per line, oldest -> newest.
44
45
  lines: list[str] = []
45
46
  for entry in session.activity_log:
46
- payload: dict[str, Any] | None = entry.payload if isinstance(entry.payload, dict) else None
47
+ payload: dict[str, Any] | None = (
48
+ entry.payload if isinstance(entry.payload, dict) else None
49
+ )
47
50
  file_paths = None
48
- if payload and "file_paths" in payload and isinstance(payload["file_paths"], list):
51
+ if (
52
+ payload
53
+ and "file_paths" in payload
54
+ and isinstance(payload["file_paths"], list)
55
+ ):
49
56
  file_paths = payload["file_paths"]
50
57
 
51
58
  event = {
@@ -66,7 +73,9 @@ def export_sessions_to_jsonl(
66
73
  }
67
74
  lines.append(json.dumps(event, ensure_ascii=False, default=str))
68
75
 
69
- out_path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
76
+ out_path.write_text(
77
+ "\n".join(lines) + ("\n" if lines else ""), encoding="utf-8"
78
+ )
70
79
  written += 1
71
80
 
72
81
  return {"written": written, "skipped": skipped, "failed": failed}
@@ -0,0 +1,49 @@
1
+ """Custom exceptions for HtmlGraph."""
2
+
3
+
4
+ class HtmlGraphError(Exception):
5
+ """Base exception for all HtmlGraph errors with debugging guidance."""
6
+
7
+ def __str__(self) -> str:
8
+ """Return error message with debugging guidance."""
9
+ base_message = super().__str__()
10
+ guidance = (
11
+ "\n\n💡 Debugging help:"
12
+ "\n - See DEBUGGING.md for systematic troubleshooting"
13
+ "\n - Use researcher agent for unfamiliar errors"
14
+ "\n - Run 'htmlgraph --help' for available commands"
15
+ "\n - Run 'htmlgraph debug' for diagnostic tools"
16
+ )
17
+ return f"{base_message}{guidance}"
18
+
19
+
20
+ class NodeNotFoundError(HtmlGraphError):
21
+ """Raised when a node cannot be found."""
22
+
23
+ def __init__(self, node_type: str, node_id: str):
24
+ self.node_type = node_type
25
+ self.node_id = node_id
26
+ super().__init__(f"{node_type.capitalize()} not found: {node_id}")
27
+
28
+
29
+ class SessionNotFoundError(HtmlGraphError):
30
+ """Raised when a session cannot be found."""
31
+
32
+ def __init__(self, session_id: str):
33
+ self.session_id = session_id
34
+ super().__init__(f"Session not found: {session_id}")
35
+
36
+
37
+ class ClaimConflictError(HtmlGraphError):
38
+ """Raised when a claim operation conflicts with existing claim."""
39
+
40
+ def __init__(self, node_id: str, current_owner: str):
41
+ self.node_id = node_id
42
+ self.current_owner = current_owner
43
+ super().__init__(f"Node {node_id} already claimed by {current_owner}")
44
+
45
+
46
+ class ValidationError(HtmlGraphError):
47
+ """Raised when validation fails."""
48
+
49
+ pass
htmlgraph/file_watcher.py CHANGED
@@ -1,15 +1,34 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  File watcher for automatic graph reloading.
3
7
 
4
8
  Monitors .htmlgraph/**/*.html files and reloads collections when changes are detected.
5
9
  """
6
10
 
7
- import time
11
+ import fnmatch
8
12
  import threading
13
+ from collections.abc import Callable
9
14
  from pathlib import Path
10
- from typing import Callable
15
+ from typing import Any
16
+
17
+ from watchdog.events import FileSystemEvent, FileSystemEventHandler
11
18
  from watchdog.observers import Observer
12
- from watchdog.events import FileSystemEventHandler, FileSystemEvent
19
+
20
+ # Collection-specific file patterns for smart filtering
21
+ COLLECTION_PATTERNS = {
22
+ "features": ["feat-*.html", "feature-*.html"],
23
+ "bugs": ["bug-*.html"],
24
+ "spikes": ["spk-*.html", "spike-*.html"],
25
+ "sessions": ["sess-*.html", "session-*.html"],
26
+ "tracks": ["trk-*.html", "track-*.html"],
27
+ "chores": ["chore-*.html"],
28
+ "insights": ["insi-*.html", "insight-*.html"],
29
+ "patterns": ["patt-*.html", "pattern-*.html"],
30
+ "metrics": ["metr-*.html", "metric-*.html"],
31
+ }
13
32
 
14
33
 
15
34
  class GraphFileHandler(FileSystemEventHandler):
@@ -28,12 +47,34 @@ class GraphFileHandler(FileSystemEventHandler):
28
47
  self.debounce_timer: threading.Timer | None = None
29
48
  self.debounce_delay = 0.5 # 500ms debounce
30
49
 
31
- def _trigger_reload(self):
50
+ def _is_relevant_file(self, filepath: str) -> bool:
51
+ """
52
+ Check if changed file is relevant to this watcher's collection.
53
+
54
+ Args:
55
+ filepath: Path to the file that changed
56
+
57
+ Returns:
58
+ True if the file matches this collection's patterns
59
+ """
60
+ filename = Path(filepath).name
61
+
62
+ # Skip non-HTML files
63
+ if not filename.endswith(".html"):
64
+ return False
65
+
66
+ # Get patterns for this collection (default to all HTML if not found)
67
+ patterns = COLLECTION_PATTERNS.get(self.collection, ["*.html"])
68
+
69
+ # Check if filename matches any pattern
70
+ return any(fnmatch.fnmatch(filename, pattern) for pattern in patterns)
71
+
72
+ def _trigger_reload(self) -> None:
32
73
  """Trigger a reload after debounce delay."""
33
- print(f"[FileWatcher] Reloading collection: {self.collection}")
74
+ logger.info(f"[FileWatcher] Reloading collection: {self.collection}")
34
75
  self.reload_callback()
35
76
 
36
- def _debounced_reload(self):
77
+ def _debounced_reload(self) -> None:
37
78
  """Debounce rapid file changes to avoid excessive reloads."""
38
79
  if self.debounce_timer:
39
80
  self.debounce_timer.cancel()
@@ -41,29 +82,58 @@ class GraphFileHandler(FileSystemEventHandler):
41
82
  self.debounce_timer = threading.Timer(self.debounce_delay, self._trigger_reload)
42
83
  self.debounce_timer.start()
43
84
 
44
- def on_created(self, event: FileSystemEvent):
85
+ def on_created(self, event: FileSystemEvent) -> None:
45
86
  """Handle file creation."""
46
- if not event.is_directory and event.src_path.endswith('.html'):
47
- print(f"[FileWatcher] {self.collection}: File created - {Path(event.src_path).name}")
48
- self._debounced_reload()
87
+ if event.is_directory:
88
+ return
89
+
90
+ # Skip if not relevant to our collection
91
+ if not self._is_relevant_file(str(event.src_path)):
92
+ return
93
+
94
+ logger.debug(
95
+ f"[FileWatcher] {self.collection}: File created - {Path(str(event.src_path)).name}"
96
+ )
97
+ self._debounced_reload()
49
98
 
50
- def on_modified(self, event: FileSystemEvent):
99
+ def on_modified(self, event: FileSystemEvent) -> None:
51
100
  """Handle file modification."""
52
- if not event.is_directory and event.src_path.endswith('.html'):
53
- print(f"[FileWatcher] {self.collection}: File modified - {Path(event.src_path).name}")
54
- self._debounced_reload()
101
+ if event.is_directory:
102
+ return
55
103
 
56
- def on_deleted(self, event: FileSystemEvent):
104
+ # Skip if not relevant to our collection
105
+ if not self._is_relevant_file(str(event.src_path)):
106
+ return
107
+
108
+ logger.debug(
109
+ f"[FileWatcher] {self.collection}: File modified - {Path(str(event.src_path)).name}"
110
+ )
111
+ self._debounced_reload()
112
+
113
+ def on_deleted(self, event: FileSystemEvent) -> None:
57
114
  """Handle file deletion."""
58
- if not event.is_directory and event.src_path.endswith('.html'):
59
- print(f"[FileWatcher] {self.collection}: File deleted - {Path(event.src_path).name}")
60
- self._debounced_reload()
115
+ if event.is_directory:
116
+ return
117
+
118
+ # Skip if not relevant to our collection
119
+ if not self._is_relevant_file(str(event.src_path)):
120
+ return
121
+
122
+ logger.debug(
123
+ f"[FileWatcher] {self.collection}: File deleted - {Path(str(event.src_path)).name}"
124
+ )
125
+ self._debounced_reload()
61
126
 
62
127
 
63
128
  class GraphWatcher:
64
129
  """Watches graph directories and triggers reloads on changes."""
65
130
 
66
- def __init__(self, graph_dir: Path, collections: list[str], get_graph_callback: Callable[[str], any]):
131
+ def __init__(
132
+ self,
133
+ graph_dir: Path,
134
+ collections: list[str],
135
+ get_graph_callback: Callable[[str], Any],
136
+ ) -> None:
67
137
  """
68
138
  Initialize watcher.
69
139
 
@@ -78,9 +148,11 @@ class GraphWatcher:
78
148
  self.observer = Observer()
79
149
  self.handlers: dict[str, GraphFileHandler] = {}
80
150
 
81
- def start(self):
151
+ def start(self) -> None:
82
152
  """Start watching for file changes."""
83
- print(f"[FileWatcher] Starting file watcher for {len(self.collections)} collections...")
153
+ logger.info(
154
+ f"[FileWatcher] Starting file watcher for {len(self.collections)} collections..."
155
+ )
84
156
 
85
157
  for collection in self.collections:
86
158
  collection_dir = self.graph_dir / collection
@@ -88,11 +160,12 @@ class GraphWatcher:
88
160
  continue
89
161
 
90
162
  # Create handler with reload callback
91
- def make_reload_callback(coll):
92
- def reload():
163
+ def make_reload_callback(coll: str) -> Callable[[], None]:
164
+ def reload() -> None:
93
165
  graph = self.get_graph_callback(coll)
94
166
  count = graph.reload()
95
- print(f"[FileWatcher] Reloaded {count} nodes in {coll}")
167
+ logger.info(f"[FileWatcher] Reloaded {count} nodes in {coll}")
168
+
96
169
  return reload
97
170
 
98
171
  handler = GraphFileHandler(collection, make_reload_callback(collection))
@@ -100,15 +173,15 @@ class GraphWatcher:
100
173
 
101
174
  # Watch the collection directory
102
175
  # Use recursive=True for tracks since they're stored in subdirectories
103
- recursive = (collection == "tracks")
176
+ recursive = collection == "tracks"
104
177
  self.observer.schedule(handler, str(collection_dir), recursive=recursive)
105
178
 
106
179
  self.observer.start()
107
- print(f"[FileWatcher] Watching {self.graph_dir} for changes...")
180
+ logger.info(f"[FileWatcher] Watching {self.graph_dir} for changes...")
108
181
 
109
- def stop(self):
182
+ def stop(self) -> None:
110
183
  """Stop watching for file changes."""
111
- print("[FileWatcher] Stopping file watcher...")
184
+ logger.info("[FileWatcher] Stopping file watcher...")
112
185
  self.observer.stop()
113
186
  self.observer.join()
114
187