htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) 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 +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Work Validation Module for HtmlGraph Hooks
3
7
 
@@ -6,6 +10,9 @@ Provides intelligent guidance for HtmlGraph workflow based on:
6
10
  2. Recent tool usage patterns (anti-pattern detection)
7
11
  3. Learned patterns from transcript analytics
8
12
 
13
+ Subagents spawned via Task() have unrestricted tool access.
14
+ Detection uses 5-level strategy: env vars, session state, database.
15
+
9
16
  This module can be used by hook scripts or imported directly for validation logic.
10
17
 
11
18
  Main API:
@@ -19,42 +26,57 @@ Example:
19
26
  result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
20
27
 
21
28
  if result["decision"] == "block":
22
- print(result["reason"])
29
+ logger.debug("Validation reason: %s", result["reason"])
23
30
  elif "guidance" in result:
24
- print(result["guidance"])
31
+ logger.debug("Validation guidance: %s", result["guidance"])
25
32
  """
26
33
 
27
34
  import json
28
35
  import re
29
- from datetime import datetime, timezone
30
36
  from pathlib import Path
31
37
  from typing import Any, cast
32
38
 
33
- # Anti-patterns to detect (tool sequence -> warning message)
34
- ANTI_PATTERNS = {
35
- (
36
- "Bash",
37
- "Bash",
38
- "Bash",
39
- "Bash",
40
- ): "4 consecutive Bash commands. Check for errors or consider a different approach.",
41
- (
42
- "Edit",
43
- "Edit",
44
- "Edit",
45
- ): "3 consecutive Edits. Consider batching changes or reading file first.",
46
- (
47
- "Grep",
48
- "Grep",
49
- "Grep",
50
- ): "3 consecutive Greps. Consider reading results before searching more.",
51
- (
52
- "Read",
53
- "Read",
54
- "Read",
55
- "Read",
56
- ): "4 consecutive Reads. Consider caching file content.",
57
- }
39
+ from htmlgraph.hooks.subagent_detection import is_subagent_context
40
+ from htmlgraph.orchestrator_config import load_orchestrator_config
41
+
42
+
43
+ def get_anti_patterns(config: Any | None = None) -> dict[tuple[str, ...], str]:
44
+ """
45
+ Build anti-pattern rules from configuration.
46
+
47
+ Args:
48
+ config: Optional OrchestratorConfig. If None, loads from file.
49
+
50
+ Returns:
51
+ Dict mapping tool sequences to warning messages
52
+ """
53
+ if config is None:
54
+ config = load_orchestrator_config()
55
+
56
+ patterns = config.anti_patterns
57
+
58
+ return {
59
+ tuple(["Bash"] * patterns.consecutive_bash): (
60
+ f"{patterns.consecutive_bash} consecutive Bash commands. "
61
+ "Check for errors or consider a different approach."
62
+ ),
63
+ tuple(["Edit"] * patterns.consecutive_edit): (
64
+ f"{patterns.consecutive_edit} consecutive Edits. "
65
+ "Consider batching changes or reading file first."
66
+ ),
67
+ tuple(["Grep"] * patterns.consecutive_grep): (
68
+ f"{patterns.consecutive_grep} consecutive Greps. "
69
+ "Consider reading results before searching more."
70
+ ),
71
+ tuple(["Read"] * patterns.consecutive_read): (
72
+ f"{patterns.consecutive_read} consecutive Reads. "
73
+ "Consider caching file content."
74
+ ),
75
+ }
76
+
77
+
78
+ # Legacy constant for backwards compatibility (now uses config)
79
+ ANTI_PATTERNS = get_anti_patterns()
58
80
 
59
81
  # Tools that indicate exploration/implementation (require work item in strict mode)
60
82
  EXPLORATION_TOOLS = {"Grep", "Glob", "Task"}
@@ -67,68 +89,89 @@ OPTIMAL_PATTERNS = {
67
89
  ("Edit", "Bash"): "Good: Edit then test - verify changes.",
68
90
  }
69
91
 
70
- # Session tool history file
71
- TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
92
+ # Maximum number of recent tool calls to consider for pattern detection
72
93
  MAX_HISTORY = 20
73
94
 
74
95
 
75
- def load_tool_history() -> list[dict]:
76
- """Load recent tool history from temp file."""
77
- if TOOL_HISTORY_FILE.exists():
78
- try:
79
- data = json.loads(TOOL_HISTORY_FILE.read_text())
80
-
81
- # Handle both formats: {"history": [...]} and [...] (legacy)
82
- if isinstance(data, dict):
83
- data = data.get("history", [])
84
-
85
- # Filter to last hour only
86
- cutoff = datetime.now(timezone.utc).timestamp() - 3600
87
-
88
- # Handle both "ts" (old) and "timestamp" (new) formats
89
- filtered = []
90
- for t in data:
91
- ts = t.get("ts", 0)
92
- if not ts and "timestamp" in t:
93
- # Parse ISO format timestamp
94
- try:
95
- ts = datetime.fromisoformat(
96
- t["timestamp"].replace("Z", "+00:00")
97
- ).timestamp()
98
- except Exception:
99
- ts = 0
100
- if ts > cutoff:
101
- filtered.append(t)
102
-
103
- return filtered[-MAX_HISTORY:]
104
- except Exception:
105
- pass
106
- return []
96
+ def load_tool_history(session_id: str) -> list[dict]:
97
+ """
98
+ Load recent tool history from database (session-isolated).
107
99
 
