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,362 @@
1
+ """
2
+ Context resolver for subagent context injection.
3
+
4
+ Resolves various context sources for injecting into subagent prompts:
5
+ - summary_markdown: Parent session's summary
6
+ - compact_markdown: Parent session's handoff context
7
+ - session_id:<id>: Lookup specific session summary
8
+ - transcript:<n>: Last N messages from parent session
9
+ - file:<path>: Read file content with security checks
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import re
16
+ from pathlib import Path
17
+ from typing import TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from gobby.storage.session_messages import LocalSessionMessageManager
21
+ from gobby.storage.sessions import LocalSessionManager
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class ContextResolutionError(Exception):
27
+ """Raised when context resolution fails."""
28
+
29
+ pass
30
+
31
+
32
+ class ContextResolver:
33
+ """
34
+ Resolves context from various sources for subagent injection.
35
+
36
+ Supports the following source formats:
37
+ - "summary_markdown": Parent session's summary_markdown field
38
+ - "compact_markdown": Parent session's compact_markdown (handoff context)
39
+ - "session_id:<id>": Summary from a specific session by ID
40
+ - "transcript:<n>": Last N messages from parent session
41
+ - "file:<path>": Read file content (project-scoped with security checks)
42
+
43
+ Example:
44
+ >>> resolver = ContextResolver(session_manager, message_manager, project_path)
45
+ >>> context = await resolver.resolve("summary_markdown", "sess-abc123")
46
+ >>> context = await resolver.resolve("transcript:10", "sess-abc123")
47
+ >>> context = await resolver.resolve("file:docs/context.md", "sess-abc123")
48
+ """
49
+
50
+ # Pattern matchers for source formats
51
+ SESSION_ID_PATTERN = re.compile(r"^session_id:(.+)$")
52
+ TRANSCRIPT_PATTERN = re.compile(r"^transcript:(\d+)$")
53
+ FILE_PATTERN = re.compile(r"^file:(.+)$")
54
+
55
+ def __init__(
56
+ self,
57
+ session_manager: LocalSessionManager,
58
+ message_manager: LocalSessionMessageManager,
59
+ project_path: str | Path | None = None,
60
+ max_file_size: int = 51200, # 50KB default
61
+ max_content_size: int = 51200, # 50KB default for all content types
62
+ max_transcript_messages: int = 100,
63
+ truncation_suffix: str = "\n\n[truncated: {bytes} bytes remaining]",
64
+ ):
65
+ """
66
+ Initialize the context resolver.
67
+
68
+ Args:
69
+ session_manager: Session storage manager for session lookups.
70
+ message_manager: Message storage manager for transcript lookups.
71
+ project_path: Project root path for file security checks.
72
+ max_file_size: Maximum file size in bytes (default: 50KB).
73
+ max_content_size: Maximum content size for all sources (default: 50KB).
74
+ max_transcript_messages: Maximum transcript messages to fetch.
75
+ truncation_suffix: Suffix template when content is truncated.
76
+ """
77
+ self._session_manager = session_manager
78
+ self._message_manager = message_manager
79
+ self._project_path = Path(project_path) if project_path else None
80
+ self._truncation_suffix = truncation_suffix
81
+ self._max_file_size = max_file_size
82
+ self._max_content_size = max_content_size
83
+ self._max_transcript_messages = max_transcript_messages
84
+
85
+ async def resolve(self, source: str, session_id: str) -> str:
86
+ """
87
+ Resolve context from the specified source.
88
+
89
+ Args:
90
+ source: Context source specification.
91
+ session_id: Parent session ID for context lookups.
92
+
93
+ Returns:
94
+ Resolved context string (uncompressed), truncated if exceeding max_content_size.
95
+
96
+ Raises:
97
+ ContextResolutionError: If resolution fails.
98
+ """
99
+ content: str = ""
100
+
101
+ # Handle simple source types
102
+ if source == "summary_markdown":
103
+ content = self._resolve_summary_markdown(session_id)
104
+
105
+ elif source == "compact_markdown":
106
+ content = self._resolve_compact_markdown(session_id)
107
+
108
+ # Handle parameterized source types
109
+ elif match := self.SESSION_ID_PATTERN.match(source):
110
+ target_session_id = match.group(1)
111
+ content = self._resolve_session_id(target_session_id)
112
+
113
+ elif match := self.TRANSCRIPT_PATTERN.match(source):
114
+ count = int(match.group(1))
115
+ content = await self._resolve_transcript(session_id, count)
116
+
117
+ elif match := self.FILE_PATTERN.match(source):
118
+ file_path = match.group(1)
119
+ # File resolution has its own truncation logic
120
+ return self._resolve_file(file_path)
121
+
122
+ else:
123
+ # Unknown source format
124
+ raise ContextResolutionError(f"Unknown context source format: {source}")
125
+
126
+ # Apply truncation to all non-file sources
127
+ return self._truncate_content(content, self._max_content_size)
128
+
129
+ def _resolve_summary_markdown(self, session_id: str) -> str:
130
+ """
131
+ Resolve summary_markdown from parent session.
132
+
133
+ Args:
134
+ session_id: Parent session ID.
135
+
136
+ Returns:
137
+ Summary markdown content, or empty string if not available.
138
+ """
139
+ session = self._session_manager.get(session_id)
140
+ if not session:
141
+ raise ContextResolutionError(f"Session not found: {session_id}")
142
+
143
+ return session.summary_markdown or ""
144
+
145
+ def _resolve_compact_markdown(self, session_id: str) -> str:
146
+ """
147
+ Resolve compact_markdown (handoff context) from parent session.
148
+
149
+ Args:
150
+ session_id: Parent session ID.
151
+
152
+ Returns:
153
+ Compact markdown content, or empty string if not available.
154
+ """
155
+ session = self._session_manager.get(session_id)
156
+ if not session:
157
+ raise ContextResolutionError(f"Session not found: {session_id}")
158
+
159
+ return session.compact_markdown or ""
160
+
161
+ def _resolve_session_id(self, target_session_id: str) -> str:
162
+ """
163
+ Resolve summary from a specific session by ID.
164
+
165
+ Args:
166
+ target_session_id: Target session ID to lookup.
167
+
168
+ Returns:
169
+ Summary markdown from the target session.
170
+
171
+ Raises:
172
+ ContextResolutionError: If session not found.
173
+ """
174
+ session = self._session_manager.get(target_session_id)
175
+ if not session:
176
+ raise ContextResolutionError(f"Session not found: {target_session_id}")
177
+
178
+ return session.summary_markdown or ""
179
+
180
+ async def _resolve_transcript(self, session_id: str, count: int) -> str:
181
+ """
182
+ Resolve last N messages from parent session as transcript.
183
+
184
+ Args:
185
+ session_id: Parent session ID.
186
+ count: Number of recent messages to include.
187
+
188
+ Returns:
189
+ Formatted transcript of recent messages, or empty string if none.
190
+ """
191
+ # Clamp count to max
192
+ count = min(count, self._max_transcript_messages)
193
+
194
+ messages = await self._message_manager.get_messages(
195
+ session_id=session_id,
196
+ limit=count,
197
+ offset=0,
198
+ )
199
+
200
+ if not messages:
201
+ return ""
202
+
203
+ # Format messages into transcript
204
+ lines = []
205
+ for msg in messages:
206
+ role = msg.get("role", "unknown")
207
+ content = msg.get("content", "")
208
+ if content:
209
+ lines.append(f"**{role}**: {content}")
210
+
211
+ return "\n\n".join(lines)
212
+
213
+ def _resolve_file(self, file_path: str) -> str:
214
+ """
215
+ Resolve content from a file with security checks.
216
+
217
+ Security checks:
218
+ - Path must be within project directory
219
+ - No path traversal (..)
220
+ - No absolute paths
221
+ - Symlinks must resolve within project
222
+ - File must be valid UTF-8 (no binary)
223
+ - File size must be within limit
224
+
225
+ Args:
226
+ file_path: Relative path to file within project.
227
+
228
+ Returns:
229
+ File content, possibly truncated.
230
+
231
+ Raises:
232
+ ContextResolutionError: If file not found, not readable, or fails security checks.
233
+ """
234
+ if not self._project_path:
235
+ raise ContextResolutionError("No project path configured for file resolution")
236
+
237
+ # Parse the path and check for security issues
238
+ parsed_path = Path(file_path)
239
+
240
+ # Reject absolute paths
241
+ if parsed_path.is_absolute():
242
+ raise ContextResolutionError(f"Absolute paths not allowed: {file_path}")
243
+
244
+ # Reject path traversal attempts by checking path components
245
+ if ".." in parsed_path.parts:
246
+ raise ContextResolutionError(f"Path traversal not allowed: {file_path}")
247
+
248
+ # Resolve the full path
249
+ try:
250
+ full_path = (self._project_path / file_path).resolve()
251
+ except Exception as e:
252
+ raise ContextResolutionError(f"Invalid file path: {file_path}") from e
253
+
254
+ # Check path is within project
255
+ try:
256
+ full_path.relative_to(self._project_path.resolve())
257
+ except ValueError:
258
+ raise ContextResolutionError(
259
+ f"File path outside project directory: {file_path}"
260
+ ) from None
261
+
262
+ # Check file exists
263
+ if not full_path.exists():
264
+ raise ContextResolutionError(f"File not found: {file_path}")
265
+
266
+ if not full_path.is_file():
267
+ raise ContextResolutionError(f"Path is not a file: {file_path}")
268
+
269
+ # Check file size
270
+ file_size = full_path.stat().st_size
271
+ if file_size > self._max_file_size:
272
+ # Read up to limit and truncate
273
+ try:
274
+ with open(full_path, encoding="utf-8") as f:
275
+ content = f.read(self._max_file_size)
276
+ remaining = file_size - self._max_file_size
277
+ return content + self._truncation_suffix.format(bytes=remaining)
278
+ except UnicodeDecodeError:
279
+ raise ContextResolutionError(
280
+ f"File is not valid UTF-8 (binary): {file_path}"
281
+ ) from None
282
+
283
+ # Read file content
284
+ try:
285
+ with open(full_path, encoding="utf-8") as f:
286
+ return f.read()
287
+ except UnicodeDecodeError:
288
+ raise ContextResolutionError(f"File is not valid UTF-8 (binary): {file_path}") from None
289
+ except PermissionError:
290
+ raise ContextResolutionError(f"Permission denied: {file_path}") from None
291
+ except Exception as e:
292
+ raise ContextResolutionError(f"Failed to read file {file_path}: {e}") from e
293
+
294
+ def _truncate_content(self, content: str, max_bytes: int) -> str:
295
+ """
296
+ Truncate content to max bytes with suffix.
297
+
298
+ Args:
299
+ content: Content to potentially truncate.
300
+ max_bytes: Maximum bytes allowed.
301
+
302
+ Returns:
303
+ Content, possibly truncated with suffix.
304
+ """
305
+ encoded = content.encode("utf-8")
306
+ if len(encoded) <= max_bytes:
307
+ return content
308
+
309
+ # Truncate and add suffix
310
+ truncated = encoded[:max_bytes].decode("utf-8", errors="ignore")
311
+ remaining = len(encoded) - max_bytes
312
+ return truncated + self._truncation_suffix.format(bytes=remaining)
313
+
314
+
315
+ # Default template for context injection
316
+ DEFAULT_CONTEXT_TEMPLATE = """## Context from Parent Session
317
+ *Injected by Gobby subagent spawning*
318
+
319
+ {{ context }}
320
+
321
+ ---
322
+
323
+ ## Task
324
+
325
+ {{ prompt }}"""
326
+
327
+
328
+ def format_injected_prompt(context: str, prompt: str, template: str | None = None) -> str:
329
+ """
330
+ Format the injected prompt with context prepended.
331
+
332
+ Args:
333
+ context: Resolved context to inject.
334
+ prompt: Original prompt for the agent.
335
+ template: Optional custom template with {{ context }} and {{ prompt }} placeholders.
336
+ If None, uses the default template.
337
+
338
+ Returns:
339
+ Formatted prompt with context, or original prompt if context is empty.
340
+ """
341
+ if not context or not context.strip():
342
+ return prompt
343
+
344
+ # Use default template if none provided
345
+ effective_template = template or DEFAULT_CONTEXT_TEMPLATE
346
+
347
+ # Simple string substitution for {{ context }} and {{ prompt }}
348
+ result = effective_template
349
+ result = result.replace("{{ context }}", context)
350
+ result = result.replace("{{ prompt }}", prompt)
351
+
352
+ # Also support {context} and {prompt} for Python format-style
353
+ # but only if {{ }} placeholders are not in the template
354
+ if "{{ context }}" not in effective_template and "{{ prompt }}" not in effective_template:
355
+ try:
356
+ result = effective_template.format(context=context, prompt=prompt)
357
+ except (KeyError, IndexError):
358
+ # If format fails due to missing placeholders or positional braces like {0},
359
+ # return as-is
360
+ pass
361
+
362
+ return result
@@ -0,0 +1,133 @@
1
+ """
2
+ Named Agent Definitions.
3
+
4
+ This module defines the schema and loading logic for named agents (Agents V2).
5
+ Named agents are reusable configurations that allow child agents to have distinct
6
+ lifecycle behavior, solving recursion loops in delegation.
7
+ """
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import yaml
14
+ from pydantic import BaseModel, Field
15
+
16
+ from gobby.utils.project_context import get_project_context
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class AgentDefinition(BaseModel):
22
+ """
23
+ Configuration for a named agent.
24
+ """
25
+
26
+ name: str
27
+ description: str | None = None
28
+
29
+ # Execution parameters
30
+ model: str | None = None
31
+ mode: str = "headless" # Default to headless for stability
32
+
33
+ # Workflow configuration
34
+ workflow: str | None = None
35
+
36
+ # Lifecycle variables to override parent's lifecycle settings
37
+ lifecycle_variables: dict[str, Any] = Field(default_factory=dict)
38
+
39
+ # Default variables passed to the agent
40
+ default_variables: dict[str, Any] = Field(default_factory=dict)
41
+
42
+ # Execution limits
43
+ timeout: float = 120.0
44
+ max_turns: int = 10
45
+
46
+
47
+ class AgentDefinitionLoader:
48
+ """
49
+ Loads agent definitions from YAML files.
50
+
51
+ Search priority (later overrides earlier):
52
+ 1. Built-in: src/gobby/install/shared/agents/
53
+ 2. User-level: ~/.gobby/agents/
54
+ 3. Project-level: .gobby/agents/
55
+ """
56
+
57
+ def __init__(self) -> None:
58
+ # Determine paths
59
+ # Built-in path relative to this file
60
+ # src/gobby/agents/definitions.py -> src/gobby/install/shared/agents/
61
+ base_dir = Path(__file__).parent.parent
62
+ self._shared_path = base_dir / "install" / "shared" / "agents"
63
+
64
+ # User path
65
+ self._user_path = Path.home() / ".gobby" / "agents"
66
+
67
+ # Project path (tried dynamically based on current context)
68
+ self._project_path: Path | None = None
69
+
70
+ def _get_project_path(self) -> Path | None:
71
+ """Get current project path from context."""
72
+ ctx = get_project_context()
73
+ if ctx and ctx.get("project_path"):
74
+ return Path(ctx["project_path"]) / ".gobby" / "agents"
75
+ return None
76
+
77
+ def _find_agent_file(self, name: str) -> Path | None:
78
+ """Find the agent definition file in search paths."""
79
+ filename = f"{name}.yaml"
80
+
81
+ # Check project first (highest priority for finding logic, but technically
82
+ # we want to load from lowest to highest if we were merging, but we just
83
+ # want the "winner" here. Since we don't merge partial definitions,
84
+ # finding the first one in priority order is sufficient.)
85
+
86
+ # 1. Project
87
+ project_agents = self._get_project_path()
88
+ if project_agents and project_agents.exists():
89
+ f = project_agents / filename
90
+ if f.exists():
91
+ return f
92
+
93
+ # 2. User
94
+ if self._user_path.exists():
95
+ f = self._user_path / filename
96
+ if f.exists():
97
+ return f
98
+
99
+ # 3. Built-in (Shared)
100
+ if self._shared_path.exists():
101
+ f = self._shared_path / filename
102
+ if f.exists():
103
+ return f
104
+
105
+ return None
106
+
107
+ def load(self, name: str) -> AgentDefinition | None:
108
+ """
109
+ Load an agent definition by name.
110
+
111
+ Args:
112
+ name: Name of the agent (e.g. "validation-runner")
113
+
114
+ Returns:
115
+ AgentDefinition if found, None otherwise.
116
+ """
117
+ path = self._find_agent_file(name)
118
+ if not path:
119
+ logger.debug(f"Agent definition '{name}' not found")
120
+ return None
121
+
122
+ try:
123
+ with open(path, encoding="utf-8") as f:
124
+ data = yaml.safe_load(f)
125
+
126
+ # Ensure name matches filename/request if not specified
127
+ if "name" not in data:
128
+ data["name"] = name
129
+
130
+ return AgentDefinition(**data)
131
+ except Exception as e:
132
+ logger.error(f"Failed to load agent definition '{name}' from {path}: {e}")
133
+ return None
@@ -0,0 +1,111 @@
1
+ """Gemini session ID capture utility.
2
+
3
+ Captures Gemini's session_id via stream-json output before launching interactive mode.
4
+ This is necessary because Gemini CLI in interactive mode cannot introspect its own
5
+ session_id, but we need it for:
6
+ 1. Linking to Gobby sessions (external_id)
7
+ 2. Resume functionality with `gemini -r {session_id}`
8
+ 3. MCP tool calls that require session context
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ from dataclasses import dataclass
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class GeminiSessionInfo:
21
+ """Captured Gemini session information."""
22
+
23
+ session_id: str
24
+ model: str | None = None
25
+
26
+
27
+ async def capture_gemini_session_id(
28
+ timeout: float = 10.0,
29
+ ) -> GeminiSessionInfo:
30
+ """Capture Gemini's session_id via preflight stream-json call.
31
+
32
+ Launches Gemini with minimal prompt in stream-json mode,
33
+ filters through token error noise to find init JSON,
34
+ extracts session_id, then terminates.
35
+
36
+ Note: Gemini CLI outputs token errors to stdout (not stderr),
37
+ so we must filter line-by-line for valid JSON.
38
+
39
+ Args:
40
+ timeout: Max seconds to wait for init JSON (default 10s to account
41
+ for auth wait time which can take ~4s)
42
+
43
+ Returns:
44
+ GeminiSessionInfo with captured session_id and model
45
+
46
+ Raises:
47
+ asyncio.TimeoutError: If init JSON not received within timeout
48
+ ValueError: If session_id not found in output
49
+ FileNotFoundError: If gemini CLI is not installed
50
+ """
51
+ logger.debug("Starting Gemini preflight to capture session_id")
52
+
53
+ try:
54
+ proc = await asyncio.create_subprocess_exec(
55
+ "gemini",
56
+ ".",
57
+ "-o",
58
+ "stream-json",
59
+ "--allowed-mcp-server-names",
60
+ "",
61
+ stdout=asyncio.subprocess.PIPE,
62
+ stderr=asyncio.subprocess.DEVNULL,
63
+ )
64
+ except FileNotFoundError:
65
+ raise FileNotFoundError(
66
+ "Gemini CLI not found. Install with: npm install -g @google/gemini-cli"
67
+ ) from None
68
+
69
+ try:
70
+
71
+ async def read_init() -> GeminiSessionInfo:
72
+ """Read lines until we find the init JSON."""
73
+ if proc.stdout is None:
74
+ raise RuntimeError("Process stdout is not available")
75
+ async for line in proc.stdout:
76
+ text = line.decode().strip()
77
+
78
+ # Skip non-JSON lines (token error noise)
79
+ if not text.startswith("{"):
80
+ continue
81
+
82
+ try:
83
+ data = json.loads(text)
84
+ if data.get("type") == "init":
85
+ session_id = data.get("session_id")
86
+ if not session_id:
87
+ raise ValueError("Init JSON missing session_id field")
88
+
89
+ logger.debug(
90
+ f"Captured Gemini session_id: {session_id}, model: {data.get('model')}"
91
+ )
92
+ return GeminiSessionInfo(
93
+ session_id=session_id,
94
+ model=data.get("model"),
95
+ )
96
+ except json.JSONDecodeError:
97
+ # Not valid JSON, skip
98
+ continue
99
+
100
+ raise ValueError("No init JSON found in Gemini output")
101
+
102
+ return await asyncio.wait_for(read_init(), timeout=timeout)
103
+
104
+ finally:
105
+ # Terminate the preflight process
106
+ proc.terminate()
107
+ try:
108
+ await asyncio.wait_for(proc.wait(), timeout=5.0)
109
+ except TimeoutError:
110
+ proc.kill()
111
+ await proc.wait()