htmlgraph 0.26.4__py3-none-any.whl → 0.26.6__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 (69) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
  2. htmlgraph/__init__.py +1 -1
  3. htmlgraph/api/main.py +50 -10
  4. htmlgraph/api/templates/dashboard-redesign.html +608 -54
  5. htmlgraph/api/templates/partials/activity-feed.html +21 -0
  6. htmlgraph/api/templates/partials/features.html +81 -12
  7. htmlgraph/api/templates/partials/orchestration.html +35 -0
  8. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  9. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  10. htmlgraph/cli/__init__.py +42 -0
  11. htmlgraph/cli/__main__.py +6 -0
  12. htmlgraph/cli/analytics.py +939 -0
  13. htmlgraph/cli/base.py +660 -0
  14. htmlgraph/cli/constants.py +206 -0
  15. htmlgraph/cli/core.py +856 -0
  16. htmlgraph/cli/main.py +143 -0
  17. htmlgraph/cli/models.py +462 -0
  18. htmlgraph/cli/templates/__init__.py +1 -0
  19. htmlgraph/cli/templates/cost_dashboard.py +398 -0
  20. htmlgraph/cli/work/__init__.py +159 -0
  21. htmlgraph/cli/work/features.py +567 -0
  22. htmlgraph/cli/work/orchestration.py +675 -0
  23. htmlgraph/cli/work/sessions.py +465 -0
  24. htmlgraph/cli/work/tracks.py +485 -0
  25. htmlgraph/dashboard.html +6414 -634
  26. htmlgraph/db/schema.py +8 -3
  27. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
  28. htmlgraph/docs/README.md +2 -3
  29. htmlgraph/hooks/event_tracker.py +189 -35
  30. htmlgraph/hooks/git_commands.py +175 -0
  31. htmlgraph/hooks/orchestrator.py +137 -71
  32. htmlgraph/hooks/orchestrator_reflector.py +23 -0
  33. htmlgraph/hooks/pretooluse.py +29 -6
  34. htmlgraph/hooks/session_handler.py +28 -0
  35. htmlgraph/hooks/session_summary.py +391 -0
  36. htmlgraph/hooks/subagent_detection.py +202 -0
  37. htmlgraph/hooks/validator.py +192 -79
  38. htmlgraph/operations/__init__.py +18 -0
  39. htmlgraph/operations/initialization.py +596 -0
  40. htmlgraph/operations/initialization.py.backup +228 -0
  41. htmlgraph/orchestration/__init__.py +16 -1
  42. htmlgraph/orchestration/claude_launcher.py +185 -0
  43. htmlgraph/orchestration/command_builder.py +71 -0
  44. htmlgraph/orchestration/headless_spawner.py +72 -1332
  45. htmlgraph/orchestration/plugin_manager.py +136 -0
  46. htmlgraph/orchestration/prompts.py +137 -0
  47. htmlgraph/orchestration/spawners/__init__.py +16 -0
  48. htmlgraph/orchestration/spawners/base.py +194 -0
  49. htmlgraph/orchestration/spawners/claude.py +170 -0
  50. htmlgraph/orchestration/spawners/codex.py +442 -0
  51. htmlgraph/orchestration/spawners/copilot.py +299 -0
  52. htmlgraph/orchestration/spawners/gemini.py +478 -0
  53. htmlgraph/orchestration/subprocess_runner.py +33 -0
  54. htmlgraph/orchestration.md +563 -0
  55. htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
  56. htmlgraph/orchestrator_config.py +357 -0
  57. htmlgraph/orchestrator_mode.py +45 -12
  58. htmlgraph/transcript.py +16 -4
  59. htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
  60. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
  61. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
  62. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
  63. htmlgraph/cli.py +0 -7256
  64. htmlgraph-0.26.4.data/data/htmlgraph/dashboard.html +0 -812
  65. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
  66. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  67. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  68. {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  69. {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/WHEEL +0 -0
@@ -6,6 +6,9 @@ Provides intelligent guidance for HtmlGraph workflow based on:
6
6
  2. Recent tool usage patterns (anti-pattern detection)
7
7
  3. Learned patterns from transcript analytics
8
8
 
9
+ Subagents spawned via Task() have unrestricted tool access.
10
+ Detection uses 5-level strategy: env vars, session state, database.
11
+
9
12
  This module can be used by hook scripts or imported directly for validation logic.
10
13
 
11
14
  Main API:
@@ -26,35 +29,50 @@ Example:
26
29
 
27
30
  import json
28
31
  import re
29
- from datetime import datetime, timezone
30
32
  from pathlib import Path
31
33
  from typing import Any, cast
32
34
 
33
- # Anti-patterns to detect (tool sequence -> warning message)
34
- ANTI_PATTERNS = {
35
- (
36
- "Bash",
37
- "Bash",
38
- "Bash",
39
- "Bash",
40
- ): "4 consecutive Bash commands. Check for errors or consider a different approach.",
41
- (
42
- "Edit",
43
- "Edit",
44
- "Edit",
45
- ): "3 consecutive Edits. Consider batching changes or reading file first.",
46
- (
47
- "Grep",
48
- "Grep",
49
- "Grep",
50
- ): "3 consecutive Greps. Consider reading results before searching more.",
51
- (
52
- "Read",
53
- "Read",
54
- "Read",
55
- "Read",
56
- ): "4 consecutive Reads. Consider caching file content.",
57
- }
35
+ from htmlgraph.hooks.subagent_detection import is_subagent_context
36
+ from htmlgraph.orchestrator_config import load_orchestrator_config
37
+
38
+
39
+ def get_anti_patterns(config: Any | None = None) -> dict[tuple[str, ...], str]:
40
+ """
41
+ Build anti-pattern rules from configuration.
42
+
43
+ Args:
44
+ config: Optional OrchestratorConfig. If None, loads from file.
45
+
46
+ Returns:
47
+ Dict mapping tool sequences to warning messages
48
+ """
49
+ if config is None:
50
+ config = load_orchestrator_config()
51
+
52
+ patterns = config.anti_patterns
53
+
54
+ return {
55
+ tuple(["Bash"] * patterns.consecutive_bash): (
56
+ f"{patterns.consecutive_bash} consecutive Bash commands. "
57
+ "Check for errors or consider a different approach."
58
+ ),
59
+ tuple(["Edit"] * patterns.consecutive_edit): (
60
+ f"{patterns.consecutive_edit} consecutive Edits. "
61
+ "Consider batching changes or reading file first."
62
+ ),
63
+ tuple(["Grep"] * patterns.consecutive_grep): (
64
+ f"{patterns.consecutive_grep} consecutive Greps. "
65
+ "Consider reading results before searching more."
66
+ ),
67
+ tuple(["Read"] * patterns.consecutive_read): (
68
+ f"{patterns.consecutive_read} consecutive Reads. "
69
+ "Consider caching file content."
70
+ ),
71
+ }
72
+
73
+
74
+ # Legacy constant for backwards compatibility (now uses config)
75
+ ANTI_PATTERNS = get_anti_patterns()
58
76
 
59
77
  # Tools that indicate exploration/implementation (require work item in strict mode)
60
78
  EXPLORATION_TOOLS = {"Grep", "Glob", "Task"}
@@ -67,68 +85,89 @@ OPTIMAL_PATTERNS = {
67
85
  ("Edit", "Bash"): "Good: Edit then test - verify changes.",
68
86
  }
69
87
 
70
- # Session tool history file
71
- TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
88
+ # Maximum number of recent tool calls to consider for pattern detection
72
89
  MAX_HISTORY = 20
73
90
 
74
91
 
75
- def load_tool_history() -> list[dict]:
76
- """Load recent tool history from temp file."""
77
- if TOOL_HISTORY_FILE.exists():
78
- try:
79
- data = json.loads(TOOL_HISTORY_FILE.read_text())
80
-
81
- # Handle both formats: {"history": [...]} and [...] (legacy)
82
- if isinstance(data, dict): # type: ignore[arg-type]
83
- data = data.get("history", [])
84
-
85
- # Filter to last hour only
86
- cutoff = datetime.now(timezone.utc).timestamp() - 3600
87
-
88
- # Handle both "ts" (old) and "timestamp" (new) formats
89
- filtered = []
90
- for t in data:
91
- ts = t.get("ts", 0)
92
- if not ts and "timestamp" in t:
93
- # Parse ISO format timestamp
94
- try:
95
- ts = datetime.fromisoformat(
96
- t["timestamp"].replace("Z", "+00:00")
97
- ).timestamp()
98
- except Exception:
99
- ts = 0
100
- if ts > cutoff:
101
- filtered.append(t)
102
-
103
- return filtered[-MAX_HISTORY:]
104
- except Exception:
105
- pass
106
- return []
92
+ def load_tool_history(session_id: str) -> list[dict]:
93
+ """
94
+ Load recent tool history from database (session-isolated).
107
95
 
96
+ Args:
97
+ session_id: Session identifier to filter tool history
108
98
 
109
- def save_tool_history(history: list[dict]) -> None:
110
- """Save tool history to temp file."""
99
+ Returns:
100
+ List of recent tool calls with tool name and timestamp
101
+ """
111
102
  try:
112
- # Use wrapped format to match orchestrator-enforce.py
113
- TOOL_HISTORY_FILE.write_text(
114
- json.dumps({"history": history[-MAX_HISTORY:]}, indent=2)
103
+ from htmlgraph.db.schema import HtmlGraphDB
104
+
105
+ # Find database path
106
+ cwd = Path.cwd()
107
+ graph_dir = cwd / ".htmlgraph"
108
+ if not graph_dir.exists():
109
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
110
+ candidate = parent / ".htmlgraph"
111
+ if candidate.exists():
112
+ graph_dir = candidate
113
+ break
114
+
115
+ db_path = graph_dir / "htmlgraph.db"
116
+ if not db_path.exists():
117
+ return []
118
+
119
+ db = HtmlGraphDB(str(db_path))
120
+ if db.connection is None:
121
+ return []
122
+
123
+ cursor = db.connection.cursor()
124
+ cursor.execute(
125
+ """
126
+ SELECT tool_name, timestamp
127
+ FROM agent_events
128
+ WHERE session_id = ?
129
+ ORDER BY timestamp DESC
130
+ LIMIT ?
131
+ """,
132
+ (session_id, MAX_HISTORY),
115
133
  )
134
+
135
+ # Return in chronological order (oldest first) for pattern detection
136
+ rows = cursor.fetchall()
137
+ db.disconnect()
138
+
139
+ return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
116
140
  except Exception:
117
- pass
141
+ # Graceful degradation - return empty history on error
142
+ return []
143
+
144
+
145
+ def record_tool(tool: str, session_id: str) -> None:
146
+ """
147
+ Record a tool use in database.
118
148
 
149
+ Note: This is now handled by track-event.py hook, so this function
150
+ is kept for backward compatibility but does nothing.
119
151
 
120
- def record_tool(tool: str, history: list[dict]) -> list[dict]:
121
- """Record a tool use in history."""
122
- # Use same format as orchestrator-enforce.py for consistency
123
- history.append({"tool": tool, "timestamp": datetime.now(timezone.utc).isoformat()})
124
- return history[-MAX_HISTORY:]
152
+ Args:
153
+ tool: Tool name being called
154
+ session_id: Session identifier for isolation
155
+ """
156
+ # Tool recording is now handled by track-event.py PostToolUse hook
157
+ # This function is kept for backward compatibility but does nothing
158
+ pass
125
159
 
126
160
 
127
161
  def detect_anti_pattern(tool: str, history: list[dict]) -> str | None:
128
- """Check if adding this tool creates an anti-pattern."""
129
- recent_tools = [h["tool"] for h in history[-4:]] + [tool]
162
+ """Check if adding this tool creates an anti-pattern (uses configurable thresholds)."""
163
+ # Load fresh anti-patterns from config
164
+ anti_patterns = get_anti_patterns()
165
+
166
+ # Get max pattern length to know how far to look back
167
+ max_pattern_len = max(len(p) for p in anti_patterns.keys()) if anti_patterns else 5
168
+ recent_tools = [h["tool"] for h in history[-max_pattern_len:]] + [tool]
130
169
 
131
- for pattern, message in ANTI_PATTERNS.items():
170
+ for pattern, message in anti_patterns.items():
132
171
  pattern_len = len(pattern)
133
172
  if len(recent_tools) >= pattern_len:
134
173
  # Check if recent tools end with this pattern
@@ -229,6 +268,15 @@ def is_always_allowed(
229
268
  # Read-only Bash patterns
230
269
  if tool == "Bash":
231
270
  command = params.get("command", "")
271
+
272
+ # Check git commands using shared classification
273
+ if command.strip().startswith("git"):
274
+ from htmlgraph.hooks.git_commands import should_allow_git_command
275
+
276
+ if should_allow_git_command(command):
277
+ return True
278
+
279
+ # Check other bash patterns
232
280
  for pattern in config.get("always_allow", {}).get("bash_patterns", []):
233
281
  if re.match(pattern, command):
234
282
  return True
@@ -292,7 +340,9 @@ def get_active_work_item() -> dict | None:
292
340
  return None
293
341
 
294
342
 
295
- def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | None:
343
+ def check_orchestrator_violation(
344
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
345
+ ) -> dict | None:
296
346
  """
297
347
  Check if operation violates orchestrator mode rules.
298
348
 
@@ -302,6 +352,7 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
302
352
  Args:
303
353
  tool: Tool name
304
354
  params: Tool parameters
355
+ session_id: Session identifier for loading tool history
305
356
 
306
357
  Returns:
307
358
  Blocking response dict if violation detected in strict mode, None otherwise
@@ -339,7 +390,9 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
339
390
  is_allowed_orchestrator_operation,
340
391
  )
341
392
 
342
- is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
393
+ is_allowed, reason, category = is_allowed_orchestrator_operation(
394
+ tool, params, session_id
395
+ )
343
396
 
344
397
  # If orchestrator would block (but returns continue=True), we block here
345
398
  if not is_allowed:
@@ -367,33 +420,49 @@ def check_orchestrator_violation(tool: str, params: dict[str, Any]) -> dict | No
367
420
 
368
421
 
369
422
  def validate_tool_call(
370
- tool: str, params: dict[str, Any], config: dict[str, Any], history: list[dict]
423
+ tool: str,
424
+ params: dict[str, Any],
425
+ config: dict[str, Any],
426
+ history: list[dict],
427
+ session_id: str | None = None,
371
428
  ) -> dict[str, Any]:
372
429
  """
373
430
  Validate tool call and return GUIDANCE with active learning.
374
431
 
432
+ Subagents spawned via Task() have unrestricted tool access.
433
+ Detection uses 5-level strategy: env vars, session state, database.
434
+
375
435
  Args:
376
436
  tool: Tool name (e.g., "Edit", "Bash", "Read")
377
437
  params: Tool parameters (e.g., {"file_path": "test.py"})
378
438
  config: Validation configuration (from load_validation_config())
379
- history: Tool usage history (from load_tool_history())
439
+ history: Tool usage history (from load_tool_history(session_id))
440
+ session_id: Optional session ID for loading history if not provided
380
441
 
381
442
  Returns:
382
443
  dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
383
444
  All operations are ALLOWED unless blocked for safety reasons.
384
445
 
385
446
  Example:
447
+ session_id = tool_input.get("session_id", "unknown")
448
+ history = load_tool_history(session_id)
386
449
  result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
387
450
  if result["decision"] == "block":
388
451
  print(result["reason"])
389
452
  elif "guidance" in result:
390
453
  print(result["guidance"])
391
454
  """
455
+ # Check if this is a subagent context - subagents have unrestricted tool access
456
+ if is_subagent_context():
457
+ return {"decision": "allow"}
458
+
392
459
  result = {"decision": "allow"}
393
460
  guidance_parts = []
394
461
 
395
462
  # Step 0a: Check orchestrator mode violations (if enabled)
396
- orchestrator_violation = check_orchestrator_violation(tool, params)
463
+ orchestrator_violation = check_orchestrator_violation(
464
+ tool, params, session_id or "unknown"
465
+ )
397
466
  if orchestrator_violation:
398
467
  # BLOCK orchestrator violations in strict mode
399
468
  return orchestrator_violation
@@ -509,3 +578,47 @@ def validate_tool_call(
509
578
  result["guidance"] = " | ".join(guidance_parts)
510
579
 
511
580
  return result
581
+
582
+
583
+ def main() -> None:
584
+ """Hook entry point for script wrapper."""
585
+ import sys
586
+
587
+ try:
588
+ # Read tool input from stdin
589
+ tool_input = json.load(sys.stdin)
590
+
591
+ # Claude Code uses "name" and "input", fallback to "tool" and "params"
592
+ tool = tool_input.get("name", "") or tool_input.get("tool", "")
593
+ params = tool_input.get("input", {}) or tool_input.get("params", {})
594
+
595
+ # Get session_id from hook_input (NEW: required for session-isolated history)
596
+ session_id = tool_input.get("session_id", "unknown")
597
+
598
+ # Load config
599
+ config = load_validation_config()
600
+
601
+ # Load session-isolated tool history (NEW: from database, not file)
602
+ history = load_tool_history(session_id)
603
+
604
+ # Get guidance with pattern awareness
605
+ result = validate_tool_call(tool, params, config, history)
606
+
607
+ # Note: Tool recording is now handled by track-event.py PostToolUse hook
608
+ # No need to call record_tool() or save_tool_history() here
609
+
610
+ # Output JSON with guidance/block message
611
+ print(json.dumps(result))
612
+
613
+ # Exit 1 to BLOCK if decision is "block", otherwise allow
614
+ if result.get("decision") == "block":
615
+ sys.exit(1)
616
+ else:
617
+ sys.exit(0)
618
+
619
+ except Exception as e:
620
+ # Graceful degradation - allow on error
621
+ print(
622
+ json.dumps({"decision": "allow", "guidance": f"Validation hook error: {e}"})
623
+ )
624
+ sys.exit(0)
@@ -24,6 +24,16 @@ from .hooks import (
24
24
  list_hooks,
25
25
  validate_hook_config,
26
26
  )
27
+ from .initialization import (
28
+ create_analytics_index,
29
+ create_config_files,
30
+ create_database,
31
+ create_directory_structure,
32
+ initialize_htmlgraph,
33
+ install_git_hooks,
34
+ update_gitignore,
35
+ validate_directory,
36
+ )
27
37
  from .server import (
28
38
  ServerHandle,
29
39
  ServerStartResult,
@@ -58,4 +68,12 @@ __all__ = [
58
68
  "start_server",
59
69
  "stop_server",
60
70
  "get_server_status",
71
+ "initialize_htmlgraph",
72
+ "validate_directory",
73
+ "create_directory_structure",
74
+ "create_database",
75
+ "create_analytics_index",
76
+ "create_config_files",
77
+ "update_gitignore",
78
+ "install_git_hooks",
61
79
  ]