htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +114 -51
  11. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  12. htmlgraph/api/templates/dashboard.html +3 -3
  13. htmlgraph/api/templates/partials/work-items.html +613 -0
  14. htmlgraph/attribute_index.py +2 -1
  15. htmlgraph/builders/base.py +2 -1
  16. htmlgraph/builders/bug.py +2 -1
  17. htmlgraph/builders/chore.py +2 -1
  18. htmlgraph/builders/epic.py +2 -1
  19. htmlgraph/builders/feature.py +2 -1
  20. htmlgraph/builders/insight.py +2 -1
  21. htmlgraph/builders/metric.py +2 -1
  22. htmlgraph/builders/pattern.py +2 -1
  23. htmlgraph/builders/phase.py +2 -1
  24. htmlgraph/builders/spike.py +2 -1
  25. htmlgraph/builders/track.py +28 -1
  26. htmlgraph/cli/analytics.py +2 -1
  27. htmlgraph/cli/base.py +33 -8
  28. htmlgraph/cli/core.py +2 -1
  29. htmlgraph/cli/main.py +2 -1
  30. htmlgraph/cli/models.py +2 -1
  31. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  32. htmlgraph/cli/work/__init__.py +76 -1
  33. htmlgraph/cli/work/browse.py +115 -0
  34. htmlgraph/cli/work/features.py +2 -1
  35. htmlgraph/cli/work/orchestration.py +2 -1
  36. htmlgraph/cli/work/report.py +2 -1
  37. htmlgraph/cli/work/sessions.py +2 -1
  38. htmlgraph/cli/work/snapshot.py +559 -0
  39. htmlgraph/cli/work/tracks.py +2 -1
  40. htmlgraph/collections/base.py +43 -4
  41. htmlgraph/collections/bug.py +2 -1
  42. htmlgraph/collections/chore.py +2 -1
  43. htmlgraph/collections/epic.py +2 -1
  44. htmlgraph/collections/feature.py +2 -1
  45. htmlgraph/collections/insight.py +2 -1
  46. htmlgraph/collections/metric.py +2 -1
  47. htmlgraph/collections/pattern.py +2 -1
  48. htmlgraph/collections/phase.py +2 -1
  49. htmlgraph/collections/session.py +12 -7
  50. htmlgraph/collections/spike.py +6 -1
  51. htmlgraph/collections/task_delegation.py +7 -2
  52. htmlgraph/collections/todo.py +14 -1
  53. htmlgraph/collections/traces.py +15 -10
  54. htmlgraph/context_analytics.py +2 -1
  55. htmlgraph/converter.py +11 -0
  56. htmlgraph/dependency_models.py +2 -1
  57. htmlgraph/edge_index.py +2 -1
  58. htmlgraph/event_log.py +81 -66
  59. htmlgraph/event_migration.py +2 -1
  60. htmlgraph/file_watcher.py +12 -8
  61. htmlgraph/find_api.py +2 -1
  62. htmlgraph/git_events.py +6 -2
  63. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  64. htmlgraph/hooks/drift_handler.py +3 -3
  65. htmlgraph/hooks/event_tracker.py +40 -61
  66. htmlgraph/hooks/installer.py +5 -1
  67. htmlgraph/hooks/orchestrator.py +92 -14
  68. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  69. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  70. htmlgraph/hooks/posttooluse.py +4 -0
  71. htmlgraph/hooks/prompt_analyzer.py +5 -5
  72. htmlgraph/hooks/session_handler.py +5 -2
  73. htmlgraph/hooks/session_summary.py +6 -2
  74. htmlgraph/hooks/validator.py +8 -4
  75. htmlgraph/ids.py +2 -1
  76. htmlgraph/learning.py +2 -1
  77. htmlgraph/mcp_server.py +2 -1
  78. htmlgraph/models.py +18 -1
  79. htmlgraph/operations/analytics.py +2 -1
  80. htmlgraph/operations/bootstrap.py +2 -1
  81. htmlgraph/operations/events.py +2 -1
  82. htmlgraph/operations/fastapi_server.py +2 -1
  83. htmlgraph/operations/hooks.py +2 -1
  84. htmlgraph/operations/initialization.py +2 -1
  85. htmlgraph/operations/server.py +2 -1
  86. htmlgraph/orchestration/__init__.py +4 -0
  87. htmlgraph/orchestration/claude_launcher.py +23 -20
  88. htmlgraph/orchestration/command_builder.py +2 -1
  89. htmlgraph/orchestration/headless_spawner.py +6 -2
  90. htmlgraph/orchestration/model_selection.py +7 -3
  91. htmlgraph/orchestration/plugin_manager.py +25 -21
  92. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  93. htmlgraph/orchestration/spawners/claude.py +5 -2
  94. htmlgraph/orchestration/spawners/codex.py +12 -19
  95. htmlgraph/orchestration/spawners/copilot.py +13 -18
  96. htmlgraph/orchestration/spawners/gemini.py +12 -19
  97. htmlgraph/orchestration/subprocess_runner.py +6 -3
  98. htmlgraph/orchestration/task_coordination.py +16 -8
  99. htmlgraph/orchestrator.py +2 -1
  100. htmlgraph/parallel.py +2 -1
  101. htmlgraph/query_builder.py +2 -1
  102. htmlgraph/reflection.py +2 -1
  103. htmlgraph/refs.py +344 -0
  104. htmlgraph/repo_hash.py +2 -1
  105. htmlgraph/sdk/__init__.py +398 -0
  106. htmlgraph/sdk/__init__.pyi +14 -0
  107. htmlgraph/sdk/analytics/__init__.py +19 -0
  108. htmlgraph/sdk/analytics/engine.py +155 -0
  109. htmlgraph/sdk/analytics/helpers.py +178 -0
  110. htmlgraph/sdk/analytics/registry.py +109 -0
  111. htmlgraph/sdk/base.py +484 -0
  112. htmlgraph/sdk/constants.py +216 -0
  113. htmlgraph/sdk/core.pyi +308 -0
  114. htmlgraph/sdk/discovery.py +120 -0
  115. htmlgraph/sdk/help/__init__.py +12 -0
  116. htmlgraph/sdk/help/mixin.py +699 -0
  117. htmlgraph/sdk/mixins/__init__.py +15 -0
  118. htmlgraph/sdk/mixins/attribution.py +113 -0
  119. htmlgraph/sdk/mixins/mixin.py +410 -0
  120. htmlgraph/sdk/operations/__init__.py +12 -0
  121. htmlgraph/sdk/operations/mixin.py +427 -0
  122. htmlgraph/sdk/orchestration/__init__.py +17 -0
  123. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  124. htmlgraph/sdk/orchestration/spawner.py +204 -0
  125. htmlgraph/sdk/planning/__init__.py +19 -0
  126. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  127. htmlgraph/sdk/planning/mixin.py +211 -0
  128. htmlgraph/sdk/planning/parallel.py +186 -0
  129. htmlgraph/sdk/planning/queue.py +210 -0
  130. htmlgraph/sdk/planning/recommendations.py +87 -0
  131. htmlgraph/sdk/planning/smart_planning.py +319 -0
  132. htmlgraph/sdk/session/__init__.py +19 -0
  133. htmlgraph/sdk/session/continuity.py +57 -0
  134. htmlgraph/sdk/session/handoff.py +110 -0
  135. htmlgraph/sdk/session/info.py +309 -0
  136. htmlgraph/sdk/session/manager.py +103 -0
  137. htmlgraph/server.py +21 -17
  138. htmlgraph/session_manager.py +1 -7
  139. htmlgraph/session_warning.py +2 -1
  140. htmlgraph/sessions/handoff.py +10 -3
  141. htmlgraph/system_prompts.py +2 -1
  142. htmlgraph/track_builder.py +14 -1
  143. htmlgraph/transcript.py +2 -1
  144. htmlgraph/watch.py +2 -1
  145. htmlgraph/work_type_utils.py +2 -1
  146. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  147. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
  148. htmlgraph/sdk.py +0 -3430
  149. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  150. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  151. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  152. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  153. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  154. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  155. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,309 @@
