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
  Orchestrator Enforcement Module
3
7
 
@@ -10,6 +14,8 @@ Architecture:
10
14
  - Classifies operations into ALLOWED vs BLOCKED categories
11
15
  - Tracks tool usage sequences to detect exploration patterns
12
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
13
19
 
14
20
  Operation Categories:
15
21
  1. ALWAYS ALLOWED - Task, AskUserQuestion, TodoWrite, SDK operations
@@ -21,85 +27,154 @@ Enforcement Levels:
21
27
  - guidance: ALLOWS but provides warnings and suggestions
22
28
 
23
29
  Public API:
24
- - enforce_orchestrator_mode(tool: str, params: dict) -> dict
30
+ - enforce_orchestrator_mode(tool: str, params: dict[str, Any]) -> dict
25
31
  Main entry point for hook scripts. Returns hook response dict.
26
32
  """
27
33
 
28
34
  import json
29
35
  import re
30
- from datetime import datetime, timezone
31
36
  from pathlib import Path
32
- from typing import Any, cast
37
+ from typing import Any
33
38
 
39
+ from htmlgraph.hooks.subagent_detection import is_subagent_context
40
+ from htmlgraph.orchestrator_config import load_orchestrator_config
34
41
  from htmlgraph.orchestrator_mode import OrchestratorModeManager
35
42
  from htmlgraph.orchestrator_validator import OrchestratorValidator
36
43
 
37
- # Tool history file (temporary storage for session)
38
- TOOL_HISTORY_FILE = Path("/tmp/htmlgraph-tool-history.json")
44
+ # Maximum number of recent tool calls to consider for pattern detection
39
45
  MAX_HISTORY_SIZE = 50 # Keep last 50 tool calls
40
46
 
41
47
 
42
- def load_tool_history() -> list[dict]:
48
+ def load_tool_history(session_id: str) -> list[dict]:
43
49
  """
44
- Load recent tool history from temp file.
50
+ Load recent tool history from database (session-isolated).
51
+
52
+ Args:
53
+ session_id: Session identifier to filter tool history
45
54
 
46
55
  Returns:
47
56
  List of recent tool calls with tool name and timestamp
48
57
  """
49
- if not TOOL_HISTORY_FILE.exists():
50
- return []
51
-
52
58
  try:
53
- data = json.loads(TOOL_HISTORY_FILE.read_text())
54
- # Handle both formats: {"history": [...]} and [...] (legacy)
55
- if isinstance(data, list):
56
- return cast(list[dict[Any, Any]], data)
57
- return cast(list[dict[Any, Any]], data.get("history", []))
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)]
58
96
  except Exception:
97
+ # Graceful degradation - return empty history on error
59
98
  return []
60
99
 
61
100
 
62
- def save_tool_history(history: list[dict]) -> None:
101
+ def record_tool_event(tool_name: str, session_id: str) -> None:
63
102
  """
64
- Save tool history to temp file.
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.
65
107
 
66
108
  Args:
67
- history: List of tool calls to persist
109
+ tool_name: Name of the tool being called
110
+ session_id: Session identifier for isolation
68
111
  """
69
112
  try:
70
- # Keep only recent history
71
- recent = (
72
- history[-MAX_HISTORY_SIZE:] if len(history) > MAX_HISTORY_SIZE else history
73
- )
74
- TOOL_HISTORY_FILE.write_text(json.dumps({"history": recent}, indent=2))
75
- except Exception:
76
- pass # Fail silently on history save errors
113
+ import datetime
114
+ import uuid
77
115
 
116
+ from htmlgraph.db.schema import HtmlGraphDB
78
117
 
79
- def add_to_tool_history(tool: str) -> None:
80
- """
81
- Add a tool call to history.
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
82
127
 
83
- Args:
84
- tool: Name of the tool being called
85
- """
86
- history = load_tool_history()
87
- history.append(
88
- {
89
- "tool": tool,
90
- "timestamp": datetime.now(timezone.utc).isoformat(),
91
- }
92
- )
93
- save_tool_history(history)
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
94
166
 
95
167
 
96
- def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, str, str]:
168
+ def is_allowed_orchestrator_operation(
169
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
170
+ ) -> tuple[bool, str, str]:
97
171
  """
98
172
  Check if operation is allowed for orchestrators.
99
173
 
100
174
  Args:
101
175
  tool: Tool name (e.g., "Read", "Edit", "Bash")
102
176
  params: Tool parameters dict
177
+ session_id: Session identifier for loading tool history
103
178
 
104
179
  Returns:
105
180
  Tuple of (is_allowed, reason_if_not, category)
@@ -107,6 +182,23 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
107
182
  - reason_if_not: Explanation if blocked (empty if allowed)
108
183
  - category: Operation category for logging
109
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
+
110
202
  # Use OrchestratorValidator for comprehensive validation
111
203
  validator = OrchestratorValidator()
