htmlgraph 0.20.1__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 (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,756 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Session Handoff and Continuity - Phase 2 Feature 3
5
+
6
+ Provides cross-session continuity features:
7
+ - HandoffBuilder: Fluent API for creating handoffs with context
8
+ - SessionResume: Load and resume from previous session
9
+ - HandoffTracker: Track handoff effectiveness metrics
10
+ - ContextRecommender: Suggest files to keep context for next session
11
+
12
+ Usage:
13
+ # End session with handoff
14
+ sdk.sessions.end(
15
+ summary="Completed OAuth integration",
16
+ next_focus="Implement JWT token refresh",
17
+ blockers=["Waiting for security review"],
18
+ keep_context=["src/auth/", "docs/security"]
19
+ )
20
+
21
+ # Resume next session
22
+ resumed = sdk.sessions.continue_from_last()
23
+ if resumed:
24
+ logger.info("%s", resumed.summary)
25
+ logger.info("%s", resumed.recommended_files)
26
+ """
27
+
28
+
29
+ import json
30
+ import logging
31
+ import subprocess
32
+ from dataclasses import dataclass
33
+ from datetime import datetime, timedelta, timezone
34
+ from pathlib import Path
35
+ from typing import TYPE_CHECKING, Any
36
+
37
+ if TYPE_CHECKING:
38
+ from htmlgraph.models import Session
39
+ from htmlgraph.sdk import SDK
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ @dataclass
45
+ class SessionResumeInfo:
46
+ """Information loaded from previous session for resumption."""
47
+
48
+ session_id: str
49
+ agent: str
50
+ ended_at: datetime | None
51
+ summary: str | None # handoff_notes
52
+ next_focus: str | None # recommended_next
53
+ blockers: list[str]
54
+ recommended_files: list[str]
55
+ worked_on_features: list[str]
56
+ recent_commits: list[dict[str, str]]
57
+ time_since_last: timedelta | None
58
+
59
+
60
+ @dataclass
61
+ class HandoffMetrics:
62
+ """Metrics for a session handoff."""
63
+
64
+ handoff_id: str
65
+ from_session_id: str
66
+ to_session_id: str | None
67
+ items_in_context: int
68
+ items_accessed: int
69
+ time_to_resume_seconds: int
70
+ user_rating: int | None
71
+ created_at: datetime
72
+ resumed_at: datetime | None
73
+
74
+
75
+ class ContextRecommender:
76
+ """
77
+ Recommends files to keep context for next session.
78
+
79
+ Uses git history to identify recently edited files and
80
+ combines with feature context.
81
+ """
82
+
83
+ def __init__(self, repo_root: Path | None = None):
84
+ """
85
+ Initialize ContextRecommender.
86
+
87
+ Args:
88
+ repo_root: Root of git repository (auto-detected if None)
89
+ """
90
+ self.repo_root = repo_root or self._find_repo_root()
91
+
92
+ def _find_repo_root(self) -> Path | None:
93
+ """Find git repository root."""
94
+ try:
95
+ result = subprocess.run(
96
+ ["git", "rev-parse", "--show-toplevel"],
97
+ capture_output=True,
98
+ text=True,
99
+ check=True,
100
+ timeout=5,
101
+ )
102
+ return Path(result.stdout.strip())
103
+ except (
104
+ subprocess.CalledProcessError,
105
+ FileNotFoundError,
106
+ subprocess.TimeoutExpired,
107
+ ):
108
+ return None
109
+
110
+ def get_recent_files(
111
+ self,
112
+ since_minutes: int = 60,
113
+ max_files: int = 10,
114
+ exclude_patterns: list[str] | None = None,
115
+ ) -> list[str]:
116
+ """
117
+ Get recently edited files from git.
118
+
119
+ Args:
120
+ since_minutes: Time window to check
121
+ max_files: Maximum files to return
122
+ exclude_patterns: Patterns to exclude (e.g., ["*.md", "tests/*"])
123
+
124
+ Returns:
125
+ List of file paths (relative to repo root)
126
+ """
127
+ if not self.repo_root:
128
+ return []
129
+
130
+ exclude_patterns = exclude_patterns or []
131
+
132
+ try:
133
+ # Get files changed in last N minutes
134
+ result = subprocess.run(
135
+ [
136
+ "git",
137
+ "log",
138
+ f"--since={since_minutes} minutes ago",
139
+ "--name-only",
140
+ "--pretty=format:",
141
+ "--diff-filter=AMR", # Added, Modified, Renamed
142
+ ],
143
+ cwd=str(self.repo_root),
144
+ capture_output=True,
145
+ text=True,
146
+ check=True,
147
+ timeout=10,
148
+ )
149
+
150
+ # Parse files and deduplicate
151
+ files = []
152
+ seen = set()
153
+ for line in result.stdout.strip().split("\n"):
154
+ line = line.strip()
155
+ if not line or line in seen:
156
+ continue
157
+
158
+ # Check exclusion patterns
159
+ excluded = False
160
+ for pattern in exclude_patterns:
161
+ if self._matches_pattern(line, pattern):
162
+ excluded = True
163
+ break
164
+
165
+ if not excluded:
166
+ files.append(line)
167
+ seen.add(line)
168
+
169
+ if len(files) >= max_files:
170
+ break
171
+
172
+ return files
173
+
174
+ except (
175
+ subprocess.CalledProcessError,
176
+ subprocess.TimeoutExpired,
177
+ FileNotFoundError,
178
+ ):
179
+ logger.debug("Could not get recent files from git")
180
+ return []
181
+
182
+ def _matches_pattern(self, path: str, pattern: str) -> bool:
183
+ """Check if path matches glob pattern."""
184
+ import fnmatch
185
+
186
+ return fnmatch.fnmatch(path, pattern)
187
+
188
+ def recommend_for_session(
189
+ self,
190
+ session: Session,
191
+ max_files: int = 10,
192
+ ) -> list[str]:
193
+ """
194
+ Recommend files to keep context for next session.
195
+
196
+ Args:
197
+ session: Session ending with handoff
198
+ max_files: Maximum files to recommend
199
+
200
+ Returns:
201
+ List of recommended file paths
202
+ """
203
+ # Get recently edited files
204
+ recent_files = self.get_recent_files(
205
+ since_minutes=120, # 2 hours
206
+ max_files=max_files,
207
+ exclude_patterns=["*.md", "*.txt", "*.json", "__pycache__/*"],
208
+ )
209
+
210
+ # TODO: Could enhance this by:
211
+ # - Checking which files were Read/Edit in session activity log
212
+ # - Prioritizing files related to features worked on
213
+ # - Using file change frequency
214
+
215
+ return recent_files[:max_files]
216
+
217
+
218
+ class HandoffBuilder:
219
+ """
220
+ Fluent builder for creating session handoffs.
221
+
222
+ Example:
223
+ handoff = HandoffBuilder(session)
224
+ .add_summary("Completed OAuth integration")
225
+ .add_next_focus("Implement JWT token refresh")
226
+ .add_blockers(["Waiting for security review"])
227
+ .add_context_files(["src/auth/oauth.py", "docs/security.md"])
228
+ .build()
229
+ """
230
+
231
+ def __init__(self, session: Session):
232
+ """
233
+ Initialize HandoffBuilder.
234
+
235
+ Args:
236
+ session: Session to add handoff to
237
+ """
238
+ self.session = session
239
+ self._summary: str | None = None
240
+ self._next_focus: str | None = None
241
+ self._blockers: list[str] = []
242
+ self._context_files: list[str] = []
243
+
244
+ def add_summary(self, summary: str) -> HandoffBuilder:
245
+ """
246
+ Add handoff summary (what was accomplished).
247
+
248
+ Args:
249
+ summary: Summary of what was done
250
+
251
+ Returns:
252
+ Self for chaining
253
+ """
254
+ self._summary = summary
255
+ return self
256
+
257
+ def add_next_focus(self, next_focus: str) -> HandoffBuilder:
258
+ """
259
+ Add recommended next focus.
260
+
261
+ Args:
262
+ next_focus: What should be done next
263
+
264
+ Returns:
265
+ Self for chaining
266
+ """
267
+ self._next_focus = next_focus
268
+ return self
269
+
270
+ def add_blocker(self, blocker: str) -> HandoffBuilder:
271
+ """
272
+ Add a single blocker.
273
+
274
+ Args:
275
+ blocker: Description of blocker
276
+
277
+ Returns:
278
+ Self for chaining
279
+ """
280
+ self._blockers.append(blocker)
281
+ return self
282
+
283
+ def add_blockers(self, blockers: list[str]) -> HandoffBuilder:
284
+ """
285
+ Add multiple blockers.
286
+
287
+ Args:
288
+ blockers: List of blocker descriptions
289
+
290
+ Returns:
291
+ Self for chaining
292
+ """
293
+ self._blockers.extend(blockers)
294
+ return self
295
+
296
+ def add_context_file(self, file_path: str) -> HandoffBuilder:
297
+ """
298
+ Add a file to keep context for.
299
+
300
+ Args:
301
+ file_path: Path to file
302
+
303
+ Returns:
304
+ Self for chaining
305
+ """
306
+ self._context_files.append(file_path)
307
+ return self
308
+
309
+ def add_context_files(self, file_paths: list[str]) -> HandoffBuilder:
310
+ """
311
+ Add multiple files to keep context for.
312
+
313
+ Args:
314
+ file_paths: List of file paths
315
+
316
+ Returns:
317
+ Self for chaining
318
+ """
319
+ self._context_files.extend(file_paths)
320
+ return self
321
+
322
+ def auto_recommend_context(
323
+ self,
324
+ recommender: ContextRecommender | None = None,
325
+ max_files: int = 10,
326
+ ) -> HandoffBuilder:
327
+ """
328
+ Automatically recommend context files.
329
+
330
+ Args:
331
+ recommender: ContextRecommender instance (creates new if None)
332
+ max_files: Maximum files to recommend
333
+
334
+ Returns:
335
+ Self for chaining
336
+ """
337
+ if recommender is None:
338
+ recommender = ContextRecommender()
339
+
340
+ recommended = recommender.recommend_for_session(
341
+ self.session, max_files=max_files
342
+ )
343
+ self._context_files.extend(recommended)
344
+ return self
345
+
346
+ def build(self) -> dict[str, Any]:
347
+ """
348
+ Build handoff data dictionary.
349
+
350
+ Returns:
351
+ Dictionary with handoff data
352
+ """
353
+ return {
354
+ "handoff_notes": self._summary,
355
+ "recommended_next": self._next_focus,
356
+ "blockers": self._blockers,
357
+ "recommended_context": self._context_files,
358
+ }
359
+
360
+
361
+ class SessionResume:
362
+ """
363
+ Loads and presents context from previous session for resumption.
364
+ """
365
+
366
+ def __init__(self, sdk: SDK):
367
+ """
368
+ Initialize SessionResume.
369
+
370
+ Args:
371
+ sdk: SDK instance
372
+ """
373
+ self.sdk = sdk
374
+ self.graph_dir = sdk._directory
375
+
376
+ def get_last_session(self, agent: str | None = None) -> Session | None:
377
+ """
378
+ Get the most recent completed session.
379
+
380
+ Args:
381
+ agent: Filter by agent (None = any agent)
382
+
383
+ Returns:
384
+ Most recent session or None
385
+ """
386
+ from htmlgraph.converter import SessionConverter
387
+
388
+ converter = SessionConverter(self.graph_dir / "sessions")
389
+ sessions = converter.load_all()
390
+
391
+ # Filter by ended sessions
392
+ ended = [s for s in sessions if s.status == "ended"]
393
+
394
+ # Filter by agent if specified
395
+ if agent:
396
+ ended = [s for s in ended if s.agent == agent]
397
+
398
+ if not ended:
399
+ return None
400
+
401
+ # Sort by ended_at (most recent first)
402
+ ended.sort(key=lambda s: s.ended_at or datetime.min, reverse=True)
403
+ return ended[0]
404
+
405
+ def build_resume_info(self, session: Session) -> SessionResumeInfo:
406
+ """
407
+ Build resumption information from a session.
408
+
409
+ Args:
410
+ session: Previous session
411
+
412
+ Returns:
413
+ SessionResumeInfo with context for resumption
414
+ """
415
+ # Calculate time since last session
416
+ time_since = None
417
+ if session.ended_at:
418
+ time_since = datetime.now(timezone.utc) - session.ended_at
419
+
420
+ # Get recent commits
421
+ recent_commits = self._get_recent_commits(since_commit=session.start_commit)
422
+
423
+ return SessionResumeInfo(
424
+ session_id=session.id,
425
+ agent=session.agent,
426
+ ended_at=session.ended_at,
427
+ summary=session.handoff_notes,
428
+ next_focus=session.recommended_next,
429
+ blockers=session.blockers,
430
+ recommended_files=self._parse_json_list(session, "recommended_context"),
431
+ worked_on_features=session.worked_on,
432
+ recent_commits=recent_commits,
433
+ time_since_last=time_since,
434
+ )
435
+
436
+ def _parse_json_list(self, session: Session, field_name: str) -> list[str]:
437
+ """Parse JSON list field from session."""
438
+ # Session model stores these as Python lists already
439
+ value = getattr(session, field_name, None)
440
+ if isinstance(value, list):
441
+ return [str(item) for item in value] # Ensure list[str]
442
+ if isinstance(value, str):
443
+ try:
444
+ result = json.loads(value)
445
+ return (
446
+ [str(item) for item in result] if isinstance(result, list) else []
447
+ )
448
+ except json.JSONDecodeError:
449
+ return []
450
+ return []
451
+
452
+ def _get_recent_commits(
453
+ self, since_commit: str | None = None, limit: int = 5
454
+ ) -> list[dict[str, str]]:
455
+ """
456
+ Get recent git commits.
457
+
458
+ Args:
459
+ since_commit: Get commits since this one
460
+ limit: Maximum commits to return
461
+
462
+ Returns:
463
+ List of commit dictionaries with hash, message, author, date
464
+ """
465
+ try:
466
+ args = ["git", "log", f"-{limit}", "--oneline", "--no-merges"]
467
+ if since_commit:
468
+ args.append(f"{since_commit}..HEAD")
469
+
470
+ result = subprocess.run(
471
+ args,
472
+ capture_output=True,
473
+ text=True,
474
+ check=True,
475
+ timeout=5,
476
+ )
477
+
478
+ commits = []
479
+ for line in result.stdout.strip().split("\n"):
480
+ if not line:
481
+ continue
482
+ parts = line.split(" ", 1)
483
+ if len(parts) == 2:
484
+ commits.append({"hash": parts[0], "message": parts[1]})
485
+
486
+ return commits
487
+
488
+ except (
489
+ subprocess.CalledProcessError,
490
+ subprocess.TimeoutExpired,
491
+ FileNotFoundError,
492
+ ):
493
+ logger.debug("Could not get recent commits")
494
+ return []
495
+
496
+ def format_resume_prompt(self, info: SessionResumeInfo) -> str:
497
+ """
498
+ Format a user-friendly resumption prompt.
499
+
500
+ Args:
501
+ info: Session resumption information
502
+
503
+ Returns:
504
+ Formatted multi-line string for display
505
+ """
506
+ lines = [
507
+ "═" * 70,
508
+ "CONTINUE FROM LAST SESSION",
509
+ "═" * 70,
510
+ ]
511
+
512
+ # Session info
513
+ if info.ended_at:
514
+ lines.append(
515
+ f'Last: {info.ended_at.strftime("%A %I:%M %p")} - "{info.summary or "No summary"}"'
516
+ )
517
+ else:
518
+ lines.append(f"Last: {info.session_id}")
519
+
520
+ # Time gap
521
+ if info.time_since_last:
522
+ hours = info.time_since_last.total_seconds() / 3600
523
+ if hours < 1:
524
+ time_str = (
525
+ f"{int(info.time_since_last.total_seconds() / 60)} minutes ago"
526
+ )
527
+ elif hours < 24:
528
+ time_str = f"{int(hours)} hours ago"
529
+ else:
530
+ time_str = f"{int(hours / 24)} days ago"
531
+ lines.append(f"Gap: {time_str}")
532
+
533
+ lines.append("")
534
+
535
+ # Next focus
536
+ if info.next_focus:
537
+ lines.append("Next Focus:")
538
+ lines.append(f" {info.next_focus}")
539
+ lines.append("")
540
+
541
+ # Blockers
542
+ if info.blockers:
543
+ lines.append("Blockers:")
544
+ for blocker in info.blockers:
545
+ lines.append(f" ⚠️ {blocker}")
546
+ lines.append("")
547
+
548
+ # Context files
549
+ if info.recommended_files:
550
+ lines.append("Context to Load:")
551
+ for i, file_path in enumerate(info.recommended_files[:5], 1):
552
+ lines.append(f" {i}. {file_path}")
553
+ if len(info.recommended_files) > 5:
554
+ lines.append(f" ... and {len(info.recommended_files) - 5} more")
555
+ lines.append("")
556
+
557
+ # Features worked on
558
+ if info.worked_on_features:
559
+ lines.append("Features in Progress:")
560
+ for feature_id in info.worked_on_features[:3]:
561
+ lines.append(f" - {feature_id}")
562
+ if len(info.worked_on_features) > 3:
563
+ lines.append(f" ... and {len(info.worked_on_features) - 3} more")
564
+ lines.append("")
565
+
566
+ # Recent commits
567
+ if info.recent_commits:
568
+ lines.append("Recent Commits:")
569
+ for commit in info.recent_commits[:3]:
570
+ lines.append(f" {commit['hash']} {commit['message']}")
571
+ lines.append("")
572
+
573
+ lines.append(
574
+ "[L]oad context files [O]pen in editor [S]how summary [C]ontinue"
575
+ )
576
+
577
+ return "\n".join(lines)
578
+
579
+
580
+ class HandoffTracker:
581
+ """
582
+ Tracks handoff effectiveness metrics.
583
+
584
+ Records how helpful handoffs are and enables optimization.
585
+ """
586
+
587
+ def __init__(self, sdk: SDK):
588
+ """
589
+ Initialize HandoffTracker.
590
+
591
+ Args:
592
+ sdk: SDK instance
593
+ """
594
+ self.sdk = sdk
595
+ self.db = getattr(sdk, "_db", None)
596
+
597
+ def create_handoff(
598
+ self,
599
+ from_session_id: str,
600
+ items_in_context: int = 0,
601
+ ) -> str:
602
+ """
603
+ Create a handoff tracking record.
604
+
605
+ Args:
606
+ from_session_id: Session ending with handoff
607
+ items_in_context: Number of context items provided
608
+
609
+ Returns:
610
+ Handoff ID
611
+ """
612
+ from htmlgraph.ids import generate_id
613
+
614
+ handoff_id = generate_id("hand")
615
+
616
+ if self.db and self.db.connection:
617
+ # Ensure session exists in database (handles FK constraint)
618
+ self.db._ensure_session_exists(from_session_id)
619
+
620
+ cursor = self.db.connection.cursor()
621
+ cursor.execute(
622
+ """
623
+ INSERT INTO handoff_tracking
624
+ (handoff_id, from_session_id, items_in_context)
625
+ VALUES (?, ?, ?)
626
+ """,
627
+ (handoff_id, from_session_id, items_in_context),
628
+ )
629
+ self.db.connection.commit()
630
+
631
+ return handoff_id
632
+
633
+ def resume_handoff(
634
+ self,
635
+ handoff_id: str,
636
+ to_session_id: str,
637
+ items_accessed: int = 0,
638
+ time_to_resume_seconds: int = 0,
639
+ ) -> bool:
640
+ """
641
+ Update handoff with resumption data.
642
+
643
+ Args:
644
+ handoff_id: Handoff ID
645
+ to_session_id: New session ID
646
+ items_accessed: Number of context items accessed
647
+ time_to_resume_seconds: Time to resume work (seconds)
648
+
649
+ Returns:
650
+ True if successful
651
+ """
652
+ if not self.db or not self.db.connection:
653
+ return False
654
+
655
+ try:
656
+ # Ensure to_session exists in database (handles FK constraint)
657
+ self.db._ensure_session_exists(to_session_id)
658
+
659
+ cursor = self.db.connection.cursor()
660
+ cursor.execute(
661
+ """
662
+ UPDATE handoff_tracking
663
+ SET to_session_id = ?,
664
+ items_accessed = ?,
665
+ time_to_resume_seconds = ?,
666
+ resumed_at = CURRENT_TIMESTAMP
667
+ WHERE handoff_id = ?
668
+ """,
669
+ (to_session_id, items_accessed, time_to_resume_seconds, handoff_id),
670
+ )
671
+ self.db.connection.commit()
672
+ return True
673
+ except Exception as e:
674
+ logger.error(f"Error updating handoff: {e}")
675
+ return False
676
+
677
+ def rate_handoff(self, handoff_id: str, rating: int) -> bool:
678
+ """
679
+ Rate handoff effectiveness (1-5 scale).
680
+
681
+ Args:
682
+ handoff_id: Handoff ID
683
+ rating: Rating (1-5)
684
+
685
+ Returns:
686
+ True if successful
687
+ """
688
+ if not 1 <= rating <= 5:
689
+ raise ValueError("Rating must be between 1 and 5")
690
+
691
+ if not self.db or not self.db.connection:
692
+ return False
693
+
694
+ try:
695
+ cursor = self.db.connection.cursor()
696
+ cursor.execute(
697
+ """
698
+ UPDATE handoff_tracking
699
+ SET user_rating = ?
700
+ WHERE handoff_id = ?
701
+ """,
702
+ (rating, handoff_id),
703
+ )
704
+ self.db.connection.commit()
705
+ return True
706
+ except Exception as e:
707
+ logger.error(f"Error rating handoff: {e}")
708
+ return False
709
+
710
+ def get_handoff_metrics(self, limit: int = 10) -> list[HandoffMetrics]:
711
+ """
712
+ Get recent handoff metrics.
713
+
714
+ Args:
715
+ limit: Maximum records to return
716
+
717
+ Returns:
718
+ List of HandoffMetrics
719
+ """
720
+ if not self.db or not self.db.connection:
721
+ return []
722
+
723
+ try:
724
+ cursor = self.db.connection.cursor()
725
+ cursor.execute(
726
+ """
727
+ SELECT handoff_id, from_session_id, to_session_id,
728
+ items_in_context, items_accessed, time_to_resume_seconds,
729
+ user_rating, created_at, resumed_at
730
+ FROM handoff_tracking
731
+ ORDER BY created_at DESC
732
+ LIMIT ?
733
+ """,
734
+ (limit,),
735
+ )
736
+
737
+ metrics = []
738
+ for row in cursor.fetchall():
739
+ metrics.append(
740
+ HandoffMetrics(
741
+ handoff_id=row[0],
742
+ from_session_id=row[1],
743
+ to_session_id=row[2],
744
+ items_in_context=row[3],
745
+ items_accessed=row[4],
746
+ time_to_resume_seconds=row[5],
747
+ user_rating=row[6],
748
+ created_at=datetime.fromisoformat(row[7]),
749
+ resumed_at=(datetime.fromisoformat(row[8]) if row[8] else None),
750
+ )
751
+ )
752
+
753
+ return metrics
754
+ except Exception as e:
755
+ logger.error(f"Error getting handoff metrics: {e}")
756
+ return []