gobby 0.2.5__py3-none-any.whl → 0.2.7__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 (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  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/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,248 @@
1
+ """Search coordination for memory recall operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from gobby.storage.memories import Memory
9
+
10
+ if TYPE_CHECKING:
11
+ from gobby.config.persistence import MemoryConfig
12
+ from gobby.memory.search import SearchBackend
13
+ from gobby.storage.database import DatabaseProtocol
14
+ from gobby.storage.memories import LocalMemoryManager
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SearchCoordinator:
20
+ """
21
+ Coordinates search operations for memory recall.
22
+
23
+ Manages the search backend lifecycle, fitting, and query execution.
24
+ Extracts search-related logic from MemoryManager for focused responsibility.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ storage: LocalMemoryManager,
30
+ config: MemoryConfig,
31
+ db: DatabaseProtocol,
32
+ ):
33
+ """
34
+ Initialize the search coordinator.
35
+
36
+ Args:
37
+ storage: Memory storage manager for accessing memories
38
+ config: Memory configuration for search settings
39
+ db: Database connection for search backend initialization
40
+ """
41
+ self._storage = storage
42
+ self._config = config
43
+ self._db = db
44
+ self._search_backend: SearchBackend | None = None
45
+ self._search_backend_fitted = False
46
+
47
+ @property
48
+ def search_backend(self) -> SearchBackend:
49
+ """
50
+ Lazy-init search backend based on configuration.
51
+
52
+ The backend type is determined by config.search_backend:
53
+ - "tfidf" (default): Zero-dependency TF-IDF search
54
+ - "text": Simple text substring matching
55
+ """
56
+ if self._search_backend is None:
57
+ from gobby.memory.search import get_search_backend
58
+
59
+ backend_type = getattr(self._config, "search_backend", "tfidf")
60
+ logger.debug(f"Initializing search backend: {backend_type}")
61
+
62
+ try:
63
+ self._search_backend = get_search_backend(
64
+ backend_type=backend_type,
65
+ db=self._db,
66
+ )
67
+ except Exception as e:
68
+ logger.warning(
69
+ f"Failed to initialize {backend_type} backend: {e}. Falling back to tfidf"
70
+ )
71
+ self._search_backend = get_search_backend("tfidf")
72
+
73
+ return self._search_backend
74
+
75
+ def ensure_fitted(self) -> None:
76
+ """Ensure the search backend is fitted with current memories."""
77
+ if self._search_backend_fitted:
78
+ return
79
+
80
+ backend = self.search_backend
81
+ if not backend.needs_refit():
82
+ self._search_backend_fitted = True
83
+ return
84
+
85
+ # Fit the backend with all memories
86
+ max_memories = getattr(self._config, "max_index_memories", 10000)
87
+ memories = self._storage.list_memories(limit=max_memories)
88
+ memory_tuples = [(m.id, m.content) for m in memories]
89
+
90
+ try:
91
+ backend.fit(memory_tuples)
92
+ self._search_backend_fitted = True
93
+ logger.info(f"Search backend fitted with {len(memory_tuples)} memories")
94
+ except Exception as e:
95
+ logger.error(f"Failed to fit search backend: {e}")
96
+ raise
97
+
98
+ def mark_refit_needed(self) -> None:
99
+ """Mark that the search backend needs to be refitted."""
100
+ self._search_backend_fitted = False
101
+
102
+ def reindex(self) -> dict[str, Any]:
103
+ """
104
+ Force rebuild of the search index.
105
+
106
+ This method explicitly rebuilds the TF-IDF (or other configured)
107
+ search index from all stored memories. Useful for:
108
+ - Initial index building
109
+ - Recovery after corruption
110
+ - After bulk memory operations
111
+
112
+ Returns:
113
+ Dict with index statistics including memory_count, backend_type, etc.
114
+ """
115
+ # Get all memories
116
+ memories = self._storage.list_memories(limit=10000)
117
+ memory_tuples = [(m.id, m.content) for m in memories]
118
+
119
+ # Force refit the backend
120
+ backend = self.search_backend
121
+ backend_type = getattr(self._config, "search_backend", "tfidf")
122
+
123
+ try:
124
+ backend.fit(memory_tuples)
125
+ self._search_backend_fitted = True
126
+
127
+ # Get backend stats
128
+ stats = backend.get_stats() if hasattr(backend, "get_stats") else {}
129
+
130
+ return {
131
+ "success": True,
132
+ "memory_count": len(memory_tuples),
133
+ "backend_type": backend_type,
134
+ "fitted": True,
135
+ **stats,
136
+ }
137
+ except Exception as e:
138
+ logger.error(f"Failed to reindex search backend: {e}")
139
+ return {
140
+ "success": False,
141
+ "error": str(e),
142
+ "memory_count": len(memory_tuples),
143
+ "backend_type": backend_type,
144
+ }
145
+
146
+ def search(
147
+ self,
148
+ query: str,
149
+ project_id: str | None = None,
150
+ limit: int = 10,
151
+ min_importance: float | None = None,
152
+ search_mode: str | None = None,
153
+ tags_all: list[str] | None = None,
154
+ tags_any: list[str] | None = None,
155
+ tags_none: list[str] | None = None,
156
+ ) -> list[Memory]:
157
+ """
158
+ Perform search using the configured search backend.
159
+
160
+ Uses the new search backend by default (TF-IDF),
161
+ falling back to legacy semantic search if configured.
162
+
163
+ Args:
164
+ query: Search query text
165
+ project_id: Filter by project
166
+ limit: Maximum results to return
167
+ min_importance: Minimum importance threshold
168
+ search_mode: Search mode (tfidf, text, etc.)
169
+ tags_all: Memory must have ALL of these tags
170
+ tags_any: Memory must have at least ONE of these tags
171
+ tags_none: Memory must have NONE of these tags
172
+
173
+ Returns:
174
+ List of matching Memory objects
175
+ """
176
+ # Determine search mode from config or parameters
177
+ if search_mode is None:
178
+ search_mode = getattr(self._config, "search_backend", "tfidf")
179
+
180
+ # Use the search backend
181
+ try:
182
+ self.ensure_fitted()
183
+ # Fetch more results to allow for filtering
184
+ fetch_multiplier = 3 if (tags_all or tags_any or tags_none) else 2
185
+ results = self.search_backend.search(query, top_k=limit * fetch_multiplier)
186
+
187
+ # Get the actual Memory objects
188
+ memory_ids = [mid for mid, _ in results]
189
+ memories = []
190
+ for mid in memory_ids:
191
+ memory = self._storage.get_memory(mid)
192
+ if memory:
193
+ # Apply filters - allow global memories (project_id is None) to pass through
194
+ if (
195
+ project_id
196
+ and memory.project_id is not None
197
+ and memory.project_id != project_id
198
+ ):
199
+ continue
200
+ if min_importance is not None and memory.importance < min_importance:
201
+ continue
202
+ # Apply tag filters
203
+ if not self._passes_tag_filter(memory, tags_all, tags_any, tags_none):
204
+ continue
205
+ memories.append(memory)
206
+ if len(memories) >= limit:
207
+ break
208
+
209
+ return memories
210
+
211
+ except Exception as e:
212
+ logger.warning(f"Search backend failed, falling back to text search: {e}")
213
+ # Fall back to text search with tag filtering
214
+ memories = self._storage.search_memories(
215
+ query_text=query,
216
+ project_id=project_id,
217
+ limit=limit * 2,
218
+ tags_all=tags_all,
219
+ tags_any=tags_any,
220
+ tags_none=tags_none,
221
+ )
222
+ if min_importance:
223
+ memories = [m for m in memories if m.importance >= min_importance]
224
+ return memories[:limit]
225
+
226
+ def _passes_tag_filter(
227
+ self,
228
+ memory: Memory,
229
+ tags_all: list[str] | None = None,
230
+ tags_any: list[str] | None = None,
231
+ tags_none: list[str] | None = None,
232
+ ) -> bool:
233
+ """Check if a memory passes the tag filter criteria."""
234
+ memory_tags = set(memory.tags) if memory.tags else set()
235
+
236
+ # Check tags_all: memory must have ALL specified tags
237
+ if tags_all and not set(tags_all).issubset(memory_tags):
238
+ return False
239
+
240
+ # Check tags_any: memory must have at least ONE specified tag
241
+ if tags_any and not memory_tags.intersection(tags_any):
242
+ return False
243
+
244
+ # Check tags_none: memory must have NONE of the specified tags
245
+ if tags_none and memory_tags.intersection(tags_none):
246
+ return False
247
+
248
+ return True
@@ -0,0 +1,5 @@
1
+ """Memory services for cross-referencing and linking."""
2
+
3
+ from gobby.memory.services.crossref import CrossrefService
4
+
5
+ __all__ = ["CrossrefService"]
@@ -0,0 +1,142 @@
1
+ """Cross-reference service for linking related memories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING
9
+
10
+ from gobby.storage.memories import Memory
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.config.persistence import MemoryConfig
14
+ from gobby.memory.search import SearchBackend
15
+ from gobby.storage.memories import LocalMemoryManager
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class CrossrefService:
21
+ """
22
+ Service for creating and managing cross-references between memories.
23
+
24
+ Cross-references link related memories based on content similarity,
25
+ enabling navigation between conceptually connected items.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ storage: LocalMemoryManager,
31
+ config: MemoryConfig,
32
+ search_backend_getter: Callable[[], SearchBackend],
33
+ ensure_fitted: Callable[[], None],
34
+ ):
35
+ """
36
+ Initialize the cross-reference service.
37
+
38
+ Args:
39
+ storage: Memory storage manager for persistence
40
+ config: Memory configuration for thresholds
41
+ search_backend_getter: Callable that returns the search backend
42
+ ensure_fitted: Callable that ensures search backend is fitted
43
+ """
44
+ self._storage = storage
45
+ self._config = config
46
+ self._get_search_backend = search_backend_getter
47
+ self._ensure_fitted = ensure_fitted
48
+
49
+ async def create_crossrefs(
50
+ self,
51
+ memory: Memory,
52
+ threshold: float | None = None,
53
+ max_links: int | None = None,
54
+ ) -> int:
55
+ """
56
+ Find and link similar memories.
57
+
58
+ Uses the search backend to find memories similar to the given one
59
+ and creates cross-references for those above the threshold.
60
+
61
+ Args:
62
+ memory: The memory to find links for
63
+ threshold: Minimum similarity to create link (default from config)
64
+ max_links: Maximum links to create (default from config)
65
+
66
+ Returns:
67
+ Number of cross-references created
68
+ """
69
+ # Get thresholds from config or use defaults
70
+ if threshold is None:
71
+ threshold = getattr(self._config, "crossref_threshold", None)
72
+ if threshold is None:
73
+ threshold = 0.3
74
+ if max_links is None:
75
+ max_links = getattr(self._config, "crossref_max_links", None)
76
+ if max_links is None:
77
+ max_links = 5
78
+
79
+ # Ensure search backend is fitted (sync check is fine - just checks a flag)
80
+ self._ensure_fitted()
81
+
82
+ # Search for similar memories (wrap sync I/O in to_thread)
83
+ search_backend = self._get_search_backend()
84
+ similar = await asyncio.to_thread(search_backend.search, memory.content, max_links + 1)
85
+
86
+ # Create cross-references
87
+ created = 0
88
+ for other_id, score in similar:
89
+ # Skip self-reference
90
+ if other_id == memory.id:
91
+ continue
92
+
93
+ # Skip below threshold
94
+ if score < threshold:
95
+ continue
96
+
97
+ # Create the crossref (wrap sync I/O in to_thread)
98
+ await asyncio.to_thread(self._storage.create_crossref, memory.id, other_id, score)
99
+ created += 1
100
+
101
+ if created >= max_links:
102
+ break
103
+
104
+ if created > 0:
105
+ logger.debug(f"Created {created} crossrefs for memory {memory.id}")
106
+
107
+ return created
108
+
109
+ async def get_related(
110
+ self,
111
+ memory_id: str,
112
+ limit: int = 5,
113
+ min_similarity: float = 0.0,
114
+ ) -> list[Memory]:
115
+ """
116
+ Get memories linked to this one via cross-references.
117
+
118
+ Args:
119
+ memory_id: The memory ID to find related memories for
120
+ limit: Maximum number of results
121
+ min_similarity: Minimum similarity threshold
122
+
123
+ Returns:
124
+ List of related Memory objects, sorted by similarity
125
+ """
126
+ crossrefs = await asyncio.to_thread(
127
+ self._storage.get_crossrefs,
128
+ memory_id,
129
+ limit=limit,
130
+ min_similarity=min_similarity,
131
+ )
132
+
133
+ # Get the actual Memory objects
134
+ memories = []
135
+ for ref in crossrefs:
136
+ # Get the "other" memory in the relationship
137
+ other_id = ref.target_id if ref.source_id == memory_id else ref.source_id
138
+ memory = await asyncio.to_thread(self._storage.get_memory, other_id)
139
+ if memory:
140
+ memories.append(memory)
141
+
142
+ return memories
gobby/prompts/loader.py CHANGED
@@ -23,8 +23,11 @@ from .models import PromptTemplate
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
25
 
