htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,300 @@
1
+ """
2
+ SessionStart Hook Integration - Initialize session registry with repo awareness.
3
+
4
+ Integrates:
5
+ - SessionRegistry: File-based session tracking
6
+ - RepoHash: Git awareness and repository identification
7
+ - AtomicFileWriter: Crash-safe writes
8
+
9
+ Called by SessionStart hook to:
10
+ 1. Register new session with repo info
11
+ 2. Export session IDs to CLAUDE_ENV_FILE
12
+ 3. Detect parent sessions from environment
13
+ 4. Initialize heartbeat mechanism
14
+ """
15
+
16
+ import logging
17
+ import os
18
+ import uuid
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def initialize_session_from_hook(env_file: str | None = None) -> str:
26
+ """
27
+ Initialize session registry from SessionStart hook.
28
+
29
+ Called automatically by SessionStart hook to set up session tracking.
30
+ Registers session with repository information and exports environment variables.
31
+
32
+ Args:
33
+ env_file: Path to CLAUDE_ENV_FILE (from hook environment).
34
+ Allows exporting session IDs to parent process.
35
+
36
+ Returns:
37
+ session_id: Generated session ID for logging/tracking
38
+
39
+ Raises:
40
+ OSError: If registry initialization fails
41
+ RuntimeError: If session registration fails
42
+ """
43
+ from htmlgraph.repo_hash import RepoHash
44
+ from htmlgraph.session_registry import SessionRegistry
45
+
46
+ try:
47
+ # Initialize registry (creates .htmlgraph/sessions/registry structure)
48
+ registry = SessionRegistry()
49
+ logger.debug(f"Initialized SessionRegistry at {registry.registry_dir}")
50
+
51
+ # Get repo information
52
+ try:
53
+ repo_hash = RepoHash()
54
+ git_info = repo_hash.get_git_info()
55
+ repo_hash_value = repo_hash.compute_repo_hash()
56
+
57
+ repo_info = {
58
+ "path": str(repo_hash.repo_path),
59
+ "hash": repo_hash_value,
60
+ "branch": git_info.get("branch"),
61
+ "commit": git_info.get("commit"),
62
+ "remote": git_info.get("remote"),
63
+ "dirty": git_info.get("dirty", False),
64
+ "is_monorepo": repo_hash.is_monorepo(),
65
+ "monorepo_project": repo_hash.get_monorepo_project(),
66
+ }
67
+ logger.debug(f"Repo info: {repo_info}")
68
+ except OSError as e:
69
+ logger.warning(f"Failed to get repo info: {e}")
70
+ repo_info = {
71
+ "path": str(Path.cwd()),
72
+ "hash": "unknown",
73
+ "branch": None,
74
+ "commit": None,
75
+ "remote": None,
76
+ "dirty": False,
77
+ "is_monorepo": False,
78
+ "monorepo_project": None,
79
+ }
80
+
81
+ # Get instance information
82
+ instance_info = {
83
+ "pid": os.getpid(),
84
+ "hostname": _get_hostname(),
85
+ "start_time": _get_utc_timestamp(),
86
+ }
87
+
88
+ # Generate session ID
89
+ session_id = f"sess-{uuid.uuid4().hex[:8]}"
90
+
91
+ # Register session atomically
92
+ try:
93
+ registry_file = registry.register_session(
94
+ session_id=session_id,
95
+ repo_info=repo_info,
96
+ instance_info=instance_info,
97
+ )
98
+ logger.info(f"Registered session {session_id} at {registry_file}")
99
+ except OSError as e:
100
+ logger.error(f"Failed to register session: {e}")
101
+ raise RuntimeError(f"Session registration failed: {e}") from e
102
+
103
+ # Export to CLAUDE_ENV_FILE if provided
104
+ if env_file:
105
+ try:
106
+ _export_to_env_file(
107
+ env_file=env_file,
108
+ session_id=session_id,
109
+ instance_id=registry.get_instance_id(),
110
+ repo_hash=repo_hash_value
111
+ if "repo_hash_value" in locals()
112
+ else "unknown",
113
+ )
114
+ logger.debug(f"Exported session environment to {env_file}")
115
+ except OSError as e:
116
+ logger.warning(f"Failed to export environment: {e}")
117
+ # Don't fail - session is registered even if export fails
118
+
119
+ # Check for parent session
120
+ parent_session_id = _get_parent_session_id()
121
+ if parent_session_id:
122
+ logger.info(f"Parent session detected: {parent_session_id}")
123
+ # Store parent relationship in environment for subprocesses
124
+ os.environ["HTMLGRAPH_PARENT_SESSION_ID"] = parent_session_id
125
+
126
+ return session_id
127
+
128
+ except Exception as e:
129
+ logger.error(f"Failed to initialize session: {e}", exc_info=True)
130
+ raise
131
+
132
+
133
+ def finalize_session(session_id: str, status: str = "ended") -> bool:
134
+ """
135
+ Finalize session (called by SessionEnd hook).
136
+
137
+ Archives the session and updates last activity timestamp.
138
+
139
+ Args:
140
+ session_id: Session ID to finalize
141
+ status: Final status (ended, failed, etc.)
142
+
143
+ Returns:
144
+ True if finalization succeeded, False otherwise
145
+ """
146
+ from htmlgraph.session_registry import SessionRegistry
147
+
148
+ try:
149
+ registry = SessionRegistry()
150
+ instance_id = registry.get_instance_id()
151
+
152
+ # Archive the session
153
+ success = registry.archive_session(instance_id)
154
+ if success:
155
+ logger.info(f"Archived session {session_id} (status: {status})")
156
+ else:
157
+ logger.warning(f"Failed to archive session {session_id}")
158
+
159
+ return success
160
+ except Exception as e:
161
+ logger.error(f"Failed to finalize session {session_id}: {e}")
162
+ return False
163
+
164
+
165
+ def heartbeat(session_id: str | None = None) -> bool:
166
+ """
167
+ Update session activity timestamp (liveness heartbeat).
168
+
169
+ Called periodically to indicate the session is still active.
170
+ Should be called on each tool use or periodically (e.g., every 5 minutes).
171
+
172
+ Args:
173
+ session_id: Optional session ID (uses current instance if None)
174
+
175
+ Returns:
176
+ True if heartbeat succeeded, False otherwise
177
+ """
178
+ from htmlgraph.session_registry import SessionRegistry
179
+
180
+ try:
181
+ registry = SessionRegistry()
182
+ instance_id = registry.get_instance_id()
183
+
184
+ success = registry.update_activity(instance_id)
185
+ if success:
186
+ logger.debug(f"Updated activity for instance {instance_id}")
187
+ else:
188
+ logger.warning(f"Failed to update activity for instance {instance_id}")
189
+
190
+ return success
191
+ except Exception as e:
192
+ logger.error(f"Failed to update activity: {e}")
193
+ return False
194
+
195
+
196
+ def get_current_session() -> dict | None:
197
+ """
198
+ Get current session for this instance.
199
+
200
+ Returns the registration data for the current instance's session.
201
+
202
+ Returns:
203
+ Session dict with instance_id, session_id, repo, etc., or None if not found
204
+ """
205
+ from htmlgraph.session_registry import SessionRegistry
206
+
207
+ try:
208
+ registry = SessionRegistry()
209
+ instance_id = registry.get_instance_id()
210
+ session = registry.read_session(instance_id)
211
+ return session
212
+ except Exception as e:
213
+ logger.error(f"Failed to get current session: {e}")
214
+ return None
215
+
216
+
217
+ def get_parent_session_id() -> str | None:
218
+ """
219
+ Get parent session ID if this is a spawned task.
220
+
221
+ Returns:
222
+ Parent session ID from environment, or None if not a spawned task
223
+ """
224
+ return _get_parent_session_id()
225
+
226
+
227
+ # Private helpers
228
+
229
+
230
+ def _get_hostname() -> str:
231
+ """Get hostname safely."""
232
+ try:
233
+ import socket
234
+
235
+ return socket.gethostname()
236
+ except Exception:
237
+ return "unknown"
238
+
239
+
240
+ def _get_utc_timestamp() -> str:
241
+ """Get current UTC timestamp in ISO 8601 format."""
242
+ now = datetime.now(timezone.utc)
243
+ return now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
244
+
245
+
246
+ def _export_to_env_file(
247
+ env_file: str,
248
+ session_id: str,
249
+ instance_id: str,
250
+ repo_hash: str,
251
+ ) -> None:
252
+ """
253
+ Export session environment variables to CLAUDE_ENV_FILE.
254
+
255
+ Appends to the file to preserve any existing variables.
256
+
257
+ Args:
258
+ env_file: Path to environment file
259
+ session_id: Session ID to export
260
+ instance_id: Instance ID to export
261
+ repo_hash: Repository hash to export
262
+
263
+ Raises:
264
+ OSError: If file write fails
265
+ """
266
+ env_path = Path(env_file)
267
+
268
+ try:
269
+ # Append to environment file
270
+ with open(env_path, "a") as f:
271
+ f.write(f"export HTMLGRAPH_SESSION_ID={session_id}\n")
272
+ f.write(f"export HTMLGRAPH_INSTANCE_ID={instance_id}\n")
273
+ f.write(f"export HTMLGRAPH_REPO_HASH={repo_hash}\n")
274
+
275
+ logger.debug(f"Exported environment variables to {env_file}")
276
+ except OSError as e:
277
+ logger.error(f"Failed to export environment to {env_file}: {e}")
278
+ raise
279
+
280
+
281
+ def _get_parent_session_id() -> str | None:
282
+ """
283
+ Detect parent session from environment.
284
+
285
+ Checks:
286
+ 1. HTMLGRAPH_PARENT_SESSION_ID env var (set by Task spawning)
287
+ 2. HTMLGRAPH_PARENT_SESSION env var (alternate name)
288
+
289
+ Returns:
290
+ Parent session ID if found, None otherwise
291
+ """
292
+ parent_id = os.environ.get("HTMLGRAPH_PARENT_SESSION_ID")
293
+ if parent_id:
294
+ return parent_id
295
+
296
+ parent_id = os.environ.get("HTMLGRAPH_PARENT_SESSION")
297
+ if parent_id:
298
+ return parent_id
299
+
300
+ return None
@@ -27,7 +27,7 @@ from htmlgraph.event_log import EventRecord, JsonlEventLog
27
27
  from htmlgraph.exceptions import SessionNotFoundError
