htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/hooks/orchestrator.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Orchestrator Enforcement Module
|
|
3
7
|
|
|
@@ -10,6 +14,8 @@ Architecture:
|
|
|
10
14
|
- Classifies operations into ALLOWED vs BLOCKED categories
|
|
11
15
|
- Tracks tool usage sequences to detect exploration patterns
|
|
12
16
|
- Provides clear Task delegation suggestions when blocking
|
|
17
|
+
- Subagents spawned via Task() have unrestricted tool access
|
|
18
|
+
- Detection uses 5-level strategy: env vars, session state, database
|
|
13
19
|
|
|
14
20
|
Operation Categories:
|
|
15
21
|
1. ALWAYS ALLOWED - Task, AskUserQuestion, TodoWrite, SDK operations
|
|
@@ -21,85 +27,154 @@ Enforcement Levels:
|
|
|
21
27
|
- guidance: ALLOWS but provides warnings and suggestions
|
|
22
28
|
|
|
23
29
|
Public API:
|
|
24
|
-
- enforce_orchestrator_mode(tool: str, params: dict) -> dict
|
|
30
|
+
- enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
|
|
25
31
|
Main entry point for hook scripts. Returns hook response dict.
|
|
26
32
|
"""
|
|
27
33
|
|
|
28
34
|
import json
|
|
29
35
|
import re
|
|
30
|
-
from datetime import datetime, timezone
|
|
31
36
|
from pathlib import Path
|
|
32
|
-
from typing import Any
|
|
37
|
+
from typing import Any
|
|
33
38
|
|
|
39
|
+
from htmlgraph.hooks.subagent_detection import is_subagent_context
|
|
40
|
+
from htmlgraph.orchestrator_config import load_orchestrator_config
|
|
34
41
|
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
35
42
|
from htmlgraph.orchestrator_validator import OrchestratorValidator
|
|
36
43
|
|
|
37
|
-
#
|
|
38
|
-
TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
|
|
44
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
39
45
|
MAX_HISTORY_SIZE = 50 # Keep last 50 tool calls
|
|
40
46
|
|
|
41
47
|
|
|
42
|
-
def load_tool_history() -> list[dict]:
|
|
48
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
43
49
|
"""
|
|
44
|
-
Load recent tool history from
|
|
50
|
+
Load recent tool history from database (session-isolated).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
session_id: Session identifier to filter tool history
|
|
45
54
|
|
|
46
55
|
Returns:
|
|
47
56
|
List of recent tool calls with tool name and timestamp
|
|
48
57
|
"""
|
|
49
|
-
if not TOOL_HISTORY_FILE.exists():
|
|
50
|
-
return []
|
|
51
|
-
|
|
52
58
|
try:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
60
|
+
|
|
61
|
+
# Find database path
|
|
62
|
+
cwd = Path.cwd()
|
|
63
|
+
graph_dir = cwd / ".htmlgraph"
|
|
64
|
+
if not graph_dir.exists():
|
|
65
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
66
|
+
candidate = parent / ".htmlgraph"
|
|
67
|
+
if candidate.exists():
|
|
68
|
+
graph_dir = candidate
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
72
|
+
if not db_path.exists():
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
db = HtmlGraphDB(str(db_path))
|
|
76
|
+
if db.connection is None:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
cursor = db.connection.cursor()
|
|
80
|
+
cursor.execute(
|
|
81
|
+
"""
|
|
82
|
+
SELECT tool_name, timestamp
|
|
83
|
+
FROM agent_events
|
|
84
|
+
WHERE session_id = ?
|
|
85
|
+
ORDER BY timestamp DESC
|
|
86
|
+
LIMIT ?
|
|
87
|
+
""",
|
|
88
|
+
(session_id, MAX_HISTORY_SIZE),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Return in chronological order (oldest first) for pattern detection
|
|
92
|
+
rows = cursor.fetchall()
|
|
93
|
+
db.disconnect()
|
|
94
|
+
|
|
95
|
+
return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
|
|
58
96
|
except Exception:
|
|
97
|
+
# Graceful degradation - return empty history on error
|
|
59
98
|
return []
|
|
60
99
|
|
|
61
100
|
|
|
62
|
-
def
|
|
101
|
+
def record_tool_event(tool_name: str, session_id: str) -> None:
|
|
63
102
|
"""
|
|
64
|
-
|
|
103
|
+
Record a tool event to the database for history tracking.
|
|
104
|
+
|
|
105
|
+
This is called at the end of PreToolUse hook execution to track
|
|
106
|
+
tool usage patterns for sequence detection.
|
|
65
107
|
|
|
66
108
|
Args:
|
|
67
|
-
|
|
109
|
+
tool_name: Name of the tool being called
|
|
110
|
+
session_id: Session identifier for isolation
|
|
68
111
|
"""
|
|
69
112
|
try:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
history[-MAX_HISTORY_SIZE:] if len(history) > MAX_HISTORY_SIZE else history
|
|
73
|
-
)
|
|
74
|
-
TOOL_HISTORY_FILE.write_text(json.dumps({"history": recent}, indent=2))
|
|
75
|
-
except Exception:
|
|
76
|
-
pass # Fail silently on history save errors
|
|
113
|
+
import datetime
|
|
114
|
+
import uuid
|
|
77
115
|
|
|
116
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
78
117
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
# Find database path
|
|
119
|
+
cwd = Path.cwd()
|
|
120
|
+
graph_dir = cwd / ".htmlgraph"
|
|
121
|
+
if not graph_dir.exists():
|
|
122
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
123
|
+
candidate = parent / ".htmlgraph"
|
|
124
|
+
if candidate.exists():
|
|
125
|
+
graph_dir = candidate
|
|
126
|
+
break
|
|
82
127
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
if not graph_dir.exists():
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
132
|
+
db = HtmlGraphDB(str(db_path))
|
|
133
|
+
if db.connection is None:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
cursor = db.connection.cursor()
|
|
137
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
|
138
|
+
|
|
139
|
+
# Ensure session exists (required by FK constraint)
|
|
140
|
+
cursor.execute(
|
|
141
|
+
"""
|
|
142
|
+
INSERT OR IGNORE INTO sessions (session_id, agent_assigned, created_at, status)
|
|
143
|
+
VALUES (?, ?, ?, ?)
|
|
144
|
+
""",
|
|
145
|
+
(session_id, "orchestrator-hook", timestamp, "active"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Record the tool event using the actual schema
|
|
149
|
+
# Schema has: event_id, agent_id, event_type, timestamp, tool_name, session_id, etc.
|
|
150
|
+
event_id = str(uuid.uuid4())
|
|
151
|
+
agent_id = "orchestrator-hook" # Identifier for the hook
|
|
152
|
+
|
|
153
|
+
cursor.execute(
|
|
154
|
+
"""
|
|
155
|
+
INSERT INTO agent_events (event_id, agent_id, event_type, timestamp, tool_name, session_id)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
157
|
+
""",
|
|
158
|
+
(event_id, agent_id, "tool_call", timestamp, tool_name, session_id),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
db.connection.commit()
|
|
162
|
+
db.disconnect()
|
|
163
|
+
except Exception:
|
|
164
|
+
# Graceful degradation - don't fail hook on recording error
|
|
165
|
+
pass
|
|
94
166
|
|
|
95
167
|
|
|
96
|
-
def is_allowed_orchestrator_operation(
|
|
168
|
+
def is_allowed_orchestrator_operation(
|
|
169
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
170
|
+
) -> tuple[bool, str, str]:
|
|
97
171
|
"""
|
|
98
172
|
Check if operation is allowed for orchestrators.
|
|
99
173
|
|
|
100
174
|
Args:
|
|
101
175
|
tool: Tool name (e.g., "Read", "Edit", "Bash")
|
|
102
176
|
params: Tool parameters dict
|
|
177
|
+
session_id: Session identifier for loading tool history
|
|
103
178
|
|
|
104
179
|
Returns:
|
|
105
180
|
Tuple of (is_allowed, reason_if_not, category)
|
|
@@ -107,6 +182,23 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
107
182
|
- reason_if_not: Explanation if blocked (empty if allowed)
|
|
108
183
|
- category: Operation category for logging
|
|
109
184
|
"""
|
|
185
|
+
# Get enforcement level from manager
|
|
186
|
+
try:
|
|
187
|
+
cwd = Path.cwd()
|
|
188
|
+
graph_dir = cwd / ".htmlgraph"
|
|
189
|
+
if not graph_dir.exists():
|
|
190
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
191
|
+
candidate = parent / ".htmlgraph"
|
|
192
|
+
if candidate.exists():
|
|
193
|
+
graph_dir = candidate
|
|
194
|
+
break
|
|
195
|
+
manager = OrchestratorModeManager(graph_dir)
|
|
196
|
+
enforcement_level = (
|
|
197
|
+
manager.get_enforcement_level() if manager.is_enabled() else "guidance"
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
enforcement_level = "guidance"
|
|
201
|
+
|
|
110
202
|
# Use OrchestratorValidator for comprehensive validation
|
|
111
203
|
validator = OrchestratorValidator()
|
|
112
204
|
result, reason = validator.validate_tool_use(tool, params)
|
|
@@ -121,6 +213,10 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
121
213
|
if tool in ["Task", "AskUserQuestion", "TodoWrite"]:
|
|
122
214
|
return True, "", "orchestrator-core"
|
|
123
215
|
|
|
216
|
+
# FIX #2: Block Skills in strict mode (must be invoked via Task delegation)
|
|
217
|
+
if tool == "Skill" and enforcement_level == "strict":
|
|
218
|
+
return False, "Skills must be invoked via Task delegation", "skill-blocked"
|
|
219
|
+
|
|
124
220
|
# Category 2: SDK Operations - Always allowed
|
|
125
221
|
if tool == "Bash":
|
|
126
222
|
command = params.get("command", "")
|
|
@@ -129,22 +225,66 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
129
225
|
if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
|
|
130
226
|
return True, "", "sdk-command"
|
|
131
227
|
|
|
132
|
-
# Allow git read-only commands
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return True, "", "git-readonly"
|
|
228
|
+
# Allow git read-only commands using shared classification
|
|
229
|
+
if command.strip().startswith("git"):
|
|
230
|
+
from htmlgraph.hooks.git_commands import should_allow_git_command
|
|
231
|
+
|
|
232
|
+
if should_allow_git_command(command):
|
|
233
|
+
return True, "", "git-readonly"
|
|
139
234
|
|
|
140
235
|
# Allow SDK inline usage (Python inline with htmlgraph import)
|
|
141
236
|
if "from htmlgraph import" in command or "import htmlgraph" in command:
|
|
142
237
|
return True, "", "sdk-inline"
|
|
143
238
|
|
|
239
|
+
# FIX #3: Check if bash command is in allowed whitelist (strict mode only)
|
|
240
|
+
# If we've gotten here, it's not a whitelisted command above
|
|
241
|
+
# Block non-whitelisted bash commands in strict mode
|
|
242
|
+
if enforcement_level == "strict":
|
|
243
|
+
# Check if it's a blocked test/build pattern (handled below)
|
|
244
|
+
blocked_patterns = [
|
|
245
|
+
r"^npm (run|test|build)",
|
|
246
|
+
r"^pytest",
|
|
247
|
+
r"^uv run pytest",
|
|
248
|
+
r"^python -m pytest",
|
|
249
|
+
r"^cargo (build|test)",
|
|
250
|
+
r"^mvn (compile|test|package)",
|
|
251
|
+
r"^make (test|build)",
|
|
252
|
+
]
|
|
253
|
+
is_blocked_pattern = any(
|
|
254
|
+
re.match(pattern, command) for pattern in blocked_patterns
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if not is_blocked_pattern:
|
|
258
|
+
# Not a specifically blocked pattern, but also not whitelisted
|
|
259
|
+
# In strict mode, we should delegate
|
|
260
|
+
return (
|
|
261
|
+
False,
|
|
262
|
+
f"Bash command not in allowed list. Delegate to subagent.\n\n"
|
|
263
|
+
f"Command: {command[:100]}",
|
|
264
|
+
"bash-blocked",
|
|
265
|
+
)
|
|
266
|
+
|
|
144
267
|
# Category 3: Quick Lookups - Single operations only
|
|
145
268
|
if tool in ["Read", "Grep", "Glob"]:
|
|
146
269
|
# Check tool history to see if this is a single lookup or part of a sequence
|
|
147
|
-
history = load_tool_history()
|
|
270
|
+
history = load_tool_history(session_id)
|
|
271
|
+
|
|
272
|
+
# FIX #4: Check for mixed exploration pattern (configurable threshold)
|
|
273
|
+
config = load_orchestrator_config()
|
|
274
|
+
exploration_threshold = config.thresholds.exploration_calls
|
|
275
|
+
|
|
276
|
+
# Check last N calls (where N = threshold + 2)
|
|
277
|
+
lookback = min(exploration_threshold + 2, len(history))
|
|
278
|
+
exploration_count = sum(
|
|
279
|
+
1 for h in history[-lookback:] if h["tool"] in ["Read", "Grep", "Glob"]
|
|
280
|
+
)
|
|
281
|
+
if exploration_count >= exploration_threshold and enforcement_level == "strict":
|
|
282
|
+
return (
|
|
283
|
+
False,
|
|
284
|
+
f"Multiple exploration calls detected ({exploration_count}/{exploration_threshold}). Delegate to Explorer agent.\n\n"
|
|
285
|
+
"Use Task tool with explorer subagent.",
|
|
286
|
+
"exploration-blocked",
|
|
287
|
+
)
|
|
148
288
|
|
|
149
289
|
# Look at last 3 tool calls
|
|
150
290
|
recent_same_tool = sum(1 for h in history[-3:] if h["tool"] == tool)
|
|
@@ -181,7 +321,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
181
321
|
command = params.get("command", "")
|
|
182
322
|
|
|
183
323
|
# Block compilation, testing, building (should be in subagent)
|
|
184
|
-
|
|
324
|
+
test_build_patterns: list[tuple[str, str]] = [
|
|
185
325
|
(r"^npm (run|test|build)", "npm test/build"),
|
|
186
326
|
(r"^pytest", "pytest"),
|
|
187
327
|
(r"^uv run pytest", "pytest"),
|
|
@@ -191,7 +331,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
191
331
|
(r"^make (test|build)", "make test/build"),
|
|
192
332
|
]
|
|
193
333
|
|
|
194
|
-
for pattern, name in
|
|
334
|
+
for pattern, name in test_build_patterns:
|
|
195
335
|
if re.match(pattern, command):
|
|
196
336
|
return (
|
|
197
337
|
False,
|
|
@@ -200,11 +340,14 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
|
|
|
200
340
|
"test-build-blocked",
|
|
201
341
|
)
|
|
202
342
|
|
|
203
|
-
#
|
|
204
|
-
|
|
343
|
+
# FIX #1: Remove "allowed-default" escape hatch in strict mode
|
|
344
|
+
if enforcement_level == "strict":
|
|
345
|
+
return False, "Not in allowed whitelist", "strict-blocked"
|
|
346
|
+
else:
|
|
347
|
+
return True, "Allowed in guidance mode", "guidance-allowed"
|
|
205
348
|
|
|
206
349
|
|
|
207
|
-
def create_task_suggestion(tool: str, params: dict) -> str:
|
|
350
|
+
def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
|
|
208
351
|
"""
|
|
209
352
|
Create Task tool suggestion based on blocked operation.
|
|
210
353
|
|
|
@@ -306,21 +449,37 @@ def create_task_suggestion(tool: str, params: dict) -> str:
|
|
|
306
449
|
)
|
|
307
450
|
|
|
308
451
|
|
|
309
|
-
def enforce_orchestrator_mode(
|
|
452
|
+
def enforce_orchestrator_mode(
|
|
453
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
454
|
+
) -> dict[str, Any]:
|
|
310
455
|
"""
|
|
311
456
|
Enforce orchestrator mode rules.
|
|
312
457
|
|
|
313
458
|
This is the main public API for hook scripts. It checks if orchestrator mode
|
|
314
459
|
is enabled, classifies the operation, and returns a hook response dict.
|
|
315
460
|
|
|
461
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
462
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
463
|
+
|
|
316
464
|
Args:
|
|
317
465
|
tool: Tool being called
|
|
318
466
|
params: Tool parameters
|
|
467
|
+
session_id: Session identifier for loading tool history
|
|
319
468
|
|
|
320
469
|
Returns:
|
|
321
470
|
Hook response dict with decision (allow/block) and guidance
|
|
322
471
|
Format: {"continue": bool, "hookSpecificOutput": {...}}
|
|
323
472
|
"""
|
|
473
|
+
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
474
|
+
if is_subagent_context():
|
|
475
|
+
return {
|
|
476
|
+
"continue": True,
|
|
477
|
+
"hookSpecificOutput": {
|
|
478
|
+
"hookEventName": "PreToolUse",
|
|
479
|
+
"permissionDecision": "allow",
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
|
|
324
483
|
# Get manager and check if mode is enabled
|
|
325
484
|
try:
|
|
326
485
|
# Look for .htmlgraph directory starting from cwd
|
|
@@ -338,31 +497,59 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
|
|
|
338
497
|
manager = OrchestratorModeManager(graph_dir)
|
|
339
498
|
|
|
340
499
|
if not manager.is_enabled():
|
|
341
|
-
# Mode not active, allow everything
|
|
342
|
-
|
|
343
|
-
return {
|
|
344
|
-
"hookSpecificOutput": {
|
|
345
|
-
"hookEventName": "PreToolUse",
|
|
346
|
-
"permissionDecision": "allow",
|
|
347
|
-
},
|
|
348
|
-
}
|
|
500
|
+
# Mode not active, allow everything with no additional output
|
|
501
|
+
return {"continue": True}
|
|
349
502
|
|
|
350
503
|
enforcement_level = manager.get_enforcement_level()
|
|
351
504
|
except Exception:
|
|
352
505
|
# If we can't check mode, fail open (allow)
|
|
353
|
-
add_to_tool_history(tool)
|
|
354
506
|
return {
|
|
507
|
+
"continue": True,
|
|
355
508
|
"hookSpecificOutput": {
|
|
356
509
|
"hookEventName": "PreToolUse",
|
|
357
510
|
"permissionDecision": "allow",
|
|
358
511
|
},
|
|
359
512
|
}
|
|
360
513
|
|
|
361
|
-
# Check if
|
|
362
|
-
|
|
514
|
+
# Check if circuit breaker is triggered in strict mode (configurable threshold)
|
|
515
|
+
config = load_orchestrator_config()
|
|
516
|
+
circuit_breaker_threshold = config.thresholds.circuit_breaker_violations
|
|
517
|
+
|
|
518
|
+
if enforcement_level == "strict" and manager.is_circuit_breaker_triggered():
|
|
519
|
+
# Circuit breaker triggered - block all non-core operations
|
|
520
|
+
if tool not in ["Task", "AskUserQuestion", "TodoWrite"]:
|
|
521
|
+
violation_count = manager.get_violation_count()
|
|
522
|
+
circuit_breaker_message = (
|
|
523
|
+
"🚨 ORCHESTRATOR CIRCUIT BREAKER TRIGGERED\n\n"
|
|
524
|
+
f"You have violated delegation rules {violation_count} times this session "
|
|
525
|
+
f"(threshold: {circuit_breaker_threshold}).\n\n"
|
|
526
|
+
"Violations detected:\n"
|
|
527
|
+
"- Direct execution instead of delegation\n"
|
|
528
|
+
"- Context waste on tactical operations\n\n"
|
|
529
|
+
"Options:\n"
|
|
530
|
+
"1. Disable orchestrator mode: uv run htmlgraph orchestrator disable\n"
|
|
531
|
+
"2. Change to guidance mode: uv run htmlgraph orchestrator set-level guidance\n"
|
|
532
|
+
"3. Reset counter (acknowledge violations): uv run htmlgraph orchestrator reset-violations\n"
|
|
533
|
+
"4. Adjust thresholds: uv run htmlgraph orchestrator config set thresholds.circuit_breaker_violations <N>\n\n"
|
|
534
|
+
"To proceed, choose an option above."
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
return {
|
|
538
|
+
"continue": False,
|
|
539
|
+
"hookSpecificOutput": {
|
|
540
|
+
"hookEventName": "PreToolUse",
|
|
541
|
+
"permissionDecision": "deny",
|
|
542
|
+
"permissionDecisionReason": circuit_breaker_message,
|
|
543
|
+
},
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
# Check if operation is allowed (pass session_id for history lookup)
|
|
547
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
548
|
+
tool, params, session_id
|
|
549
|
+
)
|
|
363
550
|
|
|
364
|
-
#
|
|
365
|
-
add_to_tool_history(
|
|
551
|
+
# Note: Tool recording is now handled by track-event.py PostToolUse hook
|
|
552
|
+
# No need to call add_to_tool_history() here
|
|
366
553
|
|
|
367
554
|
# Operation is allowed
|
|
368
555
|
if is_allowed:
|
|
@@ -373,6 +560,7 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
|
|
|
373
560
|
):
|
|
374
561
|
# Provide guidance even when allowing
|
|
375
562
|
return {
|
|
563
|
+
"continue": True,
|
|
376
564
|
"hookSpecificOutput": {
|
|
377
565
|
"hookEventName": "PreToolUse",
|
|
378
566
|
"permissionDecision": "allow",
|
|
@@ -380,32 +568,51 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
|
|
|
380
568
|
},
|
|
381
569
|
}
|
|
382
570
|
return {
|
|
571
|
+
"continue": True,
|
|
383
572
|
"hookSpecificOutput": {
|
|
384
573
|
"hookEventName": "PreToolUse",
|
|
385
574
|
"permissionDecision": "allow",
|
|
386
575
|
},
|
|
387
576
|
}
|
|
388
577
|
|
|
389
|
-
# Operation not allowed - provide
|
|
390
|
-
|
|
578
|
+
# Operation not allowed - track violation and provide warnings
|
|
579
|
+
if enforcement_level == "strict":
|
|
580
|
+
# Increment violation counter
|
|
581
|
+
mode = manager.increment_violation()
|
|
582
|
+
violations = mode.violations
|
|
583
|
+
|
|
391
584
|
suggestion = create_task_suggestion(tool, params)
|
|
392
585
|
|
|
393
586
|
if enforcement_level == "strict":
|
|
394
|
-
# STRICT mode -
|
|
395
|
-
|
|
396
|
-
f"🚫 ORCHESTRATOR MODE VIOLATION: {reason}\n\n"
|
|
587
|
+
# STRICT mode - advisory warning with violation count (does not block)
|
|
588
|
+
warning_message = (
|
|
589
|
+
f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
|
|
397
590
|
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
398
591
|
f"Suggested delegation:\n"
|
|
399
592
|
f"{suggestion}\n\n"
|
|
400
|
-
f"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
|
|
401
|
-
f"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
|
|
402
593
|
)
|
|
403
594
|
|
|
595
|
+
# Add circuit breaker warning if approaching threshold
|
|
596
|
+
if violations >= circuit_breaker_threshold:
|
|
597
|
+
warning_message += (
|
|
598
|
+
"🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
|
|
599
|
+
"Reset with: uv run htmlgraph orchestrator reset-violations\n"
|
|
600
|
+
)
|
|
601
|
+
elif violations == circuit_breaker_threshold - 1:
|
|
602
|
+
warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
|
|
603
|
+
|
|
604
|
+
warning_message += (
|
|
605
|
+
"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
|
|
606
|
+
"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Advisory-only: allow operation but provide warning
|
|
404
610
|
return {
|
|
611
|
+
"continue": True,
|
|
405
612
|
"hookSpecificOutput": {
|
|
406
613
|
"hookEventName": "PreToolUse",
|
|
407
|
-
"permissionDecision": "
|
|
408
|
-
"
|
|
614
|
+
"permissionDecision": "allow",
|
|
615
|
+
"additionalContext": warning_message,
|
|
409
616
|
},
|
|
410
617
|
}
|
|
411
618
|
else:
|
|
@@ -415,9 +622,53 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
|
|
|
415
622
|
)
|
|
416
623
|
|
|
417
624
|
return {
|
|
625
|
+
"continue": True,
|
|
418
626
|
"hookSpecificOutput": {
|
|
419
627
|
"hookEventName": "PreToolUse",
|
|
420
628
|
"permissionDecision": "allow",
|
|
421
629
|
"additionalContext": warning_message,
|
|
422
630
|
},
|
|
423
631
|
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def main() -> None:
|
|
635
|
+
"""Hook entry point for script wrapper."""
|
|
636
|
+
import os
|
|
637
|
+
import sys
|
|
638
|
+
|
|
639
|
+
# Check if tracking is disabled
|
|
640
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
641
|
+
print(json.dumps({"continue": True}))
|
|
642
|
+
sys.exit(0)
|
|
643
|
+
|
|
644
|
+
# Check for orchestrator mode environment override
|
|
645
|
+
if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
|
|
646
|
+
print(json.dumps({"continue": True}))
|
|
647
|
+
sys.exit(0)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
hook_input = json.load(sys.stdin)
|
|
651
|
+
except json.JSONDecodeError:
|
|
652
|
+
hook_input = {}
|
|
653
|
+
|
|
654
|
+
# Get tool name and parameters (Claude Code uses "name" and "input")
|
|
655
|
+
tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
|
|
656
|
+
tool_input = hook_input.get("input", {}) or hook_input.get("tool_input", {})
|
|
657
|
+
|
|
658
|
+
# Get session_id from hook_input (NEW: required for session-isolated history)
|
|
659
|
+
session_id = hook_input.get("session_id", "unknown")
|
|
660
|
+
|
|
661
|
+
if not tool_name:
|
|
662
|
+
# No tool name, allow
|
|
663
|
+
print(json.dumps({"continue": True}))
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# Enforce orchestrator mode with session_id for history lookup
|
|
667
|
+
response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
|
|
668
|
+
|
|
669
|
+
# Record tool event to database for history tracking
|
|
670
|
+
# This allows subsequent calls to detect patterns (e.g., multiple Reads)
|
|
671
|
+
record_tool_event(tool_name, session_id)
|
|
672
|
+
|
|
673
|
+
# Output JSON response
|
|
674
|
+
print(json.dumps(response))
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Orchestrator Reflection Module
|
|
3
7
|
|
|
@@ -22,7 +26,7 @@ Usage:
|
|
|
22
26
|
"""
|
|
23
27
|
|
|
24
28
|
import re
|
|
25
|
-
from typing import TypedDict
|
|
29
|
+
from typing import Any, TypedDict
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class HookSpecificOutput(TypedDict):
|
|
@@ -93,7 +97,7 @@ def is_python_execution(command: str) -> bool:
|
|
|
93
97
|
return False
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
def should_reflect(hook_input: dict) -> tuple[bool, str]:
|
|
100
|
+
def should_reflect(hook_input: dict[str, Any]) -> tuple[bool, str]:
|
|
97
101
|
"""
|
|
98
102
|
Check if we should show reflection prompt.
|
|
99
103
|
|
|
@@ -156,7 +160,7 @@ Ask yourself:
|
|
|
156
160
|
Continue, but consider delegation for similar future tasks."""
|
|
157
161
|
|
|
158
162
|
|
|
159
|
-
def orchestrator_reflect(tool_input: dict) -> dict:
|
|
163
|
+
def orchestrator_reflect(tool_input: dict[str, Any]) -> dict[str, Any]:
|
|
160
164
|
"""
|
|
161
165
|
Main API function for orchestrator reflection.
|
|
162
166
|
|
|
@@ -184,7 +188,7 @@ def orchestrator_reflect(tool_input: dict) -> dict:
|
|
|
184
188
|
should_show, command_preview = should_reflect(tool_input)
|
|
185
189
|
|
|
186
190
|
# Build response
|
|
187
|
-
response: dict = {"continue": True}
|
|
191
|
+
response: dict[str, Any] = {"continue": True}
|
|
188
192
|
|
|
189
193
|
if should_show:
|
|
190
194
|
reflection = build_reflection_message(command_preview)
|
|
@@ -194,3 +198,26 @@ def orchestrator_reflect(tool_input: dict) -> dict:
|
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
return response
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def main() -> None:
|
|
204
|
+
"""Hook entry point for script wrapper."""
|
|
205
|
+
import json
|
|
206
|
+
import os
|
|
207
|
+
import sys
|
|
208
|
+
|
|
209
|
+
# Check if tracking is disabled
|
|
210
|
+
if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
|
|
211
|
+
print(json.dumps({"continue": True}))
|
|
212
|
+
sys.exit(0)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
hook_input = json.load(sys.stdin)
|
|
216
|
+
except json.JSONDecodeError:
|
|
217
|
+
hook_input = {}
|
|
218
|
+
|
|
219
|
+
# Run reflection logic
|
|
220
|
+
response = orchestrator_reflect(hook_input)
|
|
221
|
+
|
|
222
|
+
# Output JSON response
|
|
223
|
+
print(json.dumps(response))
|