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,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)