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,628 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Work Validation Module for HtmlGraph Hooks
|
|
7
|
+
|
|
8
|
+
Provides intelligent guidance for HtmlGraph workflow based on:
|
|
9
|
+
1. Current workflow state (work items, spikes)
|
|
10
|
+
2. Recent tool usage patterns (anti-pattern detection)
|
|
11
|
+
3. Learned patterns from transcript analytics
|
|
12
|
+
|
|
13
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
14
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
15
|
+
|
|
16
|
+
This module can be used by hook scripts or imported directly for validation logic.
|
|
17
|
+
|
|
18
|
+
Main API:
|
|
19
|
+
validate_tool_call(tool_name, tool_params, config, history) -> dict
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
from htmlgraph.hooks.validator import validate_tool_call, load_validation_config, load_tool_history
|
|
23
|
+
|
|
24
|
+
config = load_validation_config()
|
|
25
|
+
history = load_tool_history()
|
|
26
|
+
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
27
|
+
|
|
28
|
+
if result["decision"] == "block":
|
|
29
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
30
|
+
elif "guidance" in result:
|
|
31
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import json
|
|
35
|
+
import re
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any, cast
|
|
38
|
+
|
|
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()
|
|
80
|
+
|
|
81
|
+
# Tools that indicate exploration/implementation (require work item in strict mode)
|
|
82
|
+
EXPLORATION_TOOLS = {"Grep", "Glob", "Task"}
|
|
83
|
+
IMPLEMENTATION_TOOLS = {"Edit", "Write", "NotebookEdit"}
|
|
84
|
+
|
|
85
|
+
# Optimal patterns to encourage
|
|
86
|
+
OPTIMAL_PATTERNS = {
|
|
87
|
+
("Grep", "Read"): "Good: Search then read - efficient exploration.",
|
|
88
|
+
("Read", "Edit"): "Good: Read then edit - informed changes.",
|
|
89
|
+
("Edit", "Bash"): "Good: Edit then test - verify changes.",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Maximum number of recent tool calls to consider for pattern detection
|
|
93
|
+
MAX_HISTORY = 20
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_tool_history(session_id: str) -> list[dict]:
|
|
97
|
+
"""
|
|
98
|
+
Load recent tool history from database (session-isolated).
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
session_id: Session identifier to filter tool history
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of recent tool calls with tool name and timestamp
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
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),
|
|
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)]
|
|
144
|
+
except Exception:
|
|
145
|
+
# Graceful degradation - return empty history on error
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
|
|
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
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_anti_pattern(tool: str, history: list[dict]) -> str | None:
|
|
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]
|
|
173
|
+
|
|
174
|
+
for pattern, message in anti_patterns.items():
|
|
175
|
+
pattern_len = len(pattern)
|
|
176
|
+
if len(recent_tools) >= pattern_len:
|
|
177
|
+
# Check if recent tools end with this pattern
|
|
178
|
+
if tuple(recent_tools[-pattern_len:]) == pattern:
|
|
179
|
+
return message
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def detect_optimal_pattern(tool: str, history: list[dict]) -> str | None:
|
|
185
|
+
"""Check if this tool continues an optimal pattern."""
|
|
186
|
+
if not history:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
last_tool = history[-1]["tool"]
|
|
190
|
+
pair = (last_tool, tool)
|
|
191
|
+
|
|
192
|
+
return OPTIMAL_PATTERNS.get(pair)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_pattern_guidance(tool: str, history: list[dict]) -> dict[str, Any]:
|
|
196
|
+
"""Get guidance based on tool patterns."""
|
|
197
|
+
# Check for anti-patterns first
|
|
198
|
+
anti_pattern = detect_anti_pattern(tool, history)
|
|
199
|
+
if anti_pattern:
|
|
200
|
+
return {"pattern_warning": f"⚠️ {anti_pattern}", "pattern_type": "anti-pattern"}
|
|
201
|
+
|
|
202
|
+
# Check for optimal patterns
|
|
203
|
+
optimal = detect_optimal_pattern(tool, history)
|
|
204
|
+
if optimal:
|
|
205
|
+
return {"pattern_note": optimal, "pattern_type": "optimal"}
|
|
206
|
+
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_session_health_hint(history: list[dict]) -> str | None:
|
|
211
|
+
"""Get a health hint based on session patterns."""
|
|
212
|
+
if len(history) < 10:
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
tools = [h["tool"] for h in history]
|
|
216
|
+
|
|
217
|
+
# Check for excessive retries
|
|
218
|
+
consecutive = 1
|
|
219
|
+
max_consecutive = 1
|
|
220
|
+
for i in range(1, len(tools)):
|
|
221
|
+
if tools[i] == tools[i - 1]:
|
|
222
|
+
consecutive += 1
|
|
223
|
+
max_consecutive = max(max_consecutive, consecutive)
|
|
224
|
+
else:
|
|
225
|
+
consecutive = 1
|
|
226
|
+
|
|
227
|
+
if max_consecutive >= 5:
|
|
228
|
+
return f"📊 High retry pattern detected ({max_consecutive} consecutive same-tool calls). Consider varying approach."
|
|
229
|
+
|
|
230
|
+
# Check tool diversity
|
|
231
|
+
unique_tools = len(set(tools))
|
|
232
|
+
if unique_tools <= 2 and len(tools) >= 10:
|
|
233
|
+
return f"📊 Low tool diversity. Only using {unique_tools} different tools. Consider using more specialized tools."
|
|
234
|
+
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def load_validation_config() -> dict[str, Any]:
|
|
239
|
+
"""Load validation config with defaults."""
|
|
240
|
+
config_path = (
|
|
241
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
242
|
+
/ ".claude"
|
|
243
|
+
/ "config"
|
|
244
|
+
/ "validation-config.json"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if config_path.exists():
|
|
248
|
+
try:
|
|
249
|
+
with open(config_path) as f:
|
|
250
|
+
return cast(dict[Any, Any], json.load(f))
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# Minimal fallback config
|
|
255
|
+
return {
|
|
256
|
+
"always_allow": {
|
|
257
|
+
"tools": ["Read", "Glob", "Grep", "LSP"],
|
|
258
|
+
"bash_patterns": ["^git status", "^git diff", "^ls", "^cat"],
|
|
259
|
+
},
|
|
260
|
+
"sdk_commands": {"patterns": ["^uv run htmlgraph ", "^htmlgraph "]},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def is_always_allowed(
|
|
265
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
266
|
+
) -> bool:
|
|
267
|
+
"""Check if tool is always allowed (read-only operations)."""
|
|
268
|
+
# Always-allow tools
|
|
269
|
+
if tool in config.get("always_allow", {}).get("tools", []):
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
# Read-only Bash patterns
|
|
273
|
+
if tool == "Bash":
|
|
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
|
|
284
|
+
for pattern in config.get("always_allow", {}).get("bash_patterns", []):
|
|
285
|
+
if re.match(pattern, command):
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def is_direct_htmlgraph_write(tool: str, params: dict[str, Any]) -> tuple[bool, str]:
|
|
292
|
+
"""Check if attempting direct write to .htmlgraph/ (always denied)."""
|
|
293
|
+
if tool not in ["Write", "Edit", "Delete", "NotebookEdit"]:
|
|
294
|
+
return False, ""
|
|
295
|
+
|
|
296
|
+
file_path = params.get("file_path", "")
|
|
297
|
+
if ".htmlgraph/" in file_path or file_path.startswith(".htmlgraph/"):
|
|
298
|
+
return True, file_path
|
|
299
|
+
|
|
300
|
+
return False, ""
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def is_sdk_command(tool: str, params: dict[str, Any], config: dict[str, Any]) -> bool:
|
|
304
|
+
"""Check if Bash command is an SDK command."""
|
|
305
|
+
if tool != "Bash":
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
command = params.get("command", "")
|
|
309
|
+
for pattern in config.get("sdk_commands", {}).get("patterns", []):
|
|
310
|
+
if re.match(pattern, command):
|
|
311
|
+
return True
|
|
312
|
+
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def is_code_operation(
|
|
317
|
+
tool: str, params: dict[str, Any], config: dict[str, Any]
|
|
318
|
+
) -> bool:
|
|
319
|
+
"""Check if operation modifies code."""
|
|
320
|
+
# Direct file operations
|
|
321
|
+
if tool in config.get("code_operations", {}).get("tools", []):
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
# Code-modifying Bash commands
|
|
325
|
+
if tool == "Bash":
|
|
326
|
+
command = params.get("command", "")
|
|
327
|
+
for pattern in config.get("code_operations", {}).get("bash_patterns", []):
|
|
328
|
+
if re.match(pattern, command):
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
return False
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_active_work_item() -> dict | None:
|
|
335
|
+
"""Get active work item using SDK."""
|
|
336
|
+
try:
|
|
337
|
+
from htmlgraph import SDK
|
|
338
|
+
|
|
339
|
+
sdk = SDK()
|
|
340
|
+
active = sdk.get_active_work_item()
|
|
341
|
+
return cast(dict | None, active)
|
|
342
|
+
except Exception:
|
|
343
|
+
# If SDK fails, assume no active work item
|
|
344
|
+
return None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def check_orchestrator_violation(
|
|
348
|
+
tool: str, params: dict[str, Any], session_id: str = "unknown"
|
|
349
|
+
) -> dict | None:
|
|
350
|
+
"""
|
|
351
|
+
Check if operation violates orchestrator mode rules.
|
|
352
|
+
|
|
353
|
+
This function detects when orchestrator.py would warn about a violation
|
|
354
|
+
and converts it to a blocking decision when in strict mode.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
tool: Tool name
|
|
358
|
+
params: Tool parameters
|
|
359
|
+
session_id: Session identifier for loading tool history
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Blocking response dict if violation detected in strict mode, None otherwise
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
from pathlib import Path
|
|
366
|
+
|
|
367
|
+
from htmlgraph.orchestrator_mode import OrchestratorModeManager
|
|
368
|
+
|
|
369
|
+
# Find .htmlgraph directory
|
|
370
|
+
cwd = Path.cwd()
|
|
371
|
+
graph_dir = cwd / ".htmlgraph"
|
|
372
|
+
|
|
373
|
+
if not graph_dir.exists():
|
|
374
|
+
for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
|
|
375
|
+
candidate = parent / ".htmlgraph"
|
|
376
|
+
if candidate.exists():
|
|
377
|
+
graph_dir = candidate
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
if not graph_dir.exists():
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
manager = OrchestratorModeManager(graph_dir)
|
|
384
|
+
|
|
385
|
+
if not manager.is_enabled():
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
if manager.get_enforcement_level() != "strict":
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
# Import orchestrator logic
|
|
392
|
+
from htmlgraph.hooks.orchestrator import (
|
|
393
|
+
create_task_suggestion,
|
|
394
|
+
is_allowed_orchestrator_operation,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
is_allowed, reason, category = is_allowed_orchestrator_operation(
|
|
398
|
+
tool, params, session_id
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# If orchestrator would block (but returns continue=True), we block here
|
|
402
|
+
if not is_allowed:
|
|
403
|
+
suggestion = create_task_suggestion(tool, params)
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
"decision": "block",
|
|
407
|
+
"reason": (
|
|
408
|
+
f"🚫 ORCHESTRATOR MODE VIOLATION: {reason}\n\n"
|
|
409
|
+
f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
|
|
410
|
+
f"Suggested delegation:\n"
|
|
411
|
+
f"{suggestion}\n\n"
|
|
412
|
+
f"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
|
|
413
|
+
f"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
|
|
414
|
+
),
|
|
415
|
+
"suggestion": "Use Task tool to delegate this work to a subagent",
|
|
416
|
+
"required_action": "DELEGATE_TO_SUBAGENT",
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
except Exception:
|
|
422
|
+
# Graceful degradation - allow on error
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def validate_tool_call(
|
|
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]:
|
|
433
|
+
"""
|
|
434
|
+
Validate tool call and return GUIDANCE with active learning.
|
|
435
|
+
|
|
436
|
+
Subagents spawned via Task() have unrestricted tool access.
|
|
437
|
+
Detection uses 5-level strategy: env vars, session state, database.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
tool: Tool name (e.g., "Edit", "Bash", "Read")
|
|
441
|
+
params: Tool parameters (e.g., {"file_path": "test.py"})
|
|
442
|
+
config: Validation configuration (from load_validation_config())
|
|
443
|
+
history: Tool usage history (from load_tool_history(session_id))
|
|
444
|
+
session_id: Optional session ID for loading history if not provided
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
|
|
448
|
+
All operations are ALLOWED unless blocked for safety reasons.
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
session_id = tool_input.get("session_id", "unknown")
|
|
452
|
+
history = load_tool_history(session_id)
|
|
453
|
+
result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
|
|
454
|
+
if result["decision"] == "block":
|
|
455
|
+
logger.debug("Validation reason: %s", result["reason"])
|
|
456
|
+
elif "guidance" in result:
|
|
457
|
+
logger.debug("Validation guidance: %s", result["guidance"])
|
|
458
|
+
"""
|
|
459
|
+
# Check if this is a subagent context - subagents have unrestricted tool access
|
|
460
|
+
if is_subagent_context():
|
|
461
|
+
return {"decision": "allow"}
|
|
462
|
+
|
|
463
|
+
result = {"decision": "allow"}
|
|
464
|
+
guidance_parts = []
|
|
465
|
+
|
|
466
|
+
# Step 0a: Check orchestrator mode violations (if enabled)
|
|
467
|
+
orchestrator_violation = check_orchestrator_violation(
|
|
468
|
+
tool, params, session_id or "unknown"
|
|
469
|
+
)
|
|
470
|
+
if orchestrator_violation:
|
|
471
|
+
# BLOCK orchestrator violations in strict mode
|
|
472
|
+
return orchestrator_violation
|
|
473
|
+
|
|
474
|
+
# Step 0b: Check for pattern-based guidance (Active Learning)
|
|
475
|
+
pattern_info = get_pattern_guidance(tool, history)
|
|
476
|
+
if pattern_info.get("pattern_warning"):
|
|
477
|
+
guidance_parts.append(pattern_info["pattern_warning"])
|
|
478
|
+
|
|
479
|
+
# Check session health
|
|
480
|
+
health_hint = get_session_health_hint(history)
|
|
481
|
+
if health_hint:
|
|
482
|
+
guidance_parts.append(health_hint)
|
|
483
|
+
|
|
484
|
+
# Step 1: Read-only tools - minimal guidance
|
|
485
|
+
if is_always_allowed(tool, params, config):
|
|
486
|
+
if guidance_parts:
|
|
487
|
+
result["guidance"] = " | ".join(guidance_parts)
|
|
488
|
+
return result
|
|
489
|
+
|
|
490
|
+
# Step 2: Direct writes to .htmlgraph/ - BLOCK (not guidance)
|
|
491
|
+
# This is the ONLY blocking rule - all other rules are guidance only
|
|
492
|
+
is_htmlgraph_write, file_path = is_direct_htmlgraph_write(tool, params)
|
|
493
|
+
if is_htmlgraph_write:
|
|
494
|
+
# Return blocking response - this will be handled specially
|
|
495
|
+
return {
|
|
496
|
+
"decision": "block",
|
|
497
|
+
"reason": f"BLOCKED: Direct edits to .htmlgraph/ files are not allowed. File: {file_path}",
|
|
498
|
+
"suggestion": "Use SDK instead: `from htmlgraph import SDK; sdk = SDK(); sdk.features.complete('id')`",
|
|
499
|
+
"documentation": "See AGENTS.md line 3: 'AI agents must NEVER edit .htmlgraph/ HTML files directly'",
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
# Step 3: Classify operation
|
|
503
|
+
is_sdk_cmd = is_sdk_command(tool, params, config)
|
|
504
|
+
is_code_op = is_code_operation(tool, params, config)
|
|
505
|
+
|
|
506
|
+
# Step 4: Get active work item
|
|
507
|
+
active = get_active_work_item()
|
|
508
|
+
|
|
509
|
+
# Step 5: No active work item
|
|
510
|
+
if active is None:
|
|
511
|
+
# Check for strict enforcement mode
|
|
512
|
+
strict_mode = config.get("enforcement", {}).get(
|
|
513
|
+
"strict_work_item_required", False
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if is_sdk_cmd:
|
|
517
|
+
guidance_parts.append("Creating work item via SDK")
|
|
518
|
+
elif strict_mode and (tool in IMPLEMENTATION_TOOLS or is_code_op):
|
|
519
|
+
# STRICT MODE: BLOCK implementation without work item
|
|
520
|
+
return {
|
|
521
|
+
"decision": "block",
|
|
522
|
+
"reason": (
|
|
523
|
+
"🛑 BLOCKED: No active work item.\n\n"
|
|
524
|
+
"You MUST create and start a work item BEFORE making code changes.\n\n"
|
|
525
|
+
"Run this FIRST:\n"
|
|
526
|
+
" sdk = SDK(agent='claude')\n"
|
|
527
|
+
" feature = sdk.features.create('Your feature title').save()\n"
|
|
528
|
+
" sdk.features.start(feature.id)\n\n"
|
|
529
|
+
"Then retry your edit."
|
|
530
|
+
),
|
|
531
|
+
"suggestion": "sdk.features.create('Title').save() then sdk.features.start(id)",
|
|
532
|
+
"required_action": "CREATE_WORK_ITEM",
|
|
533
|
+
}
|
|
534
|
+
elif strict_mode and tool in EXPLORATION_TOOLS:
|
|
535
|
+
# STRICT MODE: Strong guidance for exploration (allow but warn loudly)
|
|
536
|
+
result["required_action"] = "CREATE_WORK_ITEM"
|
|
537
|
+
result["imperative"] = (
|
|
538
|
+
"⚠️ WARNING: No active work item for exploration.\n"
|
|
539
|
+
"Consider creating a spike first:\n"
|
|
540
|
+
" sdk = SDK(agent='claude')\n"
|
|
541
|
+
" spike = sdk.spikes.create('Investigation title').save()\n"
|
|
542
|
+
" sdk.spikes.start(spike.id)"
|
|
543
|
+
)
|
|
544
|
+
guidance_parts.append("⚠️ No work item - consider creating a spike first")
|
|
545
|
+
elif tool in EXPLORATION_TOOLS or tool in IMPLEMENTATION_TOOLS or is_code_op:
|
|
546
|
+
guidance_parts.append(
|
|
547
|
+
"⚠️ No active work item. Create one to track this work."
|
|
548
|
+
)
|
|
549
|
+
result["suggestion"] = (
|
|
550
|
+
"sdk.features.create('Title').save() then sdk.features.start(id)"
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if guidance_parts:
|
|
554
|
+
result["guidance"] = " | ".join(guidance_parts)
|
|
555
|
+
return result
|
|
556
|
+
|
|
557
|
+
# Step 6: Active work is a spike (planning phase)
|
|
558
|
+
if active.get("type") == "spike":
|
|
559
|
+
spike_id = active.get("id")
|
|
560
|
+
|
|
561
|
+
if is_sdk_cmd:
|
|
562
|
+
guidance_parts.append(f"Planning with spike {spike_id}")
|
|
563
|
+
elif tool in ["Write", "Edit", "Delete", "NotebookEdit"] or is_code_op:
|
|
564
|
+
guidance_parts.append(
|
|
565
|
+
f"Active spike ({spike_id}) is for planning. Consider creating a feature for implementation."
|
|
566
|
+
)
|
|
567
|
+
result["suggestion"] = "uv run htmlgraph feature create 'Feature title'"
|
|
568
|
+
|
|
569
|
+
if guidance_parts:
|
|
570
|
+
result["guidance"] = " | ".join(guidance_parts)
|
|
571
|
+
return result
|
|
572
|
+
|
|
573
|
+
# Step 7: Active work is feature/bug/chore - all good
|
|
574
|
+
work_item_id = active.get("id")
|
|
575
|
+
guidance_parts.append(f"Working on {work_item_id}")
|
|
576
|
+
|
|
577
|
+
# Add positive reinforcement for optimal patterns
|
|
578
|
+
if pattern_info.get("pattern_note"):
|
|
579
|
+
guidance_parts.append(pattern_info["pattern_note"])
|
|
580
|
+
|
|
581
|
+
if guidance_parts:
|
|
582
|
+
result["guidance"] = " | ".join(guidance_parts)
|
|
583
|
+
|
|
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)
|