gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/runner.py CHANGED
@@ -1,14 +1,17 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import logging
3
5
  import os
4
6
  import signal
5
7
  import sys
6
8
  from pathlib import Path
7
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
8
10
 
9
11
  import uvicorn
10
12
 
11
13
  from gobby.agents.runner import AgentRunner
14
+ from gobby.app_context import ServiceContainer
12
15
  from gobby.config.app import load_config
13
16
  from gobby.llm import LLMService, create_llm_service
14
17
  from gobby.llm.resolver import ExecutorRegistry
@@ -36,6 +39,12 @@ from gobby.worktrees.git import WorktreeGitManager
36
39
 
37
40
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
38
41
 
42
+ # Type hints for pipeline components (imported lazily at runtime)
43
+ if TYPE_CHECKING:
44
+ from gobby.storage.pipelines import LocalPipelineExecutionManager
45
+ from gobby.workflows.loader import WorkflowLoader
46
+ from gobby.workflows.pipeline_executor import PipelineExecutor
47
+
39
48
  logger = logging.getLogger(__name__)
40
49
 
41
50
 
@@ -181,6 +190,36 @@ class GobbyRunner:
181
190
  except Exception as e:
182
191
  logger.debug(f"Could not initialize git manager: {e}")
183
192
 
193
+ # Initialize Pipeline Components
194
+ self.workflow_loader: WorkflowLoader | None = None
195
+ self.pipeline_execution_manager: LocalPipelineExecutionManager | None = None
196
+ self.pipeline_executor: PipelineExecutor | None = None
197
+ try:
198
+ from gobby.storage.pipelines import LocalPipelineExecutionManager
199
+ from gobby.workflows.loader import WorkflowLoader
200
+ from gobby.workflows.pipeline_executor import PipelineExecutor
201
+
202
+ self.workflow_loader = WorkflowLoader()
203
+ if self.project_id:
204
+ self.pipeline_execution_manager = LocalPipelineExecutionManager(
205
+ db=self.database,
206
+ project_id=self.project_id,
207
+ )
208
+ if self.llm_service:
209
+ self.pipeline_executor = PipelineExecutor(
210
+ db=self.database,
211
+ execution_manager=self.pipeline_execution_manager,
212
+ llm_service=self.llm_service,
213
+ loader=self.workflow_loader,
214
+ )
215
+ logger.debug("Pipeline executor initialized")
216
+ else:
217
+ logger.debug("Pipeline executor not initialized: LLM service not available")
218
+ else:
219
+ logger.debug("Pipeline execution manager not initialized: no project context")
220
+ except Exception as e:
221
+ logger.warning(f"Failed to initialize pipeline components: {e}")
222
+
184
223
  # Initialize Agent Runner (Phase 7 - Subagents)
185
224
  # Create executor registry for lazy executor creation
186
225
  self.executor_registry = ExecutorRegistry(config=self.config)
@@ -212,29 +251,41 @@ class GobbyRunner:
212
251
  )
213
252
 
214
253
  # HTTP Server
