htmlgraph 0.9.3__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 +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- 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 +192 -100
- 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 +190 -14
- htmlgraph/analytics_index.py +135 -51
- 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 +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- 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 +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- 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 +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- 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 +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- 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 +45 -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 +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- 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 +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- 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/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- 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/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- 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 +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Orchestrator Enforcement Module
|
|
7
|
+
|
|
8
|
+
This module provides the core logic for enforcing orchestrator delegation patterns
|
|
9
|
+
in HtmlGraph-enabled projects. It classifies operations into allowed vs blocked
|
|
10
|
+
categories and provides clear Task delegation suggestions.
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
- Reads orchestrator mode from .htmlgraph/orchestrator-mode.json
|
|
14
|
+
- Classifies operations into ALLOWED vs BLOCKED categories
|
|
15
|
+
- Tracks tool usage sequences to detect exploration patterns
|
|
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
|
|
19
|
+
|
|
20
|
+
Operation Categories:
|
|
21
|
+
1. ALWAYS ALLOWED - Task, AskUserQuestion, TodoWrite, SDK operations
|
|
22
|
+
2. SINGLE LOOKUP ALLOWED - First Read/Grep/Glob (check history)
|
|
23
|
+
3. BLOCKED - Edit, Write, NotebookEdit, Delete, test/build commands
|
|
24
|
+
|
|
25
|
+
Enforcement Levels:
|
|
26
|
+
- strict: BLOCKS implementation operations with clear error
|
|
27
|
+
- guidance: ALLOWS but provides warnings and suggestions
|
|
28
|
+
|
|
29
|
+
Public API:
|
|
30
|
+
- enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
|
|
31
|
+
Main entry point for hook scripts. Returns hook response dict.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from htmlgraph.hooks.subagent_detection import is_subagent_context
|
|
40
|
+
from htmlgraph.orchestrator_config import load_orchestrator_config
|
|
41
|
+
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
42
|
+
from htmlgraph.orchestrator_validator import OrchestratorValidator
|
|
43
|
+
|
|
44
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
45
|
+
MAX_HISTORY_SIZE = 50 # Keep last 50 tool calls
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
49
|
+
"""
|
|
50
|
+
Load recent tool history from database (session-isolated).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
session_id: Session identifier to filter tool history
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of recent tool calls with tool name and timestamp
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
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)]
|
|
96
|
+
except Exception:
|
|
97
|
+
# Graceful degradation - return empty history on error
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def record_tool_event(tool_name: str, session_id: str) -> None:
|
|
102
|
+
"""
|
|
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.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
tool_name: Name of the tool being called
|
|
110
|
+
session_id: Session identifier for isolation
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
import datetime
|
|
114
|
+
import uuid
|
|
115
|
+
|
|
116
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
117
|
+
|
|
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
|
|
127
|
+
|
|
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
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def is_allowed_orchestrator_operation(
|
|
169
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
170
|
+
) -> tuple[bool, str, str]:
|
|
171
|
+
"""
|
|
172
|
+
Check if operation is allowed for orchestrators.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
tool: Tool name (e.g., "Read", "Edit", "Bash")
|
|
176
|
+
params: Tool parameters dict
|
|
177
|
+
session_id: Session identifier for loading tool history
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (is_allowed, reason_if_not, category)
|
|
181
|
+
- is_allowed: True if operation should proceed
|
|
182
|
+
- reason_if_not: Explanation if blocked (empty if allowed)
|
|
183
|
+
- category: Operation category for logging
|
|
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
|
+
|
|
202
|
+
# Use OrchestratorValidator for comprehensive validation
|
|
203
|
+
validator = OrchestratorValidator()
|
|
204
|
+
result, reason = validator.validate_tool_use(tool, params)
|
|
205
|
+
|
|
206
|
+
if result == "block":
|
|
207
|
+
return False, reason, "validator-blocked"
|
|
208
|
+
elif result == "warn":
|
|
209
|
+
# Continue but with warning
|
|
210
|
+
pass # Fall through to existing checks
|
|
211
|
+
|
|
212
|
+
# Category 1: ALWAYS ALLOWED - Orchestrator core operations
|
|
213
|
+
if tool in ["Task", "AskUserQuestion", "TodoWrite"]:
|
|
214
|
+
return True, "", "orchestrator-core"
|
|
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
|
+
|
|
220
|
+
# Category 2: SDK Operations - Always allowed
|
|
221
|
+
if tool == "Bash":
|
|
222
|
+
command = params.get("command", "")
|
|
223
|
+
|
|
224
|
+
# Allow htmlgraph SDK commands
|
|
225
|
+
if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
|
|
226
|
+
return True, "", "sdk-command"
|
|
227
|
+
|
|
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"
|
|
234
|
+
|
|
235
|
+
# Allow SDK inline usage (Python inline with htmlgraph import)
|
|
236
|
+
if "from htmlgraph import" in command or "import htmlgraph" in command:
|
|
237
|
+
return True, "", "sdk-inline"
|
|
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
|
+
|
|
267
|
+
# Category 3: Quick Lookups - Single operations only
|
|
268
|
+
if tool in ["Read", "Grep", "Glob"]:
|
|
269
|
+
# Check tool history to see if this is a single lookup or part of a sequence
|
|
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
|
+
)
|
|
288
|
+
|
|
289
|
+
# Look at last 3 tool calls
|
|
290
|
+
recent_same_tool = sum(1 for h in history[-3:] if h["tool"] == tool)
|
|
291
|
+
|
|
292
|
+
if recent_same_tool == 0: # First use
|
|
293
|
+
return True, "Single lookup allowed", "single-lookup"
|
|
294
|
+
else:
|
|
295
|
+
return (
|
|
296
|
+
False,
|
|
297
|
+
f"Multiple {tool} calls detected. This is exploration work.\n\n"
|
|
298
|
+
f"Delegate to Explorer subagent using Task tool.",
|
|
299
|
+
"multi-lookup-blocked",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Category 4: BLOCKED - Implementation tools
|
|
303
|
+
if tool in ["Edit", "Write", "NotebookEdit"]:
|
|
304
|
+
return (
|
|
305
|
+
False,
|
|
306
|
+
f"{tool} is implementation work.\n\n"
|
|
307
|
+
f"Delegate to Coder subagent using Task tool.",
|
|
308
|
+
"implementation-blocked",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if tool == "Delete":
|
|
312
|
+
return (
|
|
313
|
+
False,
|
|
314
|
+
"Delete is a destructive implementation operation.\n\n"
|
|
315
|
+
"Delegate to Coder subagent using Task tool.",
|
|
316
|
+
"delete-blocked",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Category 5: BLOCKED - Testing/Building
|
|
320
|
+
if tool == "Bash":
|
|
321
|
+
command = params.get("command", "")
|
|
322
|
+
|
|
323
|
+
# Block compilation, testing, building (should be in subagent)
|
|
324
|
+
test_build_patterns: list[tuple[str, str]] = [
|
|
325
|
+
(r"^npm (run|test|build)", "npm test/build"),
|
|
326
|
+
(r"^pytest", "pytest"),
|
|
327
|
+
(r"^uv run pytest", "pytest"),
|
|
328
|
+
(r"^python -m pytest", "pytest"),
|
|
329
|
+
(r"^cargo (build|test)", "cargo build/test"),
|
|
330
|
+
(r"^mvn (compile|test|package)", "maven build/test"),
|
|
331
|
+
(r"^make (test|build)", "make test/build"),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
for pattern, name in test_build_patterns:
|
|
335
|
+
if re.match(pattern, command):
|
|
336
|
+
return (
|
|
337
|
+
False,
|
|
338
|
+
f"Testing/building ({name}) should be delegated to subagent.\n\n"
|
|
339
|
+
f"Use Task tool to run tests and report results.",
|
|
340
|
+
"test-build-blocked",
|
|
341
|
+
)
|
|
342
|
+
|
|
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"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Create Task tool suggestion based on blocked operation.
|
|
353
|
+
|
|
354
|
+
Includes HtmlGraph reporting pattern for result retrieval.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
tool: Tool that was blocked
|
|
358
|
+
params: Tool parameters
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Example Task() code with HtmlGraph reporting pattern
|
|
362
|
+
"""
|
|
363
|
+
if tool in ["Edit", "Write", "NotebookEdit"]:
|
|
364
|
+
file_path = params.get("file_path", "<file>")
|
|
365
|
+
return (
|
|
366
|
+
"# Delegate to Coder subagent:\n"
|
|
367
|
+
"Task(\n"
|
|
368
|
+
f" prompt='''Implement changes to {file_path}\n\n"
|
|
369
|
+
" 🔴 CRITICAL - Report Results:\n"
|
|
370
|
+
" from htmlgraph import SDK\n"
|
|
371
|
+
" sdk = SDK(agent='coder')\n"
|
|
372
|
+
" sdk.spikes.create('Code Changes Complete') \\\\\n"
|
|
373
|
+
" .set_findings('Changes made: ...') \\\\\n"
|
|
374
|
+
" .save()\n"
|
|
375
|
+
" ''',\n"
|
|
376
|
+
" subagent_type='general-purpose'\n"
|
|
377
|
+
")\n"
|
|
378
|
+
"# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='coder')[0].findings)\""
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
elif tool in ["Read", "Grep", "Glob"]:
|
|
382
|
+
pattern = params.get("pattern", params.get("file_path", "<pattern>"))
|
|
383
|
+
return (
|
|
384
|
+
"# Delegate to Explorer subagent:\n"
|
|
385
|
+
"Task(\n"
|
|
386
|
+
f" prompt='''Find {pattern} in codebase\n\n"
|
|
387
|
+
" 🔴 CRITICAL - Report Results:\n"
|
|
388
|
+
" from htmlgraph import SDK\n"
|
|
389
|
+
" sdk = SDK(agent='explorer')\n"
|
|
390
|
+
" sdk.spikes.create('Search Results') \\\\\n"
|
|
391
|
+
" .set_findings('Found files: ...') \\\\\n"
|
|
392
|
+
" .save()\n"
|
|
393
|
+
" ''',\n"
|
|
394
|
+
" subagent_type='Explore'\n"
|
|
395
|
+
")\n"
|
|
396
|
+
"# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='explorer')[0].findings)\""
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
elif tool == "Bash":
|
|
400
|
+
command = params.get("command", "")
|
|
401
|
+
if "test" in command.lower() or "pytest" in command.lower():
|
|
402
|
+
return (
|
|
403
|
+
"# Delegate testing to subagent:\n"
|
|
404
|
+
"Task(\n"
|
|
405
|
+
" prompt='''Run tests and report results\n\n"
|
|
406
|
+
" 🔴 CRITICAL - Report Results:\n"
|
|
407
|
+
" from htmlgraph import SDK\n"
|
|
408
|
+
" sdk = SDK(agent='tester')\n"
|
|
409
|
+
" sdk.spikes.create('Test Results') \\\\\n"
|
|
410
|
+
" .set_findings('Tests passed: X, failed: Y') \\\\\n"
|
|
411
|
+
" .save()\n"
|
|
412
|
+
" ''',\n"
|
|
413
|
+
" subagent_type='general-purpose'\n"
|
|
414
|
+
")\n"
|
|
415
|
+
"# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='tester')[0].findings)\""
|
|
416
|
+
)
|
|
417
|
+
elif any(x in command.lower() for x in ["build", "compile", "make"]):
|
|
418
|
+
return (
|
|
419
|
+
"# Delegate build to subagent:\n"
|
|
420
|
+
"Task(\n"
|
|
421
|
+
" prompt='''Build project and report any errors\n\n"
|
|
422
|
+
" 🔴 CRITICAL - Report Results:\n"
|
|
423
|
+
" from htmlgraph import SDK\n"
|
|
424
|
+
" sdk = SDK(agent='builder')\n"
|
|
425
|
+
" sdk.spikes.create('Build Results') \\\\\n"
|
|
426
|
+
" .set_findings('Build status: ...') \\\\\n"
|
|
427
|
+
" .save()\n"
|
|
428
|
+
" ''',\n"
|
|
429
|
+
" subagent_type='general-purpose'\n"
|
|
430
|
+
")\n"
|
|
431
|
+
"# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='builder')[0].findings)\""
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Generic suggestion
|
|
435
|
+
return (
|
|
436
|
+
"# Use Task tool with HtmlGraph reporting:\n"
|
|
437
|
+
"Task(\n"
|
|
438
|
+
" prompt='''<describe task>\n\n"
|
|
439
|
+
" 🔴 CRITICAL - Report Results:\n"
|
|
440
|
+
" from htmlgraph import SDK\n"
|
|
441
|
+
" sdk = SDK(agent='subagent')\n"
|
|
442
|
+
" sdk.spikes.create('Task Results') \\\\\n"
|
|
443
|
+
" .set_findings('...') \\\\\n"
|
|
444
|
+
" .save()\n"
|
|
445
|
+
" ''',\n"
|
|
446
|
+
" subagent_type='general-purpose'\n"
|
|
447
|
+
")\n"
|
|
448
|
+
"# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='subagent')[0].findings)\""
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def enforce_orchestrator_mode(
|
|
453
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
454
|
+
) -> dict[str, Any]:
|
|
455
|
+
"""
|
|
456
|
+
Enforce orchestrator mode rules.
|
|
457
|
+
|
|
458
|
+
This is the main public API for hook scripts. It checks if orchestrator mode
|
|
459
|
+
is enabled, classifies the operation, and returns a hook response dict.
|
|
460
|
+
|
|
461
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
462
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
tool: Tool being called
|
|
466
|
+
params: Tool parameters
|
|
467
|
+
session_id: Session identifier for loading tool history
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Hook response dict with decision (allow/block) and guidance
|
|
471
|
+
Format: {"continue": bool, "hookSpecificOutput": {...}}
|
|
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
|
+
|
|
483
|
+
# Get manager and check if mode is enabled
|
|
484
|
+
try:
|
|
485
|
+
# Look for .htmlgraph directory starting from cwd
|
|
486
|
+
cwd = Path.cwd()
|
|
487
|
+
graph_dir = cwd / ".htmlgraph"
|
|
488
|
+
|
|
489
|
+
# If not found in cwd, try parent directories (up to 3 levels)
|
|
490
|
+
if not graph_dir.exists():
|
|
491
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
492
|
+
candidate = parent / ".htmlgraph"
|
|
493
|
+
if candidate.exists():
|
|
494
|
+
graph_dir = candidate
|
|
495
|
+
break
|
|
496
|
+
|
|
497
|
+
manager = OrchestratorModeManager(graph_dir)
|
|
498
|
+
|
|
499
|
+
if not manager.is_enabled():
|
|
500
|
+
# Mode not active, allow everything with no additional output
|
|
501
|
+
return {"continue": True}
|
|
502
|
+
|
|
503
|
+
enforcement_level = manager.get_enforcement_level()
|
|
504
|
+
except Exception:
|
|
505
|
+
# If we can't check mode, fail open (allow)
|
|
506
|
+
return {
|
|
507
|
+
"continue": True,
|
|
508
|
+
"hookSpecificOutput": {
|
|
509
|
+
"hookEventName": "PreToolUse",
|
|
510
|
+
"permissionDecision": "allow",
|
|
511
|
+
},
|
|
512
|
+
}
|
|
513
|
+
|
|
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
|
+
)
|
|
550
|
+
|
|
551
|
+
# Note: Tool recording is now handled by track-event.py PostToolUse hook
|
|
552
|
+
# No need to call add_to_tool_history() here
|
|
553
|
+
|
|
554
|
+
# Operation is allowed
|
|
555
|
+
if is_allowed:
|
|
556
|
+
if (
|
|
557
|
+
reason
|
|
558
|
+
and enforcement_level == "strict"
|
|
559
|
+
and category not in ["orchestrator-core", "sdk-command"]
|
|
560
|
+
):
|
|
561
|
+
# Provide guidance even when allowing
|
|
562
|
+
return {
|
|
563
|
+
"continue": True,
|
|
564
|
+
"hookSpecificOutput": {
|
|
565
|
+
"hookEventName": "PreToolUse",
|
|
566
|
+
"permissionDecision": "allow",
|
|
567
|
+
"additionalContext": f"✅ {reason}",
|
|
568
|
+
},
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
"continue": True,
|
|
572
|
+
"hookSpecificOutput": {
|
|
573
|
+
"hookEventName": "PreToolUse",
|
|
574
|
+
"permissionDecision": "allow",
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
|
|
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
|
+
|
|
584
|
+
suggestion = create_task_suggestion(tool, params)
|
|
585
|
+
|
|
586
|
+
if enforcement_level == "strict":
|
|
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"
|
|
590
|
+
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
591
|
+
f"Suggested delegation:\n"
|
|
592
|
+
f"{suggestion}\n\n"
|
|
593
|
+
)
|
|
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
|
|
610
|
+
return {
|
|
611
|
+
"continue": True,
|
|
612
|
+
"hookSpecificOutput": {
|
|
613
|
+
"hookEventName": "PreToolUse",
|
|
614
|
+
"permissionDecision": "allow",
|
|
615
|
+
"additionalContext": warning_message,
|
|
616
|
+
},
|
|
617
|
+
}
|
|
618
|
+
else:
|
|
619
|
+
# GUIDANCE mode - softer warning
|
|
620
|
+
warning_message = (
|
|
621
|
+
f"⚠️ ORCHESTRATOR: {reason}\n\nSuggested delegation:\n{suggestion}"
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
"continue": True,
|
|
626
|
+
"hookSpecificOutput": {
|
|
627
|
+
"hookEventName": "PreToolUse",
|
|
628
|
+
"permissionDecision": "allow",
|
|
629
|
+
"additionalContext": warning_message,
|
|
630
|
+
},
|
|
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))
|