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.
Files changed (331) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {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
+ ]