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,471 @@
1
+ """Gemini spawner implementation."""
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import time
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from .base import AIResult, BaseSpawner
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ if TYPE_CHECKING:
14
+ from htmlgraph.sdk import SDK
15
+
16
+
17
+ class GeminiSpawner(BaseSpawner):
18
+ """Spawner for Google Gemini CLI.
19
+
20
+ Model Selection:
21
+ The `model` parameter defaults to None, which is the RECOMMENDED approach.
22
+ When model=None, the Gemini CLI automatically selects the best available model
23
+ based on the task and current availability.
24
+
25
+ As of Gemini CLI v0.22+, the default models include:
26
+ - gemini-2.5-flash-lite: Fast, efficient model for most tasks
27
+ - gemini-3-flash-preview: Preview of Gemini 3 with enhanced capabilities
28
+
29
+ Explicitly specifying a model is DISCOURAGED because:
30
+ 1. Older models (gemini-2.0-flash, gemini-1.5-flash) may fail due to
31
+ "thinking mode" incompatibility in newer CLI versions
32
+ 2. Using None automatically benefits from Google's model updates
33
+ 3. The CLI handles model selection and fallback logic
34
+
35
+ Supported models (if you must specify):
36
+ - None (recommended): CLI chooses best available model
37
+ - "gemini-2.5-flash-lite": Fast, efficient
38
+ - "gemini-3-flash-preview": Gemini 3 preview (enhanced capabilities)
39
+ - "gemini-2.5-pro": More capable, slower
40
+
41
+ DEPRECATED models (may cause errors):
42
+ - "gemini-2.0-flash": Deprecated, use None instead
43
+ - "gemini-1.5-flash": Deprecated, use None instead
44
+ - "gemini-1.5-pro": Deprecated, use None instead
45
+ """
46
+
47
+ def _parse_and_track_events(self, jsonl_output: str, sdk: "SDK") -> list[dict]:
48
+ """
49
+ Parse Gemini stream-json events and track in HtmlGraph.
50
+
51
+ Args:
52
+ jsonl_output: JSONL output from Gemini CLI
53
+ sdk: HtmlGraph SDK instance for tracking
54
+
55
+ Returns:
56
+ Parsed events list
57
+ """
58
+ events = []
59
+
60
+ for line in jsonl_output.splitlines():
61
+ if not line.strip():
62
+ continue
63
+
64
+ try:
65
+ event = json.loads(line)
66
+ events.append(event)
67
+
68
+ # Track based on event type
69
+ event_type = event.get("type")
70
+
71
+ if event_type == "tool_use":
72
+ tool_name = event.get("tool_name", "unknown_tool")
73
+ parameters = event.get("parameters", {})
74
+ self._track_activity(
75
+ sdk,
76
+ tool="gemini_tool_call",
77
+ summary=f"Gemini called {tool_name}",
78
+ payload={
79
+ "tool_name": tool_name,
80
+ "parameters": parameters,
81
+ },
82
+ )
83
+
84
+ elif event_type == "tool_result":
85
+ status = event.get("status", "unknown")
86
+ success = status == "success"
87
+ tool_id = event.get("tool_id", "unknown")
88
+ self._track_activity(
89
+ sdk,
90
+ tool="gemini_tool_result",
91
+ summary=f"Gemini tool result: {status}",
92
+ success=success,
93
+ payload={"tool_id": tool_id, "status": status},
94
+ )
95
+
96
+ elif event_type == "message":
97
+ role = event.get("role")
98
+ if role == "assistant":
99
+ content = event.get("content", "")
100
+ # Truncate for summary
101
+ summary = (
102
+ content[:100] + "..." if len(content) > 100 else content
103
+ )
104
+ self._track_activity(
105
+ sdk,
106
+ tool="gemini_message",
107
+ summary=f"Gemini: {summary}",
108
+ payload={"role": role, "content_length": len(content)},
109
+ )
110
+
111
+ elif event_type == "result":
112
+ stats = event.get("stats", {})
113
+ self._track_activity(
114
+ sdk,
115
+ tool="gemini_completion",
116
+ summary="Gemini task completed",
117
+ payload={"stats": stats},
118
+ )
119
+
120
+ except json.JSONDecodeError:
121
+ # Skip malformed lines
122
+ continue
123
+
124
+ return events
125
+
126
+ def spawn(
127
+ self,
128
+ prompt: str,
129
+ output_format: str = "stream-json",
130
+ model: str | None = None,
131
+ include_directories: list[str] | None = None,
132
+ track_in_htmlgraph: bool = True,
133
+ timeout: int = 120,
134
+ tracker: Any = None,
135
+ parent_event_id: str | None = None,
136
+ ) -> AIResult:
137
+ """
138
+ Spawn Gemini in headless mode.
139
+
140
+ Args:
141
+ prompt: Task description for Gemini
142
+ output_format: "json" or "stream-json" (enables real-time tracking)
143
+ model: Model selection. Default: None (RECOMMENDED).
144
+
145
+ When model=None (default), the Gemini CLI automatically selects
146
+ the best available model, which includes:
147
+ - gemini-2.5-flash-lite: Fast, efficient model
148
+ - gemini-3-flash-preview: Gemini 3 with enhanced capabilities
149
+
150
+ Using None is STRONGLY RECOMMENDED because:
151
+ 1. Automatically benefits from Google's latest models
152
+ 2. Avoids deprecation issues with older model names
153
+ 3. CLI handles optimal model selection and fallback
154
+
155
+ DEPRECATED models (may cause errors with CLI v0.22+):
156
+ - gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
157
+
158
+ include_directories: Directories to include for context. Default: None
159
+ track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
160
+ timeout: Max seconds to wait
161
+ tracker: Optional SpawnerEventTracker for recording subprocess invocation
162
+ parent_event_id: Optional parent event ID for event hierarchy
163
+
164
+ Returns:
165
+ AIResult with response, error, and tracked events if tracking enabled
166
+
167
+ Example:
168
+ >>> spawner = GeminiSpawner()
169
+ >>> result = spawner.spawn(
170
+ ... prompt="Analyze this codebase",
171
+ ... # model=None is the default - uses latest Gemini models
172
+ ... track_in_htmlgraph=True
173
+ ... )
174
+ """
175
+ # Initialize tracking if enabled
176
+ sdk: SDK | None = None
177
+ tracked_events: list[dict] = []
178
+ if track_in_htmlgraph:
179
+ sdk = self._get_sdk()
180
+
181
+ # Publish live event: spawner starting
182
+ self._publish_live_event(
183
+ "spawner_start",
184
+ "gemini",
185
+ prompt=prompt,
186
+ model=model,
187
+ )
188
+ start_time = time.time()
189
+
190
+ try:
191
+ # Build command based on tested pattern from spike spk-4029eef3
192
+ cmd = ["gemini", "-p", prompt, "--output-format", output_format]
193
+
194
+ # Add model option if specified
195
+ if model:
196
+ cmd.extend(["-m", model])
197
+
198
+ # Add include directories if specified
199
+ if include_directories:
200
+ for directory in include_directories:
201
+ cmd.extend(["--include-directories", directory])
202
+
203
+ # CRITICAL: Add --yolo for headless mode (auto-approve all tools)
204
+ cmd.append("--yolo")
205
+
206
+ # Track spawner start if SDK available
207
+ if sdk:
208
+ self._track_activity(
209
+ sdk,
210
+ tool="gemini_spawn_start",
211
+ summary=f"Spawning Gemini: {prompt[:80]}",
212
+ payload={"prompt_length": len(prompt), "model": model},
213
+ )
214
+
215
+ # Publish live event: executing
216
+ self._publish_live_event(
217
+ "spawner_phase",
218
+ "gemini",
219
+ phase="executing",
220
+ details="Running Gemini CLI",
221
+ )
222
+
223
+ # Record subprocess invocation if tracker is available
224
+ subprocess_event_id = None
225
+ logger.warning(
226
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
227
+ )
228
+ if tracker and parent_event_id:
229
+ logger.debug("Recording subprocess invocation for Gemini...")
230
+ try:
231
+ subprocess_event = tracker.record_tool_call(
232
+ tool_name="subprocess.gemini",
233
+ tool_input={"cmd": cmd},
234
+ phase_event_id=parent_event_id,
235
+ spawned_agent=model or "gemini-default",
236
+ )
237
+ if subprocess_event:
238
+ subprocess_event_id = subprocess_event.get("event_id")
239
+ logger.warning(
240
+ f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}"
241
+ )
242
+ else:
243
+ logger.debug("subprocess_event was None")
244
+ except Exception as e:
245
+ # Tracking failure should not break execution
246
+ logger.warning(f"DEBUG: Exception recording Gemini subprocess: {e}")
247
+ pass
248
+ else:
249
+ logger.warning(
250
+ f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
251
+ )
252
+
253
+ # Execute with timeout and stderr redirection
254
+ # Note: Cannot use capture_output with stderr parameter
255
+ result = subprocess.run(
256
+ cmd,
257
+ stdout=subprocess.PIPE,
258
+ stderr=subprocess.DEVNULL, # Redirect stderr to avoid polluting JSON
259
+ text=True,
260
+ timeout=timeout,
261
+ )
262
+
263
+ # Complete subprocess invocation tracking
264
+ if tracker and subprocess_event_id:
265
+ try:
266
+ tracker.complete_tool_call(
267
+ event_id=subprocess_event_id,
268
+ output_summary=result.stdout[:500] if result.stdout else "",
269
+ success=result.returncode == 0,
270
+ )
271
+ except Exception:
272
+ # Tracking failure should not break execution
273
+ pass
274
+
275
+ # Publish live event: processing response
276
+ self._publish_live_event(
277
+ "spawner_phase",
278
+ "gemini",
279
+ phase="processing",
280
+ details="Parsing Gemini response",
281
+ )
282
+
283
+ # Check for command execution errors
284
+ if result.returncode != 0:
285
+ duration = time.time() - start_time
286
+ self._publish_live_event(
287
+ "spawner_complete",
288
+ "gemini",
289
+ success=False,
290
+ duration=duration,
291
+ error=f"CLI failed with exit code {result.returncode}",
292
+ )
293
+ return AIResult(
294
+ success=False,
295
+ response="",
296
+ tokens_used=None,
297
+ error=f"Gemini CLI failed with exit code {result.returncode}",
298
+ raw_output=None,
299
+ tracked_events=tracked_events,
300
+ )
301
+
302
+ # Handle stream-json format with real-time tracking
303
+ if output_format == "stream-json" and sdk:
304
+ try:
305
+ tracked_events = self._parse_and_track_events(result.stdout, sdk)
306
+ # Only use stream-json parsing if we got valid events
307
+ if tracked_events:
308
+ # For stream-json, we need to extract response differently
309
+ # Collect all assistant message content, then check result
310
+ response_text = ""
311
+ for event in tracked_events:
312
+ if event.get("type") == "message":
313
+ # Only collect assistant messages
314
+ if event.get("role") == "assistant":
315
+ content = event.get("content", "")
316
+ if content:
317
+ response_text += content
318
+ elif event.get("type") == "result":
319
+ # Result event may have response field (override if present)
320
+ if "response" in event and event["response"]:
321
+ response_text = event["response"]
322
+ # Don't break - we've already collected messages
323
+
324
+ # Token usage from stats in result event
325
+ tokens = None
326
+ for event in tracked_events:
327
+ if event.get("type") == "result":
328
+ stats = event.get("stats", {})
329
+ if stats and "models" in stats:
330
+ total_tokens = 0
331
+ for model_stats in stats["models"].values():
332
+ model_tokens = model_stats.get(
333
+ "tokens", {}
334
+ ).get("total", 0)
335
+ total_tokens += model_tokens
336
+ tokens = total_tokens if total_tokens > 0 else None
337
+ break
338
+
339
+ # Publish live event: complete
340
+ duration = time.time() - start_time
341
+ self._publish_live_event(
342
+ "spawner_complete",
343
+ "gemini",
344
+ success=True,
345
+ duration=duration,
346
+ response=response_text,
347
+ tokens=tokens,
348
+ )
349
+ return AIResult(
350
+ success=True,
351
+ response=response_text,
352
+ tokens_used=tokens,
353
+ error=None,
354
+ raw_output={"events": tracked_events},
355
+ tracked_events=tracked_events,
356
+ )
357
+
358
+ except Exception:
359
+ # Fall back to regular JSON parsing if tracking fails
360
+ pass
361
+
362
+ # Parse JSON response (for json format or fallback)
363
+ try:
364
+ output = json.loads(result.stdout)
365
+ except json.JSONDecodeError as e:
366
+ duration = time.time() - start_time
367
+ self._publish_live_event(
368
+ "spawner_complete",
369
+ "gemini",
370
+ success=False,
371
+ duration=duration,
372
+ error=f"Failed to parse JSON: {e}",
373
+ )
374
+ return AIResult(
375
+ success=False,
376
+ response="",
377
+ tokens_used=None,
378
+ error=f"Failed to parse JSON output: {e}",
379
+ raw_output={"stdout": result.stdout},
380
+ tracked_events=tracked_events,
381
+ )
382
+
383
+ # Extract response and token usage from parsed output
384
+ # Response is at top level in JSON output
385
+ response_text = output.get("response", "")
386
+
387
+ # Token usage is in stats.models (sum across all models)
388
+ tokens = None
389
+ stats = output.get("stats", {})
390
+ if stats and "models" in stats:
391
+ total_tokens = 0
392
+ for model_stats in stats["models"].values():
393
+ model_tokens = model_stats.get("tokens", {}).get("total", 0)
394
+ total_tokens += model_tokens
395
+ tokens = total_tokens if total_tokens > 0 else None
396
+
397
+ # Publish live event: complete
398
+ duration = time.time() - start_time
399
+ self._publish_live_event(
400
+ "spawner_complete",
401
+ "gemini",
402
+ success=True,
403
+ duration=duration,
404
+ response=response_text,
405
+ tokens=tokens,
406
+ )
407
+ return AIResult(
408
+ success=True,
409
+ response=response_text,
410
+ tokens_used=tokens,
411
+ error=None,
412
+ raw_output=output,
413
+ tracked_events=tracked_events,
414
+ )
415
+
416
+ except subprocess.TimeoutExpired as e:
417
+ duration = time.time() - start_time
418
+ self._publish_live_event(
419
+ "spawner_complete",
420
+ "gemini",
421
+ success=False,
422
+ duration=duration,
423
+ error=f"Timed out after {timeout} seconds",
424
+ )
425
+ return AIResult(
426
+ success=False,
427
+ response="",
428
+ tokens_used=None,
429
+ error=f"Gemini CLI timed out after {timeout} seconds",
430
+ raw_output={
431
+ "partial_stdout": e.stdout.decode() if e.stdout else None,
432
+ "partial_stderr": e.stderr.decode() if e.stderr else None,
433
+ }
434
+ if e.stdout or e.stderr
435
+ else None,
436
+ tracked_events=tracked_events,
437
+ )
438
+ except FileNotFoundError:
439
+ duration = time.time() - start_time
440
+ self._publish_live_event(
441
+ "spawner_complete",
442
+ "gemini",
443
+ success=False,
444
+ duration=duration,
445
+ error="CLI not found",
446
+ )
447
+ return AIResult(
448
+ success=False,
449
+ response="",
450
+ tokens_used=None,
451
+ error="Gemini CLI not found. Ensure 'gemini' is installed and in PATH.",
452
+ raw_output=None,
453
+ tracked_events=tracked_events,
454
+ )
455
+ except Exception as e:
456
+ duration = time.time() - start_time
457
+ self._publish_live_event(
458
+ "spawner_complete",
459
+ "gemini",
460
+ success=False,
461
+ duration=duration,
462
+ error=str(e),
463
+ )
464
+ return AIResult(
465
+ success=False,
466
+ response="",
467
+ tokens_used=None,
468
+ error=f"Unexpected error: {type(e).__name__}: {e}",
469
+ raw_output=None,
470
+ tracked_events=tracked_events,
471
+ )
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ """Subprocess execution with standardized error handling.
4
+
5
+ Provides consistent error handling for Claude Code CLI invocations.
6
+ """
7
+
8
+ import logging
9
+ import subprocess
10
+ import sys
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SubprocessRunner:
16
+ """Execute subprocess commands with error handling."""
17
+
18
+ @staticmethod
19
+ def run_claude_command(cmd: list[str]) -> None:
20
+ """Execute Claude Code CLI command with error handling.
21
+
22
+ Args:
23
+ cmd: Command list (e.g., ["claude", "--resume"])
24
+
25
+ Raises:
26
+ SystemExit: If 'claude' command not found or other error
27
+ """
28
+ try:
29
+ subprocess.run(cmd, check=False)
30
+ except FileNotFoundError:
31
+ logger.warning("Error: 'claude' command not found.")
32
+ print(
33
+ "Please install Claude Code CLI: https://code.claude.com",
34
+ file=sys.stderr,
35
+ )
36
+ sys.exit(1)
@@ -1,3 +1,7 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
4
+
1
5
  """
2
6
  Orchestration helpers for reliable parallel task coordination.
3
7
 
@@ -7,9 +11,13 @@ Provides Task ID pattern for retrieving results from parallel delegations.
7
11
  import time
8
12
  import uuid
9
13
  from datetime import datetime, timedelta
10
- from typing import Any
14
+ from typing import TYPE_CHECKING, Any
11
15
 
12
- from htmlgraph.sdk import SDK
16
+ if TYPE_CHECKING:
17
+ from htmlgraph.sdk import SDK
18
+ else:
19
+ # Avoid circular import during module initialization
20
+ SDK = None
13
21
 
14
22
 
15
23
  def generate_task_id() -> str:
@@ -72,7 +80,7 @@ Provide detailed findings in your response.
72
80
 
73
81
 
74
82
  def get_results_by_task_id(
75
- sdk: SDK,
83
+ sdk: "SDK",
76
84
  task_id: str,
77
85
  timeout: int = 60,
78
86
  poll_interval: int = 2,
@@ -138,7 +146,7 @@ def get_results_by_task_id(
138
146
 
139
147
 
140
148
  def parallel_delegate(
141
- sdk: SDK,
149
+ sdk: "SDK",
142
150
  tasks: list[dict[str, str]],
143
151
  timeout: int = 120,
144
152
  ) -> dict[str, dict[str, Any]]:
@@ -161,7 +169,7 @@ def parallel_delegate(
161
169
  ])
162
170
 
163
171
  for task_id, result in results.items():
164
- print(f"{task_id}: {result['findings']}")
172
+ logger.info(f"{task_id}: {result['findings']}")
165
173
  """
166
174
  # Generate task IDs and enhanced prompts
167
175
  task_mapping = {}
@@ -189,7 +197,7 @@ def parallel_delegate(
189
197
 
190
198
 
191
199
  def save_task_results(
192
- sdk: SDK,
200
+ sdk: "SDK",
193
201
  task_id: str,
194
202
  description: str,
195
203
  results: str,
@@ -263,7 +271,7 @@ def save_task_results(
263
271
 
264
272
 
265
273
  def validate_and_save(
266
- sdk: SDK,
274
+ sdk: "SDK",
267
275
  task_id: str,
268
276
  description: str,
269
277
  results: str,
@@ -303,7 +311,7 @@ def validate_and_save(
303
311
  )
304
312
 
305
313
  if outcome["validated"]:
306
- print(f"✅ Saved to spike: {outcome['spike_id']}")
314
+ logger.info(f"✅ Saved to spike: {outcome['spike_id']}")
307
315
  """
308
316
  validated = True
309
317
  validation_results = None