htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__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 (155) hide show
  1. htmlgraph/__init__.py +23 -1
  2. htmlgraph/__init__.pyi +123 -0
  3. htmlgraph/agent_registry.py +2 -1
  4. htmlgraph/analytics/cli.py +3 -3
  5. htmlgraph/analytics/cost_analyzer.py +5 -1
  6. htmlgraph/analytics/cross_session.py +13 -9
  7. htmlgraph/analytics/dependency.py +10 -6
  8. htmlgraph/analytics/work_type.py +15 -11
  9. htmlgraph/analytics_index.py +2 -1
  10. htmlgraph/api/main.py +114 -51
  11. htmlgraph/api/templates/dashboard-redesign.html +3 -3
  12. htmlgraph/api/templates/dashboard.html +3 -3
  13. htmlgraph/api/templates/partials/work-items.html +613 -0
  14. htmlgraph/attribute_index.py +2 -1
  15. htmlgraph/builders/base.py +2 -1
  16. htmlgraph/builders/bug.py +2 -1
  17. htmlgraph/builders/chore.py +2 -1
  18. htmlgraph/builders/epic.py +2 -1
  19. htmlgraph/builders/feature.py +2 -1
  20. htmlgraph/builders/insight.py +2 -1
  21. htmlgraph/builders/metric.py +2 -1
  22. htmlgraph/builders/pattern.py +2 -1
  23. htmlgraph/builders/phase.py +2 -1
  24. htmlgraph/builders/spike.py +2 -1
  25. htmlgraph/builders/track.py +28 -1
  26. htmlgraph/cli/analytics.py +2 -1
  27. htmlgraph/cli/base.py +33 -8
  28. htmlgraph/cli/core.py +2 -1
  29. htmlgraph/cli/main.py +2 -1
  30. htmlgraph/cli/models.py +2 -1
  31. htmlgraph/cli/templates/cost_dashboard.py +2 -1
  32. htmlgraph/cli/work/__init__.py +76 -1
  33. htmlgraph/cli/work/browse.py +115 -0
  34. htmlgraph/cli/work/features.py +2 -1
  35. htmlgraph/cli/work/orchestration.py +2 -1
  36. htmlgraph/cli/work/report.py +2 -1
  37. htmlgraph/cli/work/sessions.py +2 -1
  38. htmlgraph/cli/work/snapshot.py +559 -0
  39. htmlgraph/cli/work/tracks.py +2 -1
  40. htmlgraph/collections/base.py +43 -4
  41. htmlgraph/collections/bug.py +2 -1
  42. htmlgraph/collections/chore.py +2 -1
  43. htmlgraph/collections/epic.py +2 -1
  44. htmlgraph/collections/feature.py +2 -1
  45. htmlgraph/collections/insight.py +2 -1
  46. htmlgraph/collections/metric.py +2 -1
  47. htmlgraph/collections/pattern.py +2 -1
  48. htmlgraph/collections/phase.py +2 -1
  49. htmlgraph/collections/session.py +12 -7
  50. htmlgraph/collections/spike.py +6 -1
  51. htmlgraph/collections/task_delegation.py +7 -2
  52. htmlgraph/collections/todo.py +14 -1
  53. htmlgraph/collections/traces.py +15 -10
  54. htmlgraph/context_analytics.py +2 -1
  55. htmlgraph/converter.py +11 -0
  56. htmlgraph/dependency_models.py +2 -1
  57. htmlgraph/edge_index.py +2 -1
  58. htmlgraph/event_log.py +81 -66
  59. htmlgraph/event_migration.py +2 -1
  60. htmlgraph/file_watcher.py +12 -8
  61. htmlgraph/find_api.py +2 -1
  62. htmlgraph/git_events.py +6 -2
  63. htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
  64. htmlgraph/hooks/drift_handler.py +3 -3
  65. htmlgraph/hooks/event_tracker.py +40 -61
  66. htmlgraph/hooks/installer.py +5 -1
  67. htmlgraph/hooks/orchestrator.py +92 -14
  68. htmlgraph/hooks/orchestrator_reflector.py +4 -0
  69. htmlgraph/hooks/post_tool_use_failure.py +7 -3
  70. htmlgraph/hooks/posttooluse.py +4 -0
  71. htmlgraph/hooks/prompt_analyzer.py +5 -5
  72. htmlgraph/hooks/session_handler.py +5 -2
  73. htmlgraph/hooks/session_summary.py +6 -2
  74. htmlgraph/hooks/validator.py +8 -4
  75. htmlgraph/ids.py +2 -1
  76. htmlgraph/learning.py +2 -1
  77. htmlgraph/mcp_server.py +2 -1
  78. htmlgraph/models.py +18 -1
  79. htmlgraph/operations/analytics.py +2 -1
  80. htmlgraph/operations/bootstrap.py +2 -1
  81. htmlgraph/operations/events.py +2 -1
  82. htmlgraph/operations/fastapi_server.py +2 -1
  83. htmlgraph/operations/hooks.py +2 -1
  84. htmlgraph/operations/initialization.py +2 -1
  85. htmlgraph/operations/server.py +2 -1
  86. htmlgraph/orchestration/__init__.py +4 -0
  87. htmlgraph/orchestration/claude_launcher.py +23 -20
  88. htmlgraph/orchestration/command_builder.py +2 -1
  89. htmlgraph/orchestration/headless_spawner.py +6 -2
  90. htmlgraph/orchestration/model_selection.py +7 -3
  91. htmlgraph/orchestration/plugin_manager.py +25 -21
  92. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  93. htmlgraph/orchestration/spawners/claude.py +5 -2
  94. htmlgraph/orchestration/spawners/codex.py +12 -19
  95. htmlgraph/orchestration/spawners/copilot.py +13 -18
  96. htmlgraph/orchestration/spawners/gemini.py +12 -19
  97. htmlgraph/orchestration/subprocess_runner.py +6 -3
  98. htmlgraph/orchestration/task_coordination.py +16 -8
  99. htmlgraph/orchestrator.py +2 -1
  100. htmlgraph/parallel.py +2 -1
  101. htmlgraph/query_builder.py +2 -1
  102. htmlgraph/reflection.py +2 -1
  103. htmlgraph/refs.py +344 -0
  104. htmlgraph/repo_hash.py +2 -1
  105. htmlgraph/sdk/__init__.py +398 -0
  106. htmlgraph/sdk/__init__.pyi +14 -0
  107. htmlgraph/sdk/analytics/__init__.py +19 -0
  108. htmlgraph/sdk/analytics/engine.py +155 -0
  109. htmlgraph/sdk/analytics/helpers.py +178 -0
  110. htmlgraph/sdk/analytics/registry.py +109 -0
  111. htmlgraph/sdk/base.py +484 -0
  112. htmlgraph/sdk/constants.py +216 -0
  113. htmlgraph/sdk/core.pyi +308 -0
  114. htmlgraph/sdk/discovery.py +120 -0
  115. htmlgraph/sdk/help/__init__.py +12 -0
  116. htmlgraph/sdk/help/mixin.py +699 -0
  117. htmlgraph/sdk/mixins/__init__.py +15 -0
  118. htmlgraph/sdk/mixins/attribution.py +113 -0
  119. htmlgraph/sdk/mixins/mixin.py +410 -0
  120. htmlgraph/sdk/operations/__init__.py +12 -0
  121. htmlgraph/sdk/operations/mixin.py +427 -0
  122. htmlgraph/sdk/orchestration/__init__.py +17 -0
  123. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  124. htmlgraph/sdk/orchestration/spawner.py +204 -0
  125. htmlgraph/sdk/planning/__init__.py +19 -0
  126. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  127. htmlgraph/sdk/planning/mixin.py +211 -0
  128. htmlgraph/sdk/planning/parallel.py +186 -0
  129. htmlgraph/sdk/planning/queue.py +210 -0
  130. htmlgraph/sdk/planning/recommendations.py +87 -0
  131. htmlgraph/sdk/planning/smart_planning.py +319 -0
  132. htmlgraph/sdk/session/__init__.py +19 -0
  133. htmlgraph/sdk/session/continuity.py +57 -0
  134. htmlgraph/sdk/session/handoff.py +110 -0
  135. htmlgraph/sdk/session/info.py +309 -0
  136. htmlgraph/sdk/session/manager.py +103 -0
  137. htmlgraph/server.py +21 -17
  138. htmlgraph/session_manager.py +1 -7
  139. htmlgraph/session_warning.py +2 -1
  140. htmlgraph/sessions/handoff.py +10 -3
  141. htmlgraph/system_prompts.py +2 -1
  142. htmlgraph/track_builder.py +14 -1
  143. htmlgraph/transcript.py +2 -1
  144. htmlgraph/watch.py +2 -1
  145. htmlgraph/work_type_utils.py +2 -1
  146. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
  147. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
  148. htmlgraph/sdk.py +0 -3430
  149. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
  150. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
  151. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  152. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  153. {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  154. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
  155. {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,383 @@
1
+ """Spawner event tracking helper for internal activity tracking in spawned sessions.
2
+
3
+ This module provides utilities for tracking internal activities within spawner agents
4
+ and linking them to parent delegation events for observability in HtmlGraph.
5
+
6
+ Usage:
7
+ from htmlgraph.orchestration.spawner_event_tracker import SpawnerEventTracker
8
+
9
+ tracker = SpawnerEventTracker(
10
+ delegation_event_id="event-abc123",
11
+ parent_agent="orchestrator",
12
+ spawner_type="gemini"
13
+ )
14
+
15
+ # Track initialization phase
16
+ init_event = tracker.record_phase("Initializing Spawner", spawned_agent="gemini-2.0-flash")
17
+
18
+ # Track execution phase
19
+ exec_event = tracker.record_phase("Executing Gemini", tool_name="gemini-cli")
20
+ tracker.complete_phase(exec_event["event_id"], output_summary="Generated output...")
21
+
22
+ # Track completion
23
+ tracker.record_completion(success=True, response="Result here")
24
+ """
25
+
26
+ import os
27
+ import time
28
+ import uuid
29
+ from typing import Any
30
+
31
+
32
+ class SpawnerEventTracker:
33
+ """Track internal activities in spawner agents with parent-child linking."""
34
+
35
+ def __init__(
36
+ self,
37
+ delegation_event_id: str | None = None,
38
+ parent_agent: str = "orchestrator",
39
+ spawner_type: str = "generic",
40
+ session_id: str | None = None,
41
+ ):
42
+ """
43
+ Initialize spawner event tracker.
44
+
45
+ Args:
46
+ delegation_event_id: Parent delegation event ID to link to
47
+ parent_agent: Agent that initiated the spawning
48
+ spawner_type: Type of spawner (gemini, codex, copilot)
49
+ session_id: Session ID for events
50
+ """
51
+ self.delegation_event_id = delegation_event_id
52
+ self.parent_agent = parent_agent
53
+ self.spawner_type = spawner_type
54
+ self.session_id = session_id or f"session-{uuid.uuid4().hex[:8]}"
55
+ self.db = None
56
+ self.phase_events: dict[str, dict[str, Any]] = {}
57
+ self.start_time = time.time()
58
+
59
+ # Try to initialize database for event tracking
60
+ try:
61
+ from htmlgraph.config import get_database_path
62
+ from htmlgraph.db.schema import HtmlGraphDB
63
+
64
+ # Get correct database path from environment or project root
65
+ db_path = get_database_path()
66
+
67
+ if db_path.exists():
68
+ self.db = HtmlGraphDB(str(db_path))
69
+ except Exception:
70
+ # Tracking is optional, continue without it
71
+ pass
72
+
73
+ def record_phase(
74
+ self,
75
+ phase_name: str,
76
+ spawned_agent: str | None = None,
77
+ tool_name: str | None = None,
78
+ input_summary: str | None = None,
79
+ status: str = "running",
80
+ parent_phase_event_id: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ """
83
+ Record the start of a phase in spawner execution.
84
+
85
+ Args:
86
+ phase_name: Human-readable phase name (e.g., "Initializing Spawner")
87
+ spawned_agent: Agent being spawned (optional)
88
+ tool_name: Tool being executed (optional)
89
+ input_summary: Summary of input (optional)
90
+ status: Current status (running, completed, failed)
91
+ parent_phase_event_id: Parent phase event ID for proper nesting (optional)
92
+
93
+ Returns:
94
+ Event dictionary with event_id and metadata
95
+ """
96
+ if not self.db:
97
+ return {}
98
+
99
+ event_id = f"event-{uuid.uuid4().hex[:8]}"
100
+ event_type = "tool_call"
101
+
102
+ try:
103
+ context = {
104
+ "phase_name": phase_name,
105
+ "spawner_type": self.spawner_type,
106
+ "parent_delegation_event": self.delegation_event_id,
107
+ }
108
+ if spawned_agent:
109
+ context["spawned_agent"] = spawned_agent
110
+ if tool_name:
111
+ context["tool"] = tool_name
112
+
113
+ # Use parent_phase_event_id if provided, otherwise use delegation_event_id
114
+ actual_parent_event_id = parent_phase_event_id or self.delegation_event_id
115
+
116
+ self.db.insert_event(
117
+ event_id=event_id,
118
+ agent_id=spawned_agent or self.parent_agent,
119
+ event_type=event_type,
120
+ session_id=self.session_id,
121
+ tool_name=tool_name
122
+ or f"HeadlessSpawner.{phase_name.replace(' ', '_').lower()}",
123
+ input_summary=input_summary or phase_name,
124
+ context=context,
125
+ parent_event_id=actual_parent_event_id,
126
+ subagent_type=self.spawner_type,
127
+ )
128
+
129
+ event = {
130
+ "event_id": event_id,
131
+ "phase_name": phase_name,
132
+ "spawned_agent": spawned_agent,
133
+ "tool_name": tool_name,
134
+ "status": status,
135
+ "start_time": time.time(),
136
+ }
137
+ self.phase_events[event_id] = event
138
+ return event
139
+
140
+ except Exception:
141
+ # Non-fatal - tracking is best-effort
142
+ return {}
143
+
144
+ def complete_phase(
145
+ self,
146
+ event_id: str,
147
+ output_summary: str | None = None,
148
+ status: str = "completed",
149
+ execution_duration: float | None = None,
150
+ ) -> bool:
151
+ """
152
+ Mark a phase as completed with results.
153
+
154
+ Args:
155
+ event_id: Event ID from record_phase
156
+ output_summary: Summary of output/result
157
+ status: Final status (completed, failed)
158
+ execution_duration: Execution time in seconds (auto-calculated if not provided)
159
+
160
+ Returns:
161
+ True if update successful, False otherwise
162
+ """
163
+ if not self.db or not event_id:
164
+ return False
165
+
166
+ try:
167
+ if execution_duration is None and event_id in self.phase_events:
168
+ execution_duration = (
169
+ time.time() - self.phase_events[event_id]["start_time"]
170
+ )
171
+
172
+ if self.db.connection is None:
173
+ return False
174
+
175
+ cursor = self.db.connection.cursor()
176
+ cursor.execute(
177
+ """
178
+ UPDATE agent_events
179
+ SET output_summary = ?, status = ?, execution_duration_seconds = ?,
180
+ updated_at = CURRENT_TIMESTAMP
181
+ WHERE event_id = ?
182
+ """,
183
+ (output_summary, status, execution_duration or 0.0, event_id),
184
+ )
185
+ self.db.connection.commit()
186
+
187
+ if event_id in self.phase_events:
188
+ self.phase_events[event_id]["status"] = status
189
+ self.phase_events[event_id]["output_summary"] = output_summary
190
+
191
+ return True
192
+ except Exception:
193
+ # Non-fatal
194
+ return False
195
+
196
+ def record_completion(
197
+ self,
198
+ success: bool,
199
+ response: str | None = None,
200
+ error: str | None = None,
201
+ tokens_used: int = 0,
202
+ cost: float = 0.0,
203
+ ) -> dict[str, Any]:
204
+ """
205
+ Record final completion with overall results.
206
+
207
+ Args:
208
+ success: Whether execution succeeded
209
+ response: Successful response/output
210
+ error: Error message if failed
211
+ tokens_used: Tokens consumed
212
+ cost: Execution cost
213
+
214
+ Returns:
215
+ Completion event dictionary
216
+ """
217
+ total_duration = time.time() - self.start_time
218
+
219
+ completion_event: dict[str, Any] = {
220
+ "success": success,
221
+ "duration": total_duration,
222
+ "tokens": tokens_used,
223
+ "cost": cost,
224
+ "phase_count": len(self.phase_events),
225
+ }
226
+
227
+ if success:
228
+ completion_event["response"] = response
229
+ else:
230
+ completion_event["error"] = error
231
+
232
+ return completion_event
233
+
234
+ def get_phase_events(self) -> dict[str, dict[str, Any]]:
235
+ """Get all recorded phase events."""
236
+ return self.phase_events
237
+
238
+ def record_tool_call(
239
+ self,
240
+ tool_name: str,
241
+ tool_input: dict | None,
242
+ phase_event_id: str,
243
+ spawned_agent: str | None = None,
244
+ ) -> dict[str, Any]:
245
+ """
246
+ Record a tool call within a spawned execution phase.
247
+
248
+ Args:
249
+ tool_name: Name of the tool (bash, read_file, write_file, etc.)
250
+ tool_input: Input parameters to the tool
251
+ phase_event_id: Parent phase event ID to link to
252
+ spawned_agent: Agent making the tool call (optional)
253
+
254
+ Returns:
255
+ Event dictionary with event_id and metadata
256
+ """
257
+ if not self.db:
258
+ return {}
259
+
260
+ event_id = f"event-{uuid.uuid4().hex[:8]}"
261
+
262
+ try:
263
+ context = {
264
+ "tool_name": tool_name,
265
+ "spawner_type": self.spawner_type,
266
+ "parent_phase_event": phase_event_id,
267
+ }
268
+ if spawned_agent:
269
+ context["spawned_agent"] = spawned_agent
270
+
271
+ self.db.insert_event(
272
+ event_id=event_id,
273
+ agent_id=spawned_agent or self.parent_agent,
274
+ event_type="tool_call",
275
+ session_id=self.session_id,
276
+ tool_name=tool_name,
277
+ input_summary=(
278
+ str(tool_input)[:200] if tool_input else f"Call to {tool_name}"
279
+ ),
280
+ context=context,
281
+ parent_event_id=phase_event_id,
282
+ subagent_type=self.spawner_type,
283
+ )
284
+
285
+ tool_event = {
286
+ "event_id": event_id,
287
+ "tool_name": tool_name,
288
+ "tool_input": tool_input,
289
+ "phase_event_id": phase_event_id,
290
+ "spawned_agent": spawned_agent,
291
+ "status": "running",
292
+ "start_time": time.time(),
293
+ }
294
+ return tool_event
295
+
296
+ except Exception:
297
+ # Non-fatal - tracking is best-effort
298
+ return {}
299
+
300
+ def complete_tool_call(
301
+ self,
302
+ event_id: str,
303
+ output_summary: str | None = None,
304
+ success: bool = True,
305
+ ) -> bool:
306
+ """
307
+ Mark a tool call as complete with results.
308
+
309
+ Args:
310
+ event_id: Event ID from record_tool_call
311
+ output_summary: Summary of tool output/result
312
+ success: Whether the tool call succeeded
313
+
314
+ Returns:
315
+ True if update successful, False otherwise
316
+ """
317
+ if not self.db or not event_id:
318
+ return False
319
+
320
+ try:
321
+ if self.db.connection is None:
322
+ return False
323
+
324
+ cursor = self.db.connection.cursor()
325
+ cursor.execute(
326
+ """
327
+ UPDATE agent_events
328
+ SET output_summary = ?, status = ?, updated_at = CURRENT_TIMESTAMP
329
+ WHERE event_id = ?
330
+ """,
331
+ (
332
+ output_summary,
333
+ "completed" if success else "failed",
334
+ event_id,
335
+ ),
336
+ )
337
+ self.db.connection.commit()
338
+ return True
339
+ except Exception:
340
+ # Non-fatal
341
+ return False
342
+
343
+ def get_event_hierarchy(self) -> dict[str, Any]:
344
+ """
345
+ Get the event hierarchy for this spawner execution.
346
+
347
+ Returns:
348
+ Dictionary with delegation event as root and phases as children
349
+ """
350
+ return {
351
+ "delegation_event_id": self.delegation_event_id,
352
+ "spawner_type": self.spawner_type,
353
+ "session_id": self.session_id,
354
+ "total_duration": time.time() - self.start_time,
355
+ "phase_events": self.phase_events,
356
+ }
357
+
358
+
359
+ def create_tracker_from_env(
360
+ spawner_type: str = "generic",
361
+ ) -> SpawnerEventTracker:
362
+ """
363
+ Create a SpawnerEventTracker from environment variables.
364
+
365
+ Reads HTMLGRAPH_PARENT_EVENT, HTMLGRAPH_PARENT_AGENT, HTMLGRAPH_PARENT_SESSION
366
+ from environment to link to parent context.
367
+
368
+ Args:
369
+ spawner_type: Type of spawner (gemini, codex, copilot)
370
+
371
+ Returns:
372
+ Initialized SpawnerEventTracker
373
+ """
374
+ delegation_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
375
+ parent_agent = os.getenv("HTMLGRAPH_PARENT_AGENT", "orchestrator")
376
+ session_id = os.getenv("HTMLGRAPH_PARENT_SESSION")
377
+
378
+ return SpawnerEventTracker(
379
+ delegation_event_id=delegation_event_id,
380
+ parent_agent=parent_agent,
381
+ spawner_type=spawner_type,
382
+ session_id=session_id,
383
+ )
@@ -1,11 +1,14 @@
1
1
  """Claude spawner implementation."""
2
2
 
3
3
  import json
4
+ import logging
4
5
  import subprocess
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from .base import AIResult, BaseSpawner
8
9
 
10
+ logger = logging.getLogger(__name__)
11
+
9
12
  if TYPE_CHECKING:
10
13
  pass
11
14
 
@@ -64,8 +67,8 @@ class ClaudeSpawner(BaseSpawner):
64
67
  >>> spawner = ClaudeSpawner()
65
68
  >>> result = spawner.spawn("What is 2+2?")
66
69
  >>> if result.success:
67
- ... print(result.response) # "4"
68
- ... print(f"Cost: ${result.raw_output['total_cost_usd']}")
70
+ ... logger.info("%s", result.response) # "4"
71
+ ... logger.info(f"Cost: ${result.raw_output['total_cost_usd']}")
69
72
  """
70
73
  cmd = ["claude", "-p"]
71
74
 
@@ -1,13 +1,15 @@
1
1
  """Codex spawner implementation."""
2
2
 
3
3
  import json
4
+ import logging
4
5
  import subprocess
5
- import sys
6
6
  import time
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from .base import AIResult, BaseSpawner
10
10
 
11
+ logger = logging.getLogger(__name__)
12
+
11
13
  if TYPE_CHECKING:
12
14
  from htmlgraph.sdk import SDK
13
15
 
@@ -232,15 +234,11 @@ class CodexSpawner(BaseSpawner):
232
234
 
233
235
  # Record subprocess invocation if tracker is available
234
236
  subprocess_event_id = None
235
- print(
236
- f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
237
- file=sys.stderr,
237
+ logger.warning(
238
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
238
239
  )
239
240
  if tracker and parent_event_id:
240
- print(
241
- "DEBUG: Recording subprocess invocation for Codex...",
242
- file=sys.stderr,
243
- )
241
+ logger.debug("Recording subprocess invocation for Codex...")
244
242
  try:
245
243
  subprocess_event = tracker.record_tool_call(
246
244
  tool_name="subprocess.codex",
@@ -250,23 +248,18 @@ class CodexSpawner(BaseSpawner):
250
248
  )
251
249
  if subprocess_event:
252
250
  subprocess_event_id = subprocess_event.get("event_id")
253
- print(
254
- f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}",
255
- file=sys.stderr,
251
+ logger.warning(
252
+ f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}"
256
253
  )
257
254
  else:
258
- print("DEBUG: subprocess_event was None", file=sys.stderr)
255
+ logger.debug("subprocess_event was None")
259
256
  except Exception as e:
260
257
  # Tracking failure should not break execution
261
- print(
262
- f"DEBUG: Exception recording Codex subprocess: {e}",
263
- file=sys.stderr,
264
- )
258
+ logger.warning(f"DEBUG: Exception recording Codex subprocess: {e}")
265
259
  pass
266
260
  else:
267
- print(
268
- f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
269
- file=sys.stderr,
261
+ logger.warning(
262
+ f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
270
263
  )
271
264
 
272
265
  result = subprocess.run(
@@ -1,12 +1,14 @@
1
1
  """Copilot spawner implementation."""
2
2
 
3
+ import logging
3
4
  import subprocess
4
- import sys
5
5
  import time
6
6
  from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from .base import AIResult, BaseSpawner
9
9
 
10
+ logger = logging.getLogger(__name__)
11
+
10
12
  if TYPE_CHECKING:
11
13
  from htmlgraph.sdk import SDK
12
14
 
@@ -129,15 +131,11 @@ class CopilotSpawner(BaseSpawner):
129
131
 
130
132
  # Record subprocess invocation if tracker is available
131
133
  subprocess_event_id = None
132
- print(
133
- f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
134
- file=sys.stderr,
134
+ logger.warning(
135
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
135
136
  )
136
137
  if tracker and parent_event_id:
137
- print(
138
- "DEBUG: Recording subprocess invocation for Copilot...",
139
- file=sys.stderr,
140
- )
138
+ logger.debug("Recording subprocess invocation for Copilot...")
141
139
  try:
142
140
  subprocess_event = tracker.record_tool_call(
143
141
  tool_name="subprocess.copilot",
@@ -147,23 +145,20 @@ class CopilotSpawner(BaseSpawner):
147
145
  )
148
146
  if subprocess_event:
149
147
  subprocess_event_id = subprocess_event.get("event_id")
150
- print(
151
- f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}",
152
- file=sys.stderr,
148
+ logger.warning(
149
+ f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}"
153
150
  )
154
151
  else:
155
- print("DEBUG: subprocess_event was None", file=sys.stderr)
152
+ logger.debug("subprocess_event was None")
156
153
  except Exception as e:
157
154
  # Tracking failure should not break execution
158
- print(
159
- f"DEBUG: Exception recording Copilot subprocess: {e}",
160
- file=sys.stderr,
155
+ logger.warning(
156
+ f"DEBUG: Exception recording Copilot subprocess: {e}"
161
157
  )
162
158
  pass
163
159
  else:
164
- print(
165
- f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
166
- file=sys.stderr,
160
+ logger.warning(
161
+ f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
167
162
  )
168
163
 
169
164
  result = subprocess.run(
@@ -1,13 +1,15 @@
1
1
  """Gemini spawner implementation."""
2
2
 
3
3
  import json
4
+ import logging
4
5
  import subprocess
5
- import sys
6
6
  import time
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from .base import AIResult, BaseSpawner
10
10
 
11
+ logger = logging.getLogger(__name__)
12
+
11
13
  if TYPE_CHECKING:
12
14
  from htmlgraph.sdk import SDK
13
15
 
@@ -220,15 +222,11 @@ class GeminiSpawner(BaseSpawner):
220
222
 
221
223
  # Record subprocess invocation if tracker is available
222
224
  subprocess_event_id = None
223
- print(
224
- f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
225
- file=sys.stderr,
225
+ logger.warning(
226
+ f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
226
227
  )
227
228
  if tracker and parent_event_id:
228
- print(
229
- "DEBUG: Recording subprocess invocation for Gemini...",
230
- file=sys.stderr,
231
- )
229
+ logger.debug("Recording subprocess invocation for Gemini...")
232
230
  try:
233
231
  subprocess_event = tracker.record_tool_call(
234
232
  tool_name="subprocess.gemini",
@@ -238,23 +236,18 @@ class GeminiSpawner(BaseSpawner):
238
236
  )
239
237
  if subprocess_event:
240
238
  subprocess_event_id = subprocess_event.get("event_id")
241
- print(
242
- f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}",
243
- file=sys.stderr,
239
+ logger.warning(
240
+ f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}"
244
241
  )
245
242
  else:
246
- print("DEBUG: subprocess_event was None", file=sys.stderr)
243
+ logger.debug("subprocess_event was None")
247
244
  except Exception as e:
248
245
  # Tracking failure should not break execution
249
- print(
250
- f"DEBUG: Exception recording Gemini subprocess: {e}",
251
- file=sys.stderr,
252
- )
246
+ logger.warning(f"DEBUG: Exception recording Gemini subprocess: {e}")
253
247
  pass
254
248
  else:
255
- print(
256
- f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
257
- file=sys.stderr,
249
+ logger.warning(
250
+ f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
258
251
  )
259
252
 
260
253
  # Execute with timeout and stderr redirection
@@ -1,13 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  """Subprocess execution with standardized error handling.
2
4
 
3
5
  Provides consistent error handling for Claude Code CLI invocations.
4
6
  """
5
7
 
6
- from __future__ import annotations
7
-
8
+ import logging
8
9
  import subprocess
9
10
  import sys
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
11
14
 
12
15
  class SubprocessRunner:
13
16
  """Execute subprocess commands with error handling."""
@@ -25,7 +28,7 @@ class SubprocessRunner:
25
28
  try:
26
29
  subprocess.run(cmd, check=False)
27
30
  except FileNotFoundError:
28
- print("Error: 'claude' command not found.", file=sys.stderr)
31
+ logger.warning("Error: 'claude' command not found.")
29
32
  print(
30
33
  "Please install Claude Code CLI: https://code.claude.com",
31
34
  file=sys.stderr,