1
+ """
2
+ Session info mixin for SDK - session start info and active work tracking.
3
+
4
+ Provides optimized methods for session context gathering.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ if TYPE_CHECKING:
14
+ from pathlib import Path
15
+
16
+ from htmlgraph.session_manager import SessionManager
17
+ from htmlgraph.types import ActiveWorkItem, SessionStartInfo
18
+
19
+
20
+ class SessionInfoMixin:
21
+ """
22
+ Mixin providing session info and active work methods to SDK.
23
+
24
+ Provides optimized methods for gathering session context in single calls.
25
+ Requires SDK instance with _directory, _agent_id, session_manager attributes.
26
+ """
27
+
28
+ _directory: Path
29
+ _agent_id: str | None
30
+ session_manager: SessionManager
31
+
32
+ def get_session_start_info(
33
+ self,
34
+ include_git_log: bool = True,
35
+ git_log_count: int = 5,
36
+ analytics_top_n: int = 3,
37
+ analytics_max_agents: int = 3,
38
+ ) -> SessionStartInfo:
39
+ """
40
+ Get comprehensive session start information in a single call.
41
+
42
+ Consolidates all information needed for session start into one method,
43
+ reducing context usage from 6+ tool calls to 1.
44
+
45
+ Args:
46
+ include_git_log: Include recent git commits (default: True)
47
+ git_log_count: Number of recent commits to include (default: 5)
48
+ analytics_top_n: Number of bottlenecks/recommendations (default: 3)
49
+ analytics_max_agents: Max agents for parallel work analysis (default: 3)
50
+
51
+ Returns:
52
+ Dict with comprehensive session start context:
53
+ - status: Project status (nodes, collections, WIP)
54
+ - active_work: Current active work item (if any)
55
+ - features: List of features with status
56
+ - sessions: Recent sessions
57
+ - git_log: Recent commits (if include_git_log=True)
58
+ - analytics: Strategic insights (bottlenecks, recommendations, parallel)
59
+
60
+ Note:
61
+ Returns empty dict {} if session context unavailable.
62
+ Always check for expected keys before accessing.
63
+
64
+ Example:
65
+ >>> sdk = SDK(agent="claude")
66
+ >>> info = sdk.get_session_start_info()
67
+ >>> logger.info(f"Project: {info['status']['total_nodes']} nodes")
68
+ >>> logger.info(f"WIP: {info['status']['in_progress_count']}")
69
+ >>> if info.get('active_work'):
70
+ ... logger.info(f"Active: {info['active_work']['title']}")
71
+ >>> for bn in info['analytics']['bottlenecks']:
72
+ ... logger.info(f"Bottleneck: {bn['title']}")
73
+ """
74
+ result: dict[str, Any] = {}
75
+
76
+ # 1. Project status
77
+ result["status"] = self.get_status() # type: ignore[attr-defined]
78
+
79
+ # 2. Active work item (validation status) - always include, even if None
80
+ result["active_work"] = self.get_active_work_item()
81
+
82
+ # 3. Features list (simplified)
83
+ features_list: list[dict[str, object]] = []
84
+ for feature in self.features.all(): # type: ignore[attr-defined]
85
+ features_list.append(
86
+ {
87
+ "id": feature.id,
88
+ "title": feature.title,
89
+ "status": feature.status,
90
+ "priority": feature.priority,
91
+ "steps_total": len(feature.steps),
92
+ "steps_completed": sum(1 for s in feature.steps if s.completed),
93
+ }
94
+ )
95
+ result["features"] = features_list
96
+
97
+ # 4. Sessions list (recent 20)
98
+ sessions_list: list[dict[str, Any]] = []
99
+ for session in self.sessions.all()[:20]: # type: ignore[attr-defined]
100
+ sessions_list.append(
101
+ {
102
+ "id": session.id,
103
+ "status": session.status,
104
+ "agent": session.properties.get("agent", "unknown"),
105
+ "event_count": session.properties.get("event_count", 0),
106
+ "started": session.created.isoformat()
107
+ if hasattr(session, "created")
108
+ else None,
109
+ }
110
+ )
111
+ result["sessions"] = sessions_list
112
+
113
+ # 5. Git log (if requested)
114
+ if include_git_log:
115
+ try:
116
+ git_result = subprocess.run(
117
+ ["git", "log", "--oneline", f"-{git_log_count}"],
118
+ capture_output=True,
119
+ text=True,
120
+ check=True,
121
+ cwd=self._directory.parent,
122
+ )
123
+ git_lines: list[str] = git_result.stdout.strip().split("\n")
124
+ result["git_log"] = git_lines
125
+ except (subprocess.CalledProcessError, FileNotFoundError):
126
+ empty_list: list[str] = []
127
+ result["git_log"] = empty_list
128
+
129
+ # 6. Strategic analytics
130
+ result["analytics"] = {
131
+ "bottlenecks": self.find_bottlenecks(top_n=analytics_top_n), # type: ignore[attr-defined]
132
+ "recommendations": self.recommend_next_work(agent_count=analytics_top_n), # type: ignore[attr-defined]
133
+ "parallel": self.get_parallel_work(max_agents=analytics_max_agents), # type: ignore[attr-defined]
134
+ }
135
+
136
+ return result # type: ignore[return-value]
137
+
138
+ def get_active_work_item(
139
+ self,
140
+ agent: str | None = None,
141
+ filter_by_agent: bool = False,
142
+ work_types: list[str] | None = None,
143
+ ) -> ActiveWorkItem | None:
144
+ """
145
+ Get the currently active work item (in-progress status).
146
+
147
+ This is used by the PreToolUse validation hook to check if code changes
148
+ have an active work item for attribution.
149
+
150
+ Args:
151
+ agent: Agent ID for filtering (optional)
152
+ filter_by_agent: If True, filter by agent. If False (default), return any active work item
153
+ work_types: Work item types to check (defaults to all: features, bugs, spikes, chores, epics)
154
+
155
+ Returns:
156
+ Dict with work item details or None if no active work item found:
157
+ - id: Work item ID
158
+ - title: Work item title
159
+ - type: Work item type (feature, bug, spike, chore, epic)
160
+ - status: Should be "in-progress"
161
+ - agent: Assigned agent
162
+ - steps_total: Total steps
163
+ - steps_completed: Completed steps
164
+ - auto_generated: (spikes only) True if auto-generated spike
165
+ - spike_subtype: (spikes only) "session-init" or "transition"
166
+
167
+ Example:
168
+ >>> sdk = SDK(agent="claude")
169
+ >>> # Get any active work item
170
+ >>> active = sdk.get_active_work_item()
171
+ >>> if active:
172
+ ... logger.info(f"Working on: {active['title']}")
173
+ ...
174
+ >>> # Get only this agent's active work item
175
+ >>> active = sdk.get_active_work_item(filter_by_agent=True)
176
+ """
177
+ # Default to all work item types
178
+ if work_types is None:
179
+ work_types = ["features", "bugs", "spikes", "chores", "epics"]
180
+
181
+ # Search across all work item types
182
+ # Separate real work items from auto-generated spikes
183
+ real_work_items: list[dict[str, Any]] = []
184
+ auto_spikes: list[dict[str, Any]] = []
185
+
186
+ for work_type in work_types:
187
+ collection = getattr(self, work_type, None)
188
+ if collection is None:
189
+ continue
190
+
191
+ # Query for in-progress items
192
+ in_progress = collection.where(status="in-progress")
193
+
194
+ for item in in_progress:
195
+ # Filter by agent if requested
196
+ if filter_by_agent:
197
+ agent_id = agent or self._agent_id
198
+ if agent_id and hasattr(item, "agent_assigned"):
199
+ if item.agent_assigned != agent_id:
200
+ continue
201
+
202
+ item_dict: dict[str, Any] = {
203
+ "id": item.id,
204
+ "title": item.title,
205
+ "type": item.type,
206
+ "status": item.status,
207
+ "agent": getattr(item, "agent_assigned", None),
208
+ "steps_total": len(item.steps) if hasattr(item, "steps") else 0,
209
+ "steps_completed": sum(1 for s in item.steps if s.completed)
210
+ if hasattr(item, "steps")
211
+ else 0,
212
+ }
213
+
214
+ # Add spike-specific fields for auto-spike detection
215
+ if item.type == "spike":
216
+ item_dict["auto_generated"] = getattr(item, "auto_generated", False)
217
+ item_dict["spike_subtype"] = getattr(item, "spike_subtype", None)
218
+
219
+ # Separate auto-spikes from real work
220
+ # Auto-spikes are temporary tracking items (session-init, transition, conversation-init)
221
+ is_auto_spike = item_dict["auto_generated"] and item_dict[
222
+ "spike_subtype"
223
+ ] in ("session-init", "transition", "conversation-init")
224
+
225
+ if is_auto_spike:
226
+ auto_spikes.append(item_dict)
227
+ else:
228
+ # Real user-created spike
229
+ real_work_items.append(item_dict)
230
+ else:
231
+ # Features, bugs, chores, epics are always real work
232
+ real_work_items.append(item_dict)
233
+
234
+ # Prioritize real work items over auto-spikes
235
+ # Auto-spikes should only show if there's NO other active work item
236
+ if real_work_items:
237
+ return real_work_items[0] # type: ignore[return-value]
238
+
239
+ if auto_spikes:
240
+ return auto_spikes[0] # type: ignore[return-value]
241
+
242
+ return None
243
+
244
+ def track_activity(
245
+ self,
246
+ tool: str,
247
+ summary: str,
248
+ file_paths: list[str] | None = None,
249
+ success: bool = True,
250
+ feature_id: str | None = None,
251
+ session_id: str | None = None,
252
+ parent_activity_id: str | None = None,
253
+ payload: dict[str, Any] | None = None,
254
+ ) -> Any:
255
+ """
256
+ Track an activity in the current or specified session.
257
+
258
+ Args:
259
+ tool: Tool name (Edit, Bash, Read, etc.)
260
+ summary: Human-readable summary of the activity
261
+ file_paths: Files involved in this activity
262
+ success: Whether the tool call succeeded
263
+ feature_id: Explicit feature ID (skips attribution if provided)
264
+ session_id: Session ID (defaults to parent session if available, then active session)
265
+ parent_activity_id: ID of parent activity (e.g., Skill/Task invocation)
266
+ payload: Optional rich payload data
267
+
268
+ Returns:
269
+ Created ActivityEntry with attribution
270
+
271
+ Example:
272
+ >>> sdk = SDK(agent="claude")
273
+ >>> entry = sdk.track_activity(
274
+ ... tool="CustomTool",
275
+ ... summary="Performed custom analysis",
276
+ ... file_paths=["src/main.py"],
277
+ ... success=True
278
+ ... )
279
+ >>> logger.info(f"Tracked: [{entry.tool}] {entry.summary}")
280
+ """
281
+ # Determine target session: explicit parameter > parent_session > active > none
282
+ if not session_id:
283
+ # Priority 1: Parent session (explicitly provided or from env var)
284
+ if hasattr(self, "_parent_session") and self._parent_session: # type: ignore[attr-defined]
285
+ session_id = self._parent_session # type: ignore[attr-defined]
286
+ else:
287
+ # Priority 2: Active session for this agent
288
+ active = self.session_manager.get_active_session(agent=self._agent_id)
289
+ if active:
290
+ session_id = active.id
291
+ else:
292
+ raise ValueError(
293
+ "No active session. Start one with sdk.start_session()"
294
+ )
295
+
296
+ # Get parent activity ID from environment if not provided
297
+ if not parent_activity_id:
298
+ parent_activity_id = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
299
+
300
+ return self.session_manager.track_activity(
301
+ session_id=session_id,
302
+ tool=tool,
303
+ summary=summary,
304
+ file_paths=file_paths,
305
+ success=success,
306
+ feature_id=feature_id,
307
+ parent_activity_id=parent_activity_id,
308
+ payload=payload,
309
+ )
@@ -0,0 +1,103 @@
1
+ """
2
+ SessionManager accessor and session creation/validation for SDK.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from htmlgraph.session_manager import SessionManager
11
+
12
+
13
+ class SessionManagerMixin:
14
+ """
15
+ Provides SessionManager accessor and session lifecycle operations.
16
+
17
+ Attributes accessed by mixins:
18
+ session_manager: SessionManager instance
19
+ _db: HtmlGraphDB instance
20
+ _agent_id: Agent identifier
21
+ _parent_session: Parent session ID (if nested)
22
+ """
23
+
24
+ session_manager: SessionManager
25
+
26
+ def _ensure_session_exists(
27
+ self, session_id: str, parent_event_id: str | None = None
28
+ ) -> None:
29
+ """
30
+ Create a session record if it doesn't exist.
31
+
32
+ Args:
33
+ session_id: Session ID to ensure exists
34
+ parent_event_id: Event that spawned this session (optional)
35
+ """
36
+ if not self._db.connection: # type: ignore[attr-defined]
37
+ self._db.connect() # type: ignore[attr-defined]
38
+
39
+ cursor = self._db.connection.cursor() # type: ignore[attr-defined,union-attr]
40
+ cursor.execute(
41
+ "SELECT COUNT(*) FROM sessions WHERE session_id = ?", (session_id,)
42
+ )
43
+ exists = cursor.fetchone()[0] > 0
44
+
45
+ if not exists:
46
+ # Create session record
47
+ self._db.insert_session( # type: ignore[attr-defined]
48
+ session_id=session_id,
49
+ agent_assigned=self._agent_id, # type: ignore[attr-defined]
50
+ is_subagent=self._parent_session is not None, # type: ignore[attr-defined]
51
+ parent_session_id=self._parent_session, # type: ignore[attr-defined]
52
+ parent_event_id=parent_event_id,
53
+ )
54
+
55
+ def start_session(
56
+ self,
57
+ session_id: str | None = None,
58
+ title: str | None = None,
59
+ agent: str | None = None,
60
+ ) -> Any:
61
+ """
62
+ Start a new session.
63
+
64
+ Args:
65
+ session_id: Optional session ID
66
+ title: Optional session title
67
+ agent: Optional agent override (defaults to SDK agent)
68
+
69
+ Returns:
70
+ New Session instance
71
+ """
72
+ return self.session_manager.start_session(
73
+ session_id=session_id,
74
+ agent=agent or self._agent_id or "cli", # type: ignore[attr-defined]
75
+ title=title,
76
+ parent_session_id=self._parent_session, # type: ignore[attr-defined]
77
+ )
78
+
79
+ def end_session(
80
+ self,
81
+ session_id: str,
82
+ handoff_notes: str | None = None,
83
+ recommended_next: str | None = None,
84
+ blockers: list[str] | None = None,
85
+ ) -> Any:
86
+ """
87
+ End a session.
88
+
89
+ Args:
90
+ session_id: Session ID to end
91
+ handoff_notes: Optional handoff notes
92
+ recommended_next: Optional recommendations
93
+ blockers: Optional blockers
94
+
95
+ Returns:
96
+ Ended Session instance
97
+ """
98
+ return self.session_manager.end_session(
99
+ session_id=session_id,
100
+ handoff_notes=handoff_notes,
101
+ recommended_next=recommended_next,
102
+ blockers=blockers,
103
+ )
htmlgraph/server.py CHANGED
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  HtmlGraph REST API Server.
3
7
 
