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,394 @@
1
+ """Context injection and handoff workflow actions.
2
+
3
+ Extracted from actions.py as part of strangler fig decomposition.
4
+ These functions handle context injection, message injection, and handoff extraction.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from gobby.workflows.git_utils import get_git_status, get_recent_git_commits
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def inject_context(
20
+ session_manager: Any,
21
+ session_id: str,
22
+ state: Any,
23
+ template_engine: Any,
24
+ source: str | None = None,
25
+ template: str | None = None,
26
+ require: bool = False,
27
+ ) -> dict[str, Any] | None:
28
+ """Inject context from a source.
29
+
30
+ Args:
31
+ session_manager: The session manager instance
32
+ session_id: Current session ID
33
+ state: WorkflowState instance
34
+ template_engine: Template engine for rendering
35
+ source: Source type (previous_session_summary, handoff, artifacts, etc.)
36
+ template: Optional template for rendering
37
+ require: If True, block session when no content found (default: False)
38
+
39
+ Returns:
40
+ Dict with inject_context key, blocking decision, or None
41
+ """
42
+ # Validate required parameters
43
+ if session_manager is None:
44
+ logger.warning(f"inject_context: session_manager is None (session_id={session_id})")
45
+ return None
46
+
47
+ if state is None:
48
+ logger.warning(f"inject_context: state is None (session_id={session_id})")
49
+ return None
50
+
51
+ if template_engine is None:
52
+ logger.warning(f"inject_context: template_engine is None (session_id={session_id})")
53
+ return None
54
+
55
+ if not session_id:
56
+ logger.warning("inject_context: session_id is empty or None")
57
+ return None
58
+
59
+ # Debug logging for troubleshooting
60
+ logger.debug(
61
+ f"inject_context called: source={source!r}, "
62
+ f"template_present={template is not None}, "
63
+ f"template_len={len(template) if template else 0}, "
64
+ f"session_id={session_id}"
65
+ )
66
+
67
+ # Support template-only injection (no source lookup needed)
68
+ condition_result = (not source) and bool(template)
69
+ logger.debug(
70
+ f"inject_context: not source={not source}, bool(template)={bool(template)}, "
71
+ f"condition_result={condition_result}"
72
+ )
73
+ if not source and template:
74
+ # Render static template directly
75
+ logger.debug("inject_context: entering template-only path")
76
+ render_context: dict[str, Any] = {
77
+ "session": session_manager.get(session_id),
78
+ "state": state,
79
+ "artifacts": state.artifacts if state else {},
80
+ "observations": state.observations if state else {},
81
+ }
82
+ rendered = template_engine.render(template, render_context)
83
+ logger.debug(f"inject_context: rendered template, len={len(rendered) if rendered else 0}")
84
+ if state:
85
+ state.context_injected = True
86
+ return {"inject_context": rendered}
87
+
88
+ if not source:
89
+ return None
90
+
91
+ content = ""
92
+
93
+ if source in ["previous_session_summary", "handoff"]:
94
+ current_session = session_manager.get(session_id)
95
+ if not current_session:
96
+ logger.warning(f"Session {session_id} not found")
97
+ return None
98
+
99
+ if current_session.parent_session_id:
100
+ parent = session_manager.get(current_session.parent_session_id)
101
+ if parent:
102
+ content = parent.summary_markdown
103
+ # Failback: try reading from file if database summary is empty
104
+ # This handles cases where daemon was unavailable during /clear
105
+ if not content and hasattr(parent, "external_id") and parent.external_id:
106
+ summary_dir = Path.home() / ".gobby" / "session_summaries"
107
+ if summary_dir.exists():
108
+ for summary_file in summary_dir.glob(f"session_*_{parent.external_id}.md"):
109
+ try:
110
+ content = summary_file.read_text()
111
+ logger.info(
112
+ f"Recovered summary from failback file for {parent.external_id}"
113
+ )
114
+ break
115
+ except Exception as e:
116
+ logger.warning(f"Failed to read failback file {summary_file}: {e}")
117
+
118
+ elif source == "artifacts":
119
+ if state.artifacts:
120
+ lines = ["## Captured Artifacts"]
121
+ for name, path in state.artifacts.items():
122
+ lines.append(f"- {name}: {path}")
123
+ content = "\n".join(lines)
124
+
125
+ elif source == "observations":
126
+ if state.observations:
127
+ content = "## Observations\n" + json.dumps(state.observations, indent=2)
128
+
129
+ elif source == "workflow_state":
130
+ try:
131
+ state_dict = state.model_dump(exclude={"observations", "artifacts"})
132
+ except AttributeError:
133
+ state_dict = state.dict(exclude={"observations", "artifacts"})
134
+ content = "## Workflow State\n" + json.dumps(state_dict, indent=2, default=str)
135
+
136
+ elif source == "compact_handoff":
137
+ # Look at CURRENT session's compact_markdown (not parent)
138
+ # On compact, the same session continues - compact_markdown was saved to this session
139
+ # during pre_compact, so we read it from the current session itself.
140
+ current_session = session_manager.get(session_id)
141
+ logger.debug(
142
+ f"compact_handoff lookup: session_id={session_id}, "
143
+ f"compact_markdown exists: {bool(getattr(current_session, 'compact_markdown', None)) if current_session else False}"
144
+ )
145
+ if current_session and current_session.compact_markdown:
146
+ content = current_session.compact_markdown
147
+ logger.debug(
148
+ f"Loaded compact_markdown ({len(content)} chars) from current session {session_id}"
149
+ )
150
+
151
+ if content:
152
+ if template:
153
+ render_context = {
154
+ "session": session_manager.get(session_id),
155
+ "state": state,
156
+ "artifacts": state.artifacts,
157
+ "observations": state.observations,
158
+ }
159
+
160
+ if source in ["previous_session_summary", "handoff"]:
161
+ render_context["summary"] = content
162
+ render_context["handoff"] = {"notes": content}
163
+ elif source == "artifacts":
164
+ render_context["artifacts_list"] = content
165
+ elif source == "observations":
166
+ render_context["observations_text"] = content
167
+ elif source == "workflow_state":
168
+ render_context["workflow_state_text"] = content
169
+ elif source == "compact_handoff":
170
+ # Pass content to template (like /clear does with summary)
171
+ render_context["handoff"] = content
172
+
173
+ content = template_engine.render(template, render_context)
174
+
175
+ state.context_injected = True
176
+ return {"inject_context": content}
177
+
178
+ # No content found - block if required
179
+ if require:
180
+ reason = f"Required handoff context not found (source={source})"
181
+ logger.warning(f"inject_context: {reason}")
182
+ return {"decision": "block", "reason": reason}
183
+
184
+ return None
185
+
186
+
187
+ def inject_message(
188
+ session_manager: Any,
189
+ session_id: str,
190
+ state: Any,
191
+ template_engine: Any,
192
+ content: str | None = None,
193
+ **extra_kwargs: Any,
194
+ ) -> dict[str, Any] | None:
195
+ """Inject a message to the user/assistant, rendering it as a template.
196
+
197
+ Args:
198
+ session_manager: The session manager instance
199
+ session_id: Current session ID
200
+ state: WorkflowState instance
201
+ template_engine: Template engine for rendering
202
+ content: Template content to render
203
+ **extra_kwargs: Additional context for rendering
204
+
205
+ Returns:
206
+ Dict with inject_message key, or None
207
+ """
208
+ if not content:
209
+ return None
210
+
211
+ render_context: dict[str, Any] = {
212
+ "session": session_manager.get(session_id),
213
+ "state": state,
214
+ "artifacts": state.artifacts,
215
+ "step_action_count": state.step_action_count,
216
+ "variables": state.variables or {},
217
+ }
218
+ render_context.update(extra_kwargs)
219
+
220
+ rendered_content = template_engine.render(content, render_context)
221
+ return {"inject_message": rendered_content}
222
+
223
+
224
+ def extract_handoff_context(
225
+ session_manager: Any,
226
+ session_id: str,
227
+ config: Any | None = None,
228
+ db: Any | None = None,
229
+ worktree_manager: Any | None = None,
230
+ ) -> dict[str, Any] | None:
231
+ """Extract handoff context from transcript and save to session.compact_markdown.
232
+
233
+ Args:
234
+ session_manager: The session manager instance
235
+ session_id: Current session ID
236
+ config: Optional config with compact_handoff settings
237
+ db: Optional LocalDatabase instance for dependency injection
238
+ worktree_manager: Optional LocalWorktreeManager instance for dependency injection
239
+
240
+ Returns:
241
+ Dict with extraction result or error
242
+ """
243
+ if config:
244
+ compact_config = getattr(config, "compact_handoff", None)
245
+ if compact_config and not compact_config.enabled:
246
+ return {"skipped": True, "reason": "compact_handoff disabled"}
247
+
248
+ current_session = session_manager.get(session_id)
249
+ if not current_session:
250
+ return {"error": "Session not found"}
251
+
252
+ transcript_path = getattr(current_session, "jsonl_path", None)
253
+ if not transcript_path:
254
+ return {"error": "No transcript path"}
255
+
256
+ try:
257
+ from gobby.sessions.analyzer import TranscriptAnalyzer
258
+
259
+ path = Path(transcript_path)
260
+ if not path.exists():
261
+ return {"error": "Transcript file not found"}
262
+
263
+ turns = []
264
+ with open(path) as f:
265
+ for line in f:
266
+ if line.strip():
267
+ turns.append(json.loads(line))
268
+
269
+ analyzer = TranscriptAnalyzer()
270
+ handoff_ctx = analyzer.extract_handoff_context(turns, max_turns=100)
271
+
272
+ # Enrich with real-time git status
273
+ if not handoff_ctx.git_status:
274
+ handoff_ctx.git_status = get_git_status()
275
+
276
+ # Enrich with real git commits
277
+ real_commits = get_recent_git_commits()
278
+ if real_commits:
279
+ handoff_ctx.git_commits = real_commits
280
+
281
+ # Enrich with worktree context if session is in a worktree
282
+ try:
283
+ # Use injected worktree_manager, or create one from injected db
284
+ wt_manager = worktree_manager
285
+ if wt_manager is None and db is not None:
286
+ from gobby.storage.worktrees import LocalWorktreeManager
287
+
288
+ wt_manager = LocalWorktreeManager(db)
289
+
290
+ if wt_manager is not None:
291
+ worktrees = wt_manager.list(agent_session_id=session_id, limit=1)
292
+ if worktrees:
293
+ wt = worktrees[0]
294
+ handoff_ctx.active_worktree = {
295
+ "id": wt.id,
296
+ "branch_name": wt.branch_name,
297
+ "worktree_path": wt.worktree_path,
298
+ "base_branch": wt.base_branch,
299
+ "task_id": wt.task_id,
300
+ "status": wt.status,
301
+ }
302
+ else:
303
+ logger.debug("Skipping worktree enrichment: no worktree_manager or db provided")
304
+ except Exception as wt_err:
305
+ logger.debug(f"Failed to get worktree context: {wt_err}")
306
+
307
+ # Format as markdown (like /clear stores formatted summary)
308
+ markdown = format_handoff_as_markdown(handoff_ctx)
309
+
310
+ # Save to session.compact_markdown
311
+ session_manager.update_compact_markdown(session_id, markdown)
312
+
313
+ logger.debug(
314
+ f"Saved compact handoff markdown ({len(markdown)} chars) to session {session_id}"
315
+ )
316
+ return {"handoff_context_extracted": True, "markdown_length": len(markdown)}
317
+
318
+ except Exception as e:
319
+ logger.error(f"extract_handoff_context: Failed: {e}")
320
+ return {"error": str(e)}
321
+
322
+
323
+ def format_handoff_as_markdown(ctx: Any, prompt_template: str | None = None) -> str:
324
+ """Format HandoffContext as markdown for storage.
325
+
326
+ Args:
327
+ ctx: HandoffContext with extracted session data
328
+ prompt_template: Optional custom template (unused, reserved for future)
329
+
330
+ Returns:
331
+ Formatted markdown string with all sections
332
+ """
333
+ _ = prompt_template # Reserved for future template support
334
+ sections: list[str] = []
335
+
336
+ # Active task section
337
+ if ctx.active_gobby_task:
338
+ task = ctx.active_gobby_task
339
+ sections.append(
340
+ f"### Active Task\n"
341
+ f"**{task.get('title', 'Untitled')}** ({task.get('id', 'unknown')})\n"
342
+ f"Status: {task.get('status', 'unknown')}"
343
+ )
344
+
345
+ # Worktree context section
346
+ if ctx.active_worktree:
347
+ wt = ctx.active_worktree
348
+ lines = ["### Worktree Context"]
349
+ lines.append(f"- **Branch**: `{wt.get('branch_name', 'unknown')}`")
350
+ lines.append(f"- **Path**: `{wt.get('worktree_path', 'unknown')}`")
351
+ lines.append(f"- **Base**: `{wt.get('base_branch', 'main')}`")
352
+ if wt.get("task_id"):
353
+ lines.append(f"- **Task**: {wt.get('task_id')}")
354
+ sections.append("\n".join(lines))
355
+
356
+ # Todo state section
357
+ if ctx.todo_state:
358
+ lines = ["### In-Progress Work"]
359
+ for todo in ctx.todo_state:
360
+ status = todo.get("status", "pending")
361
+ marker = "x" if status == "completed" else ">" if status == "in_progress" else " "
362
+ lines.append(f"- [{marker}] {todo.get('content', '')}")
363
+ sections.append("\n".join(lines))
364
+
365
+ # Git commits section
366
+ if ctx.git_commits:
367
+ lines = ["### Commits This Session"]
368
+ for commit in ctx.git_commits:
369
+ lines.append(f"- `{commit.get('hash', '')[:7]}` {commit.get('message', '')}")
370
+ sections.append("\n".join(lines))
371
+
372
+ # Git status section
373
+ if ctx.git_status:
374
+ sections.append(f"### Uncommitted Changes\n```\n{ctx.git_status}\n```")
375
+
376
+ # Files modified section
377
+ if ctx.files_modified:
378
+ lines = ["### Files Being Modified"]
379
+ for f in ctx.files_modified:
380
+ lines.append(f"- {f}")
381
+ sections.append("\n".join(lines))
382
+
383
+ # Initial goal section
384
+ if ctx.initial_goal:
385
+ sections.append(f"### Original Goal\n{ctx.initial_goal}")
386
+
387
+ # Recent activity section
388
+ if ctx.recent_activity:
389
+ lines = ["### Recent Activity"]
390
+ for activity in ctx.recent_activity[-5:]:
391
+ lines.append(f"- {activity}")
392
+ sections.append("\n".join(lines))
393
+
394
+ return "\n\n".join(sections)
@@ -0,0 +1,130 @@
1
+ from datetime import UTC, datetime
2
+ from typing import Any, Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+ # --- Workflow Definition Models (YAML) ---
7
+
8
+
9
+ class WorkflowRule(BaseModel):
10
+ name: str | None = None
11
+ when: str
12
+ action: Literal["block", "allow", "require_approval", "warn"]
13
+ message: str | None = None
14
+
15
+
16
+ class WorkflowTransition(BaseModel):
17
+ to: str
18
+ when: str
19
+ on_transition: list[dict[str, Any]] = Field(default_factory=list)
20
+
21
+
22
+ class WorkflowExitCondition(BaseModel):
23
+ type: str
24
+
25
+ # Other fields depend on type (e.g. pattern, prompt, variable)
26
+ model_config = ConfigDict(extra="allow")
27
+
28
+
29
+ class PrematureStopHandler(BaseModel):
30
+ """Handler for when an agent attempts to stop before task completion."""
31
+
32
+ action: Literal["guide_continuation", "block", "warn"] = "guide_continuation"
33
+ message: str = "Task has incomplete subtasks. Use suggest_next_task() to continue."
34
+ condition: str | None = None # Optional condition to check (e.g., task_tree_complete)
35
+
36
+
37
+ class WorkflowStep(BaseModel):
38
+ name: str
39
+ description: str | None = None
40
+
41
+ on_enter: list[dict[str, Any]] = Field(default_factory=list)
42
+
43
+ # "all" or list of tool names
44
+ allowed_tools: list[str] | Literal["all"] = Field(default="all")
45
+ blocked_tools: list[str] = Field(default_factory=list)
46
+
47
+ rules: list[WorkflowRule] = Field(default_factory=list)
48
+ transitions: list[WorkflowTransition] = Field(default_factory=list)
49
+ exit_conditions: list[dict[str, Any]] = Field(default_factory=list) # flexible for now
50
+
51
+ on_exit: list[dict[str, Any]] = Field(default_factory=list)
52
+
53
+
54
+ class WorkflowDefinition(BaseModel):
55
+ name: str
56
+ description: str | None = None
57
+ version: str = "1.0"
58
+ type: Literal["lifecycle", "step"] = "step"
59
+ extends: str | None = None
60
+
61
+ @field_validator("version", mode="before")
62
+ @classmethod
63
+ def coerce_version_to_string(cls, v: Any) -> str:
64
+ """Accept numeric versions (1.0, 2) and coerce to string."""
65
+ return str(v) if v is not None else "1.0"
66
+
67
+ settings: dict[str, Any] = Field(default_factory=dict)
68
+ variables: dict[str, Any] = Field(default_factory=dict)
69
+
70
+ steps: list[WorkflowStep] = Field(default_factory=list)
71
+
72
+ # Global triggers (on_session_start, etc.)
73
+ triggers: dict[str, list[dict[str, Any]]] = Field(default_factory=dict)
74
+
75
+ on_error: list[dict[str, Any]] = Field(default_factory=list)
76
+
77
+ # Handler for premature stop attempts (step workflows only)
78
+ # Triggered when agent tries to stop but exit_condition is not met
79
+ on_premature_stop: PrematureStopHandler | None = None
80
+
81
+ # Exit condition for the entire workflow (when this is true, workflow can end)
82
+ exit_condition: str | None = None
83
+
84
+ def get_step(self, step_name: str) -> WorkflowStep | None:
85
+ for s in self.steps:
86
+ if s.name == step_name:
87
+ return s
88
+ return None
89
+
90
+
91
+ # --- Workflow State Models (Runtime) ---
92
+
93
+
94
+ class WorkflowState(BaseModel):
95
+ session_id: str
96
+ workflow_name: str
97
+ step: str
98
+ step_entered_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
99
+ step_action_count: int = 0
100
+ total_action_count: int = 0
101
+
102
+ artifacts: dict[str, str] = Field(default_factory=dict)
103
+ observations: list[dict[str, Any]] = Field(default_factory=list)
104
+
105
+ reflection_pending: bool = False
106
+ context_injected: bool = False
107
+
108
+ variables: dict[str, Any] = Field(default_factory=dict)
109
+
110
+ # Task decomposition state
111
+ task_list: list[dict[str, Any]] | None = None
112
+ current_task_index: int = 0
113
+ files_modified_this_task: int = 0
114
+
115
+ # Approval state for user_approval exit conditions
116
+ approval_pending: bool = False
117
+ approval_condition_id: str | None = None # Which condition is awaiting approval
118
+ approval_prompt: str | None = None # The prompt shown to user
119
+ approval_requested_at: datetime | None = None
120
+ approval_timeout_seconds: int | None = None # None = no timeout
121
+
122
+ # Escape hatch: temporarily disable enforcement
123
+ disabled: bool = False
124
+ disabled_reason: str | None = None
125
+
126
+ # Track initial step for reset functionality
127
+ initial_step: str | None = None
128
+
129
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
130
+ updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))