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,182 @@
1
+ """
2
+ Utilities for resolving project context.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.config.app import ProjectVerificationConfig
14
+ from gobby.config.features import HooksConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def find_project_root(cwd: Path | None = None) -> Path | None:
20
+ """
21
+ Find the project root directory by looking for .gobby/project.json.
22
+
23
+ Args:
24
+ cwd: Current working directory to start search from. Defaults to Path.cwd().
25
+
26
+ Returns:
27
+ Path to project root if found, None otherwise.
28
+ """
29
+ if cwd is None:
30
+ cwd = Path.cwd()
31
+
32
+ current = cwd.resolve()
33
+ # Traverse up
34
+ for parent in [current] + list(current.parents):
35
+ project_file = parent / ".gobby" / "project.json"
36
+ if project_file.exists():
37
+ return parent
38
+ return None
39
+
40
+
41
+ def get_project_context(cwd: Path | None = None) -> dict[str, Any] | None:
42
+ """
43
+ Get project context from .gobby/project.json.
44
+
45
+ Args:
46
+ cwd: Current working directory to start search from.
47
+
48
+ Returns:
49
+ Dictionary containing project data (id, name, verification, etc.) and 'project_path',
50
+ or None if not found.
51
+
52
+ The returned dict may include:
53
+ - id: Project ID
54
+ - name: Project name
55
+ - created_at: Creation timestamp
56
+ - project_path: Path to project root
57
+ - verification: Optional dict with unit_tests, type_check, lint, integration, custom
58
+ """
59
+ root = find_project_root(cwd)
60
+ if not root:
61
+ return None
62
+
63
+ project_file = root / ".gobby" / "project.json"
64
+ try:
65
+ with open(project_file) as f:
66
+ data = json.load(f)
67
+ data["project_path"] = str(root)
68
+ return cast(dict[str, Any], data)
69
+ except Exception as e:
70
+ logger.warning(f"Failed to read project context: {e}")
71
+ return None
72
+
73
+
74
+ def get_workflow_project_path(cwd: Path | None = None) -> Path | None:
75
+ """
76
+ Get the project path for workflow lookup.
77
+
78
+ In a worktree, returns parent_project_path (where workflows live).
79
+ In a main project, returns the project_path.
80
+
81
+ This allows worktree agents to discover workflows from the parent project
82
+ without needing to explicitly pass the project_path parameter.
83
+
84
+ Args:
85
+ cwd: Current working directory to start search from.
86
+
87
+ Returns:
88
+ Path to use for workflow discovery, or None if no project found.
89
+ """
90
+ ctx = get_project_context(cwd)
91
+ if not ctx:
92
+ return None
93
+
94
+ # If in a worktree, use parent project for workflows
95
+ parent = ctx.get("parent_project_path")
96
+ if parent:
97
+ return Path(parent)
98
+
99
+ # Otherwise use current project path
100
+ project_path = ctx.get("project_path")
101
+ return Path(project_path) if project_path else None
102
+
103
+
104
+ def get_project_mcp_dir(project_name: str) -> Path:
105
+ """
106
+ Get the directory for project-specific MCP configuration.
107
+
108
+ Args:
109
+ project_name: Name of the project.
110
+
111
+ Returns:
112
+ Path to the project's MCP directory in ~/.gobby/projects/.
113
+ """
114
+ project_name_safe = project_name.replace(" ", "_").lower()
115
+ return Path.home() / ".gobby" / "projects" / project_name_safe
116
+
117
+
118
+ def get_project_mcp_config_path(project_name: str) -> Path:
119
+ """
120
+ Get the path to the project-specific .mcp.json file.
121
+
122
+ Args:
123
+ project_name: Name of the project.
124
+
125
+ Returns:
126
+ Path to .mcp.json.
127
+ """
128
+ return get_project_mcp_dir(project_name) / ".mcp.json"
129
+
130
+
131
+ def get_verification_config(cwd: Path | None = None) -> ProjectVerificationConfig | None:
132
+ """
133
+ Get project verification configuration from .gobby/project.json.
134
+
135
+ Args:
136
+ cwd: Current working directory to start search from.
137
+
138
+ Returns:
139
+ ProjectVerificationConfig if verification section exists, None otherwise.
140
+ """
141
+ from gobby.config.app import ProjectVerificationConfig
142
+
143
+ context = get_project_context(cwd)
144
+ if not context:
145
+ return None
146
+
147
+ verification_data = context.get("verification")
148
+ if not verification_data:
149
+ return None
150
+
151
+ try:
152
+ return ProjectVerificationConfig(**verification_data)
153
+ except Exception as e:
154
+ logger.warning(f"Failed to parse verification config: {e}")
155
+ return None
156
+
157
+
158
+ def get_hooks_config(cwd: Path | None = None) -> HooksConfig | None:
159
+ """
160
+ Get git hooks configuration from .gobby/project.json.
161
+
162
+ Args:
163
+ cwd: Current working directory to start search from.
164
+
165
+ Returns:
166
+ HooksConfig if hooks section exists, None otherwise.
167
+ """
168
+ from gobby.config.features import HooksConfig
169
+
170
+ context = get_project_context(cwd)
171
+ if not context:
172
+ return None
173
+
174
+ hooks_data = context.get("hooks")
175
+ if not hooks_data:
176
+ return None
177
+
178
+ try:
179
+ return HooksConfig(**hooks_data)
180
+ except Exception as e:
181
+ logger.warning(f"Failed to parse hooks config: {e}")
182
+ return None
@@ -0,0 +1,263 @@
1
+ """
2
+ Shared project initialization utilities.
3
+
4
+ This module provides the core logic for initializing a Gobby project,
5
+ used by both the CLI and the hook system for auto-initialization.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class VerificationCommands:
21
+ """Auto-detected verification commands for a project."""
22
+
23
+ unit_tests: str | None = None
24
+ type_check: str | None = None
25
+ lint: str | None = None
26
+ integration: str | None = None
27
+ custom: dict[str, str] = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict[str, Any]:
30
+ """Convert to dictionary, excluding None values."""
31
+ result: dict[str, Any] = {}
32
+ if self.unit_tests:
33
+ result["unit_tests"] = self.unit_tests
34
+ if self.type_check:
35
+ result["type_check"] = self.type_check
36
+ if self.lint:
37
+ result["lint"] = self.lint
38
+ if self.integration:
39
+ result["integration"] = self.integration
40
+ if self.custom:
41
+ result["custom"] = self.custom
42
+ return result
43
+
44
+
45
+ @dataclass
46
+ class InitResult:
47
+ """Result of project initialization."""
48
+
49
+ project_id: str
50
+ project_name: str
51
+ project_path: str
52
+ created_at: str
53
+ already_existed: bool
54
+ verification: VerificationCommands | None = None
55
+
56
+
57
+ def detect_verification_commands(cwd: Path) -> VerificationCommands:
58
+ """
59
+ Auto-detect verification commands based on project files.
60
+
61
+ Checks for pyproject.toml (Python) or package.json (Node.js) and suggests
62
+ appropriate commands for testing, type checking, and linting.
63
+
64
+ Args:
65
+ cwd: Project root directory.
66
+
67
+ Returns:
68
+ VerificationCommands with detected commands.
69
+ """
70
+ verification = VerificationCommands()
71
+
72
+ # Check for Python project (pyproject.toml)
73
+ pyproject_path = cwd / "pyproject.toml"
74
+ if pyproject_path.exists():
75
+ logger.debug("Detected Python project (pyproject.toml)")
76
+
77
+ # Check for tests directory
78
+ tests_dir = cwd / "tests"
79
+ if tests_dir.exists() and tests_dir.is_dir():
80
+ verification.unit_tests = "uv run pytest tests/ -v"
81
+
82
+ # Check for src directory (common pattern)
83
+ src_dir = cwd / "src"
84
+ if src_dir.exists() and src_dir.is_dir():
85
+ verification.type_check = "uv run mypy src/"
86
+ verification.lint = "uv run ruff check src/"
87
+ else:
88
+ # Fall back to current directory
89
+ verification.type_check = "uv run mypy ."
90
+ verification.lint = "uv run ruff check ."
91
+
92
+ return verification
93
+
94
+ # Check for Node.js project (package.json)
95
+ package_json_path = cwd / "package.json"
96
+ if package_json_path.exists():
97
+ logger.debug("Detected Node.js project (package.json)")
98
+
99
+ try:
100
+ with open(package_json_path) as f:
101
+ package_data = json.load(f)
102
+
103
+ scripts = package_data.get("scripts", {})
104
+
105
+ # Check for test script
106
+ if "test" in scripts:
107
+ verification.unit_tests = "npm test"
108
+
109
+ # Check for lint script
110
+ if "lint" in scripts:
111
+ verification.lint = "npm run lint"
112
+
113
+ # Check for type-check script (common names)
114
+ for script_name in ["type-check", "typecheck", "types", "tsc"]:
115
+ if script_name in scripts:
116
+ verification.type_check = f"npm run {script_name}"
117
+ break
118
+
119
+ except (json.JSONDecodeError, OSError) as e:
120
+ logger.warning(f"Failed to parse package.json: {e}")
121
+
122
+ return verification
123
+
124
+ logger.debug("No recognized project type detected")
125
+ return verification
126
+
127
+
128
+ def initialize_project(
129
+ cwd: Path | None = None,
130
+ name: str | None = None,
131
+ github_url: str | None = None,
132
+ ) -> InitResult:
133
+ """
134
+ Initialize a Gobby project in the given directory.
135
+
136
+ If the project is already initialized (has .gobby/project.json),
137
+ returns the existing project info. Otherwise creates a new project
138
+ in the database and writes the local project.json file.
139
+
140
+ Args:
141
+ cwd: Directory to initialize. Defaults to current working directory.
142
+ name: Project name. Defaults to directory name if not provided.
143
+ github_url: GitHub URL. Auto-detected from git remote if not provided.
144
+
145
+ Returns:
146
+ InitResult with project details and whether it already existed.
147
+
148
+ Raises:
149
+ Exception: If project creation fails.
150
+ """
151
+ from gobby.storage.database import LocalDatabase
152
+ from gobby.storage.migrations import run_migrations
153
+ from gobby.storage.projects import LocalProjectManager
154
+ from gobby.utils.git import get_github_url as detect_github_url
155
+ from gobby.utils.project_context import get_project_context
156
+
157
+ if cwd is None:
158
+ cwd = Path.cwd()
159
+
160
+ cwd = cwd.resolve()
161
+
162
+ # Check if already initialized
163
+ project_context = get_project_context(cwd)
164
+ if project_context and project_context.get("id"):
165
+ logger.debug(f"Project already initialized: {project_context.get('name')}")
166
+ return InitResult(
167
+ project_id=str(project_context["id"]),
168
+ project_name=project_context.get("name", ""),
169
+ project_path=project_context.get("project_path", str(cwd)),
170
+ created_at=project_context.get("created_at", ""),
171
+ already_existed=True,
172
+ )
173
+
174
+ # Auto-detect name from directory if not provided
175
+ if not name:
176
+ name = cwd.name
177
+
178
+ # Auto-detect GitHub URL from git remote if not provided
179
+ if not github_url:
180
+ github_url = detect_github_url(cwd)
181
+
182
+ # Initialize database and run migrations
183
+ db = LocalDatabase()
184
+ run_migrations(db)
185
+ project_manager = LocalProjectManager(db)
186
+
187
+ # Auto-detect verification commands
188
+ verification = detect_verification_commands(cwd)
189
+
190
+ # Check if project with same name exists in database
191
+ existing = project_manager.get_by_name(name)
192
+ if existing:
193
+ # Project exists in DB but no local project.json - write it
194
+ logger.debug(f"Found existing project in database: {name}")
195
+ _write_project_json(cwd, existing.id, existing.name, existing.created_at, verification)
196
+ return InitResult(
197
+ project_id=existing.id,
198
+ project_name=existing.name,
199
+ project_path=str(cwd),
200
+ created_at=existing.created_at,
201
+ already_existed=True,
202
+ verification=verification if verification.to_dict() else None,
203
+ )
204
+
205
+ # Create new project
206
+ logger.debug(f"Creating new project: {name}")
207
+ project = project_manager.create(
208
+ name=name,
209
+ repo_path=str(cwd),
210
+ github_url=github_url,
211
+ )
212
+
213
+ # Write local .gobby/project.json
214
+ _write_project_json(cwd, project.id, project.name, project.created_at, verification)
215
+
216
+ logger.info(f"Initialized project '{name}' in {cwd}")
217
+
218
+ return InitResult(
219
+ project_id=project.id,
220
+ project_name=project.name,
221
+ project_path=str(cwd),
222
+ created_at=project.created_at,
223
+ already_existed=False,
224
+ verification=verification if verification.to_dict() else None,
225
+ )
226
+
227
+
228
+ def _write_project_json(
229
+ cwd: Path,
230
+ project_id: str,
231
+ name: str,
232
+ created_at: str,
233
+ verification: VerificationCommands | None = None,
234
+ ) -> None:
235
+ """Write the .gobby/project.json file.
236
+
237
+ Args:
238
+ cwd: Project root directory.
239
+ project_id: Project ID.
240
+ name: Project name.
241
+ created_at: Project creation timestamp.
242
+ verification: Optional verification commands to include.
243
+ """
244
+ gobby_dir = cwd / ".gobby"
245
+ gobby_dir.mkdir(exist_ok=True)
246
+
247
+ project_file = gobby_dir / "project.json"
248
+ project_data: dict[str, Any] = {
249
+ "id": project_id,
250
+ "name": name,
251
+ "created_at": created_at,
252
+ }
253
+
254
+ # Add verification config if provided and has commands
255
+ if verification:
256
+ verification_dict = verification.to_dict()
257
+ if verification_dict:
258
+ project_data["verification"] = verification_dict
259
+
260
+ with open(project_file, "w") as f:
261
+ json.dump(project_data, f, indent=2)
262
+
263
+ logger.debug(f"Wrote project.json to {project_file}")
gobby/utils/status.py ADDED
@@ -0,0 +1,256 @@
1
+ """
2
+ Status message formatting for Gobby daemon.
3
+
4
+ Provides consistent status display across CLI and MCP server.
5
+ """
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def fetch_rich_status(http_port: int, timeout: float = 2.0) -> dict[str, Any]:
16
+ """
17
+ Fetch rich status data from the daemon API.
18
+
19
+ Args:
20
+ http_port: HTTP port of the daemon
21
+ timeout: Request timeout in seconds
22
+
23
+ Returns:
24
+ Dict of status kwargs to pass to format_status_message
25
+ """
26
+ status_kwargs: dict[str, Any] = {}
27
+
28
+ try:
29
+ response = httpx.get(f"http://localhost:{http_port}/admin/status", timeout=timeout)
30
+ if response.status_code != 200:
31
+ return status_kwargs
32
+
33
+ data = response.json()
34
+
35
+ # Process metrics
36
+ process_data = data.get("process")
37
+ if process_data:
38
+ status_kwargs["memory_mb"] = process_data.get("memory_rss_mb")
39
+ status_kwargs["cpu_percent"] = process_data.get("cpu_percent")
40
+
41
+ # MCP servers
42
+ mcp_servers = data.get("mcp_servers", {})
43
+ if mcp_servers:
44
+ total = len(mcp_servers)
45
+ connected = sum(1 for s in mcp_servers.values() if s.get("connected"))
46
+ status_kwargs["mcp_total"] = total
47
+ status_kwargs["mcp_connected"] = connected
48
+ status_kwargs["mcp_tools_cached"] = data.get("mcp_tools_cached", 0)
49
+
50
+ # Find unhealthy servers
51
+ unhealthy = []
52
+ for name, info in mcp_servers.items():
53
+ health = info.get("health")
54
+ if health and health not in ("healthy", None):
55
+ unhealthy.append((name, health))
56
+ elif info.get("consecutive_failures", 0) > 0:
57
+ unhealthy.append((name, f"{info['consecutive_failures']} failures"))
58
+ if unhealthy:
59
+ status_kwargs["mcp_unhealthy"] = unhealthy
60
+
61
+ # Sessions
62
+ sessions = data.get("sessions", {})
63
+ if sessions:
64
+ status_kwargs["sessions_active"] = sessions.get("active", 0)
65
+ status_kwargs["sessions_paused"] = sessions.get("paused", 0)
66
+ status_kwargs["sessions_handoff_ready"] = sessions.get("handoff_ready", 0)
67
+
68
+ # Tasks
69
+ tasks = data.get("tasks", {})
70
+ if tasks:
71
+ status_kwargs["tasks_open"] = tasks.get("open", 0)
72
+ status_kwargs["tasks_in_progress"] = tasks.get("in_progress", 0)
73
+ status_kwargs["tasks_ready"] = tasks.get("ready", 0)
74
+ status_kwargs["tasks_blocked"] = tasks.get("blocked", 0)
75
+
76
+ # Memory
77
+ memory = data.get("memory", {})
78
+ if memory and memory.get("count", 0) > 0:
79
+ status_kwargs["memories_count"] = memory.get("count", 0)
80
+ status_kwargs["memories_avg_importance"] = memory.get("avg_importance", 0.0)
81
+
82
+ except (httpx.ConnectError, httpx.TimeoutException):
83
+ # Daemon not responding - return empty
84
+ pass
85
+ except Exception as e:
86
+ logger.debug(f"Failed to fetch daemon status: {e}")
87
+
88
+ return status_kwargs
89
+
90
+
91
+ def format_status_message(
92
+ *,
93
+ running: bool,
94
+ pid: int | None = None,
95
+ pid_file: str | None = None,
96
+ log_files: str | None = None,
97
+ uptime: str | None = None,
98
+ http_port: int | None = None,
99
+ websocket_port: int | None = None,
100
+ # Process metrics
101
+ memory_mb: float | None = None,
102
+ cpu_percent: float | None = None,
103
+ # MCP proxy info
104
+ mcp_connected: int | None = None,
105
+ mcp_total: int | None = None,
106
+ mcp_tools_cached: int | None = None,
107
+ mcp_unhealthy: list[tuple[str, str]] | None = None,
108
+ # Sessions info
109
+ sessions_active: int | None = None,
110
+ sessions_paused: int | None = None,
111
+ sessions_handoff_ready: int | None = None,
112
+ # Tasks info
113
+ tasks_open: int | None = None,
114
+ tasks_in_progress: int | None = None,
115
+ tasks_ready: int | None = None,
116
+ tasks_blocked: int | None = None,
117
+ # Memory
118
+ memories_count: int | None = None,
119
+ memories_avg_importance: float | None = None,
120
+ **kwargs: Any,
121
+ ) -> str:
122
+ """
123
+ Format Gobby daemon status message with consistent styling.
124
+
125
+ Args:
126
+ running: Whether the daemon is running
127
+ pid: Process ID
128
+ pid_file: Path to PID file
129
+ log_files: Path to log files directory
130
+ uptime: Formatted uptime string (e.g., "1h 23m 45s")
131
+ http_port: HTTP server port
132
+ websocket_port: WebSocket server port
133
+ memory_mb: Memory usage in MB
134
+ cpu_percent: CPU usage percentage
135
+ mcp_connected: Number of connected MCP servers
136
+ mcp_total: Total number of configured MCP servers
137
+ mcp_tools_cached: Number of cached tools
138
+ mcp_unhealthy: List of (server_name, status) for unhealthy servers
139
+ sessions_active: Number of active sessions
140
+ sessions_paused: Number of paused sessions
141
+ sessions_handoff_ready: Number of sessions ready for handoff
142
+ tasks_open: Number of open tasks
143
+ tasks_in_progress: Number of in-progress tasks
144
+ tasks_ready: Number of ready tasks
145
+ tasks_blocked: Number of blocked tasks
146
+ memories_count: Total number of memories
147
+ memories_avg_importance: Average memory importance
148
+
149
+
150
+ Returns:
151
+ Formatted status message string
152
+ """
153
+ lines = []
154
+
155
+ # Header
156
+ lines.append("=" * 70)
157
+ lines.append("GOBBY DAEMON STATUS")
158
+ lines.append("=" * 70)
159
+ lines.append("")
160
+
161
+ # Status section
162
+ if running:
163
+ status_line = "Status: Running"
164
+ if pid:
165
+ status_line += f" (PID: {pid})"
166
+ lines.append(status_line)
167
+
168
+ # Uptime and process metrics on same conceptual level
169
+ metrics_parts = []
170
+ if uptime:
171
+ metrics_parts.append(f"Uptime: {uptime}")
172
+ if memory_mb is not None:
173
+ metrics_parts.append(f"Memory: {memory_mb:.1f} MB")
174
+ if cpu_percent is not None:
175
+ metrics_parts.append(f"CPU: {cpu_percent:.1f}%")
176
+
177
+ if metrics_parts:
178
+ lines.append(f" {' | '.join(metrics_parts)}")
179
+ else:
180
+ lines.append("Status: Stopped")
181
+
182
+ lines.append("")
183
+
184
+ # Server Configuration section
185
+ if http_port or websocket_port:
186
+ lines.append("Server Configuration:")
187
+ if http_port:
188
+ lines.append(f" HTTP: localhost:{http_port}")
189
+ if websocket_port:
190
+ lines.append(f" WebSocket: localhost:{websocket_port}")
191
+ lines.append("")
192
+
193
+ # MCP Proxy section (only show if we have data)
194
+ if mcp_total is not None:
195
+ lines.append("MCP Proxy:")
196
+ connected = mcp_connected if mcp_connected is not None else 0
197
+ lines.append(f" Servers: {connected} connected / {mcp_total} total")
198
+ if mcp_tools_cached is not None:
199
+ lines.append(f" Tools cached: {mcp_tools_cached}")
200
+ if mcp_unhealthy:
201
+ unhealthy_str = ", ".join(f"{name} ({status})" for name, status in mcp_unhealthy)
202
+ lines.append(f" Unhealthy: {unhealthy_str}")
203
+ lines.append("")
204
+
205
+ # Sessions section (only show if we have data)
206
+ if sessions_active is not None or sessions_paused is not None:
207
+ lines.append("Sessions:")
208
+ parts = []
209
+ if sessions_active is not None:
210
+ parts.append(f"Active: {sessions_active}")
211
+ if sessions_paused is not None:
212
+ parts.append(f"Paused: {sessions_paused}")
213
+ if sessions_handoff_ready is not None:
214
+ parts.append(f"Handoff Ready: {sessions_handoff_ready}")
215
+ if parts:
216
+ lines.append(f" {' | '.join(parts)}")
217
+ lines.append("")
218
+
219
+ # Tasks section (only show if we have data)
220
+ if tasks_open is not None or tasks_in_progress is not None:
221
+ lines.append("Tasks:")
222
+ parts = []
223
+ if tasks_open is not None:
224
+ parts.append(f"Open: {tasks_open}")
225
+ if tasks_in_progress is not None:
226
+ parts.append(f"In Progress: {tasks_in_progress}")
227
+ if tasks_ready is not None:
228
+ parts.append(f"Ready: {tasks_ready}")
229
+ if tasks_blocked is not None:
230
+ parts.append(f"Blocked: {tasks_blocked}")
231
+ if parts:
232
+ lines.append(f" {' | '.join(parts)}")
233
+ lines.append("")
234
+
235
+ # Memory section (only show if we have data)
236
+ if memories_count is not None:
237
+ lines.append("Memory:")
238
+ mem_str = f"Memories: {memories_count}"
239
+ if memories_avg_importance is not None:
240
+ mem_str += f" (avg importance: {memories_avg_importance:.2f})"
241
+ lines.append(f" {mem_str}")
242
+ lines.append("")
243
+
244
+ # Paths section (only when running)
245
+ if running and (pid_file or log_files):
246
+ lines.append("Paths:")
247
+ if pid_file:
248
+ lines.append(f" PID file: {pid_file}")
249
+ if log_files:
250
+ lines.append(f" Logs: {log_files}")
251
+ lines.append("")
252
+
253
+ # Footer
254
+ lines.append("=" * 70)
255
+
256
+ return "\n".join(lines)