@@ -243,8 +247,8 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
243
247
  def do_GET(self) -> None:
244
248
  """Handle GET requests."""
245
249
  api, collection, node_id, params = self._parse_path()
246
- print(
247
- f"DEBUG do_GET: api={api}, collection={collection}, node_id={node_id}, params={params}"
250
+ logger.debug(
251
+ f"do_GET: api={api}, collection={collection}, node_id={node_id}, params={params}"
248
252
  )
249
253
 
250
254
  # Not an API request - serve static files
@@ -273,7 +277,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
273
277
 
274
278
  # GET /api/orchestration - Get delegation chains and agent coordination
275
279
  if collection == "orchestration":
276
- print(f"DEBUG: Handling orchestration request, params={params}")
280
+ logger.info(f"DEBUG: Handling orchestration request, params={params}")
277
281
  return self._handle_orchestration_view(params)
278
282
 
279
283
  # GET /api/task-delegations/stats - Get aggregated delegation statistics
@@ -1275,7 +1279,7 @@ class HtmlGraphAPIHandler(SimpleHTTPRequestHandler):
1275
1279
 
1276
1280
  def log_message(self, format: str, *args: str) -> None:
1277
1281
  """Custom log format."""
1278
- print(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
1282
+ logger.info(f"[{datetime.now().strftime('%H:%M:%S')}] {args[0]}")
1279
1283
 
1280
1284
 
1281
1285
  def find_available_port(start_port: int = 8080, max_attempts: int = 10) -> int:
@@ -1434,7 +1438,7 @@ def serve(
1434
1438
  # Print warnings if any
1435
1439
  for warning in result.warnings:
1436
1440
  if not quiet:
1437
- print(f"⚠️ {warning}")
1441
+ logger.info(f"⚠️ {warning}")
1438
1442
 
1439
1443
  # Print server info
1440
1444
  if not quiet:
@@ -1473,31 +1477,31 @@ Press Ctrl+C to stop.
1473
1477
  asyncio.run(run_fastapi_server(result.handle))
1474
1478
 
1475
1479
  except PortInUseError:
1476
- print(f"\n❌ Port {port} is already in use\n")
1477
- print("Solutions:")
1478
- print(" 1. Use a different port:")
1479
- print(f" htmlgraph serve --port {port + 1}\n")
1480
- print(" 2. Let htmlgraph automatically find an available port:")
1481
- print(" htmlgraph serve --auto-port\n")
1482
- print(f" 3. Find and kill the process using port {port}:")
1483
- print(f" lsof -ti:{port} | xargs kill -9\n")
1480
+ logger.info(f"\n❌ Port {port} is already in use\n")
1481
+ logger.info("Solutions:")
1482
+ logger.info(" 1. Use a different port:")
1483
+ logger.info(f" htmlgraph serve --port {port + 1}\n")
1484
+ logger.info(" 2. Let htmlgraph automatically find an available port:")
1485
+ logger.info(" htmlgraph serve --auto-port\n")
1486
+ logger.info(f" 3. Find and kill the process using port {port}:")
1487
+ logger.info(f" lsof -ti:{port} | xargs kill -9\n")
1484
1488
 
1485
1489
  # Try to find and suggest an available port
1486
1490
  try:
1487
1491
  alt_port = find_available_port(port + 1)
1488
- print(f"💡 Found available port: {alt_port}")
1489
- print(f" Run: htmlgraph serve --port {alt_port}\n")
1492
+ logger.info(f"💡 Found available port: {alt_port}")
1493
+ logger.info(f" Run: htmlgraph serve --port {alt_port}\n")
1490
1494
  except OSError:
1491
1495
  pass
1492
1496
 
1493
1497
  sys.exit(1)
1494
1498
 
1495
1499
  except FastAPIServerError as e:
1496
- print(f"\n❌ Server error: {e}\n")
1500
+ logger.info(f"\n❌ Server error: {e}\n")
1497
1501
  sys.exit(1)
1498
1502
 
1499
1503
  except KeyboardInterrupt:
1500
- print("\nShutting down...")
1504
+ logger.info("\nShutting down...")
1501
1505
 
1502
1506
 
1503
1507
  if __name__ == "__main__":
@@ -929,13 +929,7 @@ class SessionManager:
929
929
  session.handoff_notes = handoff_data["handoff_notes"]
930
930
  session.recommended_next = handoff_data["recommended_next"]
931
931
  session.blockers = handoff_data["blockers"]
932
-
933
- # Store recommended_context as JSON-serializable list
934
- # (Session model expects list[str], converter will handle serialization)
935
- if hasattr(session, "__dict__"):
936
- session.__dict__["recommended_context"] = handoff_data[
937
- "recommended_context"
938
- ]
932
+ session.recommended_context = handoff_data["recommended_context"]
939
933
 
940
934
  # Persist handoff data to database before ending session
941
935
  self.session_converter.save(session)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Session Warning System for AI Agents.
3
5
 
@@ -20,7 +22,6 @@ Usage:
20
22
  sdk.dismiss_session_warning()
21
23
  """
22
24
 
23
- from __future__ import annotations
24
25
 
25
26
  import json
26
27
  import sys
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Session Handoff and Continuity - Phase 2 Feature 3
3
5
 
@@ -19,11 +21,10 @@ Usage:
19
21
  # Resume next session
20
22
  resumed = sdk.sessions.continue_from_last()
21
23
  if resumed:
22
- print(resumed.summary)
23
- print(resumed.recommended_files)
24
+ logger.info("%s", resumed.summary)
25
+ logger.info("%s", resumed.recommended_files)
24
26
  """
25
27
 
26
- from __future__ import annotations
27
28
 
28
29
  import json
29
30
  import logging
@@ -613,6 +614,9 @@ class HandoffTracker:
613
614
  handoff_id = generate_id("hand")
614
615
 
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
+
616
620
  cursor = self.db.connection.cursor()
617
621
  cursor.execute(
618
622
  """
@@ -649,6 +653,9 @@ class HandoffTracker:
649
653
  return False
650
654
 
651
655
  try:
656
+ # Ensure to_session exists in database (handles FK constraint)
657
+ self.db._ensure_session_exists(to_session_id)
658
+
652
659
  cursor = self.db.connection.cursor()
653
660
  cursor.execute(
654
661
  """
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """System prompt management for HtmlGraph projects.
2
4
 
3
5
  Provides a two-tier system:
@@ -10,7 +12,6 @@ Architecture:
10
12
  - SDK provides methods for creation, validation, and management
11
13
  """
12
14
 
13
- from __future__ import annotations
14
15
 
15
16
  import logging
16
17
  from pathlib import Path
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Track Builder and Collection for agent-friendly track creation.
3
5
 
@@ -5,7 +7,6 @@ Note: TrackBuilder has been moved to builders/track.py for better organization.
5
7
  This module now provides TrackCollection and re-exports TrackBuilder for backward compatibility.
6
8
  """
7
9
 
8
- from __future__ import annotations
9
10
 
10
11
  from collections.abc import Iterator
11
12
  from contextlib import contextmanager
@@ -31,6 +32,18 @@ class TrackCollection:
31
32
  self.collection_name = "tracks" # For backward compatibility
32
33
  self.id_prefix = "track"
33
34
  self._graph: HtmlGraph | None = None # Lazy-loaded
35
+ self._ref_manager: Any = None # Set by SDK during initialization
36
+
37
+ def set_ref_manager(self, ref_manager: Any) -> None:
38
+ """
39
+ Set the ref manager for this collection.
40
+
41
+ Called by SDK during initialization to enable short ref support.
42
+
43
+ Args:
44
+ ref_manager: RefManager instance from SDK
45
+ """
46
+ self._ref_manager = ref_manager
34
47
 
35
48
  def _ensure_graph(self) -> HtmlGraph:
36
49
  """Lazy-load the graph for tracks with multi-pattern support."""
htmlgraph/transcript.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Claude Code Transcript Integration.
3
5
 
@@ -21,7 +23,6 @@ References:
21
23
  - https://github.com/simonw/claude-code-transcripts
22
24
  """
23
25
 
24
- from __future__ import annotations
25
26
 
26
27
  import json
27
28
  from collections.abc import Iterator
htmlgraph/watch.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  File-change watcher for HtmlGraph.
3
5
 
@@ -7,7 +9,6 @@ native "PostToolUse" hooks (e.g. editors/agents that write files directly).
7
9
  It batches filesystem changes and records them as activity events.
8
10
  """
9
11
 
10
- from __future__ import annotations
11
12
 
12
13
  import os
13
14
  import time
@@ -1,10 +1,11 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Utility functions for work type inference and classification.
3
5
 
4
6
  Provides automatic work type detection based on active work items.
5
7
  """
6
8
 
7
- from __future__ import annotations
8
9
 
9
10
  from typing import TYPE_CHECKING
10
11