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.
- htmlgraph/.htmlgraph/.session-warning-state.json +1 -1
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +50 -10
- htmlgraph/api/templates/dashboard-redesign.html +608 -54
- htmlgraph/api/templates/partials/activity-feed.html +21 -0
- htmlgraph/api/templates/partials/features.html +81 -12
- htmlgraph/api/templates/partials/orchestration.html +35 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +939 -0
- htmlgraph/cli/base.py +660 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +856 -0
- htmlgraph/cli/main.py +143 -0
- htmlgraph/cli/models.py +462 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +398 -0
- htmlgraph/cli/work/__init__.py +159 -0
- htmlgraph/cli/work/features.py +567 -0
- htmlgraph/cli/work/orchestration.py +675 -0
- htmlgraph/cli/work/sessions.py +465 -0
- htmlgraph/cli/work/tracks.py +485 -0
- htmlgraph/dashboard.html +6414 -634
- htmlgraph/db/schema.py +8 -3
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +20 -13
- htmlgraph/docs/README.md +2 -3
- htmlgraph/hooks/event_tracker.py +189 -35
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/orchestrator.py +137 -71
- htmlgraph/hooks/orchestrator_reflector.py +23 -0
- htmlgraph/hooks/pretooluse.py +29 -6
- htmlgraph/hooks/session_handler.py +28 -0
- htmlgraph/hooks/session_summary.py +391 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/validator.py +192 -79
- htmlgraph/operations/__init__.py +18 -0
- htmlgraph/operations/initialization.py +596 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/orchestration/__init__.py +16 -1
- htmlgraph/orchestration/claude_launcher.py +185 -0
- htmlgraph/orchestration/command_builder.py +71 -0
- htmlgraph/orchestration/headless_spawner.py +72 -1332
- htmlgraph/orchestration/plugin_manager.py +136 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +170 -0
- htmlgraph/orchestration/spawners/codex.py +442 -0
- htmlgraph/orchestration/spawners/copilot.py +299 -0
- htmlgraph/orchestration/spawners/gemini.py +478 -0
- htmlgraph/orchestration/subprocess_runner.py +33 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +620 -55
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +45 -12
- htmlgraph/transcript.py +16 -4
- htmlgraph-0.26.6.data/data/htmlgraph/dashboard.html +6592 -0
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/METADATA +1 -1
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/RECORD +67 -33
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -7256
- htmlgraph-0.26.4.data/data/htmlgraph/dashboard.html +0 -812
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.4.data → htmlgraph-0.26.6.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.4.dist-info → htmlgraph-0.26.6.dist-info}/WHEEL +0 -0
htmlgraph/hooks/orchestrator.py
CHANGED
|
@@ -10,6 +10,8 @@ Architecture:
|
|
|
10
10
|
- Classifies operations into ALLOWED vs BLOCKED categories
|
|
11
11
|
- Tracks tool usage sequences to detect exploration patterns
|
|
12
12
|
- Provides clear Task delegation suggestions when blocking
|
|
13
|
+
- Subagents spawned via Task() have unrestricted tool access
|
|
14
|
+
- Detection uses 5-level strategy: env vars, session state, database
|
|
13
15
|
|
|
14
16
|
Operation Categories:
|
|
15
17
|
1. ALWAYS ALLOWED - Task, AskUserQuestion, TodoWrite, SDK operations
|
|
@@ -27,74 +29,73 @@ Public API:
|
|
|
27
29
|
|
|
28
30
|
import json
|
|
29
31
|
import re
|
|
30
|
-
from datetime import datetime, timezone
|
|
31
32
|
from pathlib import Path
|
|
32
|
-
from typing import Any
|
|
33
|
+
from typing import Any
|
|
33
34
|
|
|
35
|
+
from htmlgraph.hooks.subagent_detection import is_subagent_context
|
|
36
|
+
from htmlgraph.orchestrator_config import load_orchestrator_config
|
|
34
37
|
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
35
38
|
from htmlgraph.orchestrator_validator import OrchestratorValidator
|
|
36
39
|
|
|
37
|
-
#
|
|
38
|
-
TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
|
|
40
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
39
41
|
MAX_HISTORY_SIZE = 50 # Keep last 50 tool calls
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def load_tool_history() -> list[dict]:
|
|
44
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
43
45
|
"""
|
|
44
|
-
Load recent tool history from
|
|
46
|
+
Load recent tool history from database (session-isolated).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
session_id: Session identifier to filter tool history
|
|
45
50
|
|
|
46
51
|
Returns:
|
|
47
52
|
List of recent tool calls with tool name and timestamp
|
|
48
53
|
"""
|
|
49
|
-
if not TOOL_HISTORY_FILE.exists():
|
|
50
|
-
return []
|
|
51
|
-
|
|
52
54
|
try:
|
|
53
|
-
|
|
54
|
-
# Handle both formats: {"history": [...]} and [...] (legacy)
|
|
55
|
-
if isinstance(data, list):
|
|
56
|
-
return cast(list[dict[Any, Any]], data)
|
|
57
|
-
return cast(list[dict[Any, Any]], data.get("history", []))
|
|
58
|
-
except Exception:
|
|
59
|
-
return []
|
|
60
|
-
|
|
55
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
61
56
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
# Find database path
|
|
58
|
+
cwd = Path.cwd()
|
|
59
|
+
graph_dir = cwd / ".htmlgraph"
|
|
60
|
+
if not graph_dir.exists():
|
|
61
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
62
|
+
candidate = parent / ".htmlgraph"
|
|
63
|
+
if candidate.exists():
|
|
64
|
+
graph_dir = candidate
|
|
65
|
+
break
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
68
|
+
if not db_path.exists():
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
db = HtmlGraphDB(str(db_path))
|
|
72
|
+
if db.connection is None:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
cursor = db.connection.cursor()
|
|
76
|
+
cursor.execute(
|
|
77
|
+
"""
|
|
78
|
+
SELECT tool_name, timestamp
|
|
79
|
+
FROM agent_events
|
|
80
|
+
WHERE session_id = ?
|
|
81
|
+
ORDER BY timestamp DESC
|
|
82
|
+
LIMIT ?
|
|
83
|
+
""",
|
|
84
|
+
(session_id, MAX_HISTORY_SIZE),
|
|
73
85
|
)
|
|
74
|
-
TOOL_HISTORY_FILE.write_text(json.dumps({"history": recent}, indent=2))
|
|
75
|
-
except Exception:
|
|
76
|
-
pass # Fail silently on history save errors
|
|
77
86
|
|
|
87
|
+
# Return in chronological order (oldest first) for pattern detection
|
|
88
|
+
rows = cursor.fetchall()
|
|
89
|
+
db.disconnect()
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
tool: Name of the tool being called
|
|
85
|
-
"""
|
|
86
|
-
history = load_tool_history()
|
|
87
|
-
history.append(
|
|
88
|
-
{
|
|
89
|
-
"tool": tool,
|
|
90
|
-
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
91
|
-
}
|
|
92
|
-
)
|
|
93
|
-
save_tool_history(history)
|
|
91
|
+
return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
|
|
92
|
+
except Exception:
|
|
93
|
+
# Graceful degradation - return empty history on error
|
|
94
|
+
return []
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def is_allowed_orchestrator_operation(
|
|
97
|
-
tool: str, params: dict[str, Any]
|
|
98
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
98
99
|
) -> tuple[bool, str, str]:
|
|
99
100
|
"""
|
|
100
101
|
Check if operation is allowed for orchestrators.
|
|
@@ -102,6 +103,7 @@ def is_allowed_orchestrator_operation(
|
|
|
102
103
|
Args:
|
|
103
104
|
tool: Tool name (e.g., "Read", "Edit", "Bash")
|
|
104
105
|
params: Tool parameters dict
|
|
106
|
+
session_id: Session identifier for loading tool history
|
|
105
107
|
|
|
106
108
|
Returns:
|
|
107
109
|
Tuple of (is_allowed, reason_if_not, category)
|
|
@@ -152,13 +154,12 @@ def is_allowed_orchestrator_operation(
|
|
|
152
154
|
if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
|
|
153
155
|
return True, "", "sdk-command"
|
|
154
156
|
|
|
155
|
-
# Allow git read-only commands
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return True, "", "git-readonly"
|
|
157
|
+
# Allow git read-only commands using shared classification
|
|
158
|
+
if command.strip().startswith("git"):
|
|
159
|
+
from htmlgraph.hooks.git_commands import should_allow_git_command
|
|
160
|
+
|
|
161
|
+
if should_allow_git_command(command):
|
|
162
|
+
return True, "", "git-readonly"
|
|
162
163
|
|
|
163
164
|
# Allow SDK inline usage (Python inline with htmlgraph import)
|
|
164
165
|
if "from htmlgraph import" in command or "import htmlgraph" in command:
|
|
@@ -195,16 +196,21 @@ def is_allowed_orchestrator_operation(
|
|
|
195
196
|
# Category 3: Quick Lookups - Single operations only
|
|
196
197
|
if tool in ["Read", "Grep", "Glob"]:
|
|
197
198
|
# Check tool history to see if this is a single lookup or part of a sequence
|
|
198
|
-
history = load_tool_history()
|
|
199
|
+
history = load_tool_history(session_id)
|
|
199
200
|
|
|
200
|
-
# FIX #4: Check for mixed exploration pattern
|
|
201
|
+
# FIX #4: Check for mixed exploration pattern (configurable threshold)
|
|
202
|
+
config = load_orchestrator_config()
|
|
203
|
+
exploration_threshold = config.thresholds.exploration_calls
|
|
204
|
+
|
|
205
|
+
# Check last N calls (where N = threshold + 2)
|
|
206
|
+
lookback = min(exploration_threshold + 2, len(history))
|
|
201
207
|
exploration_count = sum(
|
|
202
|
-
1 for h in history[-
|
|
208
|
+
1 for h in history[-lookback:] if h["tool"] in ["Read", "Grep", "Glob"]
|
|
203
209
|
)
|
|
204
|
-
if exploration_count >=
|
|
210
|
+
if exploration_count >= exploration_threshold and enforcement_level == "strict":
|
|
205
211
|
return (
|
|
206
212
|
False,
|
|
207
|
-
"Multiple exploration calls detected. Delegate to Explorer agent.\n\n"
|
|
213
|
+
f"Multiple exploration calls detected ({exploration_count}/{exploration_threshold}). Delegate to Explorer agent.\n\n"
|
|
208
214
|
"Use Task tool with explorer subagent.",
|
|
209
215
|
"exploration-blocked",
|
|
210
216
|
)
|
|
@@ -372,21 +378,36 @@ def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
|
|
|
372
378
|
)
|
|
373
379
|
|
|
374
380
|
|
|
375
|
-
def enforce_orchestrator_mode(
|
|
381
|
+
def enforce_orchestrator_mode(
|
|
382
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
383
|
+
) -> dict[str, Any]:
|
|
376
384
|
"""
|
|
377
385
|
Enforce orchestrator mode rules.
|
|
378
386
|
|
|
379
387
|
This is the main public API for hook scripts. It checks if orchestrator mode
|
|
380
388
|
is enabled, classifies the operation, and returns a hook response dict.
|
|
381
389
|
|
|
390
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
391
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
392
|
+
|
|
382
393
|
Args:
|
|
383
394
|
tool: Tool being called
|
|
384
395
|
params: Tool parameters
|
|
396
|
+
session_id: Session identifier for loading tool history
|
|
385
397
|
|
|
386
398
|
Returns:
|
|
387
399
|
Hook response dict with decision (allow/block) and guidance
|
|
388
400
|
Format: {"continue": bool, "hookSpecificOutput": {...}}
|
|
389
401
|
"""
|
|
402
|
+
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
403
|
+
if is_subagent_context():
|
|
404
|
+
return {
|
|
405
|
+
"hookSpecificOutput": {
|
|
406
|
+
"hookEventName": "PreToolUse",
|
|
407
|
+
"permissionDecision": "allow",
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
|
|
390
411
|
# Get manager and check if mode is enabled
|
|
391
412
|
try:
|
|
392
413
|
# Look for .htmlgraph directory starting from cwd
|
|
@@ -405,7 +426,6 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
405
426
|
|
|
406
427
|
if not manager.is_enabled():
|
|
407
428
|
# Mode not active, allow everything
|
|
408
|
-
add_to_tool_history(tool)
|
|
409
429
|
return {
|
|
410
430
|
"hookSpecificOutput": {
|
|
411
431
|
"hookEventName": "PreToolUse",
|
|
@@ -416,7 +436,6 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
416
436
|
enforcement_level = manager.get_enforcement_level()
|
|
417
437
|
except Exception:
|
|
418
438
|
# If we can't check mode, fail open (allow)
|
|
419
|
-
add_to_tool_history(tool)
|
|
420
439
|
return {
|
|
421
440
|
"hookSpecificOutput": {
|
|
422
441
|
"hookEventName": "PreToolUse",
|
|
@@ -424,20 +443,26 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
424
443
|
},
|
|
425
444
|
}
|
|
426
445
|
|
|
427
|
-
# Check if circuit breaker is triggered in strict mode
|
|
446
|
+
# Check if circuit breaker is triggered in strict mode (configurable threshold)
|
|
447
|
+
config = load_orchestrator_config()
|
|
448
|
+
circuit_breaker_threshold = config.thresholds.circuit_breaker_violations
|
|
449
|
+
|
|
428
450
|
if enforcement_level == "strict" and manager.is_circuit_breaker_triggered():
|
|
429
451
|
# Circuit breaker triggered - block all non-core operations
|
|
430
452
|
if tool not in ["Task", "AskUserQuestion", "TodoWrite"]:
|
|
453
|
+
violation_count = manager.get_violation_count()
|
|
431
454
|
circuit_breaker_message = (
|
|
432
455
|
"🚨 ORCHESTRATOR CIRCUIT BREAKER TRIGGERED\n\n"
|
|
433
|
-
f"You have violated delegation rules {
|
|
456
|
+
f"You have violated delegation rules {violation_count} times this session "
|
|
457
|
+
f"(threshold: {circuit_breaker_threshold}).\n\n"
|
|
434
458
|
"Violations detected:\n"
|
|
435
459
|
"- Direct execution instead of delegation\n"
|
|
436
460
|
"- Context waste on tactical operations\n\n"
|
|
437
461
|
"Options:\n"
|
|
438
462
|
"1. Disable orchestrator mode: uv run htmlgraph orchestrator disable\n"
|
|
439
463
|
"2. Change to guidance mode: uv run htmlgraph orchestrator set-level guidance\n"
|
|
440
|
-
"3. Reset counter (acknowledge violations): uv run htmlgraph orchestrator reset-violations\n
|
|
464
|
+
"3. Reset counter (acknowledge violations): uv run htmlgraph orchestrator reset-violations\n"
|
|
465
|
+
"4. Adjust thresholds: uv run htmlgraph orchestrator config set thresholds.circuit_breaker_violations <N>\n\n"
|
|
441
466
|
"To proceed, choose an option above."
|
|
442
467
|
)
|
|
443
468
|
|
|
@@ -449,11 +474,13 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
449
474
|
},
|
|
450
475
|
}
|
|
451
476
|
|
|
452
|
-
# Check if operation is allowed
|
|
453
|
-
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
477
|
+
# Check if operation is allowed (pass session_id for history lookup)
|
|
478
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
479
|
+
tool, params, session_id
|
|
480
|
+
)
|
|
454
481
|
|
|
455
|
-
#
|
|
456
|
-
add_to_tool_history(
|
|
482
|
+
# Note: Tool recording is now handled by track-event.py PostToolUse hook
|
|
483
|
+
# No need to call add_to_tool_history() here
|
|
457
484
|
|
|
458
485
|
# Operation is allowed
|
|
459
486
|
if is_allowed:
|
|
@@ -488,19 +515,19 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
488
515
|
if enforcement_level == "strict":
|
|
489
516
|
# STRICT mode - loud warning with violation count
|
|
490
517
|
error_message = (
|
|
491
|
-
f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/
|
|
518
|
+
f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
|
|
492
519
|
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
493
520
|
f"Suggested delegation:\n"
|
|
494
521
|
f"{suggestion}\n\n"
|
|
495
522
|
)
|
|
496
523
|
|
|
497
524
|
# Add circuit breaker warning if approaching threshold
|
|
498
|
-
if violations >=
|
|
525
|
+
if violations >= circuit_breaker_threshold:
|
|
499
526
|
error_message += (
|
|
500
527
|
"🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
|
|
501
528
|
"Reset with: uv run htmlgraph orchestrator reset-violations\n"
|
|
502
529
|
)
|
|
503
|
-
elif violations ==
|
|
530
|
+
elif violations == circuit_breaker_threshold - 1:
|
|
504
531
|
error_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
|
|
505
532
|
|
|
506
533
|
error_message += (
|
|
@@ -528,3 +555,42 @@ def enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict[str, An
|
|
|
528
555
|
"additionalContext": warning_message,
|
|
529
556
|
},
|
|
530
557
|
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def main() -> None:
|
|
561
|
+
"""Hook entry point for script wrapper."""
|
|
562
|
+
import os
|
|
563
|
+
import sys
|
|
564
|
+
|
|
565
|
+
# Check if tracking is disabled
|
|
566
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
567
|
+
print(json.dumps({"continue": True}))
|
|
568
|
+
sys.exit(0)
|
|
569
|
+
|
|
570
|
+
# Check for orchestrator mode environment override
|
|
571
|
+
if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
|
|
572
|
+
print(json.dumps({"continue": True}))
|
|
573
|
+
sys.exit(0)
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
hook_input = json.load(sys.stdin)
|
|
577
|
+
except json.JSONDecodeError:
|
|
578
|
+
hook_input = {}
|
|
579
|
+
|
|
580
|
+
# Get tool name and parameters (Claude Code uses "name" and "input")
|
|
581
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
582
|
+
tool_input = hook_input.get("input", {}) or hook_input.get("tool_input", {})
|
|
583
|
+
|
|
584
|
+
# Get session_id from hook_input (NEW: required for session-isolated history)
|
|
585
|
+
session_id = hook_input.get("session_id", "unknown")
|
|
586
|
+
|
|
587
|
+
if not tool_name:
|
|
588
|
+
# No tool name, allow
|
|
589
|
+
print(json.dumps({"continue": True}))
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
# Enforce orchestrator mode with session_id for history lookup
|
|
593
|
+
response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
|
|
594
|
+
|
|
595
|
+
# Output JSON response
|
|
596
|
+
print(json.dumps(response))
|
|
@@ -194,3 +194,26 @@ def orchestrator_reflect(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
return response
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main() -> None:
|
|
200
|
+
"""Hook entry point for script wrapper."""
|
|
201
|
+
import json
|
|
202
|
+
import os
|
|
203
|
+
import sys
|
|
204
|
+
|
|
205
|
+
# Check if tracking is disabled
|
|
206
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
207
|
+
print(json.dumps({"continue": True}))
|
|
208
|
+
sys.exit(0)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
hook_input = json.load(sys.stdin)
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
hook_input = {}
|
|
214
|
+
|
|
215
|
+
# Run reflection logic
|
|
216
|
+
response = orchestrator_reflect(hook_input)
|
|
217
|
+
|
|
218
|
+
# Output JSON response
|
|
219
|
+
print(json.dumps(response))
|
htmlgraph/hooks/pretooluse.py
CHANGED
|
@@ -363,7 +363,16 @@ def create_start_event(
|
|
|
363
363
|
|
|
364
364
|
cursor = db.connection.cursor() # type: ignore[union-attr]
|
|
365
365
|
|
|
366
|
-
#
|
|
366
|
+
# Determine parent event ID with proper hierarchy:
|
|
367
|
+
# 1. FIRST check HTMLGRAPH_PARENT_EVENT env var (set by Task delegation for subagents)
|
|
368
|
+
# 2. For Task() tool, create a new task_delegation event
|
|
369
|
+
# 3. Fall back to UserQuery only if no parent context available
|
|
370
|
+
#
|
|
371
|
+
# This ensures tool events executed within Task() subagents are properly
|
|
372
|
+
# nested under the Task delegation event, not flattened to UserQuery.
|
|
373
|
+
env_parent_event = os.environ.get("HTMLGRAPH_PARENT_EVENT")
|
|
374
|
+
|
|
375
|
+
# Get UserQuery event ID as fallback (for top-level tool calls)
|
|
367
376
|
user_query_event_id = None
|
|
368
377
|
try:
|
|
369
378
|
from htmlgraph.hooks.event_tracker import get_parent_user_query
|
|
@@ -384,10 +393,21 @@ def create_start_event(
|
|
|
384
393
|
|
|
385
394
|
event_id = f"evt-{str(uuid.uuid4())[:8]}"
|
|
386
395
|
|
|
387
|
-
# Determine parent
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
# Determine parent with proper hierarchy:
|
|
397
|
+
# - Task() tools: Use the newly created task_delegation event
|
|
398
|
+
# - Tools in subagent context: Use HTMLGRAPH_PARENT_EVENT (Task delegation)
|
|
399
|
+
# - Top-level tools: Fall back to UserQuery
|
|
400
|
+
if tool_name == "Task":
|
|
401
|
+
parent_event_id = task_parent_event_id
|
|
402
|
+
elif env_parent_event:
|
|
403
|
+
# Subagent context: tools should be children of Task delegation
|
|
404
|
+
parent_event_id = env_parent_event
|
|
405
|
+
logger.debug(
|
|
406
|
+
f"Using parent from environment: {env_parent_event} for {tool_name}"
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
# Top-level context: tools are children of UserQuery
|
|
410
|
+
parent_event_id = user_query_event_id
|
|
391
411
|
|
|
392
412
|
cursor.execute(
|
|
393
413
|
"""
|
|
@@ -557,10 +577,13 @@ async def run_validation_check(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
|
557
577
|
|
|
558
578
|
tool_name = tool_input.get("name", "") or tool_input.get("tool", "")
|
|
559
579
|
tool_params = tool_input.get("input", {}) or tool_input.get("params", {})
|
|
580
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
560
581
|
|
|
561
582
|
# Load config and history in thread pool
|
|
562
583
|
config = await loop.run_in_executor(None, load_validation_config)
|
|
563
|
-
history = await loop.run_in_executor(
|
|
584
|
+
history = await loop.run_in_executor(
|
|
585
|
+
None, lambda: validator_load_history(session_id)
|
|
586
|
+
)
|
|
564
587
|
|
|
565
588
|
# Run validation
|
|
566
589
|
return await loop.run_in_executor(
|
|
@@ -635,3 +635,31 @@ __all__ = [
|
|
|
635
635
|
"record_user_query_event",
|
|
636
636
|
"check_version_status",
|
|
637
637
|
]
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def main() -> None:
|
|
641
|
+
"""Hook entry point for SessionEnd hook."""
|
|
642
|
+
import json
|
|
643
|
+
import os
|
|
644
|
+
import sys
|
|
645
|
+
|
|
646
|
+
# Check if tracking is disabled
|
|
647
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
648
|
+
print(json.dumps({"continue": True}))
|
|
649
|
+
sys.exit(0)
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
hook_input = json.load(sys.stdin)
|
|
653
|
+
except json.JSONDecodeError:
|
|
654
|
+
hook_input = {}
|
|
655
|
+
|
|
656
|
+
# Create context from hook input
|
|
657
|
+
from htmlgraph.hooks.context import HookContext
|
|
658
|
+
|
|
659
|
+
context = HookContext.from_input(hook_input)
|
|
660
|
+
|
|
661
|
+
# Handle session end
|
|
662
|
+
response = handle_session_end(context)
|
|
663
|
+
|
|
664
|
+
# Output JSON response
|
|
665
|
+
print(json.dumps(response))
|