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,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()
@@ -9,6 +9,7 @@ Architecture:
9
9
  - Checks if prompt already includes save instructions
10
10
  - Auto-injects SDK save template if missing
11
11
  - Returns updatedInput with modified prompt
12
+ - Tracks parent session context and nesting depth (Phase 2)
12
13
 
13
14
  Usage:
14
15
  from htmlgraph.hooks.task_enforcer import enforce_task_saving
@@ -17,6 +18,7 @@ Usage:
17
18
  # Returns: {"continue": True, "hookSpecificOutput": {"updatedInput": {...}}}
18
19
  """
19
20
 
21
+ import os
20
22
  from typing import Any
21
23
 
22
24
 
@@ -124,8 +126,95 @@ def enforce_task_saving(tool_name: str, tool_params: dict[str, Any]) -> dict[str
124
126
  if not prompt:
125
127
  return {"continue": True}
126
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
+
127
215
  # Check if save instructions already present
128
216
  if has_save_instructions(prompt):
217
+ # Even if save instructions exist, we still need to update environment
129
218
  return {"continue": True}
130
219
 
131
220
  # Detect subagent type from prompt context
@@ -146,15 +235,21 @@ def enforce_task_saving(tool_name: str, tool_params: dict[str, Any]) -> dict[str
146
235
  updated_params = tool_params.copy()
147
236
  updated_params["prompt"] = modified_prompt
148
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
+
149
247
  # Return response with updatedInput
150
248
  return {
151
249
  "continue": True,
152
250
  "hookSpecificOutput": {
153
251
  "hookEventName": "PreToolUse",
154
252
  "updatedInput": updated_params,
155
- "additionalContext": (
156
- "📝 Auto-injected HtmlGraph save instructions into Task prompt. "
157
- "Subagent will be reminded to save findings using SDK.spikes."
158
- ),
253
+ "additionalContext": context_msg,
159
254
  },
160
255
  }