26
- # Default location for bundled prompts
27
- DEFAULTS_DIR = Path(__file__).parent / "defaults"
26
+ # Default location for bundled prompts (in install/shared/prompts for installation)
27
+ # Falls back to src location for development if install location doesn't exist
28
+ _INSTALL_PROMPTS_DIR = Path(__file__).parent.parent / "install" / "shared" / "prompts"
29
+ _DEV_PROMPTS_DIR = Path(__file__).parent / "defaults"
30
+ DEFAULTS_DIR = _INSTALL_PROMPTS_DIR if _INSTALL_PROMPTS_DIR.exists() else _DEV_PROMPTS_DIR
28
31
 
29
32
 
30
33
  class PromptLoader:
gobby/runner.py CHANGED
@@ -18,6 +18,7 @@ from gobby.servers.http import HTTPServer
18
18
  from gobby.servers.websocket import WebSocketConfig, WebSocketServer
19
19
  from gobby.sessions.lifecycle import SessionLifecycleManager
20
20
  from gobby.sessions.processor import SessionMessageProcessor
21
+ from gobby.storage.clones import LocalCloneManager
21
22
  from gobby.storage.database import DatabaseProtocol, LocalDatabase
22
23
  from gobby.storage.mcp import LocalMCPManager
23
24
  from gobby.storage.migrations import run_migrations
@@ -28,10 +29,10 @@ from gobby.storage.tasks import LocalTaskManager
28
29
  from gobby.storage.worktrees import LocalWorktreeManager
29
30
  from gobby.sync.memories import MemorySyncManager
30
31
  from gobby.sync.tasks import TaskSyncManager
31
- from gobby.tasks.expansion import TaskExpander
32
32
  from gobby.tasks.validation import TaskValidator
33
33
  from gobby.utils.logging import setup_file_logging
34
34
  from gobby.utils.machine_id import get_machine_id
35
+ from gobby.worktrees.git import WorktreeGitManager
35
36
 
36
37
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
37
38
 
@@ -43,7 +44,6 @@ class GobbyRunner:
43
44
 
44
45
  def __init__(self, config_path: Path | None = None, verbose: bool = False):
45
46
  setup_file_logging(verbose=verbose)
46
- # setup_mcp_logging(verbose=verbose) # Removed as per instruction
47
47
 
48
48
  config_file = str(config_path) if config_path else None
49
49
  self.config = load_config(config_file)
@@ -59,6 +59,16 @@ class GobbyRunner:
59
59
  self.task_manager = LocalTaskManager(self.database)
60
60
  self.session_task_manager = SessionTaskManager(self.database)
61
61
 
62
+ # Sync bundled skills to database
63
+ from gobby.skills.sync import sync_bundled_skills
64
+
65
+ try:
66
+ skill_result = sync_bundled_skills(self.database)
67
+ if skill_result["synced"] > 0:
68
+ logger.info(f"Synced {skill_result['synced']} bundled skills to database")
69
+ except Exception as e:
70
+ logger.warning(f"Failed to sync bundled skills: {e}")
71
+
62
72
  # Initialize LLM Service
63
73
  self.llm_service: LLMService | None = None # Added type hint
64
74
  try:
@@ -134,23 +144,11 @@ class GobbyRunner:
134
144
  poll_interval=self.config.message_tracking.poll_interval,
135
145
  )
136
146
 
137
- # Initialize Task Managers (Phase 7.1)
138
- self.task_expander: TaskExpander | None = None
147
+ # Initialize Task Validator (Phase 7.1)
139
148
  self.task_validator: TaskValidator | None = None
140
149
 
141
150
  if self.llm_service:
142
151
  gobby_tasks_config = self.config.gobby_tasks
143
- if gobby_tasks_config.expansion.enabled:
144
- try:
145
- self.task_expander = TaskExpander(
146
- llm_service=self.llm_service,
147
- config=gobby_tasks_config.expansion,
148
- task_manager=self.task_manager,
149
- mcp_manager=self.mcp_proxy,
150
- )
151
- except Exception as e:
152
- logger.error(f"Failed to initialize TaskExpander: {e}")
153
-
154
152
  if gobby_tasks_config.validation.enabled:
155
153
  try:
156
154
  self.task_validator = TaskValidator(
@@ -163,6 +161,26 @@ class GobbyRunner:
163
161
  # Initialize Worktree Storage (Phase 7 - Subagents)
164
162
  self.worktree_storage = LocalWorktreeManager(self.database)
165
163
 
164
+ # Initialize Clone Storage (local git clones for isolated development)
165
+ self.clone_storage = LocalCloneManager(self.database)
166
+
167
+ # Initialize Git Manager for current project (if in a git repo)
168
+ self.git_manager: WorktreeGitManager | None = None
169
+ self.project_id: str | None = None
170
+ try:
171
+ cwd = Path.cwd()
172
+ project_json = cwd / ".gobby" / "project.json"
173
+ if project_json.exists():
174
+ import json
175
+
176
+ project_data = json.loads(project_json.read_text())
177
+ repo_path = project_data.get("repo_path", str(cwd))
178
+ self.project_id = project_data.get("id")
179
+ self.git_manager = WorktreeGitManager(repo_path)
180
+ logger.info(f"Git manager initialized for project: {self.project_id}")
181
+ except Exception as e:
182
+ logger.debug(f"Could not initialize git manager: {e}")
183
+
166
184
  # Initialize Agent Runner (Phase 7 - Subagents)
167
185
  # Create executor registry for lazy executor creation
168
186
  self.executor_registry = ExecutorRegistry(config=self.config)
@@ -196,6 +214,7 @@ class GobbyRunner:
196
214
  # HTTP Server
197
215
  self.http_server = HTTPServer(
198
216
  port=self.config.daemon_port,
217
+ test_mode=self.config.test_mode,
199
218
  mcp_manager=self.mcp_proxy,
200
219
  mcp_db_manager=self.mcp_db_manager,
201
220
  config=self.config,
@@ -207,11 +226,13 @@ class GobbyRunner:
207
226
  llm_service=self.llm_service,
208
227
  message_processor=self.message_processor,
209
228
  memory_sync_manager=self.memory_sync_manager,
210
- task_expander=self.task_expander,
211
229
  task_validator=self.task_validator,
212
230
  metrics_manager=self.metrics_manager,
213
231
  agent_runner=self.agent_runner,
214
232
  worktree_storage=self.worktree_storage,
233
+ clone_storage=self.clone_storage,
234
+ git_manager=self.git_manager,
235
+ project_id=self.project_id,
215
236
  )
216
237
 
217
238
  # Ensure message_processor property is set (redundant but explicit):
gobby/search/__init__.py CHANGED
@@ -1,23 +1,65 @@
1
1
  """
2
- Shared search backend abstraction.
2
+ Unified search backend abstraction.
3
3
 
4
- Provides pluggable search backends for semantic search:
5
- - TF-IDF (default) - Built-in local search using scikit-learn (sklearn)
4
+ Provides a unified search layer with multiple backends:
5
+ - TF-IDF (default) - Built-in local search using scikit-learn
6
+ - Embedding - LiteLLM-based semantic search (OpenAI, Ollama, etc.)
7
+ - Unified - Orchestrates between backends with automatic fallback
6
8
 
7
- Usage:
8
- from gobby.search import SearchBackend, get_search_backend, TFIDFSearcher
9
+ Basic usage (sync TF-IDF):
10
+ from gobby.search import TFIDFSearcher
9
11
 
10
- backend = get_search_backend("tfidf")
12
+ backend = TFIDFSearcher()
11
13
  backend.fit([(id, content) for id, content in items])
12
14
  results = backend.search("query text", top_k=10)
15
+
16
+ Unified search (async with fallback):
17
+ from gobby.search import UnifiedSearcher, SearchConfig
18
+
19
+ config = SearchConfig(mode="auto") # auto, tfidf, embedding, hybrid
20
+ searcher = UnifiedSearcher(config)
21
+ await searcher.fit_async([(id, content) for id, content in items])
22
+ results = await searcher.search_async("query text", top_k=10)
23
+
24
+ if searcher.is_using_fallback():
25
+ print(f"Using fallback: {searcher.get_fallback_reason()}")
13
26
  """
14
27
 
28
+ # Sync search backends (backwards compatibility)
29
+ # Async backends
30
+ from gobby.search.backends import AsyncSearchBackend, EmbeddingBackend, TFIDFBackend
31
+
32
+ # Embedding utilities
33
+ from gobby.search.embeddings import (
34
+ generate_embedding,
35
+ generate_embeddings,
36
+ is_embedding_available,
37
+ )
38
+
39
+ # Unified search (async with fallback)
40
+ from gobby.search.models import FallbackEvent, SearchConfig, SearchMode
15
41
  from gobby.search.protocol import SearchBackend, SearchResult, get_search_backend
16
42
  from gobby.search.tfidf import TFIDFSearcher
43
+ from gobby.search.unified import UnifiedSearcher
17
44
 
18
45
  __all__ = [
46
+ # Sync backends (backwards compatible)
19
47
  "SearchBackend",
20
48
  "SearchResult",
21
49
  "TFIDFSearcher",
22
50
  "get_search_backend",
51
+ # Models
52
+ "SearchConfig",
53
+ "SearchMode",
54
+ "FallbackEvent",
55
+ # Unified searcher
56
+ "UnifiedSearcher",
57
+ # Async backends
58
+ "AsyncSearchBackend",
59
+ "TFIDFBackend",
60
+ "EmbeddingBackend",
61
+ # Embedding utilities
62
+ "generate_embedding",
63
+ "generate_embeddings",
64
+ "is_embedding_available",
23
65
  ]