112
204
  result, reason = validator.validate_tool_use(tool, params)
@@ -121,6 +213,10 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
121
213
  if tool in ["Task", "AskUserQuestion", "TodoWrite"]:
122
214
  return True, "", "orchestrator-core"
123
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
+
124
220
  # Category 2: SDK Operations - Always allowed
125
221
  if tool == "Bash":
126
222
  command = params.get("command", "")
@@ -129,22 +225,66 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
129
225
  if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
130
226
  return True, "", "sdk-command"
131
227
 
132
- # Allow git read-only commands
133
- if (
134
- command.startswith("git status")
135
- or command.startswith("git diff")
136
- or command.startswith("git log")
137
- ):
138
- return True, "", "git-readonly"
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"
139
234
 
140
235
  # Allow SDK inline usage (Python inline with htmlgraph import)
141
236
  if "from htmlgraph import" in command or "import htmlgraph" in command:
142
237
  return True, "", "sdk-inline"
143
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
+
144
267
  # Category 3: Quick Lookups - Single operations only
145
268
  if tool in ["Read", "Grep", "Glob"]:
146
269
  # Check tool history to see if this is a single lookup or part of a sequence
147
- history = load_tool_history()
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
+ )
148
288
 
149
289
  # Look at last 3 tool calls
150
290
  recent_same_tool = sum(1 for h in history[-3:] if h["tool"] == tool)
@@ -181,7 +321,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
181
321
  command = params.get("command", "")
182
322
 
183
323
  # Block compilation, testing, building (should be in subagent)
184
- blocked_patterns = [
324
+ test_build_patterns: list[tuple[str, str]] = [
185
325
  (r"^npm (run|test|build)", "npm test/build"),
186
326
  (r"^pytest", "pytest"),
187
327
  (r"^uv run pytest", "pytest"),
@@ -191,7 +331,7 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
191
331
  (r"^make (test|build)", "make test/build"),
192
332
  ]
193
333
 
194
- for pattern, name in blocked_patterns:
334
+ for pattern, name in test_build_patterns:
195
335
  if re.match(pattern, command):
196
336
  return (
197
337
  False,
@@ -200,11 +340,14 @@ def is_allowed_orchestrator_operation(tool: str, params: dict) -> tuple[bool, st
200
340
  "test-build-blocked",
201
341
  )
202
342
 
203
- # Default: Allow with guidance
204
- return True, "Allowed but consider if delegation would be better", "allowed-default"
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"
205
348
 
206
349
 
207
- def create_task_suggestion(tool: str, params: dict) -> str:
350
+ def create_task_suggestion(tool: str, params: dict[str, Any]) -> str:
208
351
  """
209
352
  Create Task tool suggestion based on blocked operation.
210
353
 
@@ -306,21 +449,37 @@ def create_task_suggestion(tool: str, params: dict) -> str:
306
449
  )
307
450
 
308
451
 
309
- def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
452
+ def enforce_orchestrator_mode(
453
+ tool: str, params: dict[str, Any], session_id: str = "unknown"
454
+ ) -> dict[str, Any]:
310
455
  """
311
456
  Enforce orchestrator mode rules.
312
457
 
313
458
  This is the main public API for hook scripts. It checks if orchestrator mode
314
459
  is enabled, classifies the operation, and returns a hook response dict.
315
460
 
461
+ Subagents spawned via Task() have unrestricted tool access.
462
+ Detection uses 5-level strategy: env vars, session state, database.
463
+
316
464
  Args:
317
465
  tool: Tool being called
318
466
  params: Tool parameters
467
+ session_id: Session identifier for loading tool history
319
468
 
320
469
  Returns:
321
470
  Hook response dict with decision (allow/block) and guidance
322
471
  Format: {"continue": bool, "hookSpecificOutput": {...}}
323
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
+
324
483
  # Get manager and check if mode is enabled
325
484
  try:
326
485
  # Look for .htmlgraph directory starting from cwd
@@ -338,31 +497,59 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
338
497
  manager = OrchestratorModeManager(graph_dir)
339
498
 
340
499
  if not manager.is_enabled():
341
- # Mode not active, allow everything
342
- add_to_tool_history(tool)
343
- return {
344
- "hookSpecificOutput": {
345
- "hookEventName": "PreToolUse",
346
- "permissionDecision": "allow",
347
- },
348
- }
500
+ # Mode not active, allow everything with no additional output
501
+ return {"continue": True}
349
502
 
350
503
  enforcement_level = manager.get_enforcement_level()
351
504
  except Exception:
352
505
  # If we can't check mode, fail open (allow)
353
- add_to_tool_history(tool)
354
506
  return {
507
+ "continue": True,
355
508
  "hookSpecificOutput": {
356
509
  "hookEventName": "PreToolUse",
357
510
  "permissionDecision": "allow",
358
511
  },
359
512
  }
360
513
 
361
- # Check if operation is allowed
362
- is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
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
+ )
363
550
 
364
- # Add to history (for sequence detection)
365
- add_to_tool_history(tool)
551
+ # Note: Tool recording is now handled by track-event.py PostToolUse hook
552
+ # No need to call add_to_tool_history() here
366
553
 
367
554
  # Operation is allowed
368
555
  if is_allowed:
@@ -373,6 +560,7 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
373
560
  ):
