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,674 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ """
6
+ Orchestrator Enforcement Module
7
+
8
+ This module provides the core logic for enforcing orchestrator delegation patterns
9
+ in HtmlGraph-enabled projects. It classifies operations into allowed vs blocked
10
+ categories and provides clear Task delegation suggestions.
11
+
12
+ Architecture:
13
+ - Reads orchestrator mode from .htmlgraph/orchestrator-mode.json
14
+ - Classifies operations into ALLOWED vs BLOCKED categories
15
+ - Tracks tool usage sequences to detect exploration patterns
16
+ - Provides clear Task delegation suggestions when blocking
17
+ - Subagents spawned via Task() have unrestricted tool access
18
+ - Detection uses 5-level strategy: env vars, session state, database
19
+
20
+ Operation Categories:
21
+ 1. ALWAYS ALLOWED - Task, AskUserQuestion, TodoWrite, SDK operations
22
+ 2. SINGLE LOOKUP ALLOWED - First Read/Grep/Glob (check history)
23
+ 3. BLOCKED - Edit, Write, NotebookEdit, Delete, test/build commands
24
+
25
+ Enforcement Levels:
26
+ - strict: BLOCKS implementation operations with clear error
27
+ - guidance: ALLOWS but provides warnings and suggestions
28
+
29
+ Public API:
30
+ - enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
31
+ Main entry point for hook scripts. Returns hook response dict.
32
+ """
33
+
34
+ import json
35
+ import re
36
+ from pathlib import Path
37
+ from typing import Any
38
+
39
+ from htmlgraph.hooks.subagent_detection import is_subagent_context
40
+ from htmlgraph.orchestrator_config import load_orchestrator_config
41
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
42
+ from htmlgraph.orchestrator_validator import OrchestratorValidator
43
+
44
+ # Maximum number of recent tool calls to consider for pattern detection
45
+ MAX_HISTORY_SIZE = 50 # Keep last 50 tool calls
46
+
47
+
48
+ def load_tool_history(session_id: str) -> list[dict]:
49
+ """
50
+ Load recent tool history from database (session-isolated).
51
+
52
+ Args:
53
+ session_id: Session identifier to filter tool history
54
+
55
+ Returns:
56
+ List of recent tool calls with tool name and timestamp
57
+ """
58
+ try:
59
+ from htmlgraph.db.schema import HtmlGraphDB
60
+
61
+ # Find database path
62
+ cwd = Path.cwd()
63
+ graph_dir = cwd / ".htmlgraph"
64
+ if not graph_dir.exists():
65
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
66
+ candidate = parent / ".htmlgraph"
67
+ if candidate.exists():
68
+ graph_dir = candidate
69
+ break
70
+
71
+ db_path = graph_dir / "htmlgraph.db"
72
+ if not db_path.exists():
73
+ return []
74
+
75
+ db = HtmlGraphDB(str(db_path))
76
+ if db.connection is None:
77
+ return []
78
+
79
+ cursor = db.connection.cursor()
80
+ cursor.execute(
81
+ """
82
+ SELECT tool_name, timestamp
83
+ FROM agent_events
84
+ WHERE session_id = ?
85
+ ORDER BY timestamp DESC
86
+ LIMIT ?
87
+ """,
88
+ (session_id, MAX_HISTORY_SIZE),
89
+ )
90
+
91
+ # Return in chronological order (oldest first) for pattern detection
92
+ rows = cursor.fetchall()
93
+ db.disconnect()
94
+
95
+ return [{"tool": row[0], "timestamp": row[1]} for row in reversed(rows)]
96
+ except Exception:
97
+ # Graceful degradation - return empty history on error
98
+ return []
99
+
100
+
101
+ def record_tool_event(tool_name: str, session_id: str) -> None:
102
+ """
103
+ Record a tool event to the database for history tracking.
104
+
105
+ This is called at the end of PreToolUse hook execution to track
106
+ tool usage patterns for sequence detection.
107
+
108
+ Args:
109
+ tool_name: Name of the tool being called
110
+ session_id: Session identifier for isolation
111
+ """
112
+ try:
113
+ import datetime
114
+ import uuid
115
+
116
+ from htmlgraph.db.schema import HtmlGraphDB
117
+
118
+ # Find database path
119
+ cwd = Path.cwd()
120
+ graph_dir = cwd / ".htmlgraph"
121
+ if not graph_dir.exists():
122
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
123
+ candidate = parent / ".htmlgraph"
124
+ if candidate.exists():
125
+ graph_dir = candidate
126
+ break
127
+
128
+ if not graph_dir.exists():
129
+ return
130
+
131
+ db_path = graph_dir / "htmlgraph.db"
132
+ db = HtmlGraphDB(str(db_path))
133
+ if db.connection is None:
134
+ return
135
+
136
+ cursor = db.connection.cursor()
137
+ timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
138
+
139
+ # Ensure session exists (required by FK constraint)
140
+ cursor.execute(
141
+ """
142
+ INSERT OR IGNORE INTO sessions (session_id, agent_assigned, created_at, status)
143
+ VALUES (?, ?, ?, ?)
144
+ """,
145
+ (session_id, "orchestrator-hook", timestamp, "active"),
146
+ )
147
+
148
+ # Record the tool event using the actual schema
149
+ # Schema has: event_id, agent_id, event_type, timestamp, tool_name, session_id, etc.
150
+ event_id = str(uuid.uuid4())
151
+ agent_id = "orchestrator-hook" # Identifier for the hook
152
+
153
+ cursor.execute(
154
+ """
155
+ INSERT INTO agent_events (event_id, agent_id, event_type, timestamp, tool_name, session_id)
156
+ VALUES (?, ?, ?, ?, ?, ?)
157
+ """,
158
+ (event_id, agent_id, "tool_call", timestamp, tool_name, session_id),
159
+ )
160
+
161
+ db.connection.commit()
162
+ db.disconnect()
163
+ except Exception:
164
+ # Graceful degradation - don't fail hook on recording error
165
+ pass
166
+
167
+
168
+ def is_allowed_orchestrator_operation(
169
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
170
+ ) -> tuple[bool, str, str]:
171
+ """
172
+ Check if operation is allowed for orchestrators.
173
+
174
+ Args:
175
+ tool: Tool name (e.g., "Read", "Edit", "Bash")
176
+ params: Tool parameters dict
177
+ session_id: Session identifier for loading tool history
178
+
179
+ Returns:
180
+ Tuple of (is_allowed, reason_if_not, category)
181
+ - is_allowed: True if operation should proceed
182
+ - reason_if_not: Explanation if blocked (empty if allowed)
183
+ - category: Operation category for logging
184
+ """
185
+ # Get enforcement level from manager
186
+ try:
187
+ cwd = Path.cwd()
188
+ graph_dir = cwd / ".htmlgraph"
189
+ if not graph_dir.exists():
190
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
191
+ candidate = parent / ".htmlgraph"
192
+ if candidate.exists():
193
+ graph_dir = candidate
194
+ break
195
+ manager = OrchestratorModeManager(graph_dir)
196
+ enforcement_level = (
197
+ manager.get_enforcement_level() if manager.is_enabled() else "guidance"
198
+ )
199
+ except Exception:
200
+ enforcement_level = "guidance"
201
+
202
+ # Use OrchestratorValidator for comprehensive validation
203
+ validator = OrchestratorValidator()
204
+ result, reason = validator.validate_tool_use(tool, params)
205
+
206
+ if result == "block":
207
+ return False, reason, "validator-blocked"
208
+ elif result == "warn":
209
+ # Continue but with warning
210
+ pass # Fall through to existing checks
211
+
212
+ # Category 1: ALWAYS ALLOWED - Orchestrator core operations
213
+ if tool in ["Task", "AskUserQuestion", "TodoWrite"]:
214
+ return True, "", "orchestrator-core"
215
+
216
+ # FIX #2: Block Skills in strict mode (must be invoked via Task delegation)
217
+ if tool == "Skill" and enforcement_level == "strict":
218
+ return False, "Skills must be invoked via Task delegation", "skill-blocked"
219
+
220
+ # Category 2: SDK Operations - Always allowed
221
+ if tool == "Bash":
222
+ command = params.get("command", "")
223
+
224
+ # Allow htmlgraph SDK commands
225
+ if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
226
+ return True, "", "sdk-command"
227
+
228
+ # Allow git read-only commands using shared classification
229
+ if command.strip().startswith("git"):
230
+ from htmlgraph.hooks.git_commands import should_allow_git_command
231
+
232
+ if should_allow_git_command(command):
233
+ return True, "", "git-readonly"
234
+
235
+ # Allow SDK inline usage (Python inline with htmlgraph import)
236
+ if "from htmlgraph import" in command or "import htmlgraph" in command:
237
+ return True, "", "sdk-inline"
238
+
239
+ # FIX #3: Check if bash command is in allowed whitelist (strict mode only)
240
+ # If we've gotten here, it's not a whitelisted command above
241
+ # Block non-whitelisted bash commands in strict mode
242
+ if enforcement_level == "strict":
243
+ # Check if it's a blocked test/build pattern (handled below)
244
+ blocked_patterns = [
245
+ r"^npm (run|test|build)",
246
+ r"^pytest",
247
+ r"^uv run pytest",
248
+ r"^python -m pytest",
249
+ r"^cargo (build|test)",
250
+ r"^mvn (compile|test|package)",
251
+ r"^make (test|build)",
252
+ ]
253
+ is_blocked_pattern = any(
254
+ re.match(pattern, command) for pattern in blocked_patterns
255
+ )
256
+
257
+ if not is_blocked_pattern:
258
+ # Not a specifically blocked pattern, but also not whitelisted
259
+ # In strict mode, we should delegate
260
+ return (
261
+ False,
262
+ f"Bash command not in allowed list. Delegate to subagent.\n\n"
263
+ f"Command: {command[:100]}",
264
+ "bash-blocked",
265
+ )
266
+
267
+ # Category 3: Quick Lookups - Single operations only
268
+ if tool in ["Read", "Grep", "Glob"]:
269
+ # Check tool history to see if this is a single lookup or part of a sequence
270
+ history = load_tool_history(session_id)
271
+
272
+ # FIX #4: Check for mixed exploration pattern (configurable threshold)
273
+ config = load_orchestrator_config()
274
+ exploration_threshold = config.thresholds.exploration_calls
275
+
276
+ # Check last N calls (where N = threshold + 2)
277
+ lookback = min(exploration_threshold + 2, len(history))
278
+ exploration_count = sum(
279
+ 1 for h in history[-lookback:] if h["tool"] in ["Read", "Grep", "Glob"]
280
+ )
281
+ if exploration_count >= exploration_threshold and enforcement_level == "strict":
282
+ return (
283
+ False,
284
+ f"Multiple exploration calls detected ({exploration_count}/{exploration_threshold}). Delegate to Explorer agent.\n\n"
285
+ "Use Task tool with explorer subagent.",
286
+ "exploration-blocked",
287
+ )
288
+
289
+ # Look at last 3 tool calls
290
+ recent_same_tool = sum(1 for h in history[-3:] if h["tool"] == tool)
291
+
292
+ if recent_same_tool == 0: # First use
293
+ return True, "Single lookup allowed", "single-lookup"
294
+ else:
295
+ return (
296
+ False,
297
+ f"Multiple {tool} calls detected. This is exploration work.\n\n"
298
+ f"Delegate to Explorer subagent using Task tool.",
299
+ "multi-lookup-blocked",
300
+ )
301
+
302
+ # Category 4: BLOCKED - Implementation tools
303
+ if tool in ["Edit", "Write", "NotebookEdit"]:
304
+ return (
305
+ False,
306
+ f"{tool} is implementation work.\n\n"
307
+ f"Delegate to Coder subagent using Task tool.",
308
+ "implementation-blocked",
309
+ )
310
+
311
+ if tool == "Delete":
312
+ return (
313
+ False,
314
+ "Delete is a destructive implementation operation.\n\n"
315
+ "Delegate to Coder subagent using Task tool.",
316
+ "delete-blocked",
317
+ )
318
+
319
+ # Category 5: BLOCKED - Testing/Building
320
+ if tool == "Bash":
321
+ command = params.get("command", "")
322
+
323
+ # Block compilation, testing, building (should be in subagent)
324
+ test_build_patterns: list[tuple[str, str]] = [
325
+ (r"^npm (run|test|build)", "npm test/build"),
326
+ (r"^pytest", "pytest"),
327
+ (r"^uv run pytest", "pytest"),
328
+ (r"^python -m pytest", "pytest"),
329
+ (r"^cargo (build|test)", "cargo build/test"),
330
+ (r"^mvn (compile|test|package)", "maven build/test"),
331
+ (r"^make (test|build)", "make test/build"),
332
+ ]
333
+
334
+ for pattern, name in test_build_patterns:
335
+ if re.match(pattern, command):
336
+ return (
337
+ False,
338
+ f"Testing/building ({name}) should be delegated to subagent.\n\n"
339
+ f"Use Task tool to run tests and report results.",
340
+ "test-build-blocked",
341
+ )
342
+
343
+ # FIX #1: Remove "allowed-default" escape hatch in strict mode
344
+ if enforcement_level == "strict":
345
+ return False, "Not in allowed whitelist", "strict-blocked"
346
+ else:
347
+ return True, "Allowed in guidance mode", "guidance-allowed"
348
+
349
+
350
+ def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
351
+ """
352
+ Create Task tool suggestion based on blocked operation.
353
+
354
+ Includes HtmlGraph reporting pattern for result retrieval.
355
+
356
+ Args:
357
+ tool: Tool that was blocked
358
+ params: Tool parameters
359
+
360
+ Returns:
361
+ Example Task() code with HtmlGraph reporting pattern
362
+ """
363
+ if tool in ["Edit", "Write", "NotebookEdit"]:
364
+ file_path = params.get("file_path", "<file>")
365
+ return (
366
+ "# Delegate to Coder subagent:\n"
367
+ "Task(\n"
368
+ f" prompt='''Implement changes to {file_path}\n\n"
369
+ " 🔴 CRITICAL - Report Results:\n"
370
+ " from htmlgraph import SDK\n"
371
+ " sdk = SDK(agent='coder')\n"
372
+ " sdk.spikes.create('Code Changes Complete') \\\\\n"
373
+ " .set_findings('Changes made: ...') \\\\\n"
374
+ " .save()\n"
375
+ " ''',\n"
376
+ " subagent_type='general-purpose'\n"
377
+ ")\n"
378
+ "# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='coder')[0].findings)\""
379
+ )
380
+
381
+ elif tool in ["Read", "Grep", "Glob"]:
382
+ pattern = params.get("pattern", params.get("file_path", "<pattern>"))
383
+ return (
384
+ "# Delegate to Explorer subagent:\n"
385
+ "Task(\n"
386
+ f" prompt='''Find {pattern} in codebase\n\n"
387
+ " 🔴 CRITICAL - Report Results:\n"
388
+ " from htmlgraph import SDK\n"
389
+ " sdk = SDK(agent='explorer')\n"
390
+ " sdk.spikes.create('Search Results') \\\\\n"
391
+ " .set_findings('Found files: ...') \\\\\n"
392
+ " .save()\n"
393
+ " ''',\n"
394
+ " subagent_type='Explore'\n"
395
+ ")\n"
396
+ "# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='explorer')[0].findings)\""
397
+ )
398
+
399
+ elif tool == "Bash":
400
+ command = params.get("command", "")
401
+ if "test" in command.lower() or "pytest" in command.lower():
402
+ return (
403
+ "# Delegate testing to subagent:\n"
404
+ "Task(\n"
405
+ " prompt='''Run tests and report results\n\n"
406
+ " 🔴 CRITICAL - Report Results:\n"
407
+ " from htmlgraph import SDK\n"
408
+ " sdk = SDK(agent='tester')\n"
409
+ " sdk.spikes.create('Test Results') \\\\\n"
410
+ " .set_findings('Tests passed: X, failed: Y') \\\\\n"
411
+ " .save()\n"
412
+ " ''',\n"
413
+ " subagent_type='general-purpose'\n"
414
+ ")\n"
415
+ "# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='tester')[0].findings)\""
416
+ )
417
+ elif any(x in command.lower() for x in ["build", "compile", "make"]):
418
+ return (
419
+ "# Delegate build to subagent:\n"
420
+ "Task(\n"
421
+ " prompt='''Build project and report any errors\n\n"
422
+ " 🔴 CRITICAL - Report Results:\n"
423
+ " from htmlgraph import SDK\n"
424
+ " sdk = SDK(agent='builder')\n"
425
+ " sdk.spikes.create('Build Results') \\\\\n"
426
+ " .set_findings('Build status: ...') \\\\\n"
427
+ " .save()\n"
428
+ " ''',\n"
429
+ " subagent_type='general-purpose'\n"
430
+ ")\n"
431
+ "# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='builder')[0].findings)\""
432
+ )
433
+
434
+ # Generic suggestion
435
+ return (
436
+ "# Use Task tool with HtmlGraph reporting:\n"
437
+ "Task(\n"
438
+ " prompt='''<describe task>\n\n"
439
+ " 🔴 CRITICAL - Report Results:\n"
440
+ " from htmlgraph import SDK\n"
441
+ " sdk = SDK(agent='subagent')\n"
442
+ " sdk.spikes.create('Task Results') \\\\\n"
443
+ " .set_findings('...') \\\\\n"
444
+ " .save()\n"
445
+ " ''',\n"
446
+ " subagent_type='general-purpose'\n"
447
+ ")\n"
448
+ "# Then retrieve: uv run python -c \"from htmlgraph import SDK; print(SDK().spikes.get_latest(agent='subagent')[0].findings)\""
449
+ )
450
+
451
+
452
+ def enforce_orchestrator_mode(
453
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
454
+ ) -> dict[str, Any]:
455
+ """
456
+ Enforce orchestrator mode rules.
457
+
458
+ This is the main public API for hook scripts. It checks if orchestrator mode
459
+ is enabled, classifies the operation, and returns a hook response dict.
460
+
461
+ Subagents spawned via Task() have unrestricted tool access.
462
+ Detection uses 5-level strategy: env vars, session state, database.
463
+
464
+ Args:
465
+ tool: Tool being called
466
+ params: Tool parameters
467
+ session_id: Session identifier for loading tool history
468
+
469
+ Returns:
470
+ Hook response dict with decision (allow/block) and guidance
471
+ Format: {"continue": bool, "hookSpecificOutput": {...}}
472
+ """
473
+ # Check if this is a subagent context - subagents have unrestricted tool access
474
+ if is_subagent_context():
475
+ return {
476
+ "continue": True,
477
+ "hookSpecificOutput": {
478
+ "hookEventName": "PreToolUse",
479
+ "permissionDecision": "allow",
480
+ },
481
+ }
482
+
483
+ # Get manager and check if mode is enabled
484
+ try:
485
+ # Look for .htmlgraph directory starting from cwd
486
+ cwd = Path.cwd()
487
+ graph_dir = cwd / ".htmlgraph"
488
+
489
+ # If not found in cwd, try parent directories (up to 3 levels)
490
+ if not graph_dir.exists():
491
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
492
+ candidate = parent / ".htmlgraph"
493
+ if candidate.exists():
494
+ graph_dir = candidate
495
+ break
496
+
497
+ manager = OrchestratorModeManager(graph_dir)
498
+
499
+ if not manager.is_enabled():
500
+ # Mode not active, allow everything with no additional output
501
+ return {"continue": True}
502
+
503
+ enforcement_level = manager.get_enforcement_level()
504
+ except Exception:
505
+ # If we can't check mode, fail open (allow)
506
+ return {
507
+ "continue": True,
508
+ "hookSpecificOutput": {
509
+ "hookEventName": "PreToolUse",
510
+ "permissionDecision": "allow",
511
+ },
512
+ }
513
+
514
+ # Check if circuit breaker is triggered in strict mode (configurable threshold)
515
+ config = load_orchestrator_config()
516
+ circuit_breaker_threshold = config.thresholds.circuit_breaker_violations
517
+
518
+ if enforcement_level == "strict" and manager.is_circuit_breaker_triggered():
519
+ # Circuit breaker triggered - block all non-core operations
520
+ if tool not in ["Task", "AskUserQuestion", "TodoWrite"]:
521
+ violation_count = manager.get_violation_count()
522
+ circuit_breaker_message = (
523
+ "🚨 ORCHESTRATOR CIRCUIT BREAKER TRIGGERED\n\n"
524
+ f"You have violated delegation rules {violation_count} times this session "
525
+ f"(threshold: {circuit_breaker_threshold}).\n\n"
526
+ "Violations detected:\n"
527
+ "- Direct execution instead of delegation\n"
528
+ "- Context waste on tactical operations\n\n"
529
+ "Options:\n"
530
+ "1. Disable orchestrator mode: uv run htmlgraph orchestrator disable\n"
531
+ "2. Change to guidance mode: uv run htmlgraph orchestrator set-level guidance\n"
532
+ "3. Reset counter (acknowledge violations): uv run htmlgraph orchestrator reset-violations\n"
533
+ "4. Adjust thresholds: uv run htmlgraph orchestrator config set thresholds.circuit_breaker_violations <N>\n\n"
534
+ "To proceed, choose an option above."
535
+ )
536
+
537
+ return {
538
+ "continue": False,
539
+ "hookSpecificOutput": {
540
+ "hookEventName": "PreToolUse",
541
+ "permissionDecision": "deny",
542
+ "permissionDecisionReason": circuit_breaker_message,
543
+ },
544
+ }
545
+
546
+ # Check if operation is allowed (pass session_id for history lookup)
547
+ is_allowed, reason, category = is_allowed_orchestrator_operation(
548
+ tool, params, session_id
549
+ )
550
+
551
+ # Note: Tool recording is now handled by track-event.py PostToolUse hook
552
+ # No need to call add_to_tool_history() here
553
+
554
+ # Operation is allowed
555
+ if is_allowed:
556
+ if (
557
+ reason
558
+ and enforcement_level == "strict"
559
+ and category not in ["orchestrator-core", "sdk-command"]
560
+ ):
561
+ # Provide guidance even when allowing
562
+ return {
563
+ "continue": True,
564
+ "hookSpecificOutput": {
565
+ "hookEventName": "PreToolUse",
566
+ "permissionDecision": "allow",
567
+ "additionalContext": f"✅ {reason}",
568
+ },
569
+ }
570
+ return {
571
+ "continue": True,
572
+ "hookSpecificOutput": {
573
+ "hookEventName": "PreToolUse",
574
+ "permissionDecision": "allow",
575
+ },
576
+ }
577
+
578
+ # Operation not allowed - track violation and provide warnings
579
+ if enforcement_level == "strict":
580
+ # Increment violation counter
581
+ mode = manager.increment_violation()
582
+ violations = mode.violations
583
+
584
+ suggestion = create_task_suggestion(tool, params)
585
+
586
+ if enforcement_level == "strict":
587
+ # STRICT mode - advisory warning with violation count (does not block)
588
+ warning_message = (
589
+ f"🚫 ORCHESTRATOR MODE VIOLATION ({violations}/{circuit_breaker_threshold}): {reason}\n\n"
590
+ f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
591
+ f"Suggested delegation:\n"
592
+ f"{suggestion}\n\n"
593
+ )
594
+
595
+ # Add circuit breaker warning if approaching threshold
596
+ if violations >= circuit_breaker_threshold:
597
+ warning_message += (
598
+ "🚨 CIRCUIT BREAKER TRIGGERED - Further violations will be blocked!\n\n"
599
+ "Reset with: uv run htmlgraph orchestrator reset-violations\n"
600
+ )
601
+ elif violations == circuit_breaker_threshold - 1:
602
+ warning_message += "⚠️ Next violation will trigger circuit breaker!\n\n"
603
+
604
+ warning_message += (
605
+ "See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
606
+ "To disable orchestrator mode: uv run htmlgraph orchestrator disable"
607
+ )
608
+
609
+ # Advisory-only: allow operation but provide warning
610
+ return {
611
+ "continue": True,
612
+ "hookSpecificOutput": {
613
+ "hookEventName": "PreToolUse",
614
+ "permissionDecision": "allow",
615
+ "additionalContext": warning_message,
616
+ },
617
+ }
618
+ else:
619
+ # GUIDANCE mode - softer warning
620
+ warning_message = (
621
+ f"⚠️ ORCHESTRATOR: {reason}\n\nSuggested delegation:\n{suggestion}"
622
+ )
623
+
624
+ return {
625
+ "continue": True,
626
+ "hookSpecificOutput": {
627
+ "hookEventName": "PreToolUse",
628
+ "permissionDecision": "allow",
629
+ "additionalContext": warning_message,
630
+ },
631
+ }
632
+
633
+
634
+ def main() -> None:
635
+ """Hook entry point for script wrapper."""
636
+ import os
637
+ import sys
638
+
639
+ # Check if tracking is disabled
640
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
641
+ print(json.dumps({"continue": True}))
642
+ sys.exit(0)
643
+
644
+ # Check for orchestrator mode environment override
645
+ if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
646
+ print(json.dumps({"continue": True}))
647
+ sys.exit(0)
648
+
649
+ try:
650
+ hook_input = json.load(sys.stdin)
651
+ except json.JSONDecodeError:
652
+ hook_input = {}
653
+
654
+ # Get tool name and parameters (Claude Code uses "name" and "input")
655
+ tool_name = hook_input.get("name", "") or hook_input.get("tool_name", "")
656
+ tool_input = hook_input.get("input", {}) or hook_input.get("tool_input", {})
657
+
658
+ # Get session_id from hook_input (NEW: required for session-isolated history)
659
+ session_id = hook_input.get("session_id", "unknown")
660
+
661
+ if not tool_name:
662
+ # No tool name, allow
663
+ print(json.dumps({"continue": True}))
664
+ return
665
+
666
+ # Enforce orchestrator mode with session_id for history lookup
667
+ response = enforce_orchestrator_mode(tool_name, tool_input, session_id)
668
+
669
+ # Record tool event to database for history tracking
670
+ # This allows subsequent calls to detect patterns (e.g., multiple Reads)
671
+ record_tool_event(tool_name, session_id)
672
+
673
+ # Output JSON response
674
+ print(json.dumps(response))