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.
- htmlgraph/__init__.py +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/main.py +114 -51
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +28 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +33 -8
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +76 -1
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +43 -4
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +11 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +81 -66
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +92 -14
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +5 -2
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +18 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +25 -21
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_manager.py +1 -7
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +10 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +14 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
- htmlgraph/sdk.py +0 -3430
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
247
|
-
f"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1500
|
+
logger.info(f"\n❌ Server error: {e}\n")
|
|
1497
1501
|
sys.exit(1)
|
|
1498
1502
|
|
|
1499
1503
|
except KeyboardInterrupt:
|
|
1500
|
-
|
|
1504
|
+
logger.info("\nShutting down...")
|
|
1501
1505
|
|
|
1502
1506
|
|
|
1503
1507
|
if __name__ == "__main__":
|
htmlgraph/session_manager.py
CHANGED
|
@@ -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)
|
htmlgraph/session_warning.py
CHANGED
htmlgraph/sessions/handoff.py
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
"""
|
htmlgraph/system_prompts.py
CHANGED
|
@@ -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
|
htmlgraph/track_builder.py
CHANGED
|
@@ -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
|
htmlgraph/work_type_utils.py
CHANGED
|
@@ -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
|
|