htmlgraph 0.24.1__py3-none-any.whl → 0.25.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 (103) 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 +2115 -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 +783 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +570 -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 +3315 -492
  51. htmlgraph-0.24.1.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 +1334 -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/__init__.py +8 -0
  68. htmlgraph/hooks/bootstrap.py +169 -0
  69. htmlgraph/hooks/context.py +271 -0
  70. htmlgraph/hooks/drift_handler.py +521 -0
  71. htmlgraph/hooks/event_tracker.py +405 -15
  72. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  73. htmlgraph/hooks/pretooluse.py +476 -6
  74. htmlgraph/hooks/prompt_analyzer.py +648 -0
  75. htmlgraph/hooks/session_handler.py +583 -0
  76. htmlgraph/hooks/state_manager.py +501 -0
  77. htmlgraph/hooks/subagent_stop.py +309 -0
  78. htmlgraph/hooks/task_enforcer.py +39 -0
  79. htmlgraph/models.py +111 -15
  80. htmlgraph/operations/fastapi_server.py +230 -0
  81. htmlgraph/orchestration/headless_spawner.py +22 -14
  82. htmlgraph/pydantic_models.py +476 -0
  83. htmlgraph/quality_gates.py +350 -0
  84. htmlgraph/repo_hash.py +511 -0
  85. htmlgraph/sdk.py +348 -10
  86. htmlgraph/server.py +194 -0
  87. htmlgraph/session_hooks.py +300 -0
  88. htmlgraph/session_manager.py +131 -1
  89. htmlgraph/session_registry.py +587 -0
  90. htmlgraph/session_state.py +436 -0
  91. htmlgraph/system_prompts.py +449 -0
  92. htmlgraph/templates/orchestration-view.html +350 -0
  93. htmlgraph/track_builder.py +19 -0
  94. htmlgraph/validation.py +115 -0
  95. htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +7417 -0
  96. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/METADATA +91 -64
  97. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/RECORD +103 -42
  98. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/styles.css +0 -0
  99. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  100. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  101. {htmlgraph-0.24.1.data → htmlgraph-0.25.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  102. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/WHEEL +0 -0
  103. {htmlgraph-0.24.1.dist-info → htmlgraph-0.25.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,449 @@
1
+ """System prompt management for HtmlGraph projects.
2
+
3
+ Provides a two-tier system:
4
+ 1. Plugin Default - Included with HtmlGraph plugin, available to all users
5
+ 2. Project Override - Optional, project-specific customization
6
+
7
+ Architecture:
8
+ - System prompts are injected via SessionStart hook's additionalContext
9
+ - Survives Claude Code compact/resume cycles
10
+ - SDK provides methods for creation, validation, and management
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ pass
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class SystemPromptValidator:
26
+ """Validate system prompts against token budgets and quality criteria."""
27
+
28
+ @staticmethod
29
+ def count_tokens(text: str) -> int:
30
+ """
31
+ Estimate or count tokens in text.
32
+
33
+ Uses tiktoken if available (accurate), falls back to character-based
34
+ estimation if tiktoken not installed.
35
+
36
+ Args:
37
+ text: Text to count tokens for
38
+
39
+ Returns:
40
+ Estimated or exact token count
41
+ """
42
+ if not text:
43
+ return 0
44
+
45
+ try:
46
+ import tiktoken
47
+
48
+ encoding = tiktoken.encoding_for_model("gpt-4")
49
+ return len(encoding.encode(text))
50
+ except Exception:
51
+ # Fallback: rough estimation (1 token ≈ 4 characters)
52
+ # This is a conservative estimate used by Claude
53
+ return max(1, len(text) // 4)
54
+
55
+ @staticmethod
56
+ def validate(
57
+ text: str,
58
+ max_tokens: int = 1000,
59
+ min_tokens: int = 50,
60
+ ) -> dict:
61
+ """
62
+ Validate system prompt against token budget and quality criteria.
63
+
64
+ Args:
65
+ text: Prompt text to validate
66
+ max_tokens: Maximum allowed tokens (default: 1000)
67
+ min_tokens: Minimum expected tokens (default: 50)
68
+
69
+ Returns:
70
+ Validation result dictionary:
71
+ {
72
+ "is_valid": bool,
73
+ "tokens": int,
74
+ "warnings": List[str],
75
+ "message": str
76
+ }
77
+ """
78
+ tokens = SystemPromptValidator.count_tokens(text)
79
+ warnings = []
80
+
81
+ # Token budget validation
82
+ if tokens > max_tokens:
83
+ warnings.append(f"Prompt exceeds budget: {tokens} > {max_tokens} tokens")
84
+
85
+ if tokens < min_tokens:
86
+ warnings.append(
87
+ f"Prompt is very short ({tokens} tokens) - "
88
+ f"may not provide sufficient guidance (minimum: {min_tokens})"
89
+ )
90
+
91
+ # Content quality checks
92
+ if len(text) < 100:
93
+ warnings.append(
94
+ "Prompt is very brief - consider adding more detail for better guidance"
95
+ )
96
+
97
+ # Determine validity
98
+ is_valid = min_tokens <= tokens <= max_tokens
99
+
100
+ # Build message
101
+ if is_valid:
102
+ message = (
103
+ f"Valid prompt: {tokens} tokens (within {max_tokens} token budget)"
104
+ )
105
+ elif tokens > max_tokens:
106
+ message = f"Invalid: {tokens} tokens exceeds {max_tokens} token limit"
107
+ else:
108
+ message = (
109
+ f"Warning: {tokens} tokens below recommended minimum ({min_tokens}). "
110
+ f"Prompt may not provide sufficient guidance."
111
+ )
112
+
113
+ return {
114
+ "is_valid": is_valid,
115
+ "tokens": tokens,
116
+ "warnings": warnings,
117
+ "message": message,
118
+ }
119
+
120
+
121
+ class SystemPromptManager:
122
+ """Manage system prompts for a project.
123
+
124
+ Provides methods to:
125
+ - Load plugin default system prompt
126
+ - Load/create project-level overrides
127
+ - Validate prompt token counts
128
+ - Manage prompt lifecycle
129
+
130
+ Architecture:
131
+ - Plugin Default: Included with plugin, loaded via importlib.resources
132
+ - Project Override: Optional .claude/system-prompt.md file
133
+ - Strategy: Project override takes precedence over plugin default
134
+ """
135
+
136
+ def __init__(self, graph_dir: Path | str):
137
+ """
138
+ Initialize system prompt manager.
139
+
140
+ Args:
141
+ graph_dir: Path to .htmlgraph directory
142
+ """
143
+ self.graph_dir = Path(graph_dir)
144
+ self.project_dir = self.graph_dir.parent
145
+ self.claude_dir = self.project_dir / ".claude"
146
+
147
+ def get_default(self) -> str | None:
148
+ """
149
+ Get plugin default system prompt.
150
+
151
+ Tries multiple strategies:
152
+ 1. Load via importlib.resources (when installed via pip)
153
+ 2. Load via package file path (development mode)
154
+ 3. Return None if not found
155
+
156
+ Returns:
157
+ Default prompt text, or None if not found
158
+ """
159
+ # Strategy 1: importlib.resources (standard Python 3.7+)
160
+ try:
161
+ from importlib.resources import files
162
+
163
+ try:
164
+ # Try new package structure (if htmlgraph_plugin is a package)
165
+ plugin_resources = files("htmlgraph_plugin").joinpath(
166
+ ".claude-plugin/system-prompt-default.md"
167
+ )
168
+ if plugin_resources.is_file():
169
+ return plugin_resources.read_text(encoding="utf-8")
170
+ except Exception:
171
+ pass
172
+
173
+ # Try alternative path
174
+ try:
175
+ plugin_resources = files("htmlgraph").joinpath(
176
+ "plugin/.claude-plugin/system-prompt-default.md"
177
+ )
178
+ if plugin_resources.is_file():
179
+ return plugin_resources.read_text(encoding="utf-8")
180
+ except Exception:
181
+ pass
182
+ except Exception as e:
183
+ logger.debug(f"importlib.resources not available: {e}")
184
+
185
+ # Strategy 2: Direct file path (development and package installations)
186
+ try:
187
+ import htmlgraph
188
+
189
+ htmlgraph_path = Path(htmlgraph.__file__).parent
190
+ # Try relative paths from htmlgraph package
191
+ possible_paths = [
192
+ htmlgraph_path
193
+ / "plugin"
194
+ / ".claude-plugin"
195
+ / "system-prompt-default.md",
196
+ htmlgraph_path.parent
197
+ / "packages"
198
+ / "claude-plugin"
199
+ / ".claude-plugin"
200
+ / "system-prompt-default.md",
201
+ Path(__file__).parent.parent
202
+ / "packages"
203
+ / "claude-plugin"
204
+ / ".claude-plugin"
205
+ / "system-prompt-default.md",
206
+ ]
207
+
208
+ for path in possible_paths:
209
+ if path.exists():
210
+ try:
211
+ content = path.read_text(encoding="utf-8")
212
+ logger.info(f"Loaded plugin default from {path}")
213
+ return content
214
+ except Exception as e:
215
+ logger.debug(f"Failed to read {path}: {e}")
216
+ except Exception as e:
217
+ logger.debug(f"Could not load via htmlgraph package: {e}")
218
+
219
+ logger.debug("Plugin default system prompt not found")
220
+ return None
221
+
222
+ def get_project(self) -> str | None:
223
+ """
224
+ Get project-level system prompt override.
225
+
226
+ Looks for: `.claude/system-prompt.md`
227
+
228
+ Returns:
229
+ Project prompt text if exists, None otherwise
230
+
231
+ Raises:
232
+ RuntimeError: If file exists but cannot be read
233
+ """
234
+ prompt_file = self.claude_dir / "system-prompt.md"
235
+
236
+ if not prompt_file.exists():
237
+ return None
238
+
239
+ try:
240
+ content = prompt_file.read_text(encoding="utf-8")
241
+ logger.info(f"Loaded project system prompt ({len(content)} chars)")
242
+ return content
243
+ except Exception as e:
244
+ logger.error(f"Failed to read project system prompt: {e}")
245
+ raise RuntimeError(
246
+ f"Failed to read project system prompt at {prompt_file}: {e}"
247
+ )
248
+
249
+ def get_active(self) -> str | None:
250
+ """
251
+ Get active system prompt (project override OR plugin default).
252
+
253
+ Strategy:
254
+ 1. If `.claude/system-prompt.md` exists → use it (project override)
255
+ 2. Else if plugin default exists → use it
256
+ 3. Else → return None
257
+
258
+ Returns:
259
+ Active prompt text, or None if neither available
260
+
261
+ Note:
262
+ Project override always takes precedence over plugin default.
263
+ This allows teams to customize guidance while maintaining
264
+ a sensible default for users who haven't customized yet.
265
+ """
266
+ try:
267
+ project = self.get_project()
268
+ if project:
269
+ logger.info("Using project system prompt override")
270
+ return project
271
+ except RuntimeError:
272
+ # Project file exists but couldn't be read—log but continue
273
+ logger.warning("Could not read project prompt, falling back to default")
274
+
275
+ default = self.get_default()
276
+ if default:
277
+ logger.info("Using plugin default system prompt")
278
+ return default
279
+
280
+ logger.warning(
281
+ "No system prompt found (neither project override nor plugin default)"
282
+ )
283
+ return None
284
+
285
+ def create(
286
+ self,
287
+ template: str,
288
+ overwrite: bool = False,
289
+ ) -> SystemPromptManager:
290
+ """
291
+ Create or update project system prompt.
292
+
293
+ Creates `.claude/system-prompt.md` with provided template.
294
+
295
+ Args:
296
+ template: Prompt template text
297
+ overwrite: Whether to overwrite existing prompt (default: False)
298
+
299
+ Returns:
300
+ Self for method chaining
301
+
302
+ Raises:
303
+ RuntimeError: If file exists and overwrite=False, or if write fails
304
+
305
+ Example:
306
+ sdk = SDK(agent="claude")
307
+ sdk.system_prompts.create('''
308
+ # Team Rules
309
+ - Use TypeScript, not JavaScript
310
+ - All PRs need 2 approvals
311
+ ''')
312
+ """
313
+ self.claude_dir.mkdir(parents=True, exist_ok=True)
314
+ prompt_file = self.claude_dir / "system-prompt.md"
315
+
316
+ if prompt_file.exists() and not overwrite:
317
+ raise RuntimeError(
318
+ f"System prompt already exists at {prompt_file}. "
319
+ f"Use overwrite=True to replace, or delete the file first."
320
+ )
321
+
322
+ try:
323
+ prompt_file.write_text(template, encoding="utf-8")
324
+ logger.info(
325
+ f"Created system prompt at {prompt_file} ({len(template)} chars)"
326
+ )
327
+ except Exception as e:
328
+ raise RuntimeError(f"Failed to write system prompt at {prompt_file}: {e}")
329
+
330
+ return self
331
+
332
+ def validate(
333
+ self,
334
+ text: str | None = None,
335
+ max_tokens: int = 1000,
336
+ min_tokens: int = 50,
337
+ ) -> dict:
338
+ """
339
+ Validate a system prompt.
340
+
341
+ Args:
342
+ text: Prompt to validate (uses active prompt if None)
343
+ max_tokens: Maximum allowed tokens (default: 1000)
344
+ min_tokens: Minimum expected tokens (default: 50)
345
+
346
+ Returns:
347
+ Validation result dict with keys:
348
+ - is_valid: bool
349
+ - tokens: int
350
+ - warnings: List[str]
351
+ - message: str
352
+
353
+ Example:
354
+ result = sdk.system_prompts.validate()
355
+ print(result['message'])
356
+ if not result['is_valid']:
357
+ for warning in result['warnings']:
358
+ print(f" - {warning}")
359
+ """
360
+ prompt_text = text or self.get_active() or ""
361
+ return SystemPromptValidator.validate(
362
+ prompt_text,
363
+ max_tokens=max_tokens,
364
+ min_tokens=min_tokens,
365
+ )
366
+
367
+ def delete(self) -> bool:
368
+ """
369
+ Delete project system prompt override.
370
+
371
+ Removes `.claude/system-prompt.md` if it exists.
372
+ Falls back to plugin default on next session.
373
+
374
+ Returns:
375
+ True if file was deleted, False if didn't exist
376
+
377
+ Raises:
378
+ RuntimeError: If file exists but cannot be deleted
379
+
380
+ Example:
381
+ sdk = SDK(agent="claude")
382
+ if sdk.system_prompts.delete():
383
+ print("Deleted project prompt, using plugin default")
384
+ """
385
+ prompt_file = self.claude_dir / "system-prompt.md"
386
+
387
+ if not prompt_file.exists():
388
+ return False
389
+
390
+ try:
391
+ prompt_file.unlink()
392
+ logger.info(f"Deleted system prompt at {prompt_file}")
393
+ return True
394
+ except Exception as e:
395
+ raise RuntimeError(f"Failed to delete system prompt at {prompt_file}: {e}")
396
+
397
+ def get_stats(self) -> dict:
398
+ """
399
+ Get statistics about the system prompt.
400
+
401
+ Returns:
402
+ Dictionary with:
403
+ - source: "project_override" | "plugin_default" | "none"
404
+ - tokens: int
405
+ - bytes: int
406
+ - file_path: str | None
407
+
408
+ Example:
409
+ stats = sdk.system_prompts.get_stats()
410
+ print(f"Using {stats['source']}: {stats['tokens']} tokens")
411
+ """
412
+ prompt = self.get_active()
413
+
414
+ if not prompt:
415
+ return {
416
+ "source": "none",
417
+ "tokens": 0,
418
+ "bytes": 0,
419
+ "file_path": None,
420
+ }
421
+
422
+ # Determine source
423
+ project = self.get_project()
424
+ if project:
425
+ source = "project_override"
426
+ file_path = str(self.claude_dir / "system-prompt.md")
427
+ else:
428
+ source = "plugin_default"
429
+ file_path = None # Plugin default has no single path
430
+
431
+ return {
432
+ "source": source,
433
+ "tokens": SystemPromptValidator.count_tokens(prompt),
434
+ "bytes": len(prompt.encode("utf-8")),
435
+ "file_path": file_path,
436
+ }
437
+
438
+
439
+ # Integration with SDK
440
+ def _register_system_prompts_with_sdk() -> None:
441
+ """Register system_prompts property with SDK class.
442
+
443
+ This function is called during SDK initialization to add the
444
+ system_prompts property, enabling usage like:
445
+
446
+ sdk = SDK(agent="claude")
447
+ prompt = sdk.system_prompts.get_active()
448
+ """
449
+ pass # Integration handled via SDK property decorator