100
+ Args:
101
+ session_id: Session identifier to filter tool history
108
102
 
109
- def save_tool_history(history: list[dict]) -> None:
110
- """Save tool history to temp file."""
103
+ Returns:
104
+ List of recent tool calls with tool name and timestamp
105
+ """
111
106
  try:
112
- # Use wrapped format to match orchestrator-enforce.py
113
- TOOL_HISTORY_FILE.write_text(
114
- json.dumps({"history": history[-MAX_HISTORY:]}, indent=2)
107
+ from htmlgraph.db.schema import HtmlGraphDB
108
+
109
+ # Find database path
110
+ cwd = Path.cwd()
111
+ graph_dir = cwd / ".htmlgraph"
112
+ if not graph_dir.exists():
113
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
114
+ candidate = parent / ".htmlgraph"
115
+ if candidate.exists():
116
+ graph_dir = candidate
117
+ break
118
+
119
+ db_path = graph_dir / "htmlgraph.db"
120
+ if not db_path.exists():
121
+ return []
122
+
123
+ db = HtmlGraphDB(str(db_path))
124
+ if db.connection is None:
125
+ return []
126
+
127
+ cursor = db.connection.cursor()
128
+ cursor.execute(
129
+ """
130
+ SELECT tool_name, timestamp
131
+ FROM agent_events
132
+ WHERE session_id = ?
133
+ ORDER BY timestamp DESC
134
+ LIMIT ?
135
+ """,
136
+ (session_id, MAX_HISTORY),
115
137
  )
138
+
139
+ # Return in chronological order (oldest first) for pattern detection
140
+ rows = cursor.fetchall()
141
+ db.disconnect()
142
+
143
+ return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
116
144
  except Exception:
117
- pass
145
+ # Graceful degradation - return empty history on error
146
+ return []
118
147
 
119
148
 
120
- def record_tool(tool: str, history: list[dict]) -> list[dict]:
121
- """Record a tool use in history."""
122
- # Use same format as orchestrator-enforce.py for consistency
123
- history.append({"tool": tool, "timestamp": datetime.now(timezone.utc).isoformat()})
124
- return history[-MAX_HISTORY:]
149
+ def record_tool(tool: str, session_id: str) -> None:
150
+ """
151
+ Record a tool use in database.
152
+
153
+ Note: This is now handled by track-event.py hook, so this function
154
+ is kept for backward compatibility but does nothing.
155
+
156
+ Args:
157
+ tool: Tool name being called
158
+ session_id: Session identifier for isolation
159
+ """
160
+ # Tool recording is now handled by track-event.py PostToolUse hook
161
+ # This function is kept for backward compatibility but does nothing
162
+ pass
125
163
 
126
164
 
127
165
  def detect_anti_pattern(tool: str, history: list[dict]) -> str | None:
128
- """Check if adding this tool creates an anti-pattern."""
129
- recent_tools = [h["tool"] for h in history[-4:]] + [tool]
166
+ """Check if adding this tool creates an anti-pattern (uses configurable thresholds)."""
167
+ # Load fresh anti-patterns from config
168
+ anti_patterns = get_anti_patterns()
169
+
170
+ # Get max pattern length to know how far to look back
171
+ max_pattern_len = max(len(p) for p in anti_patterns.keys()) if anti_patterns else 5
172
+ recent_tools = [h["tool"] for h in history[-max_pattern_len:]] + [tool]
130
173
 
131
- for pattern, message in ANTI_PATTERNS.items():
174
+ for pattern, message in anti_patterns.items():
132
175
  pattern_len = len(pattern)
133
176
  if len(recent_tools) >= pattern_len:
134
177
  # Check if recent tools end with this pattern
@@ -149,7 +192,7 @@ def detect_optimal_pattern(tool: str, history: list[dict]) -> str | None:
149
192
  return OPTIMAL_PATTERNS.get(pair)
150
193
 
151
194
 
152
- def get_pattern_guidance(tool: str, history: list[dict]) -> dict:
195
+ def get_pattern_guidance(tool: str, history: list[dict]) -> dict[str, Any]:
153
196
  """Get guidance based on tool patterns."""
154
197
  # Check for anti-patterns first
155
198
  anti_pattern = detect_anti_pattern(tool, history)
@@ -192,7 +235,7 @@ def get_session_health_hint(history: list[dict]) -> str | None:
192
235
  return None
193
236
 
194
237
 
195
- def load_validation_config() -> dict:
238
+ def load_validation_config() -> dict[str, Any]:
196
239
  """Load validation config with defaults."""
197
240
  config_path = (
198
241
  Path(__file__).parent.parent.parent.parent.parent
@@ -218,7 +261,9 @@ def load_validation_config() -> dict:
218
261
  }
219
262
 
220
263
 
221
- def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
264
+ def is_always_allowed(
265
+ tool: str, params: dict[str, Any], config: dict[str, Any]
266
+ ) -> bool:
222
267
  """Check if tool is always allowed (read-only operations)."""
223
268
  # Always-allow tools
224
269
  if tool in config.get("always_allow", {}).get("tools", []):
@@ -227,6 +272,15 @@ def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
227
272
  # Read-only Bash patterns
228
273
  if tool == "Bash":
229
274
  command = params.get("command", "")
275
+
276
+ # Check git commands using shared classification
277
+ if command.strip().startswith("git"):
278
+ from htmlgraph.hooks.git_commands import should_allow_git_command
279
+
280
+ if should_allow_git_command(command):
281
+ return True
282
+
283
+ # Check other bash patterns
230
284
  for pattern in config.get("always_allow", {}).get("bash_patterns", []):
231
285
  if re.match(pattern, command):
232
286
  return True
@@ -234,7 +288,7 @@ def is_always_allowed(tool: str, params: dict, config: dict) -> bool:
234
288
  return False
235
289
 
236
290
 
237
- def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
291
+ def is_direct_htmlgraph_write(tool: str, params: dict[str, Any]) -> tuple[bool, str]:
238
292
  """Check if attempting direct write to .htmlgraph/ (always denied)."""
239
293
  if tool not in ["Write", "Edit", "Delete", "NotebookEdit"]:
240
294
  return False, ""
@@ -246,7 +300,7 @@ def is_direct_htmlgraph_write(tool: str, params: dict) -> tuple[bool, str]:
246
300
  return False, ""
247
301
 
248
302
 
249
- def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
303
+ def is_sdk_command(tool: str, params: dict[str, Any], config: dict[str, Any]) -> bool:
250
304
  """Check if Bash command is an SDK command."""
251
305
  if tool != "Bash":
252
306
  return False
@@ -259,7 +313,9 @@ def is_sdk_command(tool: str, params: dict, config: dict) -> bool:
259
313
  return False
260
314
 
261
315
 
262
- def is_code_operation(tool: str, params: dict, config: dict) -> bool:
316
+ def is_code_operation(
317
+ tool: str, params: dict[str, Any], config: dict[str, Any]
318
+ ) -> bool:
263
319
  """Check if operation modifies code."""
264
320
  # Direct file operations
265
321
  if tool in config.get("code_operations", {}).get("tools", []):
@@ -288,7 +344,9 @@ def get_active_work_item() -> dict | None:
288
344
  return None
289
345
 
290
346
 
291
- def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
347
+ def check_orchestrator_violation(
348
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
349
+ ) -> dict | None:
292
350
  """