28
28
  from htmlgraph.graph import HtmlGraph
29
29
  from htmlgraph.ids import generate_id
30
- from htmlgraph.models import ActivityEntry, Node, Session
30
+ from htmlgraph.models import ActivityEntry, ErrorEntry, Node, Session
31
31
  from htmlgraph.services import ClaimingService
32
32
  from htmlgraph.spike_index import ActiveAutoSpikeIndex
33
33
 
@@ -230,6 +230,7 @@ class SessionManager:
230
230
  continued_from: str | None = None,
231
231
  start_commit: str | None = None,
232
232
  title: str | None = None,
233
+ parent_session_id: str | None = None,
233
234
  ) -> Session:
234
235
  """
235
236
  Start a new session.
@@ -241,6 +242,7 @@ class SessionManager:
241
242
  continued_from: Previous session ID if continuing
242
243
  start_commit: Git commit hash at session start
243
244
  title: Optional human-readable title
245
+ parent_session_id: ID of parent session (for subagents)
244
246
 
245
247
  Returns:
246
248
  New Session instance
@@ -309,6 +311,7 @@ class SessionManager:
309
311
  started_at=now,
310
312
  last_activity=now,
311
313
  title=title or "",
314
+ parent_session=parent_session_id,
312
315
  )
313
316
 
314
317
  # Add session start event
@@ -320,6 +323,12 @@ class SessionManager:
320
323
  )
321
324
  )
322
325
 
326
+ # Set parent session in environment for subsequent subprocesses (e.g. HeadlessSpawner)
327
+ # This ensures that any tools spawned by this session link back to it
328
+ import os
329
+
330
+ os.environ["HTMLGRAPH_PARENT_SESSION"] = session.id
331
+
323
332
  # Save to disk
324
333
  self.session_converter.save(session)
325
334
  self._sessions_cache_dirty = True
@@ -795,6 +804,126 @@ class SessionManager:
795
804
  """
