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,369 @@
1
+ """
2
+ SubagentStop Hook - Update parent events when subagents complete.
3
+
4
+ This module handles the SubagentStop hook event, which fires when a subagent
5
+ (spawned via Task()) completes. It updates the parent event with completion
6
+ status and counts child spikes created during the subagent's execution.
7
+
8
+ Architecture:
9
+ - Reads HTMLGRAPH_PARENT_EVENT from environment (set by PreToolUse hook)
10
+ - Queries database for spikes created since parent event start
11
+ - Updates parent event: status="completed", child_spike_count=N
12
+ - Handles graceful degradation if parent event not found
13
+
14
+ Parent-Child Event Nesting:
15
+ - Parent: evt-abc (Task delegation) created by PreToolUse
16
+ - Child events: spikes created by subagent during task execution
17
+ - Result: Full trace of delegation work visible in dashboard
18
+ """
19
+
20
+ import json
21
+ import logging
22
+ import os
23
+ import sqlite3
24
+ import sys
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def get_parent_event_id() -> str | None:
33
+ """
34
+ Get the parent event ID from environment.
35
+
36
+ Set by PreToolUse hook when Task() is detected.
37
+
38
+ Returns:
39
+ Parent event ID (evt-XXXXX) or None if not found
40
+ """
41
+ return os.environ.get("HTMLGRAPH_PARENT_EVENT")
42
+
43
+
44
+ def get_session_id() -> str | None:
45
+ """
46
+ Get the current session ID from environment.
47
+
48
+ Set by SessionStart hook.
49
+
50
+ Returns:
51
+ Session ID or None if not found
52
+ """
53
+ return os.environ.get("HTMLGRAPH_SESSION_ID")
54
+
55
+
56
+ def count_child_spikes(
57
+ db_path: str, parent_event_id: str, parent_start_time: str
58
+ ) -> int:
59
+ """
60
+ Count spikes created after the parent event started.
61
+
62
+ Queries the features table for spikes with created_at > parent start time.
63
+ Uses a narrow time window (5 minutes) to avoid counting unrelated spikes
64
+ from other sessions.
65
+
66
+ Args:
67
+ db_path: Path to SQLite database
68
+ parent_event_id: Parent event ID
69
+ parent_start_time: ISO8601 timestamp when parent event started
70
+
71
+ Returns:
72
+ Count of child spikes (0 if none found)
73
+ """
74
+ try:
75
+ conn = sqlite3.connect(db_path)
76
+ cursor = conn.cursor()
77
+
78
+ # Validate parent start time format (ISO8601)
79
+ try:
80
+ datetime.fromisoformat(parent_start_time)
81
+ except (ValueError, TypeError):
82
+ # If parsing fails, return 0 (couldn't validate time window)
83
+ logger.warning(f"Could not parse parent start time: {parent_start_time}")
84
+ return 0
85
+
86
+ # Query spikes created within 5 minutes after parent event
87
+ # This avoids counting unrelated spikes from other sessions
88
+ query = """
89
+ SELECT COUNT(*) FROM features
90
+ WHERE type = 'spike'
91
+ AND created_at >= ?
92
+ AND created_at <= datetime(?, '+5 minutes')
93
+ """
94
+
95
+ cursor.execute(query, (parent_start_time, parent_start_time))
96
+ result = cursor.fetchone()
97
+ count = result[0] if result else 0
98
+
99
+ conn.close()
100
+ logger.debug(f"Found {count} child spikes for parent event {parent_event_id}")
101
+ return count
102
+
103
+ except Exception as e:
104
+ logger.warning(f"Error counting child spikes: {e}")
105
+ return 0
106
+
107
+
108
+ def update_parent_event(
109
+ db_path: str,
110
+ parent_event_id: str,
111
+ child_spike_count: int,
112
+ completion_time: str | None = None,
113
+ ) -> bool:
114
+ """
115
+ Update parent event with completion status and child spike count.
116
+
117
+ Updates agent_events table:
118
+ - status: "started" → "completed"
119
+ - child_spike_count: Count of spikes created by subagent
120
+ - output_summary: JSON with completion info
121
+
122
+ Args:
123
+ db_path: Path to SQLite database
124
+ parent_event_id: Parent event ID to update
125
+ child_spike_count: Number of child spikes created
126
+ completion_time: ISO8601 timestamp (optional, defaults to now)
127
+
128
+ Returns:
129
+ True if update successful, False otherwise
130
+ """
131
+ try:
132
+ if completion_time is None:
133
+ completion_time = datetime.now(timezone.utc).isoformat()
134
+
135
+ conn = sqlite3.connect(db_path)
136
+ cursor = conn.cursor()
137
+
138
+ # Build output summary
139
+ output_summary = json.dumps(
140
+ {
141
+ "status": "completed",
142
+ "child_spike_count": child_spike_count,
143
+ "completion_time": completion_time,
144
+ }
145
+ )
146
+
147
+ # Update parent event
148
+ query = """
149
+ UPDATE agent_events
150
+ SET status = ?, child_spike_count = ?, output_summary = ?, updated_at = CURRENT_TIMESTAMP
151
+ WHERE event_id = ?
152
+ """
153
+
154
+ cursor.execute(
155
+ query,
156
+ ("completed", child_spike_count, output_summary, parent_event_id),
157
+ )
158
+
159
+ if cursor.rowcount == 0:
160
+ logger.warning(f"Parent event not found: {parent_event_id}")
161
+ conn.close()
162
+ return False
163
+
164
+ conn.commit()
165
+ conn.close()
166
+
167
+ logger.info(
168
+ f"Updated parent event {parent_event_id}: "
169
+ f"status=completed, child_spike_count={child_spike_count}"
170
+ )
171
+ return True
172
+
173
+ except Exception as e:
174
+ logger.warning(f"Error updating parent event: {e}")
175
+ return False
176
+
177
+
178
+ def get_parent_event_start_time(db_path: str, parent_event_id: str) -> str | None:
179
+ """
180
+ Get the start time of the parent event.
181
+
182
+ Used to set the time window for counting child spikes.
183
+
184
+ Args:
185
+ db_path: Path to SQLite database
186
+ parent_event_id: Parent event ID
187
+
188
+ Returns:
189
+ ISO8601 timestamp or None if not found
190
+ """
191
+ try:
192
+ conn = sqlite3.connect(db_path)
193
+ cursor = conn.cursor()
194
+
195
+ query = "SELECT timestamp FROM agent_events WHERE event_id = ?"
196
+ cursor.execute(query, (parent_event_id,))
197
+ result = cursor.fetchone()
198
+
199
+ conn.close()
200
+ return result[0] if result else None
201
+
202
+ except Exception as e:
203
+ logger.warning(f"Error getting parent event start time: {e}")
204
+ return None
205
+
206
+
207
+ def get_parent_event_from_db(db_path: str) -> str | None:
208
+ """
209
+ Query database for the most recent task_delegation event.
210
+
211
+ Used when HTMLGRAPH_PARENT_EVENT environment variable is not available
212
+ (due to inter-process communication limitations).
213
+
214
+ Args:
215
+ db_path: Path to SQLite database
216
+
217
+ Returns:
218
+ Parent event ID (evt-XXXXX) or None if not found
219
+ """
220
+ try:
221
+ conn = sqlite3.connect(db_path)
222
+ cursor = conn.cursor()
223
+
224
+ # Query for the most recent task_delegation with status='started'
225
+ # This is the task that spawned the current subagent
226
+ query = """
227
+ SELECT event_id FROM agent_events
228
+ WHERE event_type = 'task_delegation'
229
+ AND status = 'started'
230
+ ORDER BY timestamp DESC
231
+ LIMIT 1
232
+ """
233
+
234
+ cursor.execute(query)
235
+ result = cursor.fetchone()
236
+ conn.close()
237
+
238
+ if result:
239
+ parent_event_id: str = result[0]
240
+ logger.debug(
241
+ f"Found parent task_delegation from database: {parent_event_id}"
242
+ )
243
+ return parent_event_id
244
+
245
+ logger.debug("No active task_delegation found in database")
246
+ return None
247
+
248
+ except Exception as e:
249
+ logger.warning(f"Error querying for parent event: {e}")
250
+ return None
251
+
252
+
253
+ def handle_subagent_stop(hook_input: dict[str, Any]) -> dict[str, Any]:
254
+ """
255
+ Handle SubagentStop hook event.
256
+
257
+ When a subagent completes, updates the parent event with:
258
+ 1. Completion status
259
+ 2. Count of spikes created during subagent execution
260
+ 3. Completion timestamp
261
+
262
+ This closes the parent-child event trace and enables dashboard visualization
263
+ of the complete delegation hierarchy.
264
+
265
+ Args:
266
+ hook_input: Hook input data from Claude Code
267
+
268
+ Returns:
269
+ Response: {"continue": True} with optional context
270
+ """
271
+ # Try to get parent event ID from environment (set by PreToolUse hook)
272
+ parent_event_id = get_parent_event_id()
273
+
274
+ # If not available in environment, query database
275
+ # (environment variables may not be inherited across subagent process boundary)
276
+ # Get project directory and database path (reuse for both env and db lookup)
277
+ db_path = None
278
+ try:
279
+ from htmlgraph.config import get_database_path
280
+
281
+ cwd = hook_input.get("cwd", os.getcwd())
282
+ db_path = str(get_database_path(cwd))
283
+
284
+ if not Path(db_path).exists():
285
+ logger.warning(f"Database not found: {db_path}")
286
+ return {"continue": True}
287
+
288
+ except Exception as e:
289
+ logger.warning(f"Error resolving database path: {e}")
290
+ return {"continue": True}
291
+
292
+ # If parent event ID not in environment, query database
293
+ if not parent_event_id:
294
+ logger.debug("Parent event ID not in environment, querying database...")
295
+ try:
296
+ parent_event_id = get_parent_event_from_db(db_path)
297
+ except Exception as e:
298
+ logger.debug(f"Could not query database for parent event: {e}")
299
+
300
+ if not parent_event_id:
301
+ logger.debug(
302
+ "No parent event ID found (env or db), skipping subagent stop tracking"
303
+ )
304
+ return {"continue": True}
305
+
306
+ # Get parent event start time
307
+ parent_start_time = get_parent_event_start_time(db_path, parent_event_id)
308
+ if not parent_start_time:
309
+ logger.warning(f"Could not find parent event: {parent_event_id}")
310
+ return {"continue": True}
311
+
312
+ # Count child spikes
313
+ child_spike_count = count_child_spikes(db_path, parent_event_id, parent_start_time)
314
+
315
+ # Update parent event with completion info
316
+ completion_time = datetime.now(timezone.utc).isoformat()
317
+ success = update_parent_event(
318
+ db_path,
319
+ parent_event_id,
320
+ child_spike_count,
321
+ completion_time,
322
+ )
323
+
324
+ if success:
325
+ # Clear parent event from environment
326
+ os.environ.pop("HTMLGRAPH_PARENT_EVENT", None)
327
+ os.environ.pop("HTMLGRAPH_SUBAGENT_TYPE", None)
328
+
329
+ logger.info(
330
+ f"Subagent stop recorded: parent_event={parent_event_id}, "
331
+ f"child_spikes={child_spike_count}"
332
+ )
333
+
334
+ return {
335
+ "continue": True,
336
+ "hookSpecificOutput": {
337
+ "hookEventName": "SubagentStop",
338
+ "additionalContext": (
339
+ f"Task delegation completed: {child_spike_count} spike(s) created"
340
+ ),
341
+ },
342
+ }
343
+
344
+ return {"continue": True}
345
+
346
+
347
+ def main() -> None:
348
+ """Hook entry point for script wrapper."""
349
+ # Check if tracking is disabled
350
+ if os.environ.get("HTMLGRAPH_DISABLE_TRACKING") == "1":
351
+ print(json.dumps({"continue": True}))
352
+ sys.exit(0)
353
+
354
+ # Read hook input from stdin
355
+ try:
356
+ hook_input = json.load(sys.stdin)
357
+ except json.JSONDecodeError:
358
+ hook_input = {}
359
+
360
+ # Handle subagent stop
361
+ result = handle_subagent_stop(hook_input)
362
+
363
+ # Output response
364
+ print(json.dumps(result))
365
+ sys.exit(0)
366
+
367
+
368
+ if __name__ == "__main__":
369
+ main()
@@ -0,0 +1,255 @@
1
+ """
2
+ Task Enforcer - Auto-inject HtmlGraph save instructions into Task prompts.
3
+
4
+ This module provides PreToolUse enforcement for the Task tool, ensuring that
5
+ subagent prompts include instructions to save their findings to HtmlGraph.
6
+
7
+ Architecture:
8
+ - Detects Task tool calls in PreToolUse hook
9
+ - Checks if prompt already includes save instructions
10
+ - Auto-injects SDK save template if missing
11
+ - Returns updatedInput with modified prompt
12
+ - Tracks parent session context and nesting depth (Phase 2)
13
+
14
+ Usage:
15
+ from htmlgraph.hooks.task_enforcer import enforce_task_saving
16
+
17
+ result = enforce_task_saving(tool_name, tool_params)
18
+ # Returns: {"continue": True, "hookSpecificOutput": {"updatedInput": {...}}}
19
+ """
20
+
21
+ import os
22
+ from typing import Any
23
+
24
+
25
+ def has_save_instructions(prompt: str) -> bool:
26
+ """
27
+ Check if prompt already includes HtmlGraph save instructions.
28
+
29
+ Args:
30
+ prompt: Task prompt to check
31
+
32
+ Returns:
33
+ True if save instructions are present, False otherwise
34
+ """
35
+ # Keywords that indicate save instructions are already present
36
+ save_keywords = [
37
+ "sdk",
38
+ "htmlgraph",
39
+ ".save()",
40
+ "spike",
41
+ "report results",
42
+ "save findings",
43
+ "track results",
44
+ ]
45
+
46
+ prompt_lower = prompt.lower()
47
+ return any(keyword in prompt_lower for keyword in save_keywords)
48
+
49
+
50
+ def inject_save_instructions(prompt: str, subagent_type: str = "haiku") -> str:
51
+ """
52
+ Inject HtmlGraph save instructions into Task prompt.
53
+
54
+ Args:
55
+ prompt: Original task prompt
56
+ subagent_type: Type of subagent (default: "haiku")
57
+
58
+ Returns:
59
+ Modified prompt with save instructions appended
60
+ """
61
+ # Template to inject
62
+ save_template = f"""
63
+
64
+ 🔴 CRITICAL - Report Results to HtmlGraph:
65
+ After completing your research/analysis, you MUST save your findings using:
66
+
67
+ ```python
68
+ from htmlgraph import SDK
69
+ sdk = SDK(agent='{subagent_type}')
70
+ spike = sdk.spikes.create('Your Task Summary Here')
71
+ spike.set_findings(\'\'\'
72
+ # Your Comprehensive Findings
73
+
74
+ ## Summary
75
+ [Brief overview of what you discovered]
76
+
77
+ ## Key Findings
78
+ - [Finding 1]
79
+ - [Finding 2]
80
+ - [Finding 3]
81
+
82
+ ## Details
83
+ [Detailed analysis, code examples, etc.]
84
+
85
+ ## Recommendations
86
+ [Action items or next steps]
87
+ \'\'\')
88
+ spike.save()
89
+ ```
90
+
91
+ IMPORTANT:
92
+ - Replace 'Your Task Summary Here' with a concise description of your task
93
+ - Include ALL relevant findings in the set_findings() call
94
+ - This creates a permanent record that can be referenced later
95
+ - The spike will be saved to .htmlgraph/spikes/ directory
96
+ """
97
+
98
+ return prompt + save_template
99
+
100
+
101
+ def enforce_task_saving(tool_name: str, tool_params: dict[str, Any]) -> dict[str, Any]:
102
+ """
103
+ Enforce HtmlGraph result saving for Task tool calls.
104
+
105
+ Args:
106
+ tool_name: Name of the tool being called
107
+ tool_params: Tool parameters (includes "prompt" for Task)
108
+
109
+ Returns:
110
+ Hook response with updatedInput if modifications needed:
111
+ {
112
+ "continue": True,
113
+ "hookSpecificOutput": {
114
+ "hookEventName": "PreToolUse",
115
+ "updatedInput": {...}, # Modified tool_params
116
+ "additionalContext": "..." # Optional guidance
117
+ }
118
+ }
119
+ """
120
+ # Only process Task tool calls
121
+ if tool_name != "Task":
122
+ return {"continue": True}
123
+
124
+ # Get prompt from tool params
125
+ prompt = tool_params.get("prompt", "")
126
+ if not prompt:
127
+ return {"continue": True}
128
+
129
+ # Phase 2: Track parent session context and increment nesting depth
130
+ parent_session = os.environ.get("HTMLGRAPH_PARENT_SESSION")
131
+ parent_agent = os.environ.get("HTMLGRAPH_PARENT_AGENT", "claude-code")
132
+ nesting_depth = int(os.environ.get("HTMLGRAPH_NESTING_DEPTH", "0"))
133
+ current_session = os.environ.get("HTMLGRAPH_SESSION_ID", "")
134
+
135
+ # Record delegation event in database
136
+ try:
137
+ import uuid
138
+
139
+ from htmlgraph.db.schema import HtmlGraphDB
140
+
141
+ db = HtmlGraphDB()
142
+ db.connect()
143
+
144
+ # Extract to_agent from subagent_type (e.g., "haiku" -> "haiku", "gemini-spawner" -> "gemini-spawner")
145
+ to_agent = tool_params.get("subagent_type", "unknown")
146
+ task_description = tool_params.get("description", "Unnamed task")
147
+
148
+ # Determine session ID, using current > parent > auto-generated
149
+ session_id = (
150
+ current_session or parent_session or f"session-{uuid.uuid4().hex[:8]}"
151
+ )
152
+
153
+ # Ensure session exists before recording delegation (handles FK constraints)
154
+ db._ensure_session_exists(session_id, parent_agent)
155
+
156
+ # Record the delegation event
157
+ db.record_delegation_event(
158
+ from_agent=parent_agent,
159
+ to_agent=to_agent,
160
+ task_description=task_description,
161
+ session_id=session_id,
162
+ context={
163
+ "nesting_depth": nesting_depth,
164
+ "prompt_preview": prompt[:200] if prompt else "",
165
+ },
166
+ )
167
+
168
+ db.close()
169
+ except Exception:
170
+ # Graceful degradation - continue even if delegation tracking fails
171
+ pass
172
+
173
+ # Track Task invocation as activity (if parent session exists)
174
+ task_activity_id = None
175
+ if parent_session:
176
+ try:
177
+ from htmlgraph import SDK
178
+
179
+ sdk = SDK(agent=parent_agent, parent_session=parent_session)
180
+
181
+ # Track Task invocation
182
+ entry = sdk.track_activity(
183
+ tool="Task",
184
+ summary=f"Task invoked: {tool_params.get('description', 'Unnamed task')[:100]}",
185
+ payload={
186
+ "subagent_type": tool_params.get("subagent_type"),
187
+ "description": tool_params.get("description"),
188
+ "prompt_preview": prompt[:200] if prompt else "",
189
+ "nesting_depth": nesting_depth,
190
+ },
191
+ success=True,
192
+ )
193
+
194
+ if entry:
195
+ task_activity_id = entry.id
196
+
197
+ except Exception:
198
+ # Graceful degradation - continue even if tracking fails
199
+ pass
200
+
201
+ # Increment nesting depth for child
202
+ new_depth = nesting_depth + 1
203
+
204
+ # Set parent activity and increment depth in environment
205
+ if task_activity_id:
206
+ os.environ["HTMLGRAPH_PARENT_ACTIVITY"] = task_activity_id
207
+
208
+ os.environ["HTMLGRAPH_NESTING_DEPTH"] = str(new_depth)
209
+
210
+ # Warn about runaway recursion
211
+ warning = ""
212
+ if new_depth > 3:
213
+ warning = f"\n⚠️ Warning: Nesting depth exceeds 3 levels (depth={new_depth}). Consider flattening task hierarchy."
214
+
215
+ # Check if save instructions already present
216
+ if has_save_instructions(prompt):
217
+ # Even if save instructions exist, we still need to update environment
218
+ return {"continue": True}
219
+
220
+ # Detect subagent type from prompt context
221
+ prompt_lower = prompt.lower()
222
+ if "haiku" in prompt_lower:
223
+ subagent_type = "haiku"
224
+ elif "sonnet" in prompt_lower:
225
+ subagent_type = "sonnet"
226
+ elif "opus" in prompt_lower:
227
+ subagent_type = "opus"
228
+ else:
229
+ subagent_type = "haiku" # Default to haiku for most subagents
230
+
231
+ # Inject save instructions
232
+ modified_prompt = inject_save_instructions(prompt, subagent_type)
233
+
234
+ # Create updated tool params
235
+ updated_params = tool_params.copy()
236
+ updated_params["prompt"] = modified_prompt
237
+
238
+ # Build context message
239
+ context_msg = (
240
+ f"📝 Auto-injected HtmlGraph save instructions into Task prompt. "
241
+ f"Subagent will be reminded to save findings using SDK.spikes. "
242
+ f"(depth={new_depth}, parent={parent_session[:12] if parent_session else 'none'})"
243
+ )
244
+ if warning:
245
+ context_msg += warning
246
+
247
+ # Return response with updatedInput
248
+ return {
249
+ "continue": True,
250
+ "hookSpecificOutput": {
251
+ "hookEventName": "PreToolUse",
252
+ "updatedInput": updated_params,
253
+ "additionalContext": context_msg,
254
+ },
255
+ }