293
351
  Check if operation violates orchestrator mode rules.
294
352
 
@@ -298,6 +356,7 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
298
356
  Args:
299
357
  tool: Tool name
300
358
  params: Tool parameters
359
+ session_id: Session identifier for loading tool history
301
360
 
302
361
  Returns:
303
362
  Blocking response dict if violation detected in strict mode, None otherwise
@@ -335,7 +394,9 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
335
394
  is_allowed_orchestrator_operation,
336
395
  )
337
396
 
338
- is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
397
+ is_allowed, reason, category = is_allowed_orchestrator_operation(
398
+ tool, params, session_id
399
+ )
339
400
 
340
401
  # If orchestrator would block (but returns continue=True), we block here
341
402
  if not is_allowed:
@@ -363,33 +424,49 @@ def check_orchestrator_violation(tool: str, params: dict) -> dict | None:
363
424
 
364
425
 
365
426
  def validate_tool_call(
366
- tool: str, params: dict, config: dict, history: list[dict]
367
- ) -> dict:
427
+ tool: str,
428
+ params: dict[str, Any],
429
+ config: dict[str, Any],
430
+ history: list[dict],
431
+ session_id: str | None = None,
432
+ ) -> dict[str, Any]:
368
433
  """
369
434
  Validate tool call and return GUIDANCE with active learning.
370
435
 
436
+ Subagents spawned via Task() have unrestricted tool access.
437
+ Detection uses 5-level strategy: env vars, session state, database.
438
+
371
439
  Args:
372
440
  tool: Tool name (e.g., "Edit", "Bash", "Read")
373
441
  params: Tool parameters (e.g., {"file_path": "test.py"})
374
442
  config: Validation configuration (from load_validation_config())
375
- history: Tool usage history (from load_tool_history())
443
+ history: Tool usage history (from load_tool_history(session_id))
444
+ session_id: Optional session ID for loading history if not provided
376
445
 
377
446
  Returns:
378
- dict: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
447
+ dict[str, Any]: {"decision": "allow" | "block", "guidance": "...", "suggestion": "...", ...}
379
448
  All operations are ALLOWED unless blocked for safety reasons.
380
449
 
381
450
  Example:
451
+ session_id = tool_input.get("session_id", "unknown")
452
+ history = load_tool_history(session_id)
382
453
  result = validate_tool_call("Edit", {"file_path": "test.py"}, config, history)
383
454
  if result["decision"] == "block":
384
- print(result["reason"])
455
+ logger.debug("Validation reason: %s", result["reason"])
385
456
  elif "guidance" in result:
386
- print(result["guidance"])
457
+ logger.debug("Validation guidance: %s", result["guidance"])
387
458
  """