215
- self.http_server = HTTPServer(
216
- port=self.config.daemon_port,
217
- test_mode=self.config.test_mode,
218
- mcp_manager=self.mcp_proxy,
219
- mcp_db_manager=self.mcp_db_manager,
254
+ # Bundle services into container
255
+ services = ServiceContainer(
220
256
  config=self.config,
257
+ database=self.database,
221
258
  session_manager=self.session_manager,
222
259
  task_manager=self.task_manager,
223
260
  task_sync_manager=self.task_sync_manager,
224
- message_manager=self.message_manager,
261
+ memory_sync_manager=self.memory_sync_manager,
225
262
  memory_manager=self.memory_manager,
226
263
  llm_service=self.llm_service,
227
- message_processor=self.message_processor,
228
- memory_sync_manager=self.memory_sync_manager,
229
- task_validator=self.task_validator,
264
+ mcp_manager=self.mcp_proxy,
265
+ mcp_db_manager=self.mcp_db_manager,
230
266
  metrics_manager=self.metrics_manager,
231
267
  agent_runner=self.agent_runner,
268
+ message_processor=self.message_processor,
269
+ message_manager=self.message_manager,
270
+ task_validator=self.task_validator,
232
271
  worktree_storage=self.worktree_storage,
233
272
  clone_storage=self.clone_storage,
234
273
  git_manager=self.git_manager,
235
274
  project_id=self.project_id,
275
+ pipeline_executor=self.pipeline_executor,
276
+ workflow_loader=self.workflow_loader,
277
+ pipeline_execution_manager=self.pipeline_execution_manager,
236
278
  )
237
279
 
280
+ self.http_server = HTTPServer(
281
+ services=services,
282
+ port=self.config.daemon_port,
283
+ test_mode=self.config.test_mode,
284
+ )
285
+
286
+ # Inject server into container for circular ref if needed later
287
+ # self.http_server.services = services
288
+
238
289
  # Ensure message_processor property is set (redundant but explicit):
239
290
  self.http_server.message_processor = self.message_processor
240
291
 
@@ -242,7 +293,7 @@ class GobbyRunner:
242
293
  self.websocket_server: WebSocketServer | None = None
243
294
  if self.config.websocket and getattr(self.config.websocket, "enabled", True):
244
295
  websocket_config = WebSocketConfig(
245
- host="localhost",
296
+ host=self.config.bind_host,
246
297
  port=self.config.websocket.port,
247
298
  ping_interval=self.config.websocket.ping_interval,
248
299
  ping_timeout=self.config.websocket.ping_timeout,
@@ -250,6 +301,7 @@ class GobbyRunner:
250
301
  self.websocket_server = WebSocketServer(
251
302
  config=websocket_config,
252
303
  mcp_manager=self.mcp_proxy,
304
+ llm_service=self.llm_service,
253
305
  )
254
306
  # Pass WebSocket server reference to HTTP server for broadcasting
255
307
  self.http_server.websocket_server = self.websocket_server
@@ -263,6 +315,9 @@ class GobbyRunner:
263
315
  # Register agent event callback for WebSocket broadcasting
264
316
  self._setup_agent_event_broadcasting()
265
317
 
318
+ # Register pipeline event callback for WebSocket broadcasting
319
+ self._setup_pipeline_event_broadcasting()
320
+
266
321
  def _init_database(self) -> DatabaseProtocol:
267
322
  """Initialize hub database."""
268
323
  hub_db_path = Path(self.config.database_path).expanduser()
@@ -277,13 +332,23 @@ class GobbyRunner:
277
332
  return hub_db
278
333
 
279
334
  def _setup_agent_event_broadcasting(self) -> None:
280
- """Set up WebSocket broadcasting for agent lifecycle events."""
335
+ """Set up WebSocket broadcasting for agent lifecycle events and PTY reading."""
336
+ from gobby.agents.pty_reader import get_pty_reader_manager
281
337
  from gobby.agents.registry import get_running_agent_registry
282
338
 
283
339
  if not self.websocket_server:
284
340
  return
285
341
 
286
342
  registry = get_running_agent_registry()
343
+ pty_manager = get_pty_reader_manager()
344
+
345
+ # Set up PTY output callback to broadcast via WebSocket
346
+ async def broadcast_terminal_output(run_id: str, data: str) -> None:
347
+ """Broadcast terminal output via WebSocket."""
348
+ if self.websocket_server:
349
+ await self.websocket_server.broadcast_terminal_output(run_id, data)
350
+
351
+ pty_manager.set_output_callback(broadcast_terminal_output)
287
352
 
288
353
  def broadcast_agent_event(event_type: str, run_id: str, data: dict[str, Any]) -> None:
289
354
  """Broadcast agent events via WebSocket (non-blocking)."""
@@ -299,6 +364,32 @@ class GobbyRunner:
299
364
  except Exception as e:
300
365
  logger.warning(f"Failed to broadcast agent event {event_type}: {e}")
301
366
 
367
+ # Handle PTY reader start/stop for embedded agents
368
+ if event_type == "agent_started" and data.get("mode") == "embedded":
369
+ # Start PTY reader for embedded agents
370
+ agent = registry.get(run_id)
371
+ if agent and agent.master_fd is not None:
372
+
373
+ async def start_pty_reader() -> None:
374
+ await pty_manager.start_reader(agent)
375
+
376
+ task = asyncio.create_task(start_pty_reader())
377
+ task.add_done_callback(_log_broadcast_exception)
378
+
379
+ elif event_type in (
380
+ "agent_completed",
381
+ "agent_failed",
382
+ "agent_cancelled",
383
+ "agent_timeout",
384
+ ):
385
+ # Stop PTY reader when agent finishes
386
+
387
+ async def stop_pty_reader() -> None:
388
+ await pty_manager.stop_reader(run_id)
389
+
390
+ task = asyncio.create_task(stop_pty_reader())
391
+ task.add_done_callback(_log_broadcast_exception)
392
+
302
393
  # Create async task to broadcast and attach exception callback
303
394
  task = asyncio.create_task(
304
395
  self.websocket_server.broadcast_agent_event(
@@ -314,7 +405,29 @@ class GobbyRunner:
314
405
  task.add_done_callback(_log_broadcast_exception)
315
406
 
316
407
  registry.add_event_callback(broadcast_agent_event)
317
- logger.debug("Agent event broadcasting enabled")
408
+ logger.debug("Agent event broadcasting and PTY reading enabled")
409
+
410
+ def _setup_pipeline_event_broadcasting(self) -> None:
411
+ """Set up WebSocket broadcasting for pipeline execution events."""
412
+ if not self.websocket_server:
413
+ return
414
+
415
+ if not self.pipeline_executor:
416
+ logger.debug("Pipeline event broadcasting skipped: no pipeline executor")
417
+ return
418
+
419
+ async def broadcast_pipeline_event(event: str, execution_id: str, **kwargs: Any) -> None:
420
+ """Broadcast pipeline events via WebSocket."""
421
+ if self.websocket_server:
422
+ await self.websocket_server.broadcast_pipeline_event(
423
+ event=event,
424
+ execution_id=execution_id,
425
+ **kwargs,
426
+ )
427
+
428
+ # Set the callback on the pipeline executor
429
+ self.pipeline_executor.event_callback = broadcast_pipeline_event
430
+ logger.debug("Pipeline event broadcasting enabled")
318
431
 
319
432
  async def _metrics_cleanup_loop(self) -> None:
320
433
  """Background loop for periodic metrics cleanup (every 24 hours)."""
@@ -423,13 +536,14 @@ class GobbyRunner:
423
536
  websocket_task = asyncio.create_task(self.websocket_server.start())
424
537
 
425
538
  # Start HTTP server
426
- # nosec B104: 0.0.0.0 binding is intentional - daemon serves local network
539
+ graceful_shutdown_timeout = 15
427
540
  config = uvicorn.Config(
428
541
  self.http_server.app,
429
- host="0.0.0.0", # nosec B104 - local daemon needs network access
542
+ host=self.config.bind_host,
430
543
  port=self.http_server.port,
431
544
  log_level="warning",
432
545
  access_log=False,
546
+ timeout_graceful_shutdown=graceful_shutdown_timeout,
433
547
  )
434
548
  server = uvicorn.Server(config)
435
549
  server_task = asyncio.create_task(server.serve())
@@ -439,9 +553,10 @@ class GobbyRunner:
439
553
  await asyncio.sleep(0.5)
440
554
 
441
555
  # Cleanup with timeouts to prevent hanging
556
+ # Use timeout slightly longer than uvicorn's graceful shutdown to let it finish
442
557
  server.should_exit = True
443
558
  try:
444
- await asyncio.wait_for(server_task, timeout=3.0)
559
+ await asyncio.wait_for(server_task, timeout=graceful_shutdown_timeout + 5)
445
560
  except TimeoutError:
446
561
  logger.warning("HTTP server shutdown timed out")
447
562