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,525 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HtmlGraph Drift Handler Module
|
|
3
|
+
|
|
4
|
+
Centralizes drift detection and auto-classification logic for hook operations.
|
|
5
|
+
|
|
6
|
+
This module provides a unified interface for:
|
|
7
|
+
- Loading drift configuration from project or plugin defaults
|
|
8
|
+
- Detecting drift in activity results based on configurable thresholds
|
|
9
|
+
- Handling high-drift conditions with cooldown awareness
|
|
10
|
+
- Triggering auto-classification when thresholds are met
|
|
11
|
+
- Building classification prompts from queued activities
|
|
12
|
+
|
|
13
|
+
Drift detection identifies when tool usage diverges from the active feature's
|
|
14
|
+
scope, allowing automatic classification into appropriate work items (bug, feature,
|
|
15
|
+
spike, chore, hotfix).
|
|
16
|
+
|
|
17
|
+
File Locations:
|
|
18
|
+
- Config: .htmlgraph/drift-config.json (or plugin default)
|
|
19
|
+
- Queue: .htmlgraph/drift-queue.json (activities for classification)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import subprocess
|
|
26
|
+
from datetime import datetime, timedelta
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from htmlgraph.hooks.context import HookContext
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Default drift configuration thresholds and settings
|
|
35
|
+
DEFAULT_DRIFT_CONFIG = {
|
|
36
|
+
"drift_detection": {
|
|
37
|
+
"enabled": True,
|
|
38
|
+
"warning_threshold": 0.7,
|
|
39
|
+
"auto_classify_threshold": 0.85,
|
|
40
|
+
"min_activities_before_classify": 3,
|
|
41
|
+
"cooldown_minutes": 10,
|
|
42
|
+
},
|
|
43
|
+
"classification": {
|
|
44
|
+
"enabled": False,
|
|
45
|
+
"use_haiku_agent": True,
|
|
46
|
+
"use_headless": False,
|
|
47
|
+
"work_item_types": {
|
|
48
|
+
"bug": {
|
|
49
|
+
"keywords": [
|
|
50
|
+
"fix",
|
|
51
|
+
"error",
|
|
52
|
+
"broken",
|
|
53
|
+
"crash",
|
|
54
|
+
"fail",
|
|
55
|
+
"issue",
|
|
56
|
+
"wrong",
|
|
57
|
+
"incorrect",
|
|
58
|
+
],
|
|
59
|
+
"description": "Fix incorrect behavior - must include repro steps",
|
|
60
|
+
},
|
|
61
|
+
"feature": {
|
|
62
|
+
"keywords": [
|
|
63
|
+
"add",
|
|
64
|
+
"implement",
|
|
65
|
+
"create",
|
|
66
|
+
"new",
|
|
67
|
+
"build",
|
|
68
|
+
"develop",
|
|
69
|
+
],
|
|
70
|
+
"description": "Deliver user value - normal flow item",
|
|
71
|
+
},
|
|
72
|
+
"spike": {
|
|
73
|
+
"keywords": [
|
|
74
|
+
"research",
|
|
75
|
+
"explore",
|
|
76
|
+
"investigate",
|
|
77
|
+
"understand",
|
|
78
|
+
"analyze",
|
|
79
|
+
"learn",
|
|
80
|
+
],
|
|
81
|
+
"description": "Reduce uncertainty - time-boxed, ends in decision",
|
|
82
|
+
},
|
|
83
|
+
"chore": {
|
|
84
|
+
"keywords": [
|
|
85
|
+
"refactor",
|
|
86
|
+
"cleanup",
|
|
87
|
+
"update",
|
|
88
|
+
"upgrade",
|
|
89
|
+
"maintain",
|
|
90
|
+
"organize",
|
|
91
|
+
],
|
|
92
|
+
"description": "Maintenance / tech debt - first-class work",
|
|
93
|
+
},
|
|
94
|
+
"hotfix": {
|
|
95
|
+
"keywords": [
|
|
96
|
+
"urgent",
|
|
97
|
+
"critical",
|
|
98
|
+
"production",
|
|
99
|
+
"emergency",
|
|
100
|
+
"asap",
|
|
101
|
+
],
|
|
102
|
+
"description": "Emergency production fix - expedite lane only",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
"queue": {
|
|
107
|
+
"max_pending_classifications": 5,
|
|
108
|
+
"max_age_hours": 48,
|
|
109
|
+
"process_on_stop": True,
|
|
110
|
+
"process_on_threshold": True,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_drift_config(graph_dir: Path) -> dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Load drift configuration from project or fallback to defaults.
|
|
118
|
+
|
|
119
|
+
Searches for drift configuration in multiple locations with priority:
|
|
120
|
+
1. .htmlgraph/drift-config.json (project-specific)
|
|
121
|
+
2. Plugin config/drift-config.json (via CLAUDE_PLUGIN_ROOT)
|
|
122
|
+
3. Default configuration (hardcoded fallback)
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
graph_dir: Path to .htmlgraph directory
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Drift configuration dict with keys: drift_detection, classification, queue
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
OSError: If graph_dir cannot be accessed
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
```python
|
|
135
|
+
config = load_drift_config(Path(".htmlgraph"))
|
|
136
|
+
logger.info(f"Auto-classify threshold: {config['drift_detection']['auto_classify_threshold']}")
|
|
137
|
+
```
|
|
138
|
+
"""
|
|
139
|
+
graph_dir = Path(graph_dir)
|
|
140
|
+
|
|
141
|
+
# Configuration search paths in priority order
|
|
142
|
+
config_paths = [
|
|
143
|
+
graph_dir / "drift-config.json", # Project-specific (highest priority)
|
|
144
|
+
Path(os.environ.get("CLAUDE_PLUGIN_ROOT", ""))
|
|
145
|
+
/ "config"
|
|
146
|
+
/ "drift-config.json", # Plugin config
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for config_path in config_paths:
|
|
150
|
+
if config_path.exists() and config_path.is_file():
|
|
151
|
+
try:
|
|
152
|
+
with open(config_path) as f:
|
|
153
|
+
config: dict[str, Any] = json.load(f)
|
|
154
|
+
logger.debug(f"Loaded drift config from {config_path}")
|
|
155
|
+
return config
|
|
156
|
+
except json.JSONDecodeError as e:
|
|
157
|
+
logger.warning(f"Invalid JSON in {config_path}: {e}, using defaults")
|
|
158
|
+
except OSError as e:
|
|
159
|
+
logger.warning(f"Error reading {config_path}: {e}, using defaults")
|
|
160
|
+
|
|
161
|
+
logger.debug("No drift config found, using defaults")
|
|
162
|
+
return DEFAULT_DRIFT_CONFIG
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_drift(
|
|
166
|
+
activity_result: dict[str, Any], config: dict[str, Any]
|
|
167
|
+
) -> tuple[float, str | None]:
|
|
168
|
+
"""
|
|
169
|
+
Calculate drift score from activity result and check thresholds.
|
|
170
|
+
|
|
171
|
+
Drift scoring logic analyzes the activity result to determine if tool usage
|
|
172
|
+
aligns with the current feature context:
|
|
173
|
+
- Multiple "continue": true in sequence = high drift (agent exploring options)
|
|
174
|
+
- Tool errors/timeouts = high drift (unexpected behavior)
|
|
175
|
+
- Normal success = low drift (expected behavior)
|
|
176
|
+
- Errors = high drift (something went wrong)
|
|
177
|
+
|
|
178
|
+
Scoring is from 0.0 (perfect alignment) to 1.0 (high drift).
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
activity_result: Activity result dict from SessionManager.track_activity()
|
|
182
|
+
Should have attributes: drift_score (optional), feature_id
|
|
183
|
+
config: Drift configuration dict
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (drift_score: float, feature_id: str | None)
|
|
187
|
+
- drift_score: 0.0 to 1.0 (higher = more drift)
|
|
188
|
+
- feature_id: Feature ID if high drift detected, else None
|
|
189
|
+
|
|
190
|
+
Note:
|
|
191
|
+
This function extracts pre-calculated drift_score from the activity
|
|
192
|
+
result (calculated by SessionManager). If no drift_score exists,
|
|
193
|
+
returns 0.0 (no drift).
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
```python
|
|
197
|
+
score, feature_id = detect_drift(activity_result, config)
|
|
198
|
+
if score > config['drift_detection']['auto_classify_threshold']:
|
|
199
|
+
logger.info(f"HIGH DRIFT: {score:.2f}")
|
|
200
|
+
```
|
|
201
|
+
"""
|
|
202
|
+
drift_score = getattr(activity_result, "drift_score", 0.0) or 0.0
|
|
203
|
+
feature_id = getattr(activity_result, "feature_id", None)
|
|
204
|
+
|
|
205
|
+
logger.debug(f"Drift detected: score={drift_score:.2f}, feature={feature_id}")
|
|
206
|
+
|
|
207
|
+
return (drift_score, feature_id)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def handle_high_drift(
|
|
211
|
+
context: HookContext,
|
|
212
|
+
drift_score: float,
|
|
213
|
+
queue: dict[str, Any],
|
|
214
|
+
config: dict[str, Any],
|
|
215
|
+
) -> str | None:
|
|
216
|
+
"""
|
|
217
|
+
Generate nudge message for high-drift activities.
|
|
218
|
+
|
|
219
|
+
When drift exceeds the auto-classify threshold:
|
|
220
|
+
1. Adds activity to classification queue
|
|
221
|
+
2. Checks cooldown to avoid spamming nudges
|
|
222
|
+
3. Returns user-facing nudge message with guidance
|
|
223
|
+
|
|
224
|
+
The cooldown prevents excessive notifications when drift is detected
|
|
225
|
+
repeatedly in short timeframes.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
context: Hook execution context with graph_dir access
|
|
229
|
+
drift_score: Calculated drift score (0.0 to 1.0)
|
|
230
|
+
queue: Current drift queue dict from DriftQueueManager
|
|
231
|
+
config: Drift configuration dict
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Nudge message string for user, or None if high drift but on cooldown
|
|
235
|
+
|
|
236
|
+
Note:
|
|
237
|
+
This function generates nudges but does NOT trigger classification.
|
|
238
|
+
Use trigger_auto_classification() separately to check if classification
|
|
239
|
+
should be spawned.
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
```python
|
|
243
|
+
nudge = handle_high_drift(context, 0.87, queue, config)
|
|
244
|
+
if nudge:
|
|
245
|
+
logger.info("%s", nudge) # "HIGH DRIFT (0.87): Activity queued for classification..."
|
|
246
|
+
```
|
|
247
|
+
"""
|
|
248
|
+
drift_config = config.get("drift_detection", {})
|
|
249
|
+
auto_classify_threshold = drift_config.get("auto_classify_threshold", 0.85)
|
|
250
|
+
min_score = drift_config.get("warning_threshold", 0.7)
|
|
251
|
+
|
|
252
|
+
# Check if drift exceeds threshold
|
|
253
|
+
if drift_score < min_score:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
# Get queue size for nudge message
|
|
257
|
+
min_activities = drift_config.get("min_activities_before_classify", 3)
|
|
258
|
+
current_count = len(queue.get("activities", []))
|
|
259
|
+
|
|
260
|
+
if drift_score >= auto_classify_threshold:
|
|
261
|
+
# High drift - queued for classification
|
|
262
|
+
return (
|
|
263
|
+
f"Drift detected ({drift_score:.2f}): Activity queued for "
|
|
264
|
+
f"classification ({current_count}/{min_activities} needed)."
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
# Moderate drift - just warn
|
|
268
|
+
return (
|
|
269
|
+
f"Drift detected ({drift_score:.2f}): Activity may not align with "
|
|
270
|
+
f"current feature context. Consider refocusing or updating the feature."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def trigger_auto_classification(
|
|
275
|
+
context: HookContext,
|
|
276
|
+
queue: dict[str, Any],
|
|
277
|
+
feature_id: str,
|
|
278
|
+
config: dict[str, Any],
|
|
279
|
+
) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Check if auto-classification should be triggered.
|
|
282
|
+
|
|
283
|
+
Validates whether classification conditions are met:
|
|
284
|
+
1. Classification is enabled in config
|
|
285
|
+
2. Minimum activities threshold reached
|
|
286
|
+
3. Cooldown period has elapsed since last classification
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
context: Hook execution context
|
|
290
|
+
queue: Current drift queue dict
|
|
291
|
+
feature_id: Current feature ID for context
|
|
292
|
+
config: Drift configuration dict
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if classification should be triggered, False otherwise
|
|
296
|
+
|
|
297
|
+
Example:
|
|
298
|
+
```python
|
|
299
|
+
if trigger_auto_classification(context, queue, "feat-123", config):
|
|
300
|
+
prompt = build_classification_prompt(queue, feature_id)
|
|
301
|
+
# Spawn classification agent with prompt
|
|
302
|
+
```
|
|
303
|
+
"""
|
|
304
|
+
drift_config = config.get("drift_detection", {})
|
|
305
|
+
classification_config = config.get("classification", {})
|
|
306
|
+
|
|
307
|
+
# Check if classification is enabled
|
|
308
|
+
if not classification_config.get("enabled", False):
|
|
309
|
+
logger.debug("Classification disabled in config")
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
# Check minimum activities threshold
|
|
313
|
+
min_activities = drift_config.get("min_activities_before_classify", 3)
|
|
314
|
+
current_activities = len(queue.get("activities", []))
|
|
315
|
+
if current_activities < min_activities:
|
|
316
|
+
logger.debug(
|
|
317
|
+
f"Not enough activities for classification: {current_activities}/{min_activities}"
|
|
318
|
+
)
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
# Check cooldown
|
|
322
|
+
cooldown_minutes = drift_config.get("cooldown_minutes", 10)
|
|
323
|
+
last_classification = queue.get("last_classification")
|
|
324
|
+
|
|
325
|
+
if last_classification:
|
|
326
|
+
try:
|
|
327
|
+
last_time = datetime.fromisoformat(last_classification)
|
|
328
|
+
time_since = datetime.now() - last_time
|
|
329
|
+
if time_since < timedelta(minutes=cooldown_minutes):
|
|
330
|
+
logger.debug(
|
|
331
|
+
f"Classification on cooldown: {time_since.total_seconds():.0f}s "
|
|
332
|
+
f"< {cooldown_minutes}min"
|
|
333
|
+
)
|
|
334
|
+
return False
|
|
335
|
+
except (ValueError, TypeError) as e:
|
|
336
|
+
logger.warning(f"Error parsing last_classification timestamp: {e}")
|
|
337
|
+
|
|
338
|
+
logger.info(
|
|
339
|
+
f"Classification conditions met: {current_activities} activities, "
|
|
340
|
+
f"threshold {min_activities}, cooldown {cooldown_minutes}min"
|
|
341
|
+
)
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def build_classification_prompt(queue: dict[str, Any], feature_id: str) -> str:
|
|
346
|
+
"""
|
|
347
|
+
Build structured prompt for auto-classification agent.
|
|
348
|
+
|
|
349
|
+
Formats queued activities as a clear prompt for an LLM to classify into
|
|
350
|
+
appropriate work item types (bug, feature, spike, chore, hotfix).
|
|
351
|
+
|
|
352
|
+
The prompt includes:
|
|
353
|
+
- Feature context (what the current feature is supposed to do)
|
|
354
|
+
- Activity list with drift scores (what the agent actually did)
|
|
355
|
+
- Classification rules with descriptions
|
|
356
|
+
- Instruction to create work item in .htmlgraph/
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
queue: Drift queue dict with activities list
|
|
360
|
+
feature_id: Current feature ID for context
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Prompt string suitable for passing to classification agent
|
|
364
|
+
|
|
365
|
+
Example:
|
|
366
|
+
```python
|
|
367
|
+
prompt = build_classification_prompt(queue, "feat-abc123")
|
|
368
|
+
# Use with Task tool or claude CLI
|
|
369
|
+
result = subprocess.run(
|
|
370
|
+
["claude", "-p", prompt, "--model", "haiku"],
|
|
371
|
+
cwd=str(project_dir),
|
|
372
|
+
)
|
|
373
|
+
```
|
|
374
|
+
"""
|
|
375
|
+
activities = queue.get("activities", [])
|
|
376
|
+
|
|
377
|
+
# Format activity lines with summaries and drift scores
|
|
378
|
+
activity_lines = []
|
|
379
|
+
for activity in activities:
|
|
380
|
+
tool = activity.get("tool", "unknown")
|
|
381
|
+
summary = activity.get("summary", "no summary")
|
|
382
|
+
drift_score = activity.get("drift_score", 0)
|
|
383
|
+
file_paths = activity.get("file_paths", [])
|
|
384
|
+
|
|
385
|
+
# Build activity line
|
|
386
|
+
line = f"- {tool}: {summary}"
|
|
387
|
+
|
|
388
|
+
# Add file context if available
|
|
389
|
+
if file_paths:
|
|
390
|
+
files_str = ", ".join(str(f) for f in file_paths[:2])
|
|
391
|
+
line += f" (files: {files_str})"
|
|
392
|
+
|
|
393
|
+
# Add drift score
|
|
394
|
+
line += f" [drift: {drift_score:.2f}]"
|
|
395
|
+
activity_lines.append(line)
|
|
396
|
+
|
|
397
|
+
# Build classification prompt
|
|
398
|
+
prompt = f"""Classify these high-drift activities into a work item.
|
|
399
|
+
|
|
400
|
+
Current feature context: {feature_id}
|
|
401
|
+
|
|
402
|
+
Recent activities with high drift:
|
|
403
|
+
{chr(10).join(activity_lines)}
|
|
404
|
+
|
|
405
|
+
Based on the activity patterns:
|
|
406
|
+
1. Determine the work item type (bug, feature, spike, chore, or hotfix)
|
|
407
|
+
2. Create an appropriate title and description
|
|
408
|
+
3. Create the work item HTML file in .htmlgraph/
|
|
409
|
+
|
|
410
|
+
Use the classification rules:
|
|
411
|
+
- bug: fixing errors, incorrect behavior
|
|
412
|
+
- feature: new functionality, additions
|
|
413
|
+
- spike: research, exploration, investigation
|
|
414
|
+
- chore: maintenance, refactoring, cleanup
|
|
415
|
+
- hotfix: urgent production issues
|
|
416
|
+
|
|
417
|
+
Create the work item now using Write tool."""
|
|
418
|
+
|
|
419
|
+
logger.debug(f"Built classification prompt ({len(activity_lines)} activities)")
|
|
420
|
+
return prompt
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def run_headless_classification(
|
|
424
|
+
context: HookContext, prompt: str, config: dict[str, Any]
|
|
425
|
+
) -> tuple[bool, str | None]:
|
|
426
|
+
"""
|
|
427
|
+
Attempt to run auto-classification via headless claude subprocess.
|
|
428
|
+
|
|
429
|
+
Spawns a subprocess with the classification prompt to avoid blocking
|
|
430
|
+
the main hook execution. Sets HTMLGRAPH_DISABLE_TRACKING to prevent
|
|
431
|
+
recursive hook execution.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
context: Hook execution context with project_dir access
|
|
435
|
+
prompt: Classification prompt to send to claude
|
|
436
|
+
config: Drift configuration dict
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Tuple of (success: bool, nudge: str | None)
|
|
440
|
+
- success: True if classification subprocess succeeded
|
|
441
|
+
- nudge: Message to include in hook response
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
subprocess.TimeoutExpired: If classification takes > 120 seconds
|
|
445
|
+
OSError: If claude command not found
|
|
446
|
+
|
|
447
|
+
Example:
|
|
448
|
+
```python
|
|
449
|
+
success, nudge = run_headless_classification(context, prompt, config)
|
|
450
|
+
if success:
|
|
451
|
+
logger.info("Classification completed")
|
|
452
|
+
else:
|
|
453
|
+
logger.warning("Fallback to manual classification needed")
|
|
454
|
+
```
|
|
455
|
+
"""
|
|
456
|
+
classification_config = config.get("classification", {})
|
|
457
|
+
model = classification_config.get("model", "haiku")
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
result = subprocess.run(
|
|
461
|
+
[
|
|
462
|
+
"claude",
|
|
463
|
+
"-p",
|
|
464
|
+
prompt,
|
|
465
|
+
"--model",
|
|
466
|
+
model,
|
|
467
|
+
"--dangerously-skip-permissions",
|
|
468
|
+
],
|
|
469
|
+
capture_output=True,
|
|
470
|
+
text=True,
|
|
471
|
+
timeout=120,
|
|
472
|
+
cwd=context.project_dir,
|
|
473
|
+
env={
|
|
474
|
+
**os.environ,
|
|
475
|
+
# Prevent hooks from creating nested HtmlGraph sessions
|
|
476
|
+
"HTMLGRAPH_DISABLE_TRACKING": "1",
|
|
477
|
+
},
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if result.returncode == 0:
|
|
481
|
+
logger.info("Headless classification completed successfully")
|
|
482
|
+
nudge = (
|
|
483
|
+
"Drift auto-classification completed. "
|
|
484
|
+
"Check .htmlgraph/ for new work item."
|
|
485
|
+
)
|
|
486
|
+
return (True, nudge)
|
|
487
|
+
else:
|
|
488
|
+
logger.warning(f"Classification subprocess failed: {result.stderr}")
|
|
489
|
+
nudge = (
|
|
490
|
+
"HIGH DRIFT - Headless classification failed. "
|
|
491
|
+
"Please classify manually in .htmlgraph/"
|
|
492
|
+
)
|
|
493
|
+
return (False, nudge)
|
|
494
|
+
|
|
495
|
+
except subprocess.TimeoutExpired as e:
|
|
496
|
+
logger.error(f"Classification timeout after {e.timeout}s")
|
|
497
|
+
nudge = (
|
|
498
|
+
"HIGH DRIFT - Classification timeout. "
|
|
499
|
+
"Please classify manually in .htmlgraph/"
|
|
500
|
+
)
|
|
501
|
+
return (False, nudge)
|
|
502
|
+
except FileNotFoundError:
|
|
503
|
+
logger.error("claude command not found")
|
|
504
|
+
nudge = (
|
|
505
|
+
"HIGH DRIFT - claude not available. Please classify manually in .htmlgraph/"
|
|
506
|
+
)
|
|
507
|
+
return (False, nudge)
|
|
508
|
+
except Exception as e:
|
|
509
|
+
logger.error(f"Unexpected error during classification: {e}")
|
|
510
|
+
nudge = (
|
|
511
|
+
f"HIGH DRIFT - Classification error: {e}. "
|
|
512
|
+
"Please classify manually in .htmlgraph/"
|
|
513
|
+
)
|
|
514
|
+
return (False, nudge)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
__all__ = [
|
|
518
|
+
"load_drift_config",
|
|
519
|
+
"detect_drift",
|
|
520
|
+
"handle_high_drift",
|
|
521
|
+
"trigger_auto_classification",
|
|
522
|
+
"build_classification_prompt",
|
|
523
|
+
"run_headless_classification",
|
|
524
|
+
"DEFAULT_DRIFT_CONFIG",
|
|
525
|
+
]
|