459
+ # Check if this is a subagent context - subagents have unrestricted tool access
460
+ if is_subagent_context():
461
+ return {"decision": "allow"}
462
+
388
463
  result = {"decision": "allow"}
389
464
  guidance_parts = []
390
465
 
391
466
  # Step 0a: Check orchestrator mode violations (if enabled)
392
- orchestrator_violation = check_orchestrator_violation(tool, params)
467
+ orchestrator_violation = check_orchestrator_violation(
468
+ tool, params, session_id or "unknown"
469
+ )
393
470
  if orchestrator_violation:
394
471
  # BLOCK orchestrator violations in strict mode
395
472
  return orchestrator_violation
@@ -505,3 +582,47 @@ def validate_tool_call(
505
582
  result["guidance"] = " | ".join(guidance_parts)
506
583
 
507
584
  return result
585
+
586
+
587
+ def main() -> None:
588
+ """Hook entry point for script wrapper."""
589
+ import sys
590
+
591
+ try:
592
+ # Read tool input from stdin
593
+ tool_input = json.load(sys.stdin)
594
+
595
+ # Claude Code uses "name" and "input", fallback to "tool" and "params"
596
+ tool = tool_input.get("name", "") or tool_input.get("tool", "")
597
+ params = tool_input.get("input", {}) or tool_input.get("params", {})
598
+
599
+ # Get session_id from hook_input (NEW: required for session-isolated history)
600
+ session_id = tool_input.get("session_id", "unknown")
601
+
602
+ # Load config
603
+ config = load_validation_config()
604
+
605
+ # Load session-isolated tool history (NEW: from database, not file)
606
+ history = load_tool_history(session_id)
607
+
608
+ # Get guidance with pattern awareness
609
+ result = validate_tool_call(tool, params, config, history)
610
+
611
+ # Note: Tool recording is now handled by track-event.py PostToolUse hook
612
+ # No need to call record_tool() or save_tool_history() here
613
+
614
+ # Output JSON with guidance/block message
615
+ print(json.dumps(result))
616
+
617
+ # Exit 1 to BLOCK if decision is "block", otherwise allow
618
+ if result.get("decision") == "block":
619
+ sys.exit(1)
620
+ else:
621
+ sys.exit(0)
622
+
623
+ except Exception as e:
624
+ # Graceful degradation - allow on error
625
+ print(
626
+ json.dumps({"decision": "allow", "guidance": f"Validation hook error: {e}"})
627
+ )
628
+ sys.exit(0)
htmlgraph/ids.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  """
2
4
  Hash-based ID generation for HtmlGraph.
3
5
 
@@ -15,7 +17,6 @@ collision probability is effectively zero even with thousands
15
17
  of concurrent agents creating tasks simultaneously.
16
18
  """
17
19
 
18
- from __future__ import annotations
19
20
 
20
21
  import hashlib
21
22
  import os