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
@@ -0,0 +1,354 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
5
+ """
6
+ CIGS PreToolUse Enforcer - Enhanced Orchestrator Enforcement with Escalation
7
+
8
+ Integrates the Computational Imperative Guidance System (CIGS) into the PreToolUse
9
+ hook for intelligent delegation enforcement with escalating guidance.
10
+
11
+ Architecture:
12
+ 1. Uses existing OrchestratorValidator for base classification
13
+ 2. Loads session violation count from ViolationTracker
14
+ 3. Classifies operation using CostCalculator
15
+ 4. Generates imperative message with escalation via ImperativeMessageGenerator
16
+ 5. Records violation if should_delegate=True
17
+ 6. Returns hookSpecificOutput with imperative message
18
+
19
+ Escalation Levels:
20
+ - Level 0 (0 violations): Guidance - informative, no cost shown
21
+ - Level 1 (1 violation): Imperative - commanding, includes cost
22
+ - Level 2 (2 violations): Final Warning - urgent, includes consequences
23
+ - Level 3 (3+ violations): Circuit Breaker - blocking, requires acknowledgment
24
+
25
+ Design Reference:
26
+ .htmlgraph/spikes/computational-imperative-guidance-system-design.md
27
+ Part 2: CIGS PreToolUse Hook Integration
28
+ Part 4: Imperative Message Generation
29
+ """
30
+
31
+ import json
32
+ import os
33
+ import sys
34
+ from pathlib import Path
35
+ from typing import Any
36
+
37
+ from htmlgraph.cigs.cost import CostCalculator
38
+ from htmlgraph.cigs.messaging import ImperativeMessageGenerator
39
+ from htmlgraph.cigs.tracker import ViolationTracker
40
+ from htmlgraph.hooks.orchestrator import is_allowed_orchestrator_operation
41
+ from htmlgraph.orchestrator_mode import OrchestratorModeManager
42
+
43
+
44
+ class CIGSPreToolEnforcer:
45
+ """
46
+ CIGS-enhanced PreToolUse enforcement with escalating imperative messages.
47
+
48
+ Integrates all CIGS components for comprehensive delegation enforcement.
49
+ """
50
+
51
+ # Tools that are ALWAYS allowed (orchestrator core)
52
+ ALWAYS_ALLOWED = {"Task", "AskUserQuestion", "TodoWrite"}
53
+
54
+ # Exploration tools that require delegation after first use
55
+ EXPLORATION_TOOLS = {"Read", "Grep", "Glob"}
56
+
57
+ # Implementation tools that always require delegation
58
+ IMPLEMENTATION_TOOLS = {"Edit", "Write", "NotebookEdit", "Delete"}
59
+
60
+ def __init__(self, graph_dir: Path | None = None):
61
+ """
62
+ Initialize CIGS PreToolUse enforcer.
63
+
64
+ Args:
65
+ graph_dir: Root directory for HtmlGraph (defaults to .htmlgraph)
66
+ """
67
+ if graph_dir is None:
68
+ graph_dir = self._find_graph_dir()
69
+
70
+ self.graph_dir = graph_dir
71
+ self.manager = OrchestratorModeManager(graph_dir)
72
+ self.cost_calculator = CostCalculator()
73
+ self.message_generator = ImperativeMessageGenerator()
74
+ self.tracker = ViolationTracker(graph_dir)
75
+
76
+ # Ensure session ID is set (detect from environment or use current session)
77
+ if self.tracker._session_id is None:
78
+ self.tracker.set_session_id(self._get_or_create_session_id())
79
+
80
+ def _find_graph_dir(self) -> Path:
81
+ """Find .htmlgraph directory starting from cwd."""
82
+ cwd = Path.cwd()
83
+ graph_dir = cwd / ".htmlgraph"
84
+
85
+ if not graph_dir.exists():
86
+ for parent in [cwd.parent, cwd.parent.parent, cwd.parent.parent.parent]:
87
+ candidate = parent / ".htmlgraph"
88
+ if candidate.exists():
89
+ graph_dir = candidate
90
+ break
91
+
92
+ return graph_dir
93
+
94
+ def enforce(self, tool: str, params: dict[str, Any]) -> dict[str, Any]:
95
+ """
96
+ Enforce CIGS delegation rules with escalating guidance.
97
+
98
+ Args:
99
+ tool: Tool name (Read, Edit, Bash, etc.)
100
+ params: Tool parameters
101
+
102
+ Returns:
103
+ Hook response dict in Claude Code standard format:
104
+ {
105
+ "hookSpecificOutput": {
106
+ "hookEventName": "PreToolUse",
107
+ "permissionDecision": "allow" | "deny",
108
+ "additionalContext": "...", # If allow with guidance
109
+ "permissionDecisionReason": "...", # If deny
110
+ }
111
+ }
112
+ """
113
+ # Check if orchestrator mode is enabled
114
+ if not self.manager.is_enabled():
115
+ return self._allow()
116
+
117
+ enforcement_level = self.manager.get_enforcement_level()
118
+
119
+ # ALWAYS ALLOWED tools pass through
120
+ if tool in self.ALWAYS_ALLOWED:
121
+ return self._allow()
122
+
123
+ # Check if SDK operation (always allowed)
124
+ if self._is_sdk_operation(tool, params):
125
+ return self._allow()
126
+
127
+ # Get session violation summary
128
+ summary = self.tracker.get_session_violations()
129
+ violation_count = summary.total_violations
130
+
131
+ # Check circuit breaker (3+ violations)
132
+ if violation_count >= 3 and enforcement_level == "strict":
133
+ return self._circuit_breaker(violation_count)
134
+
135
+ # Classify operation using existing orchestrator logic
136
+ is_allowed, reason, category = is_allowed_orchestrator_operation(tool, params)
137
+
138
+ # CIGS enforces stricter rules in strict mode:
139
+ # - Even "single lookups" should be delegated (exploration tools)
140
+ # - All implementation tools should be delegated
141
+ should_delegate = False
142
+ if enforcement_level == "strict":
143
+ if tool in self.EXPLORATION_TOOLS or tool in self.IMPLEMENTATION_TOOLS:
144
+ should_delegate = True
145
+ # Override is_allowed - CIGS wants delegation even for first use
146
+ is_allowed = False
147
+
148
+ # If orchestrator allows and CIGS doesn't override, proceed
149
+ if is_allowed and not should_delegate:
150
+ return self._allow()
151
+
152
+ # Operation should be delegated - classify with cost analysis
153
+ classification = self.cost_calculator.classify_operation(
154
+ tool=tool,
155
+ params=params,
156
+ is_exploration_sequence=self._is_exploration_sequence(tool),
157
+ )
158
+
159
+ # Generate imperative message with escalation
160
+ imperative_message = self.message_generator.generate(
161
+ tool=tool,
162
+ classification=classification,
163
+ violation_count=violation_count,
164
+ autonomy_level=enforcement_level,
165
+ )
166
+
167
+ # Record violation for session tracking
168
+ predicted_waste = classification.predicted_cost - classification.optimal_cost
169
+ self.tracker.record_violation(
170
+ tool=tool,
171
+ params=params,
172
+ classification=classification,
173
+ predicted_waste=predicted_waste,
174
+ )
175
+
176
+ # Return response based on enforcement level and escalation
177
+ if enforcement_level == "strict":
178
+ # STRICT mode - deny with imperative message
179
+ return {
180
+ "hookSpecificOutput": {
181
+ "hookEventName": "PreToolUse",
182
+ "permissionDecision": "deny",
183
+ "permissionDecisionReason": imperative_message,
184
+ }
185
+ }
186
+ else:
187
+ # GUIDANCE mode - allow but with strong message
188
+ return {
189
+ "hookSpecificOutput": {
190
+ "hookEventName": "PreToolUse",
191
+ "permissionDecision": "allow",
192
+ "additionalContext": imperative_message,
193
+ }
194
+ }
195
+
196
+ def _allow(self) -> dict[str, Any]:
197
+ """Return allow response."""
198
+ return {
199
+ "hookSpecificOutput": {
200
+ "hookEventName": "PreToolUse",
201
+ "permissionDecision": "allow",
202
+ }
203
+ }
204
+
205
+ def _circuit_breaker(self, violation_count: int) -> dict[str, Any]:
206
+ """Return circuit breaker blocking response."""
207
+ message = (
208
+ "🚨 CIRCUIT BREAKER TRIGGERED\n\n"
209
+ f"You have violated delegation rules {violation_count} times this session.\n\n"
210
+ "**Violations detected:**\n"
211
+ "- Direct execution instead of delegation\n"
212
+ "- Context waste on tactical operations\n"
213
+ "- Ignored imperative guidance messages\n\n"
214
+ "**REQUIRED:** Acknowledge violations before proceeding:\n"
215
+ "`uv run htmlgraph orchestrator acknowledge-violation`\n\n"
216
+ "**OR** Change enforcement settings:\n"
217
+ "- Disable: `uv run htmlgraph orchestrator disable`\n"
218
+ "- Guidance mode: `uv run htmlgraph orchestrator set-level guidance`\n"
219
+ "- Reset violations: `uv run htmlgraph orchestrator reset-violations`"
220
+ )
221
+
222
+ return {
223
+ "hookSpecificOutput": {
224
+ "hookEventName": "PreToolUse",
225
+ "permissionDecision": "deny",
226
+ "permissionDecisionReason": message,
227
+ }
228
+ }
229
+
230
+ def _is_sdk_operation(self, tool: str, params: dict[str, Any]) -> bool:
231
+ """Check if operation is an SDK operation (always allowed)."""
232
+ if tool != "Bash":
233
+ return False
234
+
235
+ command = params.get("command", "")
236
+
237
+ # Allow htmlgraph SDK commands
238
+ if command.startswith("uv run htmlgraph ") or command.startswith("htmlgraph "):
239
+ return True
240
+
241
+ # Allow git read-only commands
242
+ if command.startswith(("git status", "git diff", "git log")):
243
+ return True
244
+
245
+ # Allow SDK inline usage
246
+ if "from htmlgraph import" in command or "import htmlgraph" in command:
247
+ return True
248
+
249
+ return False
250
+
251
+ def _is_exploration_sequence(self, tool: str) -> bool:
252
+ """Check if this is part of an exploration sequence."""
253
+ if tool not in self.EXPLORATION_TOOLS:
254
+ return False
255
+
256
+ # Check recent history for exploration pattern
257
+ # This is simplified - could use tool_history from orchestrator.py
258
+ summary = self.tracker.get_session_violations()
259
+
260
+ # If we've already had exploration violations, this is a sequence
261
+ exploration_violations = [
262
+ v for v in summary.violations if v.tool in self.EXPLORATION_TOOLS
263
+ ]
264
+
265
+ return len(exploration_violations) >= 1
266
+
267
+ def _get_or_create_session_id(self) -> str:
268
+ """Get or create a session ID for tracking."""
269
+ # Try to get from environment
270
+ if "HTMLGRAPH_SESSION_ID" in os.environ:
271
+ return os.environ["HTMLGRAPH_SESSION_ID"]
272
+
273
+ # Try to get from session manager
274
+ try:
275
+ from htmlgraph.session_manager import SessionManager
276
+
277
+ sm = SessionManager(self.graph_dir)
278
+ current = sm.get_active_session()
279
+ if current:
280
+ return str(current.id)
281
+ except Exception:
282
+ pass
283
+
284
+ # Fallback: create a session ID for this test/run
285
+ # Use a consistent ID for the process
286
+ if not hasattr(self.__class__, "_fallback_session_id"):
287
+ from uuid import uuid4
288
+
289
+ fallback_id: str = f"test-session-{uuid4().hex[:8]}"
290
+ setattr(self.__class__, "_fallback_session_id", fallback_id)
291
+ return fallback_id
292
+
293
+ return str(getattr(self.__class__, "_fallback_session_id"))
294
+
295
+
296
+ def enforce_cigs_pretool(tool_input: dict[str, Any]) -> dict[str, Any]:
297
+ """
298
+ Main entry point for CIGS PreToolUse enforcement.
299
+
300
+ Args:
301
+ tool_input: Hook input with tool name and parameters
302
+
303
+ Returns:
304
+ Hook response dict in Claude Code standard format
305
+ """
306
+ # Extract tool and params from input
307
+ tool = tool_input.get("name", "") or tool_input.get("tool_name", "")
308
+ params = tool_input.get("input", {}) or tool_input.get("tool_input", {})
309
+
310
+ # Create enforcer and run
311
+ try:
312
+ enforcer = CIGSPreToolEnforcer()
313
+ return enforcer.enforce(tool, params)
314
+ except Exception as e:
315
+ # Graceful degradation - allow on error
316
+ logger.warning(f"Warning: CIGS enforcement error: {e}")
317
+ return {
318
+ "hookSpecificOutput": {
319
+ "hookEventName": "PreToolUse",
320
+ "permissionDecision": "allow",
321
+ }
322
+ }
323
+
324
+
325
+ def main() -> None:
326
+ """Hook entry point for script wrapper."""
327
+ # Check environment overrides
328
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
329
+ print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
330
+ sys.exit(0)
331
+
332
+ if os.environ.get("HTMLGRAPH_ORCHESTRATOR_DISABLED") == "1":
333
+ print(json.dumps({"hookSpecificOutput": {"permissionDecision": "allow"}}))
334
+ sys.exit(0)
335
+
336
+ # Read tool input from stdin
337
+ try:
338
+ tool_input = json.load(sys.stdin)
339
+ except json.JSONDecodeError:
340
+ tool_input = {}
341
+
342
+ # Run CIGS enforcement
343
+ result = enforce_cigs_pretool(tool_input)
344
+
345
+ # Output response
346
+ print(json.dumps(result))
347
+
348
+ # Exit code based on permission decision
349
+ permission = result.get("hookSpecificOutput", {}).get("permissionDecision", "allow")
350
+ sys.exit(0 if permission == "allow" else 1)
351
+
352
+
353
+ if __name__ == "__main__":
354
+ main()
@@ -0,0 +1,208 @@
1
+ """
2
+ Concurrent Session Detection and Formatting.
3
+
4
+ Provides utilities to detect other active sessions and format them
5
+ for injection into the orchestrator's context at session start.
6
+ """
7
+
8
+ from datetime import datetime, timedelta, timezone
9
+ from typing import Any
10
+
11
+ from htmlgraph.db.schema import HtmlGraphDB
12
+
13
+
14
+ def get_concurrent_sessions(
15
+ db: HtmlGraphDB,
16
+ current_session_id: str,
17
+ minutes: int = 30,
18
+ ) -> list[dict[str, Any]]:
19
+ """
20
+ Get other sessions that are currently active.
21
+
22
+ Args:
23
+ db: Database connection
24
+ current_session_id: Current session to exclude
25
+ minutes: Look back window for activity
26
+
27
+ Returns:
28
+ List of concurrent session dicts with id, agent_id, last_user_query, etc.
29
+ """
30
+ if not db.connection:
31
+ db.connect()
32
+
33
+ try:
34
+ cursor = db.connection.cursor() # type: ignore[union-attr]
35
+ # Use datetime format that matches database (without timezone)
36
+ cutoff = (datetime.now(timezone.utc) - timedelta(minutes=minutes)).strftime(
37
+ "%Y-%m-%d %H:%M:%S"
38
+ )
39
+
40
+ cursor.execute(
41
+ """
42
+ SELECT
43
+ session_id as id,
44
+ agent_assigned as agent_id,
45
+ created_at,
46
+ status,
47
+ (SELECT input_summary FROM agent_events
48
+ WHERE session_id = sessions.session_id
49
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query,
50
+ (SELECT timestamp FROM agent_events
51
+ WHERE session_id = sessions.session_id
52
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query_at
53
+ FROM sessions
54
+ WHERE status = 'active'
55
+ AND session_id != ?
56
+ AND created_at > ?
57
+ ORDER BY created_at DESC
58
+ """,
59
+ (current_session_id, cutoff),
60
+ )
61
+
62
+ rows = cursor.fetchall()
63
+ return [dict(row) for row in rows]
64
+ except Exception: # pragma: no cover
65
+ # Gracefully handle database errors
66
+ return []
67
+
68
+
69
+ def format_concurrent_sessions_markdown(sessions: list[dict[str, Any]]) -> str:
70
+ """
71
+ Format concurrent sessions as markdown for context injection.
72
+
73
+ Args:
74
+ sessions: List of session dicts from get_concurrent_sessions
75
+
76
+ Returns:
77
+ Markdown formatted string for system prompt injection
78
+ """
79
+ if not sessions:
80
+ return ""
81
+
82
+ lines = ["## Concurrent Sessions (Active Now)", ""]
83
+
84
+ for session in sessions:
85
+ session_id = session.get("id", "unknown")
86
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
87
+ agent = session.get("agent_id", "unknown")
88
+ query = session.get("last_user_query", "No recent query")
89
+ last_active = session.get("last_user_query_at")
90
+
91
+ # Calculate time ago
92
+ time_ago = "unknown"
93
+ if last_active:
94
+ try:
95
+ last_dt = datetime.fromisoformat(
96
+ last_active.replace("Z", "+00:00")
97
+ if isinstance(last_active, str)
98
+ else last_active
99
+ )
100
+ delta = datetime.now(timezone.utc) - last_dt
101
+ if delta.total_seconds() < 60:
102
+ time_ago = "just now"
103
+ elif delta.total_seconds() < 3600:
104
+ time_ago = f"{int(delta.total_seconds() // 60)} min ago"
105
+ else:
106
+ time_ago = f"{int(delta.total_seconds() // 3600)} hours ago"
107
+ except (ValueError, TypeError, AttributeError):
108
+ time_ago = "unknown"
109
+
110
+ # Truncate query for display
111
+ query_display = (
112
+ query[:50] + "..." if query and len(query) > 50 else (query or "Unknown")
113
+ )
114
+
115
+ lines.append(f'- **{session_id}** ({agent}): "{query_display}" - {time_ago}')
116
+
117
+ lines.append("")
118
+ lines.append("*Coordinate with concurrent sessions to avoid duplicate work.*")
119
+ lines.append("")
120
+
121
+ return "\n".join(lines)
122
+
123
+
124
+ def get_recent_completed_sessions(
125
+ db: HtmlGraphDB,
126
+ hours: int = 24,
127
+ limit: int = 5,
128
+ ) -> list[dict[str, Any]]:
129
+ """
130
+ Get recently completed sessions for handoff context.
131
+
132
+ Args:
133
+ db: Database connection
134
+ hours: Look back window
135
+ limit: Maximum sessions to return
136
+
137
+ Returns:
138
+ List of recently completed session dicts
139
+ """
140
+ if not db.connection:
141
+ db.connect()
142
+
143
+ try:
144
+ cursor = db.connection.cursor() # type: ignore[union-attr]
145
+ # Use datetime format that matches database (without timezone)
146
+ cutoff = (datetime.now(timezone.utc) - timedelta(hours=hours)).strftime(
147
+ "%Y-%m-%d %H:%M:%S"
148
+ )
149
+ cursor.execute(
150
+ """
151
+ SELECT session_id as id, agent_assigned as agent_id, created_at as started_at,
152
+ completed_at, total_events,
153
+ (SELECT input_summary FROM agent_events
154
+ WHERE session_id = sessions.session_id
155
+ ORDER BY timestamp DESC LIMIT 1) as last_user_query
156
+ FROM sessions
157
+ WHERE status = 'completed'
158
+ AND completed_at > ?
159
+ ORDER BY completed_at DESC
160
+ LIMIT ?
161
+ """,
162
+ (cutoff, limit),
163
+ )
164
+ rows = cursor.fetchall()
165
+ return [dict(row) for row in rows]
166
+ except Exception: # pragma: no cover
167
+ # Gracefully handle database errors
168
+ return []
169
+
170
+
171
+ def format_recent_work_markdown(sessions: list[dict[str, Any]]) -> str:
172
+ """
173
+ Format recently completed sessions as markdown.
174
+
175
+ Args:
176
+ sessions: List of completed session dicts
177
+
178
+ Returns:
179
+ Markdown formatted string
180
+ """
181
+ if not sessions:
182
+ return ""
183
+
184
+ lines = ["## Recent Work (Last 24 Hours)", ""]
185
+
186
+ for session in sessions:
187
+ session_id = session.get("id", "unknown")
188
+ session_id = session_id[:12] if len(session_id) > 12 else session_id
189
+ query = session.get("last_user_query", "No query recorded")
190
+ total_events = session.get("total_events") or 0
191
+
192
+ query_display = (
193
+ query[:60] + "..." if query and len(query) > 60 else (query or "Unknown")
194
+ )
195
+
196
+ lines.append(f"- `{session_id}`: {query_display} ({total_events} events)")
197
+
198
+ lines.append("")
199
+
200
+ return "\n".join(lines)
201
+
202
+
203
+ __all__ = [
204
+ "get_concurrent_sessions",
205
+ "format_concurrent_sessions_markdown",
206
+ "get_recent_completed_sessions",
207
+ "format_recent_work_markdown",
208
+ ]