796
805
  return self.claiming_service.release_session_features(session_id)
797
806
 
807
+ def log_error(
808
+ self,
809
+ session_id: str,
810
+ error: Exception,
811
+ traceback_str: str,
812
+ context: dict[str, Any] | None = None,
813
+ ) -> None:
814
+ """
815
+ Log error with full traceback to session.
816
+
817
+ Stores complete error details for later retrieval via debug command.
818
+ Minimizes console output for better token efficiency.
819
+
820
+ Args:
821
+ session_id: Session ID to log error to
822
+ error: The exception object
823
+ traceback_str: Full traceback string
824
+ context: Optional context dict (e.g. current file, line number)
825
+ """
826
+ session = self.get_session(session_id)
827
+ if not session:
828
+ return
829
+
830
+ error_entry = ErrorEntry(
831
+ timestamp=datetime.now(),
832
+ error_type=error.__class__.__name__,
833
+ message=str(error),
834
+ traceback=traceback_str,
835
+ )
836
+
837
+ # Append error record to error_log
838
+ session.error_log.append(error_entry)
839
+
840
+ # Save updated session
841
+ self.session_converter.save(session)
842
+
843
+ def get_session_errors(self, session_id: str) -> list[dict[str, Any]]:
844
+ """
845
+ Retrieve all errors logged for a session.
846
+
847
+ Args:
848
+ session_id: Session ID
849
+
850
+ Returns:
851
+ List of error records, or empty list if none
852
+ """
853
+ session = self.get_session(session_id)
854
+ if not session:
855
+ return []
856
+ return [error.model_dump() for error in session.error_log]
857
+
858
+ def search_errors(
859
+ self,
860
+ session_id: str,
861
+ error_type: str | None = None,
862
+ pattern: str | None = None,
863
+ ) -> list[dict[str, Any]]:
864
+ """
865
+ Search errors in a session by type and/or pattern.
866
+
867
+ Args:
868
+ session_id: Session ID to search
869
+ error_type: Filter by exception type (e.g., "ValueError")
870
+ pattern: Regex pattern to match in error message
871
+
872
+ Returns:
873
+ List of matching error records
874
+ """
875
+ session = self.get_session(session_id)
876
+ if not session:
877
+ return []
878
+
879
+ errors = [error.model_dump() for error in session.error_log]
880
+
881
+ # Filter by error type
882
+ if error_type:
883
+ errors = [e for e in errors if e.get("error_type") == error_type]
884
+
885
+ # Filter by pattern in message
886
+ if pattern:
887
+ compiled_pattern = re.compile(pattern, re.IGNORECASE)
888
+ errors = [
889
+ e for e in errors if compiled_pattern.search(e.get("message", ""))
890
+ ]
891
+
892
+ return errors
893
+
894
+ def get_error_summary(self, session_id: str) -> dict[str, Any]:
895
+ """
896
+ Get summary statistics of errors in a session.
897
+
898
+ Args:
899
+ session_id: Session ID
900
+
901
+ Returns:
902
+ Dictionary with error summary statistics
903
+ """
904
+ session = self.get_session(session_id)
905
+ if not session or not session.error_log:
906
+ return {
907
+ "total_errors": 0,
908
+ "error_types": {},
909
+ "first_error": None,
910
+ "last_error": None,
911
+ }
912
+
913
+ errors = session.error_log
914
+ error_types: dict[str, int] = {}
915
+
916
+ for error in errors:
917
+ error_type = error.error_type
918
+ error_types[error_type] = error_types.get(error_type, 0) + 1
919
+
920
+ return {
921
+ "total_errors": len(errors),
922
+ "error_types": error_types,
923
+ "first_error": errors[0].model_dump() if errors else None,
924
+ "last_error": errors[-1].model_dump() if errors else None,
925
+ }
926
+
798
927
  # =========================================================================
799
928
  # Activity Tracking
800
929
  # =========================================================================
@@ -925,6 +1054,7 @@ class SessionManager:
925
1054
  work_type=work_type,
926
1055
  session_status=session.status,
927
1056
  file_paths=file_paths,
1057
+ parent_session_id=session.parent_session,
928
1058
  payload=entry.payload
929
1059
  if isinstance(entry.payload, dict)
930
1060
  else payload,