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/validator.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Work Validation Module for HtmlGraph Hooks
|
|
3
7
|
|
|
@@ -6,6 +10,9 @@ Provides intelligent guidance for HtmlGraph workflow based on:
|
|
|
6
10
|
2. Recent tool usage patterns (anti-pattern detection)
|
|
7
11
|
3. Learned patterns from transcript analytics
|
|
8
12
|
|
|
13
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
14
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
15
|
+
|
|
9
16
|
This module can be used by hook scripts or imported directly for validation logic.
|
|
10
17
|
|
|
11
18
|
Main API:
|
|
@@ -19,42 +26,57 @@ Example:
|
|
|
19
26
|
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
20
27
|
|
|
21
28
|
if result["decision"] == "block":
|
|
22
|
-
|
|
29
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
23
30
|
elif "guidance" in result:
|
|
24
|
-
|
|
31
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
25
32
|
"""
|
|
26
33
|
|
|
27
34
|
import json
|
|
28
35
|
import re
|
|
29
|
-
from datetime import datetime, timezone
|
|
30
36
|
from pathlib import Path
|
|
31
37
|
from typing import Any, cast
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
from htmlgraph.hooks.subagent_detection import is_subagent_context
|
|
40
|
+
from htmlgraph.orchestrator_config import load_orchestrator_config
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_anti_patterns(config: Any | None = None) -> dict[tuple[str, ...], str]:
|
|
44
|
+
"""
|
|
45
|
+
Build anti-pattern rules from configuration.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Optional OrchestratorConfig. If None, loads from file.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict mapping tool sequences to warning messages
|
|
52
|
+
"""
|
|
53
|
+
if config is None:
|
|
54
|
+
config = load_orchestrator_config()
|
|
55
|
+
|
|
56
|
+
patterns = config.anti_patterns
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
tuple(["Bash"] * patterns.consecutive_bash): (
|
|
60
|
+
f"{patterns.consecutive_bash} consecutive Bash commands. "
|
|
61
|
+
"Check for errors or consider a different approach."
|
|
62
|
+
),
|
|
63
|
+
tuple(["Edit"] * patterns.consecutive_edit): (
|
|
64
|
+
f"{patterns.consecutive_edit} consecutive Edits. "
|
|
65
|
+
"Consider batching changes or reading file first."
|
|
66
|
+
),
|
|
67
|
+
tuple(["Grep"] * patterns.consecutive_grep): (
|
|
68
|
+
f"{patterns.consecutive_grep} consecutive Greps. "
|
|
69
|
+
"Consider reading results before searching more."
|
|
70
|
+
),
|
|
71
|
+
tuple(["Read"] * patterns.consecutive_read): (
|
|
72
|
+
f"{patterns.consecutive_read} consecutive Reads. "
|
|
73
|
+
"Consider caching file content."
|
|
74
|
+
),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Legacy constant for backwards compatibility (now uses config)
|
|
79
|
+
ANTI_PATTERNS = get_anti_patterns()
|
|
58
80
|
|
|
59
81
|
# Tools that indicate exploration/implementation (require work item in strict mode)
|
|
60
82
|
EXPLORATION_TOOLS = {"Grep", "Glob", "Task"}
|
|
@@ -67,68 +89,89 @@ OPTIMAL_PATTERNS = {
|
|
|
67
89
|
("Edit", "Bash"): "Good: Edit then test - verify changes.",
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
#
|
|
71
|
-
TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
|
|
92
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
72
93
|
MAX_HISTORY = 20
|
|
73
94
|
|
|
74
95
|
|
|
75
|
-
def load_tool_history() -> list[dict]:
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
data = json.loads(TOOL_HISTORY_FILE.read_text())
|
|
80
|
-
|
|
81
|
-
# Handle both formats: {"history": [...]} and [...] (legacy)
|
|
82
|
-
if isinstance(data, dict):
|
|
83
|
-
data = data.get("history", [])
|
|
84
|
-
|
|
85
|
-
# Filter to last hour only
|
|
86
|
-
cutoff = datetime.now(timezone.utc).timestamp() - 3600
|
|
87
|
-
|
|
88
|
-
# Handle both "ts" (old) and "timestamp" (new) formats
|
|
89
|
-
filtered = []
|
|
90
|
-
for t in data:
|
|
91
|
-
ts = t.get("ts", 0)
|
|
92
|
-
if not ts and "timestamp" in t:
|
|
93
|
-
# Parse ISO format timestamp
|
|
94
|
-
try:
|
|
95
|
-
ts = datetime.fromisoformat(
|
|
96
|
-
t["timestamp"].replace("Z", "+00:00")
|
|
97
|
-
).timestamp()
|
|
98
|
-
except Exception:
|
|
99
|
-
ts = 0
|
|
100
|
-
if ts > cutoff:
|
|
101
|
-
filtered.append(t)
|
|
102
|
-
|
|
103
|
-
return filtered[-MAX_HISTORY:]
|
|
104
|
-
except Exception:
|
|
105
|
-
pass
|
|
106
|
-
return []
|
|
96
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
97
|
+
"""
|
|
98
|
+
Load recent tool history from database (session-isolated).
|
|
107
99
|
|
|
100
|
+
Args:
|
|
101
|
+
session_id: Session identifier to filter tool history
|
|
108
102
|
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
Returns:
|
|
104
|
+
List of recent tool calls with tool name and timestamp
|
|
105
|
+
"""
|
|
111
106
|
try:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
108
|
+
|
|
109
|
+
# Find database path
|
|
110
|
+
cwd = Path.cwd()
|
|
111
|
+
graph_dir = cwd / ".htmlgraph"
|
|
112
|
+
if not graph_dir.exists():
|
|
113
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
114
|
+
candidate = parent / ".htmlgraph"
|
|
115
|
+
if candidate.exists():
|
|
116
|
+
graph_dir = candidate
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
120
|
+
if not db_path.exists():
|
|
121
|
+
return []
|
|
122
|
+
|
|
123
|
+
db = HtmlGraphDB(str(db_path))
|
|
124
|
+
if db.connection is None:
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
cursor = db.connection.cursor()
|
|
128
|
+
cursor.execute(
|
|
129
|
+
"""
|
|
130
|
+
SELECT tool_name, timestamp
|
|
131
|
+
FROM agent_events
|
|
132
|
+
WHERE session_id = ?
|
|
133
|
+
ORDER BY timestamp DESC
|
|
134
|
+
LIMIT ?
|
|
135
|
+
""",
|
|
136
|
+
(session_id, MAX_HISTORY),
|
|
115
137
|
)
|
|
138
|
+
|
|
139
|
+
# Return in chronological order (oldest first) for pattern detection
|
|
140
|
+
rows = cursor.fetchall()
|
|
141
|
+
db.disconnect()
|
|
142
|
+
|
|
143
|
+
return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
|
|
116
144
|
except Exception:
|
|
117
|
-
|
|
145
|
+
# Graceful degradation - return empty history on error
|
|
146
|
+
return []
|
|
118
147
|
|
|
119
148
|
|
|
120
|
-
def record_tool(tool: str,
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
149
|
+
def record_tool(tool: str, session_id: str) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Record a tool use in database.
|
|
152
|
+
|
|
153
|
+
Note: This is now handled by track-event.py hook, so this function
|
|
154
|
+
is kept for backward compatibility but does nothing.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
tool: Tool name being called
|
|
158
|
+
session_id: Session identifier for isolation
|
|
159
|
+
"""
|
|
160
|
+
# Tool recording is now handled by track-event.py PostToolUse hook
|
|
161
|
+
# This function is kept for backward compatibility but does nothing
|
|
162
|
+
pass
|
|
125
163
|
|
|
126
164
|
|
|
127
165
|
def detect_anti_pattern(tool: str, history: list[dict]) -> str | None:
|
|
128
|
-
"""Check if adding this tool creates an anti-pattern."""
|
|
129
|
-
|
|
166
|
+
"""Check if adding this tool creates an anti-pattern (uses configurable thresholds)."""
|
|
167
|
+
# Load fresh anti-patterns from config
|
|
168
|
+
anti_patterns = get_anti_patterns()
|
|
169
|
+
|
|
170
|
+
# Get max pattern length to know how far to look back
|
|
171
|
+
max_pattern_len = max(len(p) for p in anti_patterns.keys()) if anti_patterns else 5
|
|
172
|
+
recent_tools = [h["tool"] for h in history[-max_pattern_len:]] + [tool]
|
|
130
173
|
|
|
131
|
-
for pattern, message in
|
|
174
|
+
for pattern, message in anti_patterns.items():
|
|
132
175
|
pattern_len = len(pattern)
|
|
133
176
|
if len(recent_tools) >= pattern_len:
|
|
134
177
|
# Check if recent tools end with this pattern
|
|
@@ -149,7 +192,7 @@ def detect_optimal_pattern(tool: str, history: list[dict]) -> str | None:
|
|
|
149
192
|
return OPTIMAL_PATTERNS.get(pair)
|
|
150
193
|
|
|
151
194
|
|
|
152
|
-
def get_pattern_guidance(tool: str, history: list[dict]) -> dict:
|
|
195
|
+
def get_pattern_guidance(tool: str, history: list[dict]) -> dict[str, Any]:
|
|
153
196
|
"""Get guidance based on tool patterns."""
|
|
154
197
|
# Check for anti-patterns first
|
|
155
198
|
anti_pattern = detect_anti_pattern(tool, history)
|
|
@@ -192,7 +235,7 @@ def get_session_health_hint(history: list[dict]) -> str | None:
|
|
|
192
235
|
return None
|
|
193
236
|
|
|
194
237
|
|
|
195
|
-
def load_validation_config() -> dict:
|
|
238
|
+
def load_validation_config() -> dict[str, Any]:
|
|
196
239
|
"""Load validation config with defaults."""
|
|
197
240
|
config_path = (
|
|
198
241
|
Path(__file__).parent.parent.parent.parent.parent
|
|
@@ -218,7 +261,9 @@ def load_validation_config() -> dict:
|
|
|
218
261
|
}
|
|
219
262
|
|
|
220
263
|
|
|
221
|
-
def is_always_allowed(
|
|
264
|
+
def is_always_allowed(
|
|
265
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
266
|
+
) -> bool:
|
|
222
267
|
"""Check if tool is always allowed (read-only operations)."""
|
|
223
268
|
# Always-allow tools
|
|
224
269
|
if tool in config.get("always_allow", {}).get("tools", []):
|
|
@@ -227,6 +272,15 @@ def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
|
|
|
227
272
|
# Read-only Bash patterns
|
|
228
273
|
if tool == "Bash":
|
|
229
274
|
command = params.get("command", "")
|
|
275
|
+
|
|
276
|
+
# Check git commands using shared classification
|
|
277
|
+
if command.strip().startswith("git"):
|
|
278
|
+
from htmlgraph.hooks.git_commands import should_allow_git_command
|
|
279
|
+
|
|
280
|
+
if should_allow_git_command(command):
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
# Check other bash patterns
|
|
230
284
|
for pattern in config.get("always_allow", {}).get("bash_patterns", []):
|
|
231
285
|
if re.match(pattern, command):
|
|
232
286
|
return True
|
|
@@ -234,7 +288,7 @@ def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
|
|
|
234
288
|
return False
|
|
235
289
|
|
|
236
290
|
|
|
237
|
-
def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
|
|
291
|
+
def is_direct_htmlgraph_write(tool: str, params: dict[str, Any]) -> tuple[bool, str]:
|
|
238
292
|
"""Check if attempting direct write to .htmlgraph/ (always denied)."""
|
|
239
293
|
if tool not in ["Write", "Edit", "Delete", "NotebookEdit"]:
|
|
240
294
|
return False, ""
|
|
@@ -246,7 +300,7 @@ def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
|
|
|
246
300
|
return False, ""
|
|
247
301
|
|
|
248
302
|
|
|
249
|
-
def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
|
|
303
|
+
def is_sdk_command(tool: str, params: dict[str, Any], config: dict[str, Any]) -> bool:
|
|
250
304
|
"""Check if Bash command is an SDK command."""
|
|
251
305
|
if tool != "Bash":
|
|
252
306
|
return False
|
|
@@ -259,7 +313,9 @@ def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
|
|
|
259
313
|
return False
|
|
260
314
|
|
|
261
315
|
|
|
262
|
-
def is_code_operation(
|
|
316
|
+
def is_code_operation(
|
|
317
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
318
|
+
) -> bool:
|
|
263
319
|
"""Check if operation modifies code."""
|
|
264
320
|
# Direct file operations
|
|
265
321
|
if tool in config.get("code_operations", {}).get("tools", []):
|
|
@@ -288,7 +344,9 @@ def get_active_work_item() -> dict | None:
|
|
|
288
344
|
return None
|
|
289
345
|
|
|
290
346
|
|
|
291
|
-
def check_orchestrator_violation(
|
|
347
|
+
def check_orchestrator_violation(
|
|
348
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
349
|
+
) -> dict | None:
|
|
292
350
|
"""
|
|
293
351
|
Check if operation violates orchestrator mode rules.
|
|
294
352
|
|
|
@@ -298,6 +356,7 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
|
|
|
298
356
|
Args:
|
|
299
357
|
tool: Tool name
|
|
300
358
|
params: Tool parameters
|
|
359
|
+
session_id: Session identifier for loading tool history
|
|
301
360
|
|
|
302
361
|
Returns:
|
|
303
362
|
Blocking response dict if violation detected in strict mode, None otherwise
|
|
@@ -335,7 +394,9 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
|
|
|
335
394
|
is_allowed_orchestrator_operation,
|
|
336
395
|
)
|
|
337
396
|
|
|
338
|
-
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
397
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
398
|
+
tool, params, session_id
|
|
399
|
+
)
|
|
339
400
|
|
|
340
401
|
# If orchestrator would block (but returns continue=True), we block here
|
|
341
402
|
if not is_allowed:
|
|
@@ -363,33 +424,49 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
|
|
|
363
424
|
|
|
364
425
|
|
|
365
426
|
def validate_tool_call(
|
|
366
|
-
tool: str,
|
|
367
|
-
|
|
427
|
+
tool: str,
|
|
428
|
+
params: dict[str, Any],
|
|
429
|
+
config: dict[str, Any],
|
|
430
|
+
history: list[dict],
|
|
431
|
+
session_id: str | None = None,
|
|
432
|
+
) -> dict[str, Any]:
|
|
368
433
|
"""
|
|
369
434
|
Validate tool call and return GUIDANCE with active learning.
|
|
370
435
|
|
|
436
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
437
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
438
|
+
|
|
371
439
|
Args:
|
|
372
440
|
tool: Tool name (e.g., "Edit", "Bash", "Read")
|
|
373
441
|
params: Tool parameters (e.g., {"file_path": "test.py"})
|
|
374
442
|
config: Validation configuration (from load_validation_config())
|
|
375
|
-
history: Tool usage history (from load_tool_history())
|
|
443
|
+
history: Tool usage history (from load_tool_history(session_id))
|
|
444
|
+
session_id: Optional session ID for loading history if not provided
|
|
376
445
|
|
|
377
446
|
Returns:
|
|
378
|
-
dict: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
447
|
+
dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
379
448
|
All operations are ALLOWED unless blocked for safety reasons.
|
|
380
449
|
|
|
381
450
|
Example:
|
|
451
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
452
|
+
history = load_tool_history(session_id)
|
|
382
453
|
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
383
454
|
if result["decision"] == "block":
|
|
384
|
-
|
|
455
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
385
456
|
elif "guidance" in result:
|
|
386
|
-
|
|
457
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
387
458
|
"""
|
|
459
|
+
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
460
|
+
if is_subagent_context():
|
|
461
|
+
return {"decision": "allow"}
|
|
462
|
+
|
|
388
463
|
result = {"decision": "allow"}
|
|
389
464
|
guidance_parts = []
|
|
390
465
|
|
|
391
466
|
# Step 0a: Check orchestrator mode violations (if enabled)
|
|
392
|
-
orchestrator_violation = check_orchestrator_violation(
|
|
467
|
+
orchestrator_violation = check_orchestrator_violation(
|
|
468
|
+
tool, params, session_id or "unknown"
|
|
469
|
+
)
|
|
393
470
|
if orchestrator_violation:
|
|
394
471
|
# BLOCK orchestrator violations in strict mode
|
|
395
472
|
return orchestrator_violation
|
|
@@ -505,3 +582,47 @@ def validate_tool_call(
|
|
|
505
582
|
result["guidance"] = " | ".join(guidance_parts)
|
|
506
583
|
|
|
507
584
|
return result
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def main() -> None:
|
|
588
|
+
"""Hook entry point for script wrapper."""
|
|
589
|
+
import sys
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
# Read tool input from stdin
|
|
593
|
+
tool_input = json.load(sys.stdin)
|
|
594
|
+
|
|
595
|
+
# Claude Code uses "name" and "input", fallback to "tool" and "params"
|
|
596
|
+
tool = tool_input.get("name", "") or tool_input.get("tool", "")
|
|
597
|
+
params = tool_input.get("input", {}) or tool_input.get("params", {})
|
|
598
|
+
|
|
599
|
+
# Get session_id from hook_input (NEW: required for session-isolated history)
|
|
600
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
601
|
+
|
|
602
|
+
# Load config
|
|
603
|
+
config = load_validation_config()
|
|
604
|
+
|
|
605
|
+
# Load session-isolated tool history (NEW: from database, not file)
|
|
606
|
+
history = load_tool_history(session_id)
|
|
607
|
+
|
|
608
|
+
# Get guidance with pattern awareness
|
|
609
|
+
result = validate_tool_call(tool, params, config, history)
|
|
610
|
+
|
|
611
|
+
# Note: Tool recording is now handled by track-event.py PostToolUse hook
|
|
612
|
+
# No need to call record_tool() or save_tool_history() here
|
|
613
|
+
|
|
614
|
+
# Output JSON with guidance/block message
|
|
615
|
+
print(json.dumps(result))
|
|
616
|
+
|
|
617
|
+
# Exit 1 to BLOCK if decision is "block", otherwise allow
|
|
618
|
+
if result.get("decision") == "block":
|
|
619
|
+
sys.exit(1)
|
|
620
|
+
else:
|
|
621
|
+
sys.exit(0)
|
|
622
|
+
|
|
623
|
+
except Exception as e:
|
|
624
|
+
# Graceful degradation - allow on error
|
|
625
|
+
print(
|
|
626
|
+
json.dumps({"decision": "allow", "guidance": f"Validation hook error: {e}"})
|
|
627
|
+
)
|
|
628
|
+
sys.exit(0)
|
htmlgraph/ids.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Hash-based ID generation for HtmlGraph.
|
|
3
5
|
|
|
@@ -15,7 +17,6 @@ collision probability is effectively zero even with thousands
|
|
|
15
17
|
of concurrent agents creating tasks simultaneously.
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
from __future__ import annotations
|
|
19
20
|
|
|
20
21
|
import hashlib
|
|
21
22
|
import os
|