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
@@ -0,0 +1,526 @@
1
+ """
2
+ CLI commands for hook extensions (hooks, plugins, webhooks).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import sys
9
+ from typing import TYPE_CHECKING
10
+
11
+ import click
12
+
13
+ from gobby.cli.mcp_proxy import call_mcp_api, check_daemon_running, get_daemon_client
14
+
15
+ if TYPE_CHECKING:
16
+ from gobby.hooks.events import HookEventType
17
+
18
+ # =============================================================================
19
+ # Hooks Commands
20
+ # =============================================================================
21
+
22
+
23
+ @click.group()
24
+ def hooks() -> None:
25
+ """Manage hook system configuration and testing."""
26
+
27
+
28
+ @hooks.command("list")
29
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
30
+ @click.pass_context
31
+ def hooks_list(ctx: click.Context, json_format: bool) -> None:
32
+ """List supported hook event types."""
33
+ from gobby.hooks.events import HookEventType
34
+
35
+ hook_types = [{"name": e.value, "description": _get_hook_description(e)} for e in HookEventType]
36
+
37
+ if json_format:
38
+ click.echo(json.dumps(hook_types, indent=2))
39
+ return
40
+
41
+ click.echo("Supported Hook Event Types:")
42
+ click.echo()
43
+ for hook in hook_types:
44
+ click.echo(f" {hook['name']}")
45
+ if hook["description"]:
46
+ click.echo(f" {hook['description']}")
47
+
48
+
49
+ def _get_hook_description(event_type: HookEventType) -> str:
50
+ """Get description for a hook event type."""
51
+ from gobby.hooks.events import HookEventType
52
+
53
+ descriptions = {
54
+ HookEventType.SESSION_START: "Fired when a new session starts",
55
+ HookEventType.SESSION_END: "Fired when a session ends",
56
+ HookEventType.BEFORE_AGENT: "Fired before agent turn starts",
57
+ HookEventType.AFTER_AGENT: "Fired after agent turn completes",
58
+ HookEventType.STOP: "Fired when agent attempts to stop (can block)",
59
+ HookEventType.BEFORE_TOOL: "Fired before a tool is executed (can block)",
60
+ HookEventType.AFTER_TOOL: "Fired after a tool completes",
61
+ HookEventType.BEFORE_TOOL_SELECTION: "Fired before tool selection (Gemini)",
62
+ HookEventType.BEFORE_MODEL: "Fired before model call (Gemini)",
63
+ HookEventType.AFTER_MODEL: "Fired after model call (Gemini)",
64
+ HookEventType.PRE_COMPACT: "Fired before session context is compacted",
65
+ HookEventType.NOTIFICATION: "Notification event from CLI",
66
+ }
67
+ return descriptions.get(event_type, "")
68
+
69
+
70
+ @hooks.command("test")
71
+ @click.argument("hook_type")
72
+ @click.option(
73
+ "--source",
74
+ "-s",
75
+ type=click.Choice(["claude", "gemini", "codex"]),
76
+ default="claude",
77
+ help="Source CLI to simulate",
78
+ )
79
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
80
+ @click.pass_context
81
+ def hooks_test(ctx: click.Context, hook_type: str, source: str, json_format: bool) -> None:
82
+ """Test a hook by sending a test event to the daemon.
83
+
84
+ HOOK_TYPE is the event type to test (e.g., session-start, before-tool).
85
+ """
86
+ client = get_daemon_client(ctx)
87
+ if not check_daemon_running(client):
88
+ sys.exit(1)
89
+
90
+ # Build test payload
91
+ test_payload = {
92
+ "hook_type": hook_type,
93
+ "source": source,
94
+ "input_data": {
95
+ "session_id": "test-session-cli",
96
+ "tool_name": "test_tool" if "tool" in hook_type.lower() else None,
97
+ },
98
+ }
99
+
100
+ result = call_mcp_api(
101
+ client,
102
+ "/hooks/execute",
103
+ method="POST",
104
+ json_data=test_payload,
105
+ )
106
+
107
+ if result is None:
108
+ click.echo("Failed to execute test hook", err=True)
109
+ sys.exit(1)
110
+
111
+ if json_format:
112
+ click.echo(json.dumps(result, indent=2))
113
+ return
114
+
115
+ click.echo(f"Hook test: {hook_type}")
116
+ click.echo(f" Source: {source}")
117
+ click.echo(f" Continue: {result.get('continue', 'unknown')}")
118
+ if result.get("reason"):
119
+ click.echo(f" Reason: {result.get('reason')}")
120
+ inject_context = result.get("inject_context")
121
+ if inject_context:
122
+ click.echo(f" Context: {str(inject_context)[:100]}...")
123
+
124
+
125
+ @hooks.command("run")
126
+ @click.argument(
127
+ "stage",
128
+ type=click.Choice(["pre-commit", "pre-push", "pre-merge"]),
129
+ )
130
+ @click.option("--verbose", "-v", is_flag=True, help="Show command output")
131
+ @click.option("--dry-run", is_flag=True, help="Show what would run without executing")
132
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
133
+ def hooks_run(stage: str, verbose: bool, dry_run: bool, json_format: bool) -> None:
134
+ """Run verification commands for a git hook stage.
135
+
136
+ STAGE is the hook stage to run (pre-commit, pre-push, or pre-merge).
137
+
138
+ This command reads verification commands from .gobby/project.json
139
+ and executes them according to the hooks configuration.
140
+
141
+ Example configuration in project.json:
142
+
143
+ \b
144
+ "verification": {
145
+ "lint": "ruff check src/",
146
+ "format": "ruff format --check src/"
147
+ },
148
+ "hooks": {
149
+ "pre-commit": {
150
+ "run": ["lint", "format"],
151
+ "fail_fast": true
152
+ }
153
+ }
154
+ """
155
+ from gobby.hooks.verification_runner import VerificationRunner
156
+
157
+ runner = VerificationRunner.from_project()
158
+
159
+ # Handle dry-run mode
160
+ if dry_run:
161
+ stage_config = runner.get_stage_config(stage)
162
+ if not stage_config or not stage_config.run:
163
+ click.echo(f"No commands configured for '{stage}'")
164
+ return
165
+
166
+ if not runner.verification_config:
167
+ click.echo("No verification commands defined in project.json")
168
+ return
169
+
170
+ click.echo(f"Would run for '{stage}':")
171
+ for cmd_name in stage_config.run:
172
+ command = runner.verification_config.get_command(cmd_name)
173
+ if command:
174
+ click.echo(f" {cmd_name}: {command}")
175
+ else:
176
+ click.echo(f" {cmd_name}: (not defined)")
177
+ return
178
+
179
+ # Run the stage
180
+ result = runner.run_stage(stage)
181
+
182
+ # Output as JSON
183
+ if json_format:
184
+ output = {
185
+ "stage": result.stage,
186
+ "success": result.success,
187
+ "skipped": result.skipped,
188
+ "skip_reason": result.skip_reason,
189
+ "results": [
190
+ {
191
+ "name": r.name,
192
+ "command": r.command,
193
+ "success": r.success,
194
+ "exit_code": r.exit_code,
195
+ "duration_ms": r.duration_ms,
196
+ "skipped": r.skipped,
197
+ "skip_reason": r.skip_reason,
198
+ "error": r.error,
199
+ "stdout": r.stdout if verbose else None,
200
+ "stderr": r.stderr if verbose else None,
201
+ }
202
+ for r in result.results
203
+ ],
204
+ }
205
+ click.echo(json.dumps(output, indent=2))
206
+ sys.exit(0 if result.success else 1)
207
+
208
+ # Handle skipped stage
209
+ if result.skipped:
210
+ if verbose:
211
+ click.echo(f"Skipped: {result.skip_reason}")
212
+ sys.exit(0)
213
+
214
+ # Display results
215
+ for r in result.results:
216
+ if r.skipped:
217
+ click.echo(click.style(f"⊘ {r.name}: skipped", fg="yellow"))
218
+ if r.skip_reason:
219
+ click.echo(f" {r.skip_reason}")
220
+ elif r.success:
221
+ click.echo(click.style(f"✓ {r.name}", fg="green") + f" ({r.duration_ms}ms)")
222
+ else:
223
+ click.echo(click.style(f"✗ {r.name}", fg="red") + f" ({r.duration_ms}ms)")
224
+ if r.error:
225
+ click.echo(f" Error: {r.error}")
226
+ if verbose and r.stderr:
227
+ click.echo(f" stderr:\n{_indent(r.stderr, 6)}")
228
+ elif r.stderr:
229
+ # Show first line of stderr even without verbose
230
+ first_line = r.stderr.strip().split("\n")[0]
231
+ if first_line:
232
+ click.echo(f" {first_line}")
233
+
234
+ if verbose and r.stdout:
235
+ click.echo(f" stdout:\n{_indent(r.stdout, 6)}")
236
+
237
+ # Summary
238
+ if result.results:
239
+ click.echo()
240
+ click.echo(
241
+ f"Passed: {result.passed_count}, "
242
+ f"Failed: {result.failed_count}, "
243
+ f"Skipped: {result.skipped_count}"
244
+ )
245
+
246
+ sys.exit(0 if result.success else 1)
247
+
248
+
249
+ def _indent(text: str, spaces: int) -> str:
250
+ """Indent each line of text by the specified number of spaces."""
251
+ prefix = " " * spaces
252
+ return "\n".join(prefix + line for line in text.strip().split("\n"))
253
+
254
+
255
+ @hooks.command("status")
256
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
257
+ def hooks_status(json_format: bool) -> None:
258
+ """Show current hooks configuration from project.json.
259
+
260
+ Displays which verification commands are configured to run at each
261
+ git hook stage (pre-commit, pre-push, pre-merge).
262
+ """
263
+ from gobby.utils.project_context import get_hooks_config, get_verification_config
264
+
265
+ verification_config = get_verification_config()
266
+ hooks_config = get_hooks_config()
267
+
268
+ if json_format:
269
+ output = {
270
+ "verification": verification_config.all_commands() if verification_config else {},
271
+ "hooks": {
272
+ "pre-commit": (
273
+ hooks_config.pre_commit.model_dump(by_alias=True) if hooks_config else None
274
+ ),
275
+ "pre-push": (
276
+ hooks_config.pre_push.model_dump(by_alias=True) if hooks_config else None
277
+ ),
278
+ "pre-merge": (
279
+ hooks_config.pre_merge.model_dump(by_alias=True) if hooks_config else None
280
+ ),
281
+ },
282
+ }
283
+ click.echo(json.dumps(output, indent=2))
284
+ return
285
+
286
+ # Display verification commands
287
+ click.echo("Verification Commands:")
288
+ if verification_config:
289
+ commands = verification_config.all_commands()
290
+ if commands:
291
+ for name, cmd in commands.items():
292
+ click.echo(f" {name}: {cmd}")
293
+ else:
294
+ click.echo(" (none configured)")
295
+ else:
296
+ click.echo(" (none configured)")
297
+
298
+ click.echo()
299
+
300
+ # Display hooks configuration
301
+ click.echo("Hook Stages:")
302
+ if not hooks_config:
303
+ click.echo(" (none configured)")
304
+ return
305
+
306
+ for stage_name, stage_attr in [
307
+ ("pre-commit", "pre_commit"),
308
+ ("pre-push", "pre_push"),
309
+ ("pre-merge", "pre_merge"),
310
+ ]:
311
+ stage_config = getattr(hooks_config, stage_attr)
312
+ if stage_config.run:
313
+ status = "enabled" if stage_config.enabled else "disabled"
314
+ click.echo(f" {stage_name} ({status}):")
315
+ click.echo(f" run: {', '.join(stage_config.run)}")
316
+ click.echo(f" fail_fast: {stage_config.fail_fast}")
317
+ click.echo(f" timeout: {stage_config.timeout}s")
318
+ else:
319
+ click.echo(f" {stage_name}: (no commands)")
320
+
321
+
322
+ # =============================================================================
323
+ # Plugins Commands
324
+ # =============================================================================
325
+
326
+
327
+ @click.group()
328
+ def plugins() -> None:
329
+ """Manage Python hook plugins."""
330
+
331
+
332
+ @plugins.command("list")
333
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
334
+ @click.pass_context
335
+ def plugins_list(ctx: click.Context, json_format: bool) -> None:
336
+ """List loaded plugins."""
337
+ client = get_daemon_client(ctx)
338
+ if not check_daemon_running(client):
339
+ sys.exit(1)
340
+
341
+ result = call_mcp_api(client, "/plugins")
342
+
343
+ if result is None:
344
+ click.echo("Failed to list plugins", err=True)
345
+ sys.exit(1)
346
+
347
+ if json_format:
348
+ click.echo(json.dumps(result, indent=2))
349
+ return
350
+
351
+ plugins_list = result.get("plugins", [])
352
+ enabled = result.get("enabled", False)
353
+
354
+ if not enabled:
355
+ click.echo("Plugin system is disabled in configuration.")
356
+ click.echo("Enable with: plugins.enabled: true in ~/.gobby/config.yaml")
357
+ return
358
+
359
+ if not plugins_list:
360
+ click.echo("No plugins loaded.")
361
+ click.echo()
362
+ click.echo("Plugin directories:")
363
+ for dir_path in result.get("plugin_dirs", []):
364
+ click.echo(f" {dir_path}")
365
+ return
366
+
367
+ click.echo(f"Loaded Plugins ({len(plugins_list)}):")
368
+ click.echo()
369
+ for plugin in plugins_list:
370
+ click.echo(f" {plugin['name']} v{plugin['version']}")
371
+ if plugin.get("description"):
372
+ click.echo(f" {plugin['description']}")
373
+ if plugin.get("handlers"):
374
+ click.echo(f" Handlers: {len(plugin['handlers'])}")
375
+ if plugin.get("actions"):
376
+ click.echo(f" Actions: {', '.join(a['name'] for a in plugin['actions'])}")
377
+
378
+
379
+ @plugins.command("reload")
380
+ @click.argument("plugin_name")
381
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
382
+ @click.pass_context
383
+ def plugins_reload(ctx: click.Context, plugin_name: str, json_format: bool) -> None:
384
+ """Reload a plugin by name.
385
+
386
+ PLUGIN_NAME is the name of the plugin to reload.
387
+ """
388
+ client = get_daemon_client(ctx)
389
+ if not check_daemon_running(client):
390
+ sys.exit(1)
391
+
392
+ result = call_mcp_api(
393
+ client,
394
+ "/plugins/reload",
395
+ method="POST",
396
+ json_data={"name": plugin_name},
397
+ )
398
+
399
+ if result is None:
400
+ click.echo(f"Failed to reload plugin: {plugin_name}", err=True)
401
+ sys.exit(1)
402
+
403
+ if json_format:
404
+ click.echo(json.dumps(result, indent=2))
405
+ return
406
+
407
+ if result.get("success"):
408
+ click.echo(f"Plugin '{plugin_name}' reloaded successfully.")
409
+ if result.get("version"):
410
+ click.echo(f" Version: {result.get('version')}")
411
+ else:
412
+ click.echo(f"Failed to reload plugin: {result.get('error', 'Unknown error')}", err=True)
413
+ sys.exit(1)
414
+
415
+
416
+ # =============================================================================
417
+ # Webhooks Commands
418
+ # =============================================================================
419
+
420
+
421
+ @click.group()
422
+ def webhooks() -> None:
423
+ """Manage webhook endpoints."""
424
+
425
+
426
+ @webhooks.command("list")
427
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
428
+ @click.pass_context
429
+ def webhooks_list(ctx: click.Context, json_format: bool) -> None:
430
+ """List configured webhook endpoints."""
431
+ client = get_daemon_client(ctx)
432
+ if not check_daemon_running(client):
433
+ sys.exit(1)
434
+
435
+ result = call_mcp_api(client, "/webhooks")
436
+
437
+ if result is None:
438
+ click.echo("Failed to list webhooks", err=True)
439
+ sys.exit(1)
440
+
441
+ if json_format:
442
+ click.echo(json.dumps(result, indent=2))
443
+ return
444
+
445
+ enabled = result.get("enabled", False)
446
+ endpoints = result.get("endpoints", [])
447
+
448
+ if not enabled:
449
+ click.echo("Webhook system is disabled in configuration.")
450
+ click.echo("Enable with: hook_extensions.webhooks.enabled: true")
451
+ return
452
+
453
+ if not endpoints:
454
+ click.echo("No webhook endpoints configured.")
455
+ click.echo()
456
+ click.echo("Configure webhooks in ~/.gobby/config.yaml:")
457
+ click.echo(" hook_extensions:")
458
+ click.echo(" webhooks:")
459
+ click.echo(" endpoints:")
460
+ click.echo(" - name: my-webhook")
461
+ click.echo(" url: https://example.com/hook")
462
+ return
463
+
464
+ click.echo(f"Webhook Endpoints ({len(endpoints)}):")
465
+ click.echo()
466
+ for endpoint in endpoints:
467
+ status = "enabled" if endpoint.get("enabled", True) else "disabled"
468
+ click.echo(f" {endpoint['name']} [{status}]")
469
+ click.echo(f" URL: {endpoint.get('url', 'not configured')}")
470
+ events = endpoint.get("events", [])
471
+ if events:
472
+ click.echo(f" Events: {', '.join(events)}")
473
+ else:
474
+ click.echo(" Events: all")
475
+ if endpoint.get("can_block"):
476
+ click.echo(" Can block: yes")
477
+
478
+
479
+ @webhooks.command("test")
480
+ @click.argument("webhook_name")
481
+ @click.option(
482
+ "--event",
483
+ "-e",
484
+ default="notification",
485
+ help="Event type to send (default: notification)",
486
+ )
487
+ @click.option("--json", "json_format", is_flag=True, help="Output as JSON")
488
+ @click.pass_context
489
+ def webhooks_test(ctx: click.Context, webhook_name: str, event: str, json_format: bool) -> None:
490
+ """Test a webhook endpoint by sending a test event.
491
+
492
+ WEBHOOK_NAME is the name of the webhook endpoint to test.
493
+ """
494
+ client = get_daemon_client(ctx)
495
+ if not check_daemon_running(client):
496
+ sys.exit(1)
497
+
498
+ result = call_mcp_api(
499
+ client,
500
+ "/webhooks/test",
501
+ method="POST",
502
+ json_data={
503
+ "name": webhook_name,
504
+ "event_type": event,
505
+ },
506
+ )
507
+
508
+ if result is None:
509
+ click.echo(f"Failed to test webhook: {webhook_name}", err=True)
510
+ sys.exit(1)
511
+
512
+ if json_format:
513
+ click.echo(json.dumps(result, indent=2))
514
+ return
515
+
516
+ if result.get("success"):
517
+ click.echo(f"Webhook '{webhook_name}' test successful!")
518
+ click.echo(f" Status: {result.get('status_code', 'unknown')}")
519
+ response_time = result.get("response_time_ms")
520
+ if response_time:
521
+ click.echo(f" Response time: {response_time:.0f}ms")
522
+ else:
523
+ click.echo(f"Webhook test failed: {result.get('error', 'Unknown error')}", err=True)
524
+ if result.get("status_code"):
525
+ click.echo(f" Status: {result.get('status_code')}")
526
+ sys.exit(1)