gobby 0.2.6__py3-none-any.whl → 0.2.8__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 (198) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +96 -35
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/adapters/gemini.py +140 -38
  10. gobby/agents/definitions.py +11 -1
  11. gobby/agents/isolation.py +525 -0
  12. gobby/agents/registry.py +11 -0
  13. gobby/agents/sandbox.py +261 -0
  14. gobby/agents/session.py +1 -0
  15. gobby/agents/spawn.py +42 -287
  16. gobby/agents/spawn_executor.py +415 -0
  17. gobby/agents/spawners/__init__.py +24 -0
  18. gobby/agents/spawners/command_builder.py +189 -0
  19. gobby/agents/spawners/embedded.py +21 -2
  20. gobby/agents/spawners/headless.py +21 -2
  21. gobby/agents/spawners/macos.py +26 -1
  22. gobby/agents/spawners/prompt_manager.py +125 -0
  23. gobby/cli/__init__.py +0 -2
  24. gobby/cli/install.py +4 -4
  25. gobby/cli/installers/claude.py +6 -0
  26. gobby/cli/installers/gemini.py +6 -0
  27. gobby/cli/installers/shared.py +103 -4
  28. gobby/cli/memory.py +185 -0
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/utils.py +9 -2
  31. gobby/clones/git.py +177 -0
  32. gobby/config/__init__.py +12 -97
  33. gobby/config/app.py +10 -94
  34. gobby/config/extensions.py +2 -2
  35. gobby/config/features.py +7 -130
  36. gobby/config/skills.py +31 -0
  37. gobby/config/tasks.py +4 -28
  38. gobby/hooks/__init__.py +0 -13
  39. gobby/hooks/event_handlers.py +150 -8
  40. gobby/hooks/hook_manager.py +21 -3
  41. gobby/hooks/plugins.py +1 -1
  42. gobby/hooks/webhooks.py +1 -1
  43. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  44. gobby/llm/resolver.py +3 -2
  45. gobby/mcp_proxy/importer.py +62 -4
  46. gobby/mcp_proxy/instructions.py +4 -2
  47. gobby/mcp_proxy/registries.py +22 -8
  48. gobby/mcp_proxy/services/recommendation.py +43 -11
  49. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  50. gobby/mcp_proxy/tools/agents.py +76 -740
  51. gobby/mcp_proxy/tools/artifacts.py +43 -9
  52. gobby/mcp_proxy/tools/clones.py +0 -385
  53. gobby/mcp_proxy/tools/memory.py +2 -2
  54. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  55. gobby/mcp_proxy/tools/sessions/_commits.py +239 -0
  56. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  57. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  58. gobby/mcp_proxy/tools/sessions/_handoff.py +503 -0
  59. gobby/mcp_proxy/tools/sessions/_messages.py +166 -0
  60. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  61. gobby/mcp_proxy/tools/spawn_agent.py +455 -0
  62. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  63. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  64. gobby/mcp_proxy/tools/tasks/_lifecycle.py +79 -30
  65. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  66. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  67. gobby/mcp_proxy/tools/workflows.py +84 -34
  68. gobby/mcp_proxy/tools/worktrees.py +32 -350
  69. gobby/memory/extractor.py +15 -1
  70. gobby/memory/ingestion/__init__.py +5 -0
  71. gobby/memory/ingestion/multimodal.py +221 -0
  72. gobby/memory/manager.py +62 -283
  73. gobby/memory/search/__init__.py +10 -0
  74. gobby/memory/search/coordinator.py +248 -0
  75. gobby/memory/services/__init__.py +5 -0
  76. gobby/memory/services/crossref.py +142 -0
  77. gobby/prompts/loader.py +5 -2
  78. gobby/runner.py +13 -0
  79. gobby/servers/http.py +1 -4
  80. gobby/servers/routes/admin.py +14 -0
  81. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  82. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  83. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  84. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  85. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  86. gobby/servers/routes/mcp/hooks.py +51 -4
  87. gobby/servers/routes/mcp/tools.py +48 -1506
  88. gobby/servers/websocket.py +57 -1
  89. gobby/sessions/analyzer.py +2 -2
  90. gobby/sessions/lifecycle.py +1 -1
  91. gobby/sessions/manager.py +9 -0
  92. gobby/sessions/processor.py +10 -0
  93. gobby/sessions/transcripts/base.py +1 -0
  94. gobby/sessions/transcripts/claude.py +15 -5
  95. gobby/sessions/transcripts/gemini.py +100 -34
  96. gobby/skills/parser.py +30 -2
  97. gobby/storage/database.py +9 -2
  98. gobby/storage/memories.py +32 -21
  99. gobby/storage/migrations.py +174 -368
  100. gobby/storage/sessions.py +45 -7
  101. gobby/storage/skills.py +80 -7
  102. gobby/storage/tasks/_lifecycle.py +18 -3
  103. gobby/sync/memories.py +1 -1
  104. gobby/tasks/external_validator.py +1 -1
  105. gobby/tasks/validation.py +22 -20
  106. gobby/tools/summarizer.py +91 -10
  107. gobby/utils/project_context.py +2 -3
  108. gobby/utils/status.py +13 -0
  109. gobby/workflows/actions.py +221 -1217
  110. gobby/workflows/artifact_actions.py +31 -0
  111. gobby/workflows/autonomous_actions.py +11 -0
  112. gobby/workflows/context_actions.py +50 -1
  113. gobby/workflows/detection_helpers.py +38 -24
  114. gobby/workflows/enforcement/__init__.py +47 -0
  115. gobby/workflows/enforcement/blocking.py +281 -0
  116. gobby/workflows/enforcement/commit_policy.py +283 -0
  117. gobby/workflows/enforcement/handlers.py +269 -0
  118. gobby/workflows/enforcement/task_policy.py +542 -0
  119. gobby/workflows/engine.py +93 -0
  120. gobby/workflows/evaluator.py +110 -0
  121. gobby/workflows/git_utils.py +106 -0
  122. gobby/workflows/hooks.py +41 -0
  123. gobby/workflows/llm_actions.py +30 -0
  124. gobby/workflows/mcp_actions.py +20 -1
  125. gobby/workflows/memory_actions.py +91 -0
  126. gobby/workflows/safe_evaluator.py +191 -0
  127. gobby/workflows/session_actions.py +44 -0
  128. gobby/workflows/state_actions.py +60 -1
  129. gobby/workflows/stop_signal_actions.py +55 -0
  130. gobby/workflows/summary_actions.py +217 -51
  131. gobby/workflows/task_sync_actions.py +347 -0
  132. gobby/workflows/todo_actions.py +34 -1
  133. gobby/workflows/webhook_actions.py +185 -0
  134. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/METADATA +6 -1
  135. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/RECORD +139 -163
  136. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/WHEEL +1 -1
  137. gobby/adapters/codex.py +0 -1332
  138. gobby/cli/tui.py +0 -34
  139. gobby/install/claude/commands/gobby/bug.md +0 -51
  140. gobby/install/claude/commands/gobby/chore.md +0 -51
  141. gobby/install/claude/commands/gobby/epic.md +0 -52
  142. gobby/install/claude/commands/gobby/eval.md +0 -235
  143. gobby/install/claude/commands/gobby/feat.md +0 -49
  144. gobby/install/claude/commands/gobby/nit.md +0 -52
  145. gobby/install/claude/commands/gobby/ref.md +0 -52
  146. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  147. gobby/prompts/defaults/expansion/system.md +0 -119
  148. gobby/prompts/defaults/expansion/user.md +0 -48
  149. gobby/prompts/defaults/external_validation/agent.md +0 -72
  150. gobby/prompts/defaults/external_validation/external.md +0 -63
  151. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  152. gobby/prompts/defaults/external_validation/system.md +0 -6
  153. gobby/prompts/defaults/features/import_mcp.md +0 -22
  154. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  155. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  156. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  157. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  158. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  159. gobby/prompts/defaults/features/server_description.md +0 -20
  160. gobby/prompts/defaults/features/server_description_system.md +0 -6
  161. gobby/prompts/defaults/features/task_description.md +0 -31
  162. gobby/prompts/defaults/features/task_description_system.md +0 -6
  163. gobby/prompts/defaults/features/tool_summary.md +0 -17
  164. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  165. gobby/prompts/defaults/handoff/compact.md +0 -63
  166. gobby/prompts/defaults/handoff/session_end.md +0 -57
  167. gobby/prompts/defaults/memory/extract.md +0 -61
  168. gobby/prompts/defaults/research/step.md +0 -58
  169. gobby/prompts/defaults/validation/criteria.md +0 -47
  170. gobby/prompts/defaults/validation/validate.md +0 -38
  171. gobby/storage/migrations_legacy.py +0 -1359
  172. gobby/tui/__init__.py +0 -5
  173. gobby/tui/api_client.py +0 -278
  174. gobby/tui/app.py +0 -329
  175. gobby/tui/screens/__init__.py +0 -25
  176. gobby/tui/screens/agents.py +0 -333
  177. gobby/tui/screens/chat.py +0 -450
  178. gobby/tui/screens/dashboard.py +0 -377
  179. gobby/tui/screens/memory.py +0 -305
  180. gobby/tui/screens/metrics.py +0 -231
  181. gobby/tui/screens/orchestrator.py +0 -903
  182. gobby/tui/screens/sessions.py +0 -412
  183. gobby/tui/screens/tasks.py +0 -440
  184. gobby/tui/screens/workflows.py +0 -289
  185. gobby/tui/screens/worktrees.py +0 -174
  186. gobby/tui/widgets/__init__.py +0 -21
  187. gobby/tui/widgets/chat.py +0 -210
  188. gobby/tui/widgets/conductor.py +0 -104
  189. gobby/tui/widgets/menu.py +0 -132
  190. gobby/tui/widgets/message_panel.py +0 -160
  191. gobby/tui/widgets/review_gate.py +0 -224
  192. gobby/tui/widgets/task_tree.py +0 -99
  193. gobby/tui/widgets/token_budget.py +0 -166
  194. gobby/tui/ws_client.py +0 -258
  195. gobby/workflows/task_enforcement_actions.py +0 -1343
  196. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
  197. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
  198. {gobby-0.2.6.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
@@ -22,13 +22,9 @@ from typing import TYPE_CHECKING, Any, Literal, cast
22
22
 
23
23
  from gobby.mcp_proxy.tools.internal import InternalToolRegistry
24
24
  from gobby.utils.project_context import get_project_context
25
- from gobby.workflows.definitions import WorkflowState
26
- from gobby.workflows.loader import WorkflowLoader
27
- from gobby.workflows.state_manager import WorkflowStateManager
28
25
  from gobby.worktrees.git import WorktreeGitManager
29
26
 
30
27
  if TYPE_CHECKING:
31
- from gobby.agents.runner import AgentRunner
32
28
  from gobby.storage.worktrees import LocalWorktreeManager
33
29
  from gobby.worktrees.git import WorktreeGitManager
34
30
 
@@ -291,7 +287,7 @@ def create_worktrees_registry(
291
287
  worktree_storage: LocalWorktreeManager,
292
288
  git_manager: WorktreeGitManager | None = None,
293
289
  project_id: str | None = None,
294
- agent_runner: AgentRunner | None = None,
290
+ session_manager: Any | None = None,
295
291
  ) -> InternalToolRegistry:
296
292
  """
297
293
  Create a worktree tool registry with all worktree-related tools.
@@ -300,11 +296,20 @@ def create_worktrees_registry(
300
296
  worktree_storage: LocalWorktreeManager for database operations.
301
297
  git_manager: WorktreeGitManager for git operations.
302
298
  project_id: Default project ID for operations.
303
- agent_runner: AgentRunner for spawning agents in worktrees.
299
+ session_manager: Session manager for resolving session references.
304
300
 
305
301
  Returns:
306
302
  InternalToolRegistry with all worktree tools registered.
307
303
  """
304
+
305
+ def _resolve_session_id(ref: str) -> str:
306
+ """Resolve session reference (#N, N, UUID, or prefix) to UUID."""
307
+ if session_manager is None:
308
+ return ref # No resolution available, return as-is
309
+ ctx = get_project_context()
310
+ proj_id = ctx.get("id") if ctx else project_id
311
+ return str(session_manager.resolve_session_reference(ref, proj_id))
312
+
308
313
  registry = InternalToolRegistry(
309
314
  name="gobby-worktrees",
310
315
  description="Git worktree management - create, manage, and cleanup isolated development directories",
@@ -441,7 +446,7 @@ def create_worktrees_registry(
441
446
 
442
447
  @registry.tool(
443
448
  name="list_worktrees",
444
- description="List worktrees with optional filters.",
449
+ description="List worktrees with optional filters. Accepts #N, N, UUID, or prefix for agent_session_id.",
445
450
  )
446
451
  async def list_worktrees(
447
452
  status: str | None = None,
@@ -453,16 +458,24 @@ def create_worktrees_registry(
453
458
 
454
459
  Args:
455
460
  status: Filter by status (active, stale, merged, abandoned).
456
- agent_session_id: Filter by owning session.
461
+ agent_session_id: Session reference (accepts #N, N, UUID, or prefix) to filter by owning session.
457
462
  limit: Maximum results (default: 50).
458
463
 
459
464
  Returns:
460
465
  Dict with list of worktrees.
461
466
  """
467
+ # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
468
+ resolved_session_id = agent_session_id
469
+ if agent_session_id:
470
+ try:
471
+ resolved_session_id = _resolve_session_id(agent_session_id)
472
+ except ValueError as e:
473
+ return {"success": False, "error": str(e)}
474
+
462
475
  worktrees = worktree_storage.list_worktrees(
463
476
  project_id=project_id,
464
477
  status=status,
465
- agent_session_id=agent_session_id,
478
+ agent_session_id=resolved_session_id,
466
479
  limit=limit,
467
480
  )
468
481
 
@@ -485,7 +498,7 @@ def create_worktrees_registry(
485
498
 
486
499
  @registry.tool(
487
500
  name="claim_worktree",
488
- description="Claim ownership of a worktree for an agent session.",
501
+ description="Claim ownership of a worktree for an agent session. Accepts #N, N, UUID, or prefix for session_id.",
489
502
  )
490
503
  async def claim_worktree(
491
504
  worktree_id: str,
@@ -496,11 +509,17 @@ def create_worktrees_registry(
496
509
 
497
510
  Args:
498
511
  worktree_id: The worktree ID to claim.
499
- session_id: The session ID claiming ownership.
512
+ session_id: Session reference (accepts #N, N, UUID, or prefix) claiming ownership.
500
513
 
501
514
  Returns:
502
515
  Dict with success status.
503
516
  """
517
+ # Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
518
+ try:
519
+ resolved_session_id = _resolve_session_id(session_id)
520
+ except ValueError as e:
521
+ return {"success": False, "error": str(e)}
522
+
504
523
  worktree = worktree_storage.get(worktree_id)
505
524
  if not worktree:
506
525
  return {
@@ -508,13 +527,13 @@ def create_worktrees_registry(
508
527
  "error": f"Worktree '{worktree_id}' not found",
509
528
  }
510
529
 
511
- if worktree.agent_session_id and worktree.agent_session_id != session_id:
530
+ if worktree.agent_session_id and worktree.agent_session_id != resolved_session_id:
512
531
  return {
513
532
  "success": False,
514
533
  "error": f"Worktree already claimed by session '{worktree.agent_session_id}'",
515
534
  }
516
535
 
517
- updated = worktree_storage.claim(worktree_id, session_id)
536
+ updated = worktree_storage.claim(worktree_id, resolved_session_id)
518
537
  if not updated:
519
538
  return {"error": "Failed to claim worktree"}
520
539
 
@@ -929,341 +948,4 @@ def create_worktrees_registry(
929
948
 
930
949
  return {}
931
950
 
932
- @registry.tool(
933
- name="spawn_agent_in_worktree",
934
- description="Create a worktree and spawn an agent in it.",
935
- )
936
- async def spawn_agent_in_worktree(
937
- prompt: str,
938
- branch_name: str,
939
- base_branch: str = "main",
940
- task_id: str | None = None,
941
- parent_session_id: str | None = None,
942
- mode: str = "terminal", # Note: in_process mode is not supported
943
- terminal: str = "auto",
944
- provider: Literal["claude", "gemini", "codex", "antigravity"] = "claude",
945
- model: str | None = None,
946
- workflow: str | None = None,
947
- timeout: float = 120.0,
948
- max_turns: int = 10,
949
- project_path: str | None = None,
950
- ) -> dict[str, Any]:
951
- """
952
- Create a worktree and spawn an agent to work in it.
953
-
954
- This combines worktree creation with agent spawning for isolated development.
955
-
956
- Args:
957
- prompt: The task/prompt for the agent.
958
- branch_name: Name for the new branch/worktree.
959
- base_branch: Branch to base the worktree on (default: main).
960
- task_id: Optional task ID to link to this worktree.
961
- parent_session_id: Parent session ID for context.
962
- mode: Execution mode (terminal, embedded, headless). Note: in_process is not supported.
963
- terminal: Terminal for terminal/embedded modes (auto, ghostty, etc.).
964
- provider: LLM provider (claude, gemini, etc.).
965
- model: Optional model override.
966
- workflow: Workflow name to execute.
967
- timeout: Execution timeout in seconds (default: 120).
968
- max_turns: Maximum turns (default: 10).
969
- project_path: Path to project directory (pass cwd from CLI).
970
-
971
- Returns:
972
- Dict with worktree_id, run_id, and status.
973
- """
974
- if agent_runner is None:
975
- return {
976
- "success": False,
977
- "error": "Agent runner not configured. Cannot spawn agent.",
978
- }
979
-
980
- # Resolve project context
981
- resolved_git_mgr, resolved_project_id, error = _resolve_project_context(
982
- project_path, git_manager, project_id
983
- )
984
- if error:
985
- return {"success": False, "error": error}
986
-
987
- # Type narrowing: if no error, these are guaranteed non-None
988
- if resolved_git_mgr is None or resolved_project_id is None:
989
- raise RuntimeError("Git manager or project ID unexpectedly None")
990
-
991
- if parent_session_id is None:
992
- return {
993
- "success": False,
994
- "error": "parent_session_id is required for agent spawning.",
995
- }
996
-
997
- # Handle mode aliases and validation
998
- # "interactive" is an alias for "terminal" mode
999
- if mode == "interactive":
1000
- mode = "terminal"
1001
-
1002
- valid_modes = ["terminal", "embedded", "headless"]
1003
- if mode not in valid_modes:
1004
- return {
1005
- "success": False,
1006
- "error": (
1007
- f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)} (or 'interactive' as alias for 'terminal'). "
1008
- f"Note: 'in_process' mode is not supported for spawn_agent_in_worktree."
1009
- ),
1010
- }
1011
-
1012
- # Normalize terminal parameter to lowercase for enum compatibility
1013
- # (TerminalType enum values are lowercase, e.g., "terminal.app" not "Terminal.app")
1014
- if isinstance(terminal, str):
1015
- terminal = terminal.lower()
1016
-
1017
- # Default to 'worktree-agent' workflow if not specified
1018
- # This workflow restricts tools available to spawned agents in worktrees
1019
- if workflow is None:
1020
- workflow = "worktree-agent"
1021
-
1022
- # Validate workflow (reject lifecycle workflows)
1023
- if workflow:
1024
- workflow_loader = WorkflowLoader()
1025
- is_valid, error_msg = workflow_loader.validate_workflow_for_agent(
1026
- workflow, project_path=project_path
1027
- )
1028
- if not is_valid:
1029
- return {
1030
- "success": False,
1031
- "error": error_msg,
1032
- }
1033
-
1034
- # Check if worktree already exists for this branch
1035
- existing = worktree_storage.get_by_branch(resolved_project_id, branch_name)
1036
- if existing:
1037
- # Use existing worktree
1038
- worktree = existing
1039
- logger.info(f"Using existing worktree for branch '{branch_name}'")
1040
- else:
1041
- # Generate worktree path in temp directory
1042
- project_name = Path(resolved_git_mgr.repo_path).name
1043
- worktree_path = _generate_worktree_path(branch_name, project_name)
1044
-
1045
- # Create git worktree
1046
- result = resolved_git_mgr.create_worktree(
1047
- worktree_path=worktree_path,
1048
- branch_name=branch_name,
1049
- base_branch=base_branch,
1050
- create_branch=True,
1051
- )
1052
-
1053
- if not result.success:
1054
- return {
1055
- "success": False,
1056
- "error": result.error or "Failed to create git worktree",
1057
- }
1058
-
1059
- # Record in database
1060
- worktree = worktree_storage.create(
1061
- project_id=resolved_project_id,
1062
- branch_name=branch_name,
1063
- worktree_path=worktree_path,
1064
- base_branch=base_branch,
1065
- task_id=task_id,
1066
- )
1067
-
1068
- # Copy project.json and install provider hooks
1069
- _copy_project_json_to_worktree(resolved_git_mgr.repo_path, worktree.worktree_path)
1070
- _install_provider_hooks(provider, worktree.worktree_path)
1071
-
1072
- # Check spawn depth limit
1073
- can_spawn, reason, _depth = agent_runner.can_spawn(parent_session_id)
1074
- if not can_spawn:
1075
- return {
1076
- "success": False,
1077
- "error": reason,
1078
- "worktree_id": worktree.id,
1079
- }
1080
-
1081
- # Import AgentConfig and get machine_id
1082
- from gobby.agents.runner import AgentConfig
1083
- from gobby.utils.machine_id import get_machine_id
1084
-
1085
- # Auto-detect machine_id if not provided
1086
- machine_id = get_machine_id()
1087
-
1088
- # Create agent config with worktree
1089
- config = AgentConfig(
1090
- prompt=prompt,
1091
- parent_session_id=parent_session_id,
1092
- project_id=resolved_project_id,
1093
- machine_id=machine_id,
1094
- source=provider,
1095
- workflow=workflow,
1096
- task=task_id,
1097
- session_context="summary_markdown",
1098
- mode=mode,
1099
- terminal=terminal,
1100
- worktree_id=worktree.id,
1101
- provider=provider,
1102
- model=model,
1103
- max_turns=max_turns,
1104
- timeout=timeout,
1105
- project_path=worktree.worktree_path,
1106
- )
1107
-
1108
- # For terminal/embedded/headless modes, use prepare_run + spawner
1109
- # (runner.run() is only for in_process mode)
1110
- from gobby.llm.executor import AgentResult
1111
-
1112
- prepare_result = agent_runner.prepare_run(config)
1113
- if isinstance(prepare_result, AgentResult):
1114
- # prepare_run returns AgentResult on error
1115
- return {
1116
- "success": False,
1117
- "worktree_id": worktree.id,
1118
- "worktree_path": worktree.worktree_path,
1119
- "branch_name": worktree.branch_name,
1120
- "error": prepare_result.error,
1121
- }
1122
-
1123
- # Successfully prepared - we have context with session and run
1124
- context = prepare_result
1125
-
1126
- if context.session is None or context.run is None:
1127
- return {
1128
- "success": False,
1129
- "worktree_id": worktree.id,
1130
- "error": "Internal error: context missing session or run after prepare_run",
1131
- }
1132
-
1133
- child_session = context.session
1134
- agent_run = context.run
1135
-
1136
- # Claim worktree for the child session
1137
- worktree_storage.claim(worktree.id, child_session.id)
1138
-
1139
- # Pre-save workflow state with session_task if task_id is provided
1140
- # This ensures suggest_next_task() will scope to this task's subtasks
1141
- if task_id and workflow:
1142
- try:
1143
- workflow_state_manager = WorkflowStateManager(worktree_storage.db)
1144
- initial_state = WorkflowState(
1145
- session_id=child_session.id,
1146
- workflow_name=workflow,
1147
- step="", # Will be set when workflow actually starts
1148
- variables={"session_task": task_id},
1149
- )
1150
- workflow_state_manager.save_state(initial_state)
1151
- logger.debug(
1152
- f"Pre-saved workflow state for session {child_session.id} "
1153
- f"with session_task={task_id}"
1154
- )
1155
- except Exception as e:
1156
- logger.warning(f"Failed to pre-save workflow state: {e}")
1157
- # Continue anyway - this is an optimization, not a requirement
1158
-
1159
- # Build enhanced prompt with worktree context
1160
- # This helps the agent understand it's in an isolated worktree, not the main repo
1161
- enhanced_prompt = _build_worktree_context_prompt(
1162
- original_prompt=prompt,
1163
- worktree_path=worktree.worktree_path,
1164
- branch_name=worktree.branch_name,
1165
- task_id=task_id,
1166
- main_repo_path=str(resolved_git_mgr.repo_path),
1167
- )
1168
-
1169
- # Spawn in terminal using TerminalSpawner
1170
- if mode == "terminal":
1171
- from gobby.agents.spawn import TerminalSpawner
1172
-
1173
- terminal_spawner = TerminalSpawner()
1174
- terminal_result = terminal_spawner.spawn_agent(
1175
- cli=provider, # claude, gemini, codex
1176
- cwd=worktree.worktree_path,
1177
- session_id=child_session.id,
1178
- parent_session_id=parent_session_id,
1179
- agent_run_id=agent_run.id,
1180
- project_id=resolved_project_id,
1181
- workflow_name=workflow,
1182
- agent_depth=child_session.agent_depth,
1183
- max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
1184
- terminal=terminal,
1185
- prompt=enhanced_prompt,
1186
- )
1187
-
1188
- if not terminal_result.success:
1189
- return {
1190
- "success": False,
1191
- "worktree_id": worktree.id,
1192
- "worktree_path": worktree.worktree_path,
1193
- "branch_name": worktree.branch_name,
1194
- "run_id": agent_run.id,
1195
- "child_session_id": child_session.id,
1196
- "error": terminal_result.error or terminal_result.message,
1197
- }
1198
-
1199
- return {
1200
- "success": True,
1201
- "worktree_id": worktree.id,
1202
- "worktree_path": worktree.worktree_path,
1203
- "branch_name": worktree.branch_name,
1204
- "run_id": agent_run.id,
1205
- "child_session_id": child_session.id,
1206
- "status": "pending",
1207
- "message": f"Agent spawned in {terminal_result.terminal_type} (PID: {terminal_result.pid})",
1208
- "terminal_type": terminal_result.terminal_type,
1209
- "pid": terminal_result.pid,
1210
- }
1211
-
1212
- elif mode == "embedded":
1213
- from gobby.agents.spawn import EmbeddedSpawner
1214
-
1215
- embedded_spawner = EmbeddedSpawner()
1216
- embedded_result = embedded_spawner.spawn_agent(
1217
- cli=provider,
1218
- cwd=worktree.worktree_path,
1219
- session_id=child_session.id,
1220
- parent_session_id=parent_session_id,
1221
- agent_run_id=agent_run.id,
1222
- project_id=resolved_project_id,
1223
- workflow_name=workflow,
1224
- agent_depth=child_session.agent_depth,
1225
- max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
1226
- prompt=enhanced_prompt,
1227
- )
1228
-
1229
- return {
1230
- "success": embedded_result.success,
1231
- "worktree_id": worktree.id,
1232
- "worktree_path": worktree.worktree_path,
1233
- "branch_name": worktree.branch_name,
1234
- "run_id": agent_run.id,
1235
- "child_session_id": child_session.id,
1236
- "status": "pending" if embedded_result.success else "error",
1237
- "error": embedded_result.error if not embedded_result.success else None,
1238
- }
1239
-
1240
- else: # headless
1241
- from gobby.agents.spawn import HeadlessSpawner
1242
-
1243
- headless_spawner = HeadlessSpawner()
1244
- headless_result = headless_spawner.spawn_agent(
1245
- cli=provider,
1246
- cwd=worktree.worktree_path,
1247
- session_id=child_session.id,
1248
- parent_session_id=parent_session_id,
1249
- agent_run_id=agent_run.id,
1250
- project_id=resolved_project_id,
1251
- workflow_name=workflow,
1252
- agent_depth=child_session.agent_depth,
1253
- max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
1254
- prompt=enhanced_prompt,
1255
- )
1256
-
1257
- return {
1258
- "success": headless_result.success,
1259
- "worktree_id": worktree.id,
1260
- "worktree_path": worktree.worktree_path,
1261
- "branch_name": worktree.branch_name,
1262
- "run_id": agent_run.id,
1263
- "child_session_id": child_session.id,
1264
- "status": "pending" if headless_result.success else "error",
1265
- "pid": headless_result.pid if headless_result.success else None,
1266
- "error": headless_result.error if not headless_result.success else None,
1267
- }
1268
-
1269
951
  return registry
gobby/memory/extractor.py CHANGED
@@ -153,10 +153,15 @@ class SessionMemoryExtractor:
153
153
  """
154
154
  session = self.session_manager.get(session_id)
155
155
  if not session:
156
+ logger.warning(f"Session not found for memory extraction: {session_id}")
156
157
  return None
157
158
 
158
- # Get project info
159
+ # Get project info - log for debugging NULL project_id issues
159
160
  project_id = session.project_id
161
+ logger.debug(
162
+ f"Memory extraction context: session={session_id}, "
163
+ f"project_id={project_id!r} (type={type(project_id).__name__})"
164
+ )
160
165
  project_name = "Unknown Project"
161
166
 
162
167
  if project_id:
@@ -461,6 +466,15 @@ class SessionMemoryExtractor:
461
466
  session_id: Source session ID
462
467
  project_id: Project ID for the memories
463
468
  """
469
+ # Log project_id for debugging NULL project_id issues
470
+ if project_id is None:
471
+ logger.warning(
472
+ f"Storing memories with NULL project_id for session {session_id}. "
473
+ "This may cause duplicate detection issues."
474
+ )
475
+ else:
476
+ logger.debug(f"Storing {len(candidates)} memories with project_id={project_id}")
477
+
464
478
  for candidate in candidates:
465
479
  try:
466
480
  await self.memory_manager.remember(
@@ -0,0 +1,5 @@
1
+ """Memory ingestion components for multimodal content."""
2
+
3
+ from gobby.memory.ingestion.multimodal import MultimodalIngestor
4
+
5
+ __all__ = ["MultimodalIngestor"]