374
561
  # Provide guidance even when allowing
375
562
  return {
563
+ "continue": True,
376
564
  "hookSpecificOutput": {
377
565
  "hookEventName": "PreToolUse",
378
566
  "permissionDecision": "allow",
@@ -380,32 +568,51 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
380
568
  },
381
569
  }
382
570
  return {
571
+ "continue": True,
383
572
  "hookSpecificOutput": {
384
573
  "hookEventName": "PreToolUse",
385
574
  "permissionDecision": "allow",
386
575
  },
387
576
  }
388
577
 
389
- # Operation not allowed - provide strong warnings
390
- # NOTE: {"continue": False} doesn't work in Claude Code, so we use advisory warnings only
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
+
391
584
  suggestion = create_task_suggestion(tool, params)
392
585
 
393
586
  if enforcement_level == "strict":
394
- # STRICT mode - loud warning but allow (blocking doesn't work)
395
- error_message = (
396
- f"🚫 ORCHESTRATOR MODE VIOLATION: {reason}\n\n"
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"
397
590
  f"⚠️ WARNING: Direct operations waste context and break delegation pattern!\n\n"
398
591
  f"Suggested delegation:\n"
399
592
  f"{suggestion}\n\n"
400
- f"See ORCHESTRATOR_DIRECTIVES in session context for HtmlGraph delegation pattern.\n"
401
- f"To disable orchestrator mode: uv run htmlgraph orchestrator disable"
402
593
  )
403
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
404
610
  return {
611
+ "continue": True,
405
612
  "hookSpecificOutput": {
406
613
  "hookEventName": "PreToolUse",
407
- "permissionDecision": "deny",
408
- "permissionDecisionReason": error_message,
614
+ "permissionDecision": "allow",
615
+ "additionalContext": warning_message,
409
616
  },
410
617
  }
411
618
  else:
@@ -415,9 +622,53 @@ def enforce_orchestrator_mode(tool: str, params: dict) -> dict:
415
622
  )
416
623
 
417
624
  return {
625
+ "continue": True,
418
626
  "hookSpecificOutput": {
419
627
  "hookEventName": "PreToolUse",
420
628
  "permissionDecision": "allow",
421
629
  "additionalContext": warning_message,
422
630
  },
423
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))
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Orchestrator Reflection Module
3
7
 
@@ -22,7 +26,7 @@ Usage:
22
26
  """
23
27
 
24
28
  import re
25
- from typing import TypedDict
29
+ from typing import Any, TypedDict
26
30
 
27
31
 
28
32
  class HookSpecificOutput(TypedDict):
@@ -93,7 +97,7 @@ def is_python_execution(command: str) -> bool:
93
97
  return False
94
98
 
95
99
 
96
- def should_reflect(hook_input: dict) -> tuple[bool, str]:
100
+ def should_reflect(hook_input: dict[str, Any]) -> tuple[bool, str]:
97
101
  """
98
102
  Check if we should show reflection prompt.
99
103
 
@@ -156,7 +160,7 @@ Ask yourself:
156
160
  Continue, but consider delegation for similar future tasks."""
157
161
 
158
162
 
159
- def orchestrator_reflect(tool_input: dict) -> dict:
163
+ def orchestrator_reflect(tool_input: dict[str, Any]) -> dict[str, Any]:
160
164
  """
161
165
  Main API function for orchestrator reflection.
162
166
 
@@ -184,7 +188,7 @@ def orchestrator_reflect(tool_input: dict) -> dict:
184
188
  should_show, command_preview = should_reflect(tool_input)
185
189
 
186
190
  # Build response
187
- response: dict = {"continue": True}
191
+ response: dict[str, Any] = {"continue": True}
188
192
 
189
193
  if should_show:
190
194
  reflection = build_reflection_message(command_preview)
@@ -194,3 +198,26 @@ def orchestrator_reflect(tool_input: dict) -> dict:
194
198
  }
195
199
 
196
200
  return response
201
+
202
+
203
+ def main() -> None:
204
+ """Hook entry point for script wrapper."""
205
+ import json
206
+ import os
207
+ import sys
208
+
209
+ # Check if tracking is disabled
210
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
211
+ print(json.dumps({"continue": True}))
212
+ sys.exit(0)
213
+
214
+ try:
215
+ hook_input = json.load(sys.stdin)
216
+ except json.JSONDecodeError:
217
+ hook_input = {}
218
+
219
+ # Run reflection logic
220
+ response = orchestrator_reflect(hook_input)
221
+
222
+ # Output JSON response
223
+ print(json.dumps(response))