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
gobby/cli/conductor.py ADDED
@@ -0,0 +1,266 @@
1
+ """
2
+ Conductor management CLI commands.
3
+
4
+ Commands for managing the conductor loop:
5
+ - start: Start the conductor loop
6
+ - stop: Stop the conductor loop
7
+ - restart: Restart the conductor loop
8
+ - status: Show conductor status
9
+ - chat: Send a message to the conductor
10
+ """
11
+
12
+ import json
13
+
14
+ import click
15
+ import httpx
16
+
17
+
18
+ def get_daemon_url() -> str:
19
+ """Get daemon URL from config."""
20
+ from gobby.config.app import load_config
21
+
22
+ config = load_config()
23
+ return f"http://localhost:{config.daemon_port}"
24
+
25
+
26
+ @click.group()
27
+ def conductor() -> None:
28
+ """Manage the conductor orchestration loop."""
29
+ pass
30
+
31
+
32
+ @conductor.command("start")
33
+ @click.option("--interval", "-i", type=int, default=30, help="Check interval in seconds")
34
+ @click.option("--autonomous", "-a", is_flag=True, help="Enable autonomous agent spawning")
35
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
36
+ def start_conductor(interval: int, autonomous: bool, json_format: bool) -> None:
37
+ """Start the conductor loop.
38
+
39
+ Examples:
40
+
41
+ gobby conductor start
42
+
43
+ gobby conductor start --interval 60
44
+
45
+ gobby conductor start --autonomous
46
+ """
47
+ daemon_url = get_daemon_url()
48
+
49
+ try:
50
+ response = httpx.post(
51
+ f"{daemon_url}/conductor/start",
52
+ json={"interval": interval, "autonomous": autonomous},
53
+ timeout=10.0,
54
+ )
55
+ response.raise_for_status()
56
+ result = response.json()
57
+ except httpx.ConnectError:
58
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
59
+ return
60
+ except httpx.HTTPStatusError as e:
61
+ click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
62
+ return
63
+ except ValueError as e:
64
+ click.echo(f"Error: Invalid JSON response: {e}", err=True)
65
+ return
66
+ except Exception as e:
67
+ click.echo(f"Error: {e}", err=True)
68
+ return
69
+
70
+ if json_format:
71
+ click.echo(json.dumps(result, indent=2, default=str))
72
+ return
73
+
74
+ if result.get("success"):
75
+ click.echo("Conductor started")
76
+ click.echo(f" Interval: {interval}s")
77
+ if autonomous:
78
+ click.echo(" Autonomous mode: enabled")
79
+ else:
80
+ click.echo(f"Failed to start conductor: {result.get('error')}", err=True)
81
+
82
+
83
+ @conductor.command("stop")
84
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
85
+ def stop_conductor(json_format: bool) -> None:
86
+ """Stop the conductor loop.
87
+
88
+ Examples:
89
+
90
+ gobby conductor stop
91
+ """
92
+ daemon_url = get_daemon_url()
93
+
94
+ try:
95
+ response = httpx.post(
96
+ f"{daemon_url}/conductor/stop",
97
+ json={},
98
+ timeout=10.0,
99
+ )
100
+ response.raise_for_status()
101
+ result = response.json()
102
+ except httpx.ConnectError:
103
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
104
+ return
105
+ except httpx.HTTPStatusError as e:
106
+ click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
107
+ return
108
+ except ValueError as e:
109
+ click.echo(f"Error: Invalid JSON response: {e}", err=True)
110
+ return
111
+ except Exception as e:
112
+ click.echo(f"Error: {e}", err=True)
113
+ return
114
+
115
+ if json_format:
116
+ click.echo(json.dumps(result, indent=2, default=str))
117
+ return
118
+
119
+ if result.get("success"):
120
+ click.echo("Conductor stopped")
121
+ else:
122
+ click.echo(f"Failed to stop conductor: {result.get('error')}", err=True)
123
+
124
+
125
+ @conductor.command("restart")
126
+ @click.option("--interval", "-i", type=int, default=30, help="Check interval in seconds")
127
+ @click.option("--autonomous", "-a", is_flag=True, help="Enable autonomous agent spawning")
128
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
129
+ def restart_conductor(interval: int, autonomous: bool, json_format: bool) -> None:
130
+ """Restart the conductor loop.
131
+
132
+ Examples:
133
+
134
+ gobby conductor restart
135
+
136
+ gobby conductor restart --interval 60
137
+ """
138
+ daemon_url = get_daemon_url()
139
+
140
+ try:
141
+ response = httpx.post(
142
+ f"{daemon_url}/conductor/restart",
143
+ json={"interval": interval, "autonomous": autonomous},
144
+ timeout=10.0,
145
+ )
146
+ response.raise_for_status()
147
+ result = response.json()
148
+ except httpx.ConnectError:
149
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
150
+ return
151
+ except httpx.HTTPStatusError as e:
152
+ click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
153
+ return
154
+ except ValueError as e:
155
+ click.echo(f"Error: Invalid JSON response: {e}", err=True)
156
+ return
157
+ except Exception as e:
158
+ click.echo(f"Error: {e}", err=True)
159
+ return
160
+
161
+ if json_format:
162
+ click.echo(json.dumps(result, indent=2, default=str))
163
+ return
164
+
165
+ if result.get("success"):
166
+ click.echo("Conductor restarted")
167
+ else:
168
+ click.echo(f"Failed to restart conductor: {result.get('error')}", err=True)
169
+
170
+
171
+ @conductor.command("status")
172
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
173
+ def status_conductor(json_format: bool) -> None:
174
+ """Show conductor status.
175
+
176
+ Examples:
177
+
178
+ gobby conductor status
179
+
180
+ gobby conductor status --json
181
+ """
182
+ daemon_url = get_daemon_url()
183
+
184
+ try:
185
+ response = httpx.get(
186
+ f"{daemon_url}/conductor/status",
187
+ timeout=10.0,
188
+ )
189
+ response.raise_for_status()
190
+ result = response.json()
191
+ except httpx.ConnectError:
192
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
193
+ return
194
+ except httpx.HTTPStatusError as e:
195
+ click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
196
+ return
197
+ except ValueError as e:
198
+ click.echo(f"Error: Invalid JSON response: {e}", err=True)
199
+ return
200
+ except Exception as e:
201
+ click.echo(f"Error: {e}", err=True)
202
+ return
203
+
204
+ if json_format:
205
+ click.echo(json.dumps(result, indent=2, default=str))
206
+ return
207
+
208
+ running = result.get("running", False)
209
+ if running:
210
+ click.echo("Conductor: running")
211
+ click.echo(f" Interval: {result.get('interval', 'unknown')}s")
212
+ click.echo(f" Autonomous: {result.get('autonomous', False)}")
213
+ if result.get("last_tick"):
214
+ click.echo(f" Last tick: {result['last_tick']}")
215
+ else:
216
+ click.echo("Conductor: not running")
217
+
218
+
219
+ @conductor.command("chat")
220
+ @click.argument("message")
221
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
222
+ def chat_conductor(message: str, json_format: bool) -> None:
223
+ """Send a message to the conductor.
224
+
225
+ The conductor can process commands like status checks, task queries,
226
+ or trigger manual actions.
227
+
228
+ Examples:
229
+
230
+ gobby conductor chat "Check all tasks"
231
+
232
+ gobby conductor chat "spawn agent for task-123"
233
+
234
+ gobby conductor chat --json "status check"
235
+ """
236
+ daemon_url = get_daemon_url()
237
+
238
+ try:
239
+ response = httpx.post(
240
+ f"{daemon_url}/conductor/chat",
241
+ json={"message": message},
242
+ timeout=30.0,
243
+ )
244
+ response.raise_for_status()
245
+ result = response.json()
246
+ except httpx.ConnectError:
247
+ click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
248
+ return
249
+ except httpx.HTTPStatusError as e:
250
+ click.echo(f"Error: HTTP {e.response.status_code}: {e.response.text}", err=True)
251
+ return
252
+ except ValueError as e:
253
+ click.echo(f"Error: Invalid JSON response: {e}", err=True)
254
+ return
255
+ except Exception as e:
256
+ click.echo(f"Error: {e}", err=True)
257
+ return
258
+
259
+ if json_format:
260
+ click.echo(json.dumps(result, indent=2, default=str))
261
+ return
262
+
263
+ if result.get("success"):
264
+ click.echo(result.get("response", "OK"))
265
+ else:
266
+ click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
gobby/cli/install.py CHANGED
@@ -250,7 +250,7 @@ def install(
250
250
  click.echo(f" - {cmd}")
251
251
  if result.get("plugins_installed"):
252
252
  click.echo(
253
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
253
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
254
254
  )
255
255
  for plugin in result["plugins_installed"]:
256
256
  click.echo(f" - {plugin}")
@@ -287,7 +287,7 @@ def install(
287
287
  click.echo(f" - {cmd}")
288
288
  if result.get("plugins_installed"):
289
289
  click.echo(
290
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
290
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
291
291
  )
292
292
  for plugin in result["plugins_installed"]:
293
293
  click.echo(f" - {plugin}")
@@ -334,7 +334,7 @@ def install(
334
334
  click.echo(f" - {cmd}")
335
335
  if result.get("plugins_installed"):
336
336
  click.echo(
337
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
337
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
338
338
  )
339
339
  for plugin in result["plugins_installed"]:
340
340
  click.echo(f" - {plugin}")
@@ -395,7 +395,7 @@ def install(
395
395
  click.echo(f" - {cmd}")
396
396
  if result.get("plugins_installed"):
397
397
  click.echo(
398
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
398
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
399
399
  )
400
400
  for plugin in result["plugins_installed"]:
401
401
  click.echo(f" - {plugin}")
@@ -12,7 +12,7 @@ import logging
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
15
- from .shared import configure_mcp_server_json, install_shared_skills
15
+ from .shared import configure_mcp_server_json
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -43,14 +43,8 @@ def install_antigravity(project_path: Path) -> dict[str, Any]:
43
43
  # Configure MCP server in Antigravity's MCP config (~/.gemini/antigravity/mcp_config.json)
44
44
  mcp_config = Path.home() / ".gemini" / "antigravity" / "mcp_config.json"
45
45
 
46
- # Install shared skills to ~/.antigravity/skills/ (Standard Antigravity location)
47
- try:
48
- skills_path = Path.home() / ".antigravity" / "skills"
49
- skills = install_shared_skills(skills_path)
50
- result["commands_installed"].extend([f"{s} (skill)" for s in skills])
51
- except Exception as e:
52
- logger.error(f"Failed to install shared skills: {e}")
53
- # Proceeding despite skill install failure
46
+ # Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
47
+ # No longer need to copy to .antigravity/skills/
54
48
 
55
49
  mcp_result = configure_mcp_server_json(mcp_config)
56
50
 
@@ -17,10 +17,11 @@ from typing import Any
17
17
  from gobby.cli.utils import get_install_dir
18
18
 
19
19
  from .shared import (
20
+ backup_gobby_skills,
20
21
  configure_mcp_server_json,
21
22
  install_cli_content,
23
+ install_router_skills_as_commands,
22
24
  install_shared_content,
23
- install_shared_skills,
24
25
  remove_mcp_server_json,
25
26
  )
26
27
 
@@ -55,6 +56,12 @@ def install_claude(project_path: Path) -> dict[str, Any]:
55
56
  hooks_dir = claude_path / "hooks"
56
57
  hooks_dir.mkdir(parents=True, exist_ok=True)
57
58
 
59
+ # Backup existing gobby skills (now auto-synced from database)
60
+ skills_dir = claude_path / "skills"
61
+ backup_result = backup_gobby_skills(skills_dir)
62
+ if backup_result["backed_up"] > 0:
63
+ logger.info(f"Backed up {backup_result['backed_up']} existing gobby skills")
64
+
58
65
  # Get source files
59
66
  install_dir = get_install_dir()
60
67
  claude_install_dir = install_dir / "claude"
@@ -119,14 +126,13 @@ def install_claude(project_path: Path) -> dict[str, Any]:
119
126
  result["commands_installed"] = cli.get("commands", [])
120
127
  result["plugins_installed"] = shared.get("plugins", [])
121
128
 
122
- # Install shared skills (SKILL.md)
123
- try:
124
- skills = install_shared_skills(claude_path / "skills")
125
- result["commands_installed"].extend([f"{s} (skill)" for s in skills])
126
- except Exception as e:
127
- logger.error(f"Failed to install shared skills: {e}")
128
- result["error"] = f"Failed to install shared skills: {e}"
129
- # Proceeding despite skill install failure
129
+ # Install router skills (gobby, g) as flattened commands
130
+ commands_dir = claude_path / "commands"
131
+ router_commands = install_router_skills_as_commands(commands_dir)
132
+ result["commands_installed"].extend(router_commands)
133
+
134
+ # Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
135
+ # No longer need to copy to .claude/skills/
130
136
 
131
137
  # Backup existing settings.json if it exists
132
138
  backup_file = None
@@ -18,7 +18,6 @@ from .shared import (
18
18
  configure_mcp_server_toml,
19
19
  install_cli_content,
20
20
  install_shared_content,
21
- install_shared_skills,
22
21
  remove_mcp_server_toml,
23
22
  )
24
23
 
@@ -69,13 +68,8 @@ def install_codex_notify() -> dict[str, Any]:
69
68
  # Install CLI-specific content (can override shared)
70
69
  cli = install_cli_content("codex", codex_home)
71
70
 
72
- # Install shared skills (SKILL.md)
73
- try:
74
- skills = install_shared_skills(codex_home / "skills")
75
- result["commands_installed"].extend([f"{s} (skill)" for s in skills])
76
- except Exception as e:
77
- logger.error(f"Failed to install shared skills: {e}")
78
- # Proceeding despite skill install failure
71
+ # Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
72
+ # No longer need to copy to .codex/skills/
79
73
 
80
74
  result["workflows_installed"] = shared["workflows"] + cli["workflows"]
81
75
  result["commands_installed"] = cli.get("commands", [])
@@ -17,8 +17,8 @@ from gobby.cli.utils import get_install_dir
17
17
  from .shared import (
18
18
  configure_mcp_server_json,
19
19
  install_cli_content,
20
+ install_router_skills_as_gemini_skills,
20
21
  install_shared_content,
21
- install_shared_skills,
22
22
  remove_mcp_server_json,
23
23
  )
24
24
 
@@ -81,18 +81,18 @@ def install_gemini(project_path: Path) -> dict[str, Any]:
81
81
  # Install CLI-specific content (can override shared)
82
82
  cli = install_cli_content("gemini", gemini_path)
83
83
 
84
- # Install shared skills (SKILL.md)
85
- try:
86
- skills = install_shared_skills(gemini_path / "skills")
87
- result["commands_installed"].extend([f"{s} (skill)" for s in skills])
88
- except Exception as e:
89
- logger.error(f"Failed to install shared skills: {e}")
90
- # Proceeding despite skill install failure
84
+ # Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
85
+ # No longer need to copy to .gemini/skills/
91
86
 
92
87
  result["workflows_installed"] = shared["workflows"] + cli["workflows"]
93
88
  result["commands_installed"] = cli.get("commands", [])
94
89
  result["plugins_installed"] = shared.get("plugins", [])
95
90
 
91
+ # Install router skills (gobby, g) as Gemini skills
92
+ skills_dir = gemini_path / "skills"
93
+ router_skills = install_router_skills_as_gemini_skills(skills_dir)
94
+ result["commands_installed"].extend(router_skills)
95
+
96
96
  # Backup existing settings.json if it exists
97
97
  if settings_file.exists():
98
98
  timestamp = int(time.time())
@@ -23,7 +23,8 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
23
23
  """Install shared content from src/install/shared/.
24
24
 
25
25
  Workflows are cross-CLI and go to {project_path}/.gobby/workflows/.
26
- Plugins are global and go to ~/.gobby/plugins/.
26
+ Plugins are project-scoped and go to {project_path}/.gobby/plugins/.
27
+ Prompts are project-scoped and go to {project_path}/.gobby/prompts/.
27
28
  Docs are project-local and go to {project_path}/.gobby/docs/.
28
29
 
29
30
  Args:
@@ -34,28 +35,52 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
34
35
  Dict with lists of installed items by type
35
36
  """
36
37
  shared_dir = get_install_dir() / "shared"
37
- installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "docs": []}
38
+ installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "prompts": [], "docs": []}
38
39
 
39
40
  # Install shared workflows to .gobby/workflows/ (cross-CLI)
40
41
  shared_workflows = shared_dir / "workflows"
41
42
  if shared_workflows.exists():
42
43
  target_workflows = project_path / ".gobby" / "workflows"
43
44
  target_workflows.mkdir(parents=True, exist_ok=True)
44
- for workflow_file in shared_workflows.iterdir():
45
- if workflow_file.is_file():
46
- copy2(workflow_file, target_workflows / workflow_file.name)
47
- installed["workflows"].append(workflow_file.name)
48
-
49
- # Install shared plugins to ~/.gobby/plugins/ (global)
45
+ for item in shared_workflows.iterdir():
46
+ if item.is_file():
47
+ copy2(item, target_workflows / item.name)
48
+ installed["workflows"].append(item.name)
49
+ elif item.is_dir():
50
+ # Copy subdirectories (e.g., lifecycle/)
51
+ target_subdir = target_workflows / item.name
52
+ if target_subdir.exists():
53
+ shutil.rmtree(target_subdir)
54
+ copytree(item, target_subdir)
55
+ installed["workflows"].append(f"{item.name}/")
56
+
57
+ # Install shared plugins to .gobby/plugins/ (project-scoped)
50
58
  shared_plugins = shared_dir / "plugins"
51
59
  if shared_plugins.exists():
52
- target_plugins = Path("~/.gobby/plugins").expanduser()
60
+ target_plugins = project_path / ".gobby" / "plugins"
53
61
  target_plugins.mkdir(parents=True, exist_ok=True)
54
62
  for plugin_file in shared_plugins.iterdir():
55
63
  if plugin_file.is_file() and plugin_file.suffix == ".py":
56
64
  copy2(plugin_file, target_plugins / plugin_file.name)
57
65
  installed["plugins"].append(plugin_file.name)
58
66
 
67
+ # Install shared prompts to .gobby/prompts/ (project-scoped)
68
+ shared_prompts = shared_dir / "prompts"
69
+ if shared_prompts.exists():
70
+ target_prompts = project_path / ".gobby" / "prompts"
71
+ target_prompts.mkdir(parents=True, exist_ok=True)
72
+ for item in shared_prompts.iterdir():
73
+ if item.is_file():
74
+ copy2(item, target_prompts / item.name)
75
+ installed["prompts"].append(item.name)
76
+ elif item.is_dir():
77
+ # Copy subdirectories (e.g., expansion/, validation/)
78
+ target_subdir = target_prompts / item.name
79
+ if target_subdir.exists():
80
+ shutil.rmtree(target_subdir)
81
+ copytree(item, target_subdir)
82
+ installed["prompts"].append(f"{item.name}/")
83
+
59
84
  # Install shared docs to .gobby/docs/ (project-local)
60
85
  shared_docs = shared_dir / "docs"
61
86
  if shared_docs.exists():
@@ -69,6 +94,55 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
69
94
  return installed
70
95
 
71
96
 
97
+ def backup_gobby_skills(skills_dir: Path) -> dict[str, Any]:
98
+ """Move gobby-prefixed skill directories to a backup location.
99
+
100
+ This function is called during installation to preserve existing gobby skills
101
+ before they are replaced by database-synced skills. User custom skills
102
+ (non-gobby prefixed) are not touched.
103
+
104
+ Args:
105
+ skills_dir: Path to skills directory (e.g., .claude/skills)
106
+
107
+ Returns:
108
+ Dict with:
109
+ - success: bool
110
+ - backed_up: int - number of skills moved to backup
111
+ - skipped: str (optional) - reason for skipping
112
+ """
113
+ result: dict[str, Any] = {
114
+ "success": True,
115
+ "backed_up": 0,
116
+ }
117
+
118
+ if not skills_dir.exists():
119
+ result["skipped"] = "skills directory does not exist"
120
+ return result
121
+
122
+ # Find gobby-prefixed skill directories
123
+ gobby_skills = [d for d in skills_dir.iterdir() if d.is_dir() and d.name.startswith("gobby-")]
124
+
125
+ if not gobby_skills:
126
+ return result
127
+
128
+ # Create backup directory (sibling to skills/)
129
+ backup_dir = skills_dir.parent / "skills.backup"
130
+ backup_dir.mkdir(parents=True, exist_ok=True)
131
+
132
+ # Move each gobby skill to backup
133
+ import shutil
134
+
135
+ for skill_dir in gobby_skills:
136
+ target = backup_dir / skill_dir.name
137
+ # If already exists in backup, remove it first (replace with newer)
138
+ if target.exists():
139
+ shutil.rmtree(target)
140
+ shutil.move(str(skill_dir), str(target))
141
+ result["backed_up"] += 1
142
+
143
+ return result
144
+
145
+
72
146
  def install_shared_skills(target_dir: Path) -> list[str]:
73
147
  """Install shared SKILL.md files to target directory.
74
148
 
@@ -129,6 +203,87 @@ def install_shared_skills(target_dir: Path) -> list[str]:
129
203
  return installed
130
204
 
131
205
 
206
+ def install_router_skills_as_commands(target_commands_dir: Path) -> list[str]:
207
+ """Install router skills (gobby, g) as flattened Claude commands.
208
+
209
+ Claude Code uses .claude/commands/name.md format for slash commands.
210
+ This function copies the gobby router skills from shared/skills/ to
211
+ commands/ as flattened .md files.
212
+
213
+ Args:
214
+ target_commands_dir: Path to commands directory (e.g., .claude/commands)
215
+
216
+ Returns:
217
+ List of installed command names
218
+ """
219
+ shared_skills_dir = get_install_dir() / "shared" / "skills"
220
+ installed: list[str] = []
221
+
222
+ # Router skills to install as commands
223
+ router_skills = ["gobby", "g"]
224
+
225
+ target_commands_dir.mkdir(parents=True, exist_ok=True)
226
+
227
+ for skill_name in router_skills:
228
+ source_skill_md = shared_skills_dir / skill_name / "SKILL.md"
229
+ if not source_skill_md.exists():
230
+ logger.warning(f"Router skill not found: {source_skill_md}")
231
+ continue
232
+
233
+ # Flatten: copy SKILL.md to commands/name.md
234
+ target_cmd = target_commands_dir / f"{skill_name}.md"
235
+
236
+ try:
237
+ copy2(source_skill_md, target_cmd)
238
+ installed.append(f"{skill_name}.md")
239
+ except OSError as e:
240
+ logger.error(f"Failed to copy router skill {skill_name}: {e}")
241
+
242
+ return installed
243
+
244
+
245
+ def install_router_skills_as_gemini_skills(target_skills_dir: Path) -> list[str]:
246
+ """Install router skills (gobby, g) as Gemini skills (directory structure).
247
+
248
+ Gemini CLI uses .gemini/skills/name/SKILL.md format for skills.
249
+ This function copies the gobby router skills from shared/skills/ to
250
+ the target skills directory preserving the directory structure.
251
+
252
+ Args:
253
+ target_skills_dir: Path to skills directory (e.g., .gemini/skills)
254
+
255
+ Returns:
256
+ List of installed skill names
257
+ """
258
+ shared_skills_dir = get_install_dir() / "shared" / "skills"
259
+ installed: list[str] = []
260
+
261
+ # Router skills to install
262
+ router_skills = ["gobby", "g"]
263
+
264
+ target_skills_dir.mkdir(parents=True, exist_ok=True)
265
+
266
+ for skill_name in router_skills:
267
+ source_skill_dir = shared_skills_dir / skill_name
268
+ source_skill_md = source_skill_dir / "SKILL.md"
269
+ if not source_skill_md.exists():
270
+ logger.warning(f"Router skill not found: {source_skill_md}")
271
+ continue
272
+
273
+ # Create skill directory and copy SKILL.md
274
+ target_skill_dir = target_skills_dir / skill_name
275
+ target_skill_dir.mkdir(parents=True, exist_ok=True)
276
+ target_skill_md = target_skill_dir / "SKILL.md"
277
+
278
+ try:
279
+ copy2(source_skill_md, target_skill_md)
280
+ installed.append(f"{skill_name}/")
281
+ except OSError as e:
282
+ logger.error(f"Failed to copy router skill {skill_name}: {e}")
283
+
284
+ return installed
285
+
286
+
132
287
  def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]]:
133
288
  """Install CLI-specific workflows/commands (layered on top of shared).
134
289
 
@@ -149,10 +304,17 @@ def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]
149
304
  if cli_workflows.exists():
150
305
  target_workflows = target_path / "workflows"
151
306
  target_workflows.mkdir(parents=True, exist_ok=True)
152
- for workflow_file in cli_workflows.iterdir():
153
- if workflow_file.is_file():
154
- copy2(workflow_file, target_workflows / workflow_file.name)
155
- installed["workflows"].append(workflow_file.name)
307
+ for item in cli_workflows.iterdir():
308
+ if item.is_file():
309
+ copy2(item, target_workflows / item.name)
310
+ installed["workflows"].append(item.name)
311
+ elif item.is_dir():
312
+ # Copy subdirectories
313
+ target_subdir = target_workflows / item.name
314
+ if target_subdir.exists():
315
+ shutil.rmtree(target_subdir)
316
+ copytree(item, target_subdir)
317
+ installed["workflows"].append(f"{item.name}/")
156
318
 
157
319
  # CLI-specific commands (slash commands)
158
320
  # Claude/Gemini: commands/, Codex: prompts/
gobby/cli/sessions.py CHANGED
@@ -387,7 +387,7 @@ def create_handoff(
387
387
  import time
388
388
  from pathlib import Path
389
389
 
390
- from gobby.mcp_proxy.tools.session_messages import _format_handoff_markdown
390
+ from gobby.mcp_proxy.tools.sessions._handoff import _format_handoff_markdown
391
391
  from gobby.sessions.analyzer import TranscriptAnalyzer
392
392
 
393
393
  manager = get_session_manager()