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,169 @@
1
+ #!/usr/bin/env python3
2
+ """Bootstrap utilities for hook scripts.
3
+
4
+ Centralizes environment setup and project directory resolution used by all hooks.
5
+ Handles both development (src/python) and installed (package) modes.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+
15
+ def resolve_project_dir(cwd: str | None = None) -> str:
16
+ """Resolve the project directory with sensible fallbacks.
17
+
18
+ Hierarchy:
19
+ 1. CLAUDE_PROJECT_DIR environment variable (set by Claude Code)
20
+ 2. Git repository root (via git rev-parse --show-toplevel)
21
+ 3. Current working directory (or provided cwd)
22
+
23
+ This supports running hooks in multiple contexts:
24
+ - Within a Claude Code session
25
+ - In git repositories
26
+ - In arbitrary directories
27
+
28
+ Args:
29
+ cwd: Starting directory for git search. Defaults to os.getcwd().
30
+
31
+ Returns:
32
+ Absolute path to the project directory.
33
+
34
+ Raises:
35
+ No exceptions - always returns a valid path.
36
+ """
37
+ # First priority: Claude's explicit project directory
38
+ env_dir = os.environ.get("CLAUDE_PROJECT_DIR")
39
+ if env_dir:
40
+ return env_dir
41
+
42
+ # Second priority: Git repository root
43
+ start_dir = cwd or os.getcwd()
44
+ try:
45
+ result = subprocess.run(
46
+ ["git", "rev-parse", "--show-toplevel"],
47
+ capture_output=True,
48
+ text=True,
49
+ cwd=start_dir,
50
+ timeout=5,
51
+ )
52
+ if result.returncode == 0:
53
+ return result.stdout.strip()
54
+ except Exception:
55
+ # Git not available or not a repo - continue to fallback
56
+ pass
57
+
58
+ # Final fallback: current working directory
59
+ return start_dir
60
+
61
+
62
+ def bootstrap_pythonpath(project_dir: str) -> None:
63
+ """Bootstrap Python path for htmlgraph imports.
64
+
65
+ Handles two common deployment modes:
66
+ 1. Development: Running inside htmlgraph repository (src/python exists)
67
+ 2. Installed: Running where htmlgraph is installed as a package (do nothing)
68
+
69
+ This allows hooks to work correctly whether htmlgraph is:
70
+ - Being developed locally (add src/python to path)
71
+ - Installed in a virtual environment (already in path)
72
+ - Installed globally (already in path)
73
+
74
+ Args:
75
+ project_dir: Project directory from resolve_project_dir().
76
+
77
+ Returns:
78
+ None (modifies sys.path in-place).
79
+
80
+ Side Effects:
81
+ - Modifies sys.path to ensure htmlgraph is importable
82
+ - Adds .venv/lib/pythonX.Y/site-packages if virtual environment exists
83
+ - Adds src/python if in htmlgraph repository
84
+ """
85
+ project_path = Path(project_dir)
86
+
87
+ # First, try to use local virtual environment if it exists
88
+ venv = project_path / ".venv"
89
+ if venv.exists():
90
+ pyver = f"python{sys.version_info.major}.{sys.version_info.minor}"
91
+ candidates = [
92
+ venv / "lib" / pyver / "site-packages", # macOS/Linux
93
+ venv / "Lib" / "site-packages", # Windows
94
+ ]
95
+ for candidate in candidates:
96
+ if candidate.exists():
97
+ sys.path.insert(0, str(candidate))
98
+ break
99
+
100
+ # Then, add src/python if this is the htmlgraph repository itself
101
+ repo_src = project_path / "src" / "python"
102
+ if repo_src.exists():
103
+ sys.path.insert(0, str(repo_src))
104
+
105
+
106
+ def get_graph_dir(cwd: str | None = None) -> Path:
107
+ """Get the .htmlgraph directory path, creating it if necessary.
108
+
109
+ The .htmlgraph directory is the root for all HtmlGraph tracking:
110
+ - .htmlgraph/sessions/ - Session HTML files
111
+ - .htmlgraph/features/ - Feature tracking
112
+ - .htmlgraph/events/ - Event JSON files
113
+ - .htmlgraph/htmlgraph.db - SQLite database
114
+
115
+ Args:
116
+ cwd: Starting directory for project resolution. Defaults to os.getcwd().
117
+
118
+ Returns:
119
+ Path to the .htmlgraph directory (guaranteed to exist).
120
+
121
+ Raises:
122
+ OSError: If directory creation fails (e.g., permission denied).
123
+ """
124
+ project_dir = resolve_project_dir(cwd)
125
+ graph_dir = Path(project_dir) / ".htmlgraph"
126
+ graph_dir.mkdir(parents=True, exist_ok=True)
127
+ return graph_dir
128
+
129
+
130
+ def init_logger(name: str) -> logging.Logger:
131
+ """Initialize a logger with standardized configuration.
132
+
133
+ Sets up a logger for hook scripts with:
134
+ - Consistent format across all hooks
135
+ - basicConfig applied only once (subsequent calls are ignored)
136
+ - Named logger returned (can be used for filtering)
137
+
138
+ Format: "[TIMESTAMP] [LEVEL] [logger_name] message"
139
+
140
+ Args:
141
+ name: Logger name (typically __name__ from calling module).
142
+
143
+ Returns:
144
+ logging.Logger instance configured and ready to use.
145
+
146
+ Example:
147
+ ```python
148
+ logger = init_logger(__name__)
149
+ logger.info("Hook started")
150
+ logger.error("Something went wrong")
151
+ ```
152
+ """
153
+ # Configure basicConfig only once (subsequent calls are no-ops)
154
+ logging.basicConfig(
155
+ level=logging.INFO,
156
+ format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
157
+ datefmt="%Y-%m-%d %H:%M:%S",
158
+ )
159
+
160
+ # Return named logger for this module
161
+ return logging.getLogger(name)
162
+
163
+
164
+ __all__ = [
165
+ "resolve_project_dir",
166
+ "bootstrap_pythonpath",
167
+ "get_graph_dir",
168
+ "init_logger",
169
+ ]
@@ -87,7 +87,7 @@ class CIGSPreToolEnforcer:
87
87
 
88
88
  return graph_dir
89
89
 
90
- def enforce(self, tool: str, params: dict) -> dict[str, Any]:
90
+ def enforce(self, tool: str, params: dict[str, Any]) -> dict[str, Any]:
91
91
  """
92
92
  Enforce CIGS delegation rules with escalating guidance.
93
93
 
@@ -223,7 +223,7 @@ class CIGSPreToolEnforcer:
223
223
  }
224
224
  }
225
225
 
226
- def _is_sdk_operation(self, tool: str, params: dict) -> bool:
226
+ def _is_sdk_operation(self, tool: str, params: dict[str, Any]) -> bool:
227
227
  """Check if operation is an SDK operation (always allowed)."""
228
228
  if tool != "Bash":
229
229
  return False
@@ -0,0 +1,208 @@
1
+ """
2
+ Concurrent Session Detection and Formatting.
3
+
4
+ Provides utilities to detect other active sessions and format them
5
+ for injection into the orchestrator's context at session start.
6
+ """
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any
10
+
11
+ from htmlgraph.db.schema import HtmlGraphDB
12
+
13
+
14
+ def get_concurrent_sessions(
15
+ db: HtmlGraphDB,
16
+ current_session_id: str,
17
+ minutes: int = 30,
18
+ ) -> list[dict[str, Any]]:
19
+ """
20
+ Get other sessions that are currently active.
21
+
22
+ Args:
23
+ db: Database connection
24
+ current_session_id: Current session to exclude
25
+ minutes: Look back window for activity
26
+
27
+ Returns:
28
+ List of concurrent session dicts with id, agent_id, last_user_query, etc.
29
+ """
30
+ if not db.connection:
31
+ db.connect()
32
+
33
+ try:
34
+ cursor = db.connection.cursor() # type: ignore[union-attr]
35
+ # Use datetime format that matches database (without timezone)
36
+ cutoff = (datetime.now(timezone.utc) - timedelta(minutes=minutes)).strftime(
37
+ "%Y-%m-%d %H:%M:%S"
38
+ )
39
+
40
+ cursor.execute(
41
+ """
42
+ SELECT
43
+ session_id as id,
44
+ agent_assigned as agent_id,
45
+ created_at,
46
+ status,
47
+ (SELECT input_summary FROM agent_events
48
+ WHERE session_id = sessions.session_id
49
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query,
50
+ (SELECT timestamp FROM agent_events
51
+ WHERE session_id = sessions.session_id
52
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query_at
53
+ FROM sessions
54
+ WHERE status = 'active'
55
+ AND session_id != ?
56
+ AND created_at > ?
57
+ ORDER BY created_at DESC
58
+ """,
59
+ (current_session_id, cutoff),
60
+ )
61
+
62
+ rows = cursor.fetchall()
63
+ return [dict(row) for row in rows]
64
+ except Exception: # pragma: no cover
65
+ # Gracefully handle database errors
66
+ return []
67
+
68
+
69
+ def format_concurrent_sessions_markdown(sessions: list[dict[str, Any]]) -> str:
70
+ """
71
+ Format concurrent sessions as markdown for context injection.
72
+
73
+ Args:
74
+ sessions: List of session dicts from get_concurrent_sessions
75
+
76
+ Returns:
77
+ Markdown formatted string for system prompt injection
78
+ """
79
+ if not sessions:
80
+ return ""
81
+
82
+ lines = ["## Concurrent Sessions (Active Now)", ""]
83
+
84
+ for session in sessions:
85
+ session_id = session.get("id", "unknown")
86
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
87
+ agent = session.get("agent_id", "unknown")
88
+ query = session.get("last_user_query", "No recent query")
89
+ last_active = session.get("last_user_query_at")
90
+
91
+ # Calculate time ago
92
+ time_ago = "unknown"
93
+ if last_active:
94
+ try:
95
+ last_dt = datetime.fromisoformat(
96
+ last_active.replace("Z", "+00:00")
97
+ if isinstance(last_active, str)
98
+ else last_active
99
+ )
100
+ delta = datetime.now(timezone.utc) - last_dt
101
+ if delta.total_seconds() < 60:
102
+ time_ago = "just now"
103
+ elif delta.total_seconds() < 3600:
104
+ time_ago = f"{int(delta.total_seconds() // 60)} min ago"
105
+ else:
106
+ time_ago = f"{int(delta.total_seconds() // 3600)} hours ago"
107
+ except (ValueError, TypeError, AttributeError):
108
+ time_ago = "unknown"
109
+
110
+ # Truncate query for display
111
+ query_display = (
112
+ query[:50] + "..." if query and len(query) > 50 else (query or "Unknown")
113
+ )
114
+
115
+ lines.append(f'- **{session_id}** ({agent}): "{query_display}" - {time_ago}')
116
+
117
+ lines.append("")
118
+ lines.append("*Coordinate with concurrent sessions to avoid duplicate work.*")
119
+ lines.append("")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def get_recent_completed_sessions(
125
+ db: HtmlGraphDB,
126
+ hours: int = 24,
127
+ limit: int = 5,
128
+ ) -> list[dict[str, Any]]:
129
+ """
130
+ Get recently completed sessions for handoff context.
131
+
132
+ Args:
133
+ db: Database connection
134
+ hours: Look back window
135
+ limit: Maximum sessions to return
136
+
137
+ Returns:
138
+ List of recently completed session dicts
139
+ """
140
+ if not db.connection:
141
+ db.connect()
142
+
143
+ try:
144
+ cursor = db.connection.cursor() # type: ignore[union-attr]
145
+ # Use datetime format that matches database (without timezone)
146
+ cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
147
+ "%Y-%m-%d %H:%M:%S"
148
+ )
149
+ cursor.execute(
150
+ """
151
+ SELECT session_id as id, agent_assigned as agent_id, created_at as started_at,
152
+ completed_at, total_events,
153
+ (SELECT input_summary FROM agent_events
154
+ WHERE session_id = sessions.session_id
155
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query
156
+ FROM sessions
157
+ WHERE status = 'completed'
158
+ AND completed_at > ?
159
+ ORDER BY completed_at DESC
160
+ LIMIT ?
161
+ """,
162
+ (cutoff, limit),
163
+ )
164
+ rows = cursor.fetchall()
165
+ return [dict(row) for row in rows]
166
+ except Exception: # pragma: no cover
167
+ # Gracefully handle database errors
168
+ return []
169
+
170
+
171
+ def format_recent_work_markdown(sessions: list[dict[str, Any]]) -> str:
172
+ """
173
+ Format recently completed sessions as markdown.
174
+
175
+ Args:
176
+ sessions: List of completed session dicts
177
+
178
+ Returns:
179
+ Markdown formatted string
180
+ """
181
+ if not sessions:
182
+ return ""
183
+
184
+ lines = ["## Recent Work (Last 24 Hours)", ""]
185
+
186
+ for session in sessions:
187
+ session_id = session.get("id", "unknown")
188
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
189
+ query = session.get("last_user_query", "No query recorded")
190
+ total_events = session.get("total_events") or 0
191
+
192
+ query_display = (
193
+ query[:60] + "..." if query and len(query) > 60 else (query or "Unknown")
194
+ )
195
+
196
+ lines.append(f"- `{session_id}`: {query_display} ({total_events} events)")
197
+
198
+ lines.append("")
199
+
200
+ return "\n".join(lines)
201
+
202
+
203
+ __all__ = [
204
+ "get_concurrent_sessions",
205
+ "format_concurrent_sessions_markdown",
206
+ "get_recent_completed_sessions",
207
+ "format_recent_work_markdown",
208
+ ]