gobby 0.2.5__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 (383) hide show
  1. gobby/__init__.py +3 -0
  2. gobby/adapters/__init__.py +30 -0
  3. gobby/adapters/base.py +93 -0
  4. gobby/adapters/claude_code.py +276 -0
  5. gobby/adapters/codex.py +1292 -0
  6. gobby/adapters/gemini.py +343 -0
  7. gobby/agents/__init__.py +37 -0
  8. gobby/agents/codex_session.py +120 -0
  9. gobby/agents/constants.py +112 -0
  10. gobby/agents/context.py +362 -0
  11. gobby/agents/definitions.py +133 -0
  12. gobby/agents/gemini_session.py +111 -0
  13. gobby/agents/registry.py +618 -0
  14. gobby/agents/runner.py +968 -0
  15. gobby/agents/session.py +259 -0
  16. gobby/agents/spawn.py +916 -0
  17. gobby/agents/spawners/__init__.py +77 -0
  18. gobby/agents/spawners/base.py +142 -0
  19. gobby/agents/spawners/cross_platform.py +266 -0
  20. gobby/agents/spawners/embedded.py +225 -0
  21. gobby/agents/spawners/headless.py +226 -0
  22. gobby/agents/spawners/linux.py +125 -0
  23. gobby/agents/spawners/macos.py +277 -0
  24. gobby/agents/spawners/windows.py +308 -0
  25. gobby/agents/tty_config.py +319 -0
  26. gobby/autonomous/__init__.py +32 -0
  27. gobby/autonomous/progress_tracker.py +447 -0
  28. gobby/autonomous/stop_registry.py +269 -0
  29. gobby/autonomous/stuck_detector.py +383 -0
  30. gobby/cli/__init__.py +67 -0
  31. gobby/cli/__main__.py +8 -0
  32. gobby/cli/agents.py +529 -0
  33. gobby/cli/artifacts.py +266 -0
  34. gobby/cli/daemon.py +329 -0
  35. gobby/cli/extensions.py +526 -0
  36. gobby/cli/github.py +263 -0
  37. gobby/cli/init.py +53 -0
  38. gobby/cli/install.py +614 -0
  39. gobby/cli/installers/__init__.py +37 -0
  40. gobby/cli/installers/antigravity.py +65 -0
  41. gobby/cli/installers/claude.py +363 -0
  42. gobby/cli/installers/codex.py +192 -0
  43. gobby/cli/installers/gemini.py +294 -0
  44. gobby/cli/installers/git_hooks.py +377 -0
  45. gobby/cli/installers/shared.py +737 -0
  46. gobby/cli/linear.py +250 -0
  47. gobby/cli/mcp.py +30 -0
  48. gobby/cli/mcp_proxy.py +698 -0
  49. gobby/cli/memory.py +304 -0
  50. gobby/cli/merge.py +384 -0
  51. gobby/cli/projects.py +79 -0
  52. gobby/cli/sessions.py +622 -0
  53. gobby/cli/tasks/__init__.py +30 -0
  54. gobby/cli/tasks/_utils.py +658 -0
  55. gobby/cli/tasks/ai.py +1025 -0
  56. gobby/cli/tasks/commits.py +169 -0
  57. gobby/cli/tasks/crud.py +685 -0
  58. gobby/cli/tasks/deps.py +135 -0
  59. gobby/cli/tasks/labels.py +63 -0
  60. gobby/cli/tasks/main.py +273 -0
  61. gobby/cli/tasks/search.py +178 -0
  62. gobby/cli/tui.py +34 -0
  63. gobby/cli/utils.py +513 -0
  64. gobby/cli/workflows.py +927 -0
  65. gobby/cli/worktrees.py +481 -0
  66. gobby/config/__init__.py +129 -0
  67. gobby/config/app.py +551 -0
  68. gobby/config/extensions.py +167 -0
  69. gobby/config/features.py +472 -0
  70. gobby/config/llm_providers.py +98 -0
  71. gobby/config/logging.py +66 -0
  72. gobby/config/mcp.py +346 -0
  73. gobby/config/persistence.py +247 -0
  74. gobby/config/servers.py +141 -0
  75. gobby/config/sessions.py +250 -0
  76. gobby/config/tasks.py +784 -0
  77. gobby/hooks/__init__.py +104 -0
  78. gobby/hooks/artifact_capture.py +213 -0
  79. gobby/hooks/broadcaster.py +243 -0
  80. gobby/hooks/event_handlers.py +723 -0
  81. gobby/hooks/events.py +218 -0
  82. gobby/hooks/git.py +169 -0
  83. gobby/hooks/health_monitor.py +171 -0
  84. gobby/hooks/hook_manager.py +856 -0
  85. gobby/hooks/hook_types.py +575 -0
  86. gobby/hooks/plugins.py +813 -0
  87. gobby/hooks/session_coordinator.py +396 -0
  88. gobby/hooks/verification_runner.py +268 -0
  89. gobby/hooks/webhooks.py +339 -0
  90. gobby/install/claude/commands/gobby/bug.md +51 -0
  91. gobby/install/claude/commands/gobby/chore.md +51 -0
  92. gobby/install/claude/commands/gobby/epic.md +52 -0
  93. gobby/install/claude/commands/gobby/eval.md +235 -0
  94. gobby/install/claude/commands/gobby/feat.md +49 -0
  95. gobby/install/claude/commands/gobby/nit.md +52 -0
  96. gobby/install/claude/commands/gobby/ref.md +52 -0
  97. gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
  98. gobby/install/claude/hooks/hook_dispatcher.py +364 -0
  99. gobby/install/claude/hooks/validate_settings.py +102 -0
  100. gobby/install/claude/hooks-template.json +118 -0
  101. gobby/install/codex/hooks/hook_dispatcher.py +153 -0
  102. gobby/install/codex/prompts/forget.md +7 -0
  103. gobby/install/codex/prompts/memories.md +7 -0
  104. gobby/install/codex/prompts/recall.md +7 -0
  105. gobby/install/codex/prompts/remember.md +13 -0
  106. gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
  107. gobby/install/gemini/hooks-template.json +138 -0
  108. gobby/install/shared/plugins/code_guardian.py +456 -0
  109. gobby/install/shared/plugins/example_notify.py +331 -0
  110. gobby/integrations/__init__.py +10 -0
  111. gobby/integrations/github.py +145 -0
  112. gobby/integrations/linear.py +145 -0
  113. gobby/llm/__init__.py +40 -0
  114. gobby/llm/base.py +120 -0
  115. gobby/llm/claude.py +578 -0
  116. gobby/llm/claude_executor.py +503 -0
  117. gobby/llm/codex.py +322 -0
  118. gobby/llm/codex_executor.py +513 -0
  119. gobby/llm/executor.py +316 -0
  120. gobby/llm/factory.py +34 -0
  121. gobby/llm/gemini.py +258 -0
  122. gobby/llm/gemini_executor.py +339 -0
  123. gobby/llm/litellm.py +287 -0
  124. gobby/llm/litellm_executor.py +303 -0
  125. gobby/llm/resolver.py +499 -0
  126. gobby/llm/service.py +236 -0
  127. gobby/mcp_proxy/__init__.py +29 -0
  128. gobby/mcp_proxy/actions.py +175 -0
  129. gobby/mcp_proxy/daemon_control.py +198 -0
  130. gobby/mcp_proxy/importer.py +436 -0
  131. gobby/mcp_proxy/lazy.py +325 -0
  132. gobby/mcp_proxy/manager.py +798 -0
  133. gobby/mcp_proxy/metrics.py +609 -0
  134. gobby/mcp_proxy/models.py +139 -0
  135. gobby/mcp_proxy/registries.py +215 -0
  136. gobby/mcp_proxy/schema_hash.py +381 -0
  137. gobby/mcp_proxy/semantic_search.py +706 -0
  138. gobby/mcp_proxy/server.py +549 -0
  139. gobby/mcp_proxy/services/__init__.py +0 -0
  140. gobby/mcp_proxy/services/fallback.py +306 -0
  141. gobby/mcp_proxy/services/recommendation.py +224 -0
  142. gobby/mcp_proxy/services/server_mgmt.py +214 -0
  143. gobby/mcp_proxy/services/system.py +72 -0
  144. gobby/mcp_proxy/services/tool_filter.py +231 -0
  145. gobby/mcp_proxy/services/tool_proxy.py +309 -0
  146. gobby/mcp_proxy/stdio.py +565 -0
  147. gobby/mcp_proxy/tools/__init__.py +27 -0
  148. gobby/mcp_proxy/tools/agents.py +1103 -0
  149. gobby/mcp_proxy/tools/artifacts.py +207 -0
  150. gobby/mcp_proxy/tools/hub.py +335 -0
  151. gobby/mcp_proxy/tools/internal.py +337 -0
  152. gobby/mcp_proxy/tools/memory.py +543 -0
  153. gobby/mcp_proxy/tools/merge.py +422 -0
  154. gobby/mcp_proxy/tools/metrics.py +283 -0
  155. gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
  156. gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
  157. gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
  158. gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
  159. gobby/mcp_proxy/tools/orchestration/review.py +736 -0
  160. gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
  161. gobby/mcp_proxy/tools/session_messages.py +1056 -0
  162. gobby/mcp_proxy/tools/task_dependencies.py +219 -0
  163. gobby/mcp_proxy/tools/task_expansion.py +591 -0
  164. gobby/mcp_proxy/tools/task_github.py +393 -0
  165. gobby/mcp_proxy/tools/task_linear.py +379 -0
  166. gobby/mcp_proxy/tools/task_orchestration.py +77 -0
  167. gobby/mcp_proxy/tools/task_readiness.py +522 -0
  168. gobby/mcp_proxy/tools/task_sync.py +351 -0
  169. gobby/mcp_proxy/tools/task_validation.py +843 -0
  170. gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
  171. gobby/mcp_proxy/tools/tasks/_context.py +112 -0
  172. gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
  173. gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
  174. gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
  175. gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
  176. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
  177. gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
  178. gobby/mcp_proxy/tools/tasks/_search.py +215 -0
  179. gobby/mcp_proxy/tools/tasks/_session.py +125 -0
  180. gobby/mcp_proxy/tools/workflows.py +973 -0
  181. gobby/mcp_proxy/tools/worktrees.py +1264 -0
  182. gobby/mcp_proxy/transports/__init__.py +0 -0
  183. gobby/mcp_proxy/transports/base.py +95 -0
  184. gobby/mcp_proxy/transports/factory.py +44 -0
  185. gobby/mcp_proxy/transports/http.py +139 -0
  186. gobby/mcp_proxy/transports/stdio.py +213 -0
  187. gobby/mcp_proxy/transports/websocket.py +136 -0
  188. gobby/memory/backends/__init__.py +116 -0
  189. gobby/memory/backends/mem0.py +408 -0
  190. gobby/memory/backends/memu.py +485 -0
  191. gobby/memory/backends/null.py +111 -0
  192. gobby/memory/backends/openmemory.py +537 -0
  193. gobby/memory/backends/sqlite.py +304 -0
  194. gobby/memory/context.py +87 -0
  195. gobby/memory/manager.py +1001 -0
  196. gobby/memory/protocol.py +451 -0
  197. gobby/memory/search/__init__.py +66 -0
  198. gobby/memory/search/text.py +127 -0
  199. gobby/memory/viz.py +258 -0
  200. gobby/prompts/__init__.py +13 -0
  201. gobby/prompts/defaults/expansion/system.md +119 -0
  202. gobby/prompts/defaults/expansion/user.md +48 -0
  203. gobby/prompts/defaults/external_validation/agent.md +72 -0
  204. gobby/prompts/defaults/external_validation/external.md +63 -0
  205. gobby/prompts/defaults/external_validation/spawn.md +83 -0
  206. gobby/prompts/defaults/external_validation/system.md +6 -0
  207. gobby/prompts/defaults/features/import_mcp.md +22 -0
  208. gobby/prompts/defaults/features/import_mcp_github.md +17 -0
  209. gobby/prompts/defaults/features/import_mcp_search.md +16 -0
  210. gobby/prompts/defaults/features/recommend_tools.md +32 -0
  211. gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
  212. gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
  213. gobby/prompts/defaults/features/server_description.md +20 -0
  214. gobby/prompts/defaults/features/server_description_system.md +6 -0
  215. gobby/prompts/defaults/features/task_description.md +31 -0
  216. gobby/prompts/defaults/features/task_description_system.md +6 -0
  217. gobby/prompts/defaults/features/tool_summary.md +17 -0
  218. gobby/prompts/defaults/features/tool_summary_system.md +6 -0
  219. gobby/prompts/defaults/research/step.md +58 -0
  220. gobby/prompts/defaults/validation/criteria.md +47 -0
  221. gobby/prompts/defaults/validation/validate.md +38 -0
  222. gobby/prompts/loader.py +346 -0
  223. gobby/prompts/models.py +113 -0
  224. gobby/py.typed +0 -0
  225. gobby/runner.py +488 -0
  226. gobby/search/__init__.py +23 -0
  227. gobby/search/protocol.py +104 -0
  228. gobby/search/tfidf.py +232 -0
  229. gobby/servers/__init__.py +7 -0
  230. gobby/servers/http.py +636 -0
  231. gobby/servers/models.py +31 -0
  232. gobby/servers/routes/__init__.py +23 -0
  233. gobby/servers/routes/admin.py +416 -0
  234. gobby/servers/routes/dependencies.py +118 -0
  235. gobby/servers/routes/mcp/__init__.py +24 -0
  236. gobby/servers/routes/mcp/hooks.py +135 -0
  237. gobby/servers/routes/mcp/plugins.py +121 -0
  238. gobby/servers/routes/mcp/tools.py +1337 -0
  239. gobby/servers/routes/mcp/webhooks.py +159 -0
  240. gobby/servers/routes/sessions.py +582 -0
  241. gobby/servers/websocket.py +766 -0
  242. gobby/sessions/__init__.py +13 -0
  243. gobby/sessions/analyzer.py +322 -0
  244. gobby/sessions/lifecycle.py +240 -0
  245. gobby/sessions/manager.py +563 -0
  246. gobby/sessions/processor.py +225 -0
  247. gobby/sessions/summary.py +532 -0
  248. gobby/sessions/transcripts/__init__.py +41 -0
  249. gobby/sessions/transcripts/base.py +125 -0
  250. gobby/sessions/transcripts/claude.py +386 -0
  251. gobby/sessions/transcripts/codex.py +143 -0
  252. gobby/sessions/transcripts/gemini.py +195 -0
  253. gobby/storage/__init__.py +21 -0
  254. gobby/storage/agents.py +409 -0
  255. gobby/storage/artifact_classifier.py +341 -0
  256. gobby/storage/artifacts.py +285 -0
  257. gobby/storage/compaction.py +67 -0
  258. gobby/storage/database.py +357 -0
  259. gobby/storage/inter_session_messages.py +194 -0
  260. gobby/storage/mcp.py +680 -0
  261. gobby/storage/memories.py +562 -0
  262. gobby/storage/merge_resolutions.py +550 -0
  263. gobby/storage/migrations.py +860 -0
  264. gobby/storage/migrations_legacy.py +1359 -0
  265. gobby/storage/projects.py +166 -0
  266. gobby/storage/session_messages.py +251 -0
  267. gobby/storage/session_tasks.py +97 -0
  268. gobby/storage/sessions.py +817 -0
  269. gobby/storage/task_dependencies.py +223 -0
  270. gobby/storage/tasks/__init__.py +42 -0
  271. gobby/storage/tasks/_aggregates.py +180 -0
  272. gobby/storage/tasks/_crud.py +449 -0
  273. gobby/storage/tasks/_id.py +104 -0
  274. gobby/storage/tasks/_lifecycle.py +311 -0
  275. gobby/storage/tasks/_manager.py +889 -0
  276. gobby/storage/tasks/_models.py +300 -0
  277. gobby/storage/tasks/_ordering.py +119 -0
  278. gobby/storage/tasks/_path_cache.py +110 -0
  279. gobby/storage/tasks/_queries.py +343 -0
  280. gobby/storage/tasks/_search.py +143 -0
  281. gobby/storage/workflow_audit.py +393 -0
  282. gobby/storage/worktrees.py +547 -0
  283. gobby/sync/__init__.py +29 -0
  284. gobby/sync/github.py +333 -0
  285. gobby/sync/linear.py +304 -0
  286. gobby/sync/memories.py +284 -0
  287. gobby/sync/tasks.py +641 -0
  288. gobby/tasks/__init__.py +8 -0
  289. gobby/tasks/build_verification.py +193 -0
  290. gobby/tasks/commits.py +633 -0
  291. gobby/tasks/context.py +747 -0
  292. gobby/tasks/criteria.py +342 -0
  293. gobby/tasks/enhanced_validator.py +226 -0
  294. gobby/tasks/escalation.py +263 -0
  295. gobby/tasks/expansion.py +626 -0
  296. gobby/tasks/external_validator.py +764 -0
  297. gobby/tasks/issue_extraction.py +171 -0
  298. gobby/tasks/prompts/expand.py +327 -0
  299. gobby/tasks/research.py +421 -0
  300. gobby/tasks/tdd.py +352 -0
  301. gobby/tasks/tree_builder.py +263 -0
  302. gobby/tasks/validation.py +712 -0
  303. gobby/tasks/validation_history.py +357 -0
  304. gobby/tasks/validation_models.py +89 -0
  305. gobby/tools/__init__.py +0 -0
  306. gobby/tools/summarizer.py +170 -0
  307. gobby/tui/__init__.py +5 -0
  308. gobby/tui/api_client.py +281 -0
  309. gobby/tui/app.py +327 -0
  310. gobby/tui/screens/__init__.py +25 -0
  311. gobby/tui/screens/agents.py +333 -0
  312. gobby/tui/screens/chat.py +450 -0
  313. gobby/tui/screens/dashboard.py +377 -0
  314. gobby/tui/screens/memory.py +305 -0
  315. gobby/tui/screens/metrics.py +231 -0
  316. gobby/tui/screens/orchestrator.py +904 -0
  317. gobby/tui/screens/sessions.py +412 -0
  318. gobby/tui/screens/tasks.py +442 -0
  319. gobby/tui/screens/workflows.py +289 -0
  320. gobby/tui/screens/worktrees.py +174 -0
  321. gobby/tui/widgets/__init__.py +21 -0
  322. gobby/tui/widgets/chat.py +210 -0
  323. gobby/tui/widgets/conductor.py +104 -0
  324. gobby/tui/widgets/menu.py +132 -0
  325. gobby/tui/widgets/message_panel.py +160 -0
  326. gobby/tui/widgets/review_gate.py +224 -0
  327. gobby/tui/widgets/task_tree.py +99 -0
  328. gobby/tui/widgets/token_budget.py +166 -0
  329. gobby/tui/ws_client.py +258 -0
  330. gobby/utils/__init__.py +3 -0
  331. gobby/utils/daemon_client.py +235 -0
  332. gobby/utils/git.py +222 -0
  333. gobby/utils/id.py +38 -0
  334. gobby/utils/json_helpers.py +161 -0
  335. gobby/utils/logging.py +376 -0
  336. gobby/utils/machine_id.py +135 -0
  337. gobby/utils/metrics.py +589 -0
  338. gobby/utils/project_context.py +182 -0
  339. gobby/utils/project_init.py +263 -0
  340. gobby/utils/status.py +256 -0
  341. gobby/utils/validation.py +80 -0
  342. gobby/utils/version.py +23 -0
  343. gobby/workflows/__init__.py +4 -0
  344. gobby/workflows/actions.py +1310 -0
  345. gobby/workflows/approval_flow.py +138 -0
  346. gobby/workflows/artifact_actions.py +103 -0
  347. gobby/workflows/audit_helpers.py +110 -0
  348. gobby/workflows/autonomous_actions.py +286 -0
  349. gobby/workflows/context_actions.py +394 -0
  350. gobby/workflows/definitions.py +130 -0
  351. gobby/workflows/detection_helpers.py +208 -0
  352. gobby/workflows/engine.py +485 -0
  353. gobby/workflows/evaluator.py +669 -0
  354. gobby/workflows/git_utils.py +96 -0
  355. gobby/workflows/hooks.py +169 -0
  356. gobby/workflows/lifecycle_evaluator.py +613 -0
  357. gobby/workflows/llm_actions.py +70 -0
  358. gobby/workflows/loader.py +333 -0
  359. gobby/workflows/mcp_actions.py +60 -0
  360. gobby/workflows/memory_actions.py +272 -0
  361. gobby/workflows/premature_stop.py +164 -0
  362. gobby/workflows/session_actions.py +139 -0
  363. gobby/workflows/state_actions.py +123 -0
  364. gobby/workflows/state_manager.py +104 -0
  365. gobby/workflows/stop_signal_actions.py +163 -0
  366. gobby/workflows/summary_actions.py +344 -0
  367. gobby/workflows/task_actions.py +249 -0
  368. gobby/workflows/task_enforcement_actions.py +901 -0
  369. gobby/workflows/templates.py +52 -0
  370. gobby/workflows/todo_actions.py +84 -0
  371. gobby/workflows/webhook.py +223 -0
  372. gobby/workflows/webhook_executor.py +399 -0
  373. gobby/worktrees/__init__.py +5 -0
  374. gobby/worktrees/git.py +690 -0
  375. gobby/worktrees/merge/__init__.py +20 -0
  376. gobby/worktrees/merge/conflict_parser.py +177 -0
  377. gobby/worktrees/merge/resolver.py +485 -0
  378. gobby-0.2.5.dist-info/METADATA +351 -0
  379. gobby-0.2.5.dist-info/RECORD +383 -0
  380. gobby-0.2.5.dist-info/WHEEL +5 -0
  381. gobby-0.2.5.dist-info/entry_points.txt +2 -0
  382. gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
  383. gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/memory.py ADDED
@@ -0,0 +1,304 @@
1
+ import asyncio
2
+
3
+ import click
4
+
5
+ from gobby.cli.utils import resolve_project_ref
6
+ from gobby.config.app import DaemonConfig
7
+ from gobby.memory.manager import MemoryManager
8
+ from gobby.storage.database import LocalDatabase
9
+
10
+
11
+ def get_memory_manager(ctx: click.Context) -> MemoryManager:
12
+ """Get memory manager."""
13
+ config: DaemonConfig = ctx.obj["config"]
14
+ db = LocalDatabase()
15
+
16
+ return MemoryManager(db, config.memory)
17
+
18
+
19
+ @click.group()
20
+ def memory() -> None:
21
+ """Manage Gobby memories."""
22
+ pass
23
+
24
+
25
+ @memory.command()
26
+ @click.argument("content")
27
+ @click.option(
28
+ "--type", "-t", "memory_type", default="fact", help="Type of memory (fact, preference, etc.)"
29
+ )
30
+ @click.option("--importance", "-i", type=float, default=0.5, help="Importance (0.0 - 1.0)")
31
+ @click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
32
+ @click.pass_context
33
+ def create(
34
+ ctx: click.Context, content: str, memory_type: str, importance: float, project_ref: str | None
35
+ ) -> None:
36
+ """Create a new memory."""
37
+ project_id = resolve_project_ref(project_ref) if project_ref else None
38
+ manager = get_memory_manager(ctx)
39
+ memory = asyncio.run(
40
+ manager.remember(
41
+ content=content,
42
+ memory_type=memory_type,
43
+ importance=importance,
44
+ project_id=project_id,
45
+ source_type="cli",
46
+ )
47
+ )
48
+ click.echo(f"Created memory: {memory.id} - {memory.content}")
49
+
50
+
51
+ @memory.command()
52
+ @click.argument("query", required=False)
53
+ @click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
54
+ @click.option("--limit", "-n", default=10, help="Max results")
55
+ @click.option("--tags-all", "tags_all", help="Require ALL tags (comma-separated)")
56
+ @click.option("--tags-any", "tags_any", help="Require ANY tag (comma-separated)")
57
+ @click.option("--tags-none", "tags_none", help="Exclude memories with these tags (comma-separated)")
58
+ @click.pass_context
59
+ def recall(
60
+ ctx: click.Context,
61
+ query: str | None,
62
+ project_ref: str | None,
63
+ limit: int,
64
+ tags_all: str | None,
65
+ tags_any: str | None,
66
+ tags_none: str | None,
67
+ ) -> None:
68
+ """Retrieve memories with optional tag filtering."""
69
+ project_id = resolve_project_ref(project_ref) if project_ref else None
70
+ manager = get_memory_manager(ctx)
71
+
72
+ # Parse comma-separated tags
73
+ tags_all_list = [t.strip() for t in tags_all.split(",") if t.strip()] if tags_all else None
74
+ tags_any_list = [t.strip() for t in tags_any.split(",") if t.strip()] if tags_any else None
75
+ tags_none_list = [t.strip() for t in tags_none.split(",") if t.strip()] if tags_none else None
76
+
77
+ memories = manager.recall(
78
+ query=query,
79
+ project_id=project_id,
80
+ limit=limit,
81
+ tags_all=tags_all_list,
82
+ tags_any=tags_any_list,
83
+ tags_none=tags_none_list,
84
+ )
85
+ if not memories:
86
+ click.echo("No memories found.")
87
+ return
88
+
89
+ for mem in memories:
90
+ tags_str = f" [{', '.join(mem.tags)}]" if mem.tags else ""
91
+ click.echo(f"[{mem.id[:8]}] ({mem.memory_type}, {mem.importance}){tags_str} {mem.content}")
92
+
93
+
94
+ @memory.command()
95
+ @click.argument("memory_ref")
96
+ @click.pass_context
97
+ def delete(ctx: click.Context, memory_ref: str) -> None:
98
+ """Delete a memory by ID (UUID or prefix)."""
99
+ manager = get_memory_manager(ctx)
100
+ memory_id = resolve_memory_id(manager, memory_ref)
101
+ success = manager.forget(memory_id)
102
+ if success:
103
+ click.echo(f"Deleted memory: {memory_id}")
104
+ else:
105
+ click.echo(f"Memory not found: {memory_id}")
106
+
107
+
108
+ @memory.command("list")
109
+ @click.option("--type", "-t", "memory_type", help="Filter by memory type")
110
+ @click.option("--min-importance", "-i", type=float, help="Minimum importance threshold")
111
+ @click.option("--limit", "-n", default=50, help="Max results")
112
+ @click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
113
+ @click.option("--tags-all", "tags_all", help="Require ALL tags (comma-separated)")
114
+ @click.option("--tags-any", "tags_any", help="Require ANY tag (comma-separated)")
115
+ @click.option("--tags-none", "tags_none", help="Exclude memories with these tags (comma-separated)")
116
+ @click.pass_context
117
+ def list_memories(
118
+ ctx: click.Context,
119
+ memory_type: str | None,
120
+ min_importance: float | None,
121
+ project_ref: str | None,
122
+ limit: int,
123
+ tags_all: str | None,
124
+ tags_any: str | None,
125
+ tags_none: str | None,
126
+ ) -> None:
127
+ """List all memories with optional filtering."""
128
+ project_id = resolve_project_ref(project_ref) if project_ref else None
129
+ manager = get_memory_manager(ctx)
130
+
131
+ # Parse comma-separated tags
132
+ tags_all_list = [t.strip() for t in tags_all.split(",") if t.strip()] if tags_all else None
133
+ tags_any_list = [t.strip() for t in tags_any.split(",") if t.strip()] if tags_any else None
134
+ tags_none_list = [t.strip() for t in tags_none.split(",") if t.strip()] if tags_none else None
135
+
136
+ memories = manager.list_memories(
137
+ project_id=project_id,
138
+ memory_type=memory_type,
139
+ min_importance=min_importance,
140
+ limit=limit,
141
+ tags_all=tags_all_list,
142
+ tags_any=tags_any_list,
143
+ tags_none=tags_none_list,
144
+ )
145
+ if not memories:
146
+ click.echo("No memories found.")
147
+ return
148
+
149
+ for mem in memories:
150
+ tags_str = f" [{', '.join(mem.tags)}]" if mem.tags else ""
151
+ click.echo(f"[{mem.id[:8]}] ({mem.memory_type}, {mem.importance:.2f}){tags_str}")
152
+ click.echo(f" {mem.content[:100]}{'...' if len(mem.content) > 100 else ''}")
153
+
154
+
155
+ @memory.command("show")
156
+ @click.argument("memory_ref")
157
+ @click.pass_context
158
+ def show_memory(ctx: click.Context, memory_ref: str) -> None:
159
+ """Show details of a specific memory (UUID or prefix)."""
160
+ manager = get_memory_manager(ctx)
161
+ memory_id = resolve_memory_id(manager, memory_ref)
162
+ memory = manager.get_memory(memory_id)
163
+ if not memory:
164
+ click.echo(f"Memory not found: {memory_id}")
165
+ return
166
+
167
+ click.echo(f"ID: {memory.id}")
168
+ click.echo(f"Type: {memory.memory_type}")
169
+ click.echo(f"Importance: {memory.importance}")
170
+ click.echo(f"Created: {memory.created_at}")
171
+ click.echo(f"Updated: {memory.updated_at}")
172
+ click.echo(f"Source: {memory.source_type}")
173
+ click.echo(f"Access Count: {memory.access_count}")
174
+ if memory.tags:
175
+ click.echo(f"Tags: {', '.join(memory.tags)}")
176
+ click.echo(f"Content:\n{memory.content}")
177
+
178
+
179
+ @memory.command("update")
180
+ @click.argument("memory_ref")
181
+ @click.option("--content", "-c", help="New content")
182
+ @click.option("--importance", "-i", type=float, help="New importance (0.0-1.0)")
183
+ @click.option("--tags", "-t", help="New tags (comma-separated)")
184
+ @click.pass_context
185
+ def update_memory(
186
+ ctx: click.Context,
187
+ memory_ref: str,
188
+ content: str | None,
189
+ importance: float | None,
190
+ tags: str | None,
191
+ ) -> None:
192
+ """Update an existing memory (UUID or prefix)."""
193
+ manager = get_memory_manager(ctx)
194
+ memory_id = resolve_memory_id(manager, memory_ref)
195
+
196
+ # Parse tags if provided
197
+ tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None
198
+ if tag_list is not None and len(tag_list) == 0:
199
+ tag_list = None
200
+
201
+ try:
202
+ memory = manager.update_memory(
203
+ memory_id=memory_id,
204
+ content=content,
205
+ importance=importance,
206
+ tags=tag_list,
207
+ )
208
+ click.echo(f"Updated memory: {memory.id}")
209
+ click.echo(f" Content: {memory.content[:80]}{'...' if len(memory.content) > 80 else ''}")
210
+ click.echo(f" Importance: {memory.importance}")
211
+ except ValueError as e:
212
+ click.echo(f"Error: {e}")
213
+
214
+
215
+ @memory.command("stats")
216
+ @click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
217
+ @click.pass_context
218
+ def memory_stats(ctx: click.Context, project_ref: str | None) -> None:
219
+ """Show memory system statistics."""
220
+ project_id = resolve_project_ref(project_ref) if project_ref else None
221
+ manager = get_memory_manager(ctx)
222
+ stats = manager.get_stats(project_id=project_id)
223
+
224
+ click.echo("Memory Statistics:")
225
+ click.echo(f" Total Memories: {stats['total_count']}")
226
+ click.echo(f" Average Importance: {stats['avg_importance']:.3f}")
227
+ if stats["by_type"]:
228
+ click.echo(" By Type:")
229
+ for mem_type, count in stats["by_type"].items():
230
+ click.echo(f" {mem_type}: {count}")
231
+
232
+
233
+ @memory.command("export")
234
+ @click.option("--project", "-p", "project_ref", help="Project (name or UUID)")
235
+ @click.option(
236
+ "--output", "-o", "output_file", type=click.Path(), help="Output file (stdout if not specified)"
237
+ )
238
+ @click.option("--no-metadata", is_flag=True, help="Exclude memory metadata")
239
+ @click.option("--no-stats", is_flag=True, help="Exclude summary statistics")
240
+ @click.pass_context
241
+ def export_memories(
242
+ ctx: click.Context,
243
+ project_ref: str | None,
244
+ output_file: str | None,
245
+ no_metadata: bool,
246
+ no_stats: bool,
247
+ ) -> None:
248
+ """Export memories as markdown.
249
+
250
+ Exports all memories (or filtered by project) to a formatted markdown document.
251
+ Output goes to stdout by default, or to a file with --output.
252
+
253
+ Examples:
254
+
255
+ gobby memory export # Export all to stdout
256
+
257
+ gobby memory export -o memories.md # Export to file
258
+
259
+ gobby memory export -p myproject # Export specific project
260
+
261
+ gobby memory export --no-metadata # Content only, no metadata
262
+ """
263
+ project_id = resolve_project_ref(project_ref) if project_ref else None
264
+ manager = get_memory_manager(ctx)
265
+
266
+ markdown = manager.export_markdown(
267
+ project_id=project_id,
268
+ include_metadata=not no_metadata,
269
+ include_stats=not no_stats,
270
+ )
271
+
272
+ if output_file:
273
+ from pathlib import Path
274
+
275
+ path = Path(output_file)
276
+ try:
277
+ path.write_text(markdown, encoding="utf-8")
278
+ click.echo(f"Exported memories to {output_file}")
279
+ except OSError as e:
280
+ raise click.ClickException(f"Failed to write to {output_file}: {e}") from e
281
+ else:
282
+ click.echo(markdown)
283
+
284
+
285
+ def resolve_memory_id(manager: MemoryManager, memory_ref: str) -> str:
286
+ """Resolve memory reference (UUID or prefix) to full ID."""
287
+ # Try exact match first
288
+ # Optimization: check 36 chars?
289
+ if len(memory_ref) == 36 and manager.get_memory(memory_ref):
290
+ return memory_ref
291
+
292
+ # Try prefix match using MemoryManager method
293
+ memories = manager.find_by_prefix(memory_ref, limit=5)
294
+
295
+ if not memories:
296
+ raise click.ClickException(f"Memory not found: {memory_ref}")
297
+
298
+ if len(memories) > 1:
299
+ click.echo(f"Ambiguous memory reference '{memory_ref}' matches:", err=True)
300
+ for mem in memories:
301
+ click.echo(f" {mem.id}", err=True)
302
+ raise click.ClickException(f"Ambiguous memory reference: {memory_ref}")
303
+
304
+ return memories[0].id
gobby/cli/merge.py ADDED
@@ -0,0 +1,384 @@
1
+ """
2
+ Merge conflict resolution CLI commands.
3
+
4
+ Commands for managing merge operations:
5
+ - start: Start a merge with AI-powered resolution
6
+ - status: Show merge resolution status
7
+ - resolve: Resolve a specific file conflict
8
+ - apply: Apply resolved changes and complete merge
9
+ - abort: Abort the merge operation
10
+ """
11
+
12
+ import json
13
+ from typing import Any
14
+
15
+ import click
16
+
17
+ from gobby.storage.database import LocalDatabase
18
+ from gobby.storage.merge_resolutions import MergeResolutionManager
19
+
20
+
21
+ def get_merge_manager() -> MergeResolutionManager:
22
+ """Get initialized merge resolution manager."""
23
+ db = LocalDatabase()
24
+ return MergeResolutionManager(db)
25
+
26
+
27
+ def get_merge_resolver() -> Any:
28
+ """Get merge resolver for AI-powered resolution."""
29
+ from gobby.worktrees.merge import MergeResolver
30
+
31
+ return MergeResolver()
32
+
33
+
34
+ def get_project_context() -> dict[str, Any] | None:
35
+ """Get current project context."""
36
+ import os
37
+ from pathlib import Path
38
+
39
+ # Look for .gobby/project.json in current directory or parents
40
+ cwd = Path(os.getcwd())
41
+ for parent in [cwd, *cwd.parents]:
42
+ project_file = parent / ".gobby" / "project.json"
43
+ if project_file.exists():
44
+ import json as json_module
45
+
46
+ result: dict[str, Any] = json_module.loads(project_file.read_text())
47
+ return result
48
+ return None
49
+
50
+
51
+ def get_worktree_context() -> dict[str, Any] | None:
52
+ """Get current worktree context if in a worktree."""
53
+ import os
54
+ from pathlib import Path
55
+
56
+ from gobby.storage.worktrees import LocalWorktreeManager
57
+
58
+ db = LocalDatabase()
59
+ manager = LocalWorktreeManager(db)
60
+
61
+ # Check if current directory is a worktree
62
+ cwd = Path(os.getcwd()).resolve()
63
+ worktrees = manager.list_worktrees()
64
+ for wt in worktrees:
65
+ if wt.worktree_path:
66
+ worktree_path = Path(wt.worktree_path).resolve()
67
+ # Use is_relative_to for proper path containment check
68
+ try:
69
+ cwd.relative_to(worktree_path)
70
+ # If we get here, cwd is inside worktree_path
71
+ return {
72
+ "id": wt.id,
73
+ "branch_name": wt.branch_name,
74
+ "worktree_path": wt.worktree_path,
75
+ "base_branch": wt.base_branch,
76
+ }
77
+ except ValueError:
78
+ # cwd is not relative to worktree_path
79
+ continue
80
+ return None
81
+
82
+
83
+ @click.group()
84
+ def merge() -> None:
85
+ """Manage merge operations with AI-powered conflict resolution."""
86
+ pass
87
+
88
+
89
+ @merge.command("start")
90
+ @click.argument("source_branch")
91
+ @click.option(
92
+ "--target",
93
+ "-t",
94
+ "target_branch",
95
+ default="main",
96
+ help="Target branch to merge into (default: main)",
97
+ )
98
+ @click.option(
99
+ "--strategy",
100
+ "-s",
101
+ type=click.Choice(["auto", "ai-only", "human"]),
102
+ default="auto",
103
+ help="Resolution strategy (default: auto)",
104
+ )
105
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
106
+ def merge_start(
107
+ source_branch: str,
108
+ target_branch: str,
109
+ strategy: str,
110
+ json_format: bool,
111
+ ) -> None:
112
+ """Start a merge operation with AI-powered conflict resolution.
113
+
114
+ Examples:
115
+
116
+ gobby merge start feature/my-feature
117
+
118
+ gobby merge start feature/auth --target develop --strategy ai-only
119
+ """
120
+ project = get_project_context()
121
+ if not project:
122
+ click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
123
+ raise SystemExit(1)
124
+
125
+ # Get worktree context if available
126
+ worktree = get_worktree_context()
127
+ worktree_id = worktree["id"] if worktree else project.get("id", "default")
128
+
129
+ manager = get_merge_manager()
130
+
131
+ try:
132
+ # Create resolution record with strategy
133
+ resolution = manager.create_resolution(
134
+ worktree_id=worktree_id,
135
+ source_branch=source_branch,
136
+ target_branch=target_branch,
137
+ status="pending",
138
+ tier_used=strategy,
139
+ )
140
+
141
+ if json_format:
142
+ click.echo(json.dumps(resolution.to_dict(), indent=2, default=str))
143
+ return
144
+
145
+ click.echo(f"Started merge: {resolution.id}")
146
+ click.echo(f" Source: {source_branch}")
147
+ click.echo(f" Target: {target_branch}")
148
+ click.echo(f" Strategy: {strategy}")
149
+ click.echo(f" Status: {resolution.status}")
150
+
151
+ except Exception as e:
152
+ click.echo(f"Error starting merge: {e}", err=True)
153
+ raise SystemExit(1) from None
154
+
155
+
156
+ @merge.command("status")
157
+ @click.option("--verbose", "-v", is_flag=True, help="Show detailed conflict information")
158
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
159
+ def merge_status(verbose: bool, json_format: bool) -> None:
160
+ """Show the status of current merge operation.
161
+
162
+ Examples:
163
+
164
+ gobby merge status
165
+
166
+ gobby merge status --verbose
167
+ """
168
+ project = get_project_context()
169
+ if not project:
170
+ click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
171
+ raise SystemExit(1)
172
+
173
+ manager = get_merge_manager()
174
+
175
+ # Get worktree context for filtering
176
+ worktree = get_worktree_context()
177
+ worktree_id = worktree["id"] if worktree else None
178
+
179
+ # List active resolutions
180
+ resolutions = manager.list_resolutions(
181
+ worktree_id=worktree_id,
182
+ status="pending",
183
+ )
184
+
185
+ if json_format:
186
+ output = []
187
+ for res in resolutions:
188
+ res_dict = res.to_dict()
189
+ res_dict["conflicts"] = [
190
+ c.to_dict() for c in manager.list_conflicts(resolution_id=res.id)
191
+ ]
192
+ output.append(res_dict)
193
+ click.echo(json.dumps(output, indent=2, default=str))
194
+ return
195
+
196
+ if not resolutions:
197
+ click.echo("No active merge operations found.")
198
+ return
199
+
200
+ for res in resolutions:
201
+ conflicts = manager.list_conflicts(resolution_id=res.id)
202
+ pending_count = sum(1 for c in conflicts if c.status == "pending")
203
+ resolved_count = sum(1 for c in conflicts if c.status == "resolved")
204
+
205
+ click.echo(f"Merge: {res.id}")
206
+ click.echo(f" Source: {res.source_branch} -> {res.target_branch}")
207
+ click.echo(f" Status: {res.status}")
208
+ click.echo(f" Conflicts: {pending_count} pending, {resolved_count} resolved")
209
+
210
+ if verbose and conflicts:
211
+ click.echo(" Files:")
212
+ for conflict in conflicts:
213
+ status_icon = "✓" if conflict.status == "resolved" else "○"
214
+ click.echo(f" {status_icon} {conflict.file_path} ({conflict.status})")
215
+
216
+
217
+ @merge.command("resolve")
218
+ @click.argument("file_path")
219
+ @click.option(
220
+ "--strategy",
221
+ "-s",
222
+ type=click.Choice(["ai", "human"]),
223
+ default="ai",
224
+ help="Resolution strategy (default: ai)",
225
+ )
226
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
227
+ def merge_resolve(file_path: str, strategy: str, json_format: bool) -> None:
228
+ """Resolve a specific file conflict.
229
+
230
+ Examples:
231
+
232
+ gobby merge resolve src/main.py
233
+
234
+ gobby merge resolve src/config.py --strategy human
235
+ """
236
+ project = get_project_context()
237
+ if not project:
238
+ click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
239
+ raise SystemExit(1)
240
+
241
+ manager = get_merge_manager()
242
+
243
+ try:
244
+ # Find conflict by file path
245
+ conflict = manager.get_conflict_by_path(file_path)
246
+ if not conflict:
247
+ click.echo(f"Error: No conflict found for file '{file_path}'", err=True)
248
+ raise SystemExit(1)
249
+
250
+ if strategy == "ai":
251
+ # AI resolution
252
+ get_merge_resolver() # Validates resolver is available
253
+ # Would call AI resolver here
254
+ click.echo(f"Resolving {file_path} with AI...")
255
+ manager.update_conflict(conflict.id, status="resolved")
256
+ else:
257
+ # Human resolution - just mark as pending human review
258
+ click.echo(f"Marked {file_path} for human resolution")
259
+
260
+ if json_format:
261
+ updated = manager.get_conflict(conflict.id)
262
+ if updated:
263
+ click.echo(json.dumps(updated.to_dict(), indent=2, default=str))
264
+ return
265
+
266
+ click.echo(f"Resolved: {file_path}")
267
+
268
+ except AttributeError:
269
+ # get_conflict_by_path may not exist
270
+ click.echo(f"Error: Conflict not found for '{file_path}'", err=True)
271
+ raise SystemExit(1) from None
272
+ except Exception as e:
273
+ click.echo(f"Error resolving conflict: {e}", err=True)
274
+ raise SystemExit(1) from None
275
+
276
+
277
+ @merge.command("apply")
278
+ @click.option("--force", "-f", is_flag=True, help="Force apply even with pending conflicts")
279
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
280
+ def merge_apply(force: bool, json_format: bool) -> None:
281
+ """Apply resolved changes and complete the merge.
282
+
283
+ Examples:
284
+
285
+ gobby merge apply
286
+
287
+ gobby merge apply --force
288
+ """
289
+ project = get_project_context()
290
+ if not project:
291
+ click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
292
+ raise SystemExit(1)
293
+
294
+ manager = get_merge_manager()
295
+
296
+ try:
297
+ # Get active resolution
298
+ resolution = manager.get_active_resolution()
299
+ if not resolution:
300
+ click.echo("Error: No active merge operation found.", err=True)
301
+ raise SystemExit(1)
302
+
303
+ # Check for pending conflicts
304
+ conflicts = manager.list_conflicts(resolution_id=resolution.id)
305
+ pending = [c for c in conflicts if c.status == "pending"]
306
+
307
+ if pending and not force:
308
+ click.echo(
309
+ f"Error: {len(pending)} pending conflict(s). "
310
+ "Resolve them or use --force to apply anyway.",
311
+ err=True,
312
+ )
313
+ raise SystemExit(1)
314
+
315
+ # Apply merge
316
+ manager.update_resolution(resolution.id, status="resolved")
317
+
318
+ if json_format:
319
+ updated = manager.get_resolution(resolution.id)
320
+ if updated:
321
+ click.echo(json.dumps(updated.to_dict(), indent=2, default=str))
322
+ return
323
+
324
+ click.echo(f"Applied merge: {resolution.id}")
325
+ click.echo(f" {len(conflicts)} file(s) merged")
326
+
327
+ except AttributeError:
328
+ # get_active_resolution may not exist
329
+ click.echo("Error: No active merge operation found.", err=True)
330
+ raise SystemExit(1) from None
331
+ except Exception as e:
332
+ click.echo(f"Error applying merge: {e}", err=True)
333
+ raise SystemExit(1) from None
334
+
335
+
336
+ @merge.command("abort")
337
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
338
+ def merge_abort(json_format: bool) -> None:
339
+ """Abort the current merge operation.
340
+
341
+ Examples:
342
+
343
+ gobby merge abort
344
+ """
345
+ project = get_project_context()
346
+ if not project:
347
+ click.echo("Error: Not in a Gobby project. Run 'gobby init' first.", err=True)
348
+ raise SystemExit(1)
349
+
350
+ manager = get_merge_manager()
351
+
352
+ try:
353
+ # Get active resolution
354
+ resolution = manager.get_active_resolution()
355
+ if not resolution:
356
+ click.echo("Error: No active merge operation to abort.", err=True)
357
+ raise SystemExit(1)
358
+
359
+ # Check if already resolved
360
+ if resolution.status == "resolved":
361
+ click.echo("Error: Cannot abort an already resolved merge.", err=True)
362
+ raise SystemExit(1)
363
+
364
+ # Delete resolution (cascades to conflicts)
365
+ resolution_id = resolution.id
366
+ deleted = manager.delete_resolution(resolution_id)
367
+
368
+ if json_format:
369
+ click.echo(json.dumps({"aborted": deleted, "resolution_id": resolution_id}))
370
+ return
371
+
372
+ if deleted:
373
+ click.echo(f"Aborted merge: {resolution_id}")
374
+ else:
375
+ click.echo("Failed to abort merge.", err=True)
376
+ raise SystemExit(1)
377
+
378
+ except AttributeError:
379
+ # get_active_resolution may not exist
380
+ click.echo("Error: No active merge operation to abort.", err=True)
381
+ raise SystemExit(1) from None
382
+ except Exception as e:
383
+ click.echo(f"Error aborting merge: {e}", err=True)
384
+ raise SystemExit(1) from None