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,138 @@
1
+ """
2
+ Approval flow handling for workflow engine.
3
+
4
+ Extracted from engine.py to reduce complexity.
5
+ Handles user approval requests and responses for workflow gates.
6
+ """
7
+
8
+ import logging
9
+ from datetime import UTC, datetime
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from gobby.hooks.events import HookEvent, HookResponse
13
+
14
+ from .evaluator import check_approval_response
15
+
16
+ if TYPE_CHECKING:
17
+ from .definitions import WorkflowState
18
+ from .evaluator import ConditionEvaluator
19
+ from .state_manager import WorkflowStateManager
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def handle_approval_response(
25
+ event: HookEvent,
26
+ state: "WorkflowState",
27
+ current_step: Any,
28
+ evaluator: "ConditionEvaluator",
29
+ state_manager: "WorkflowStateManager",
30
+ ) -> HookResponse | None:
31
+ """
32
+ Handle user response to approval request.
33
+
34
+ Called on BEFORE_AGENT events to check if user is responding to
35
+ a pending approval request.
36
+
37
+ Args:
38
+ event: The hook event
39
+ state: Current workflow state
40
+ current_step: Current workflow step definition
41
+ evaluator: Condition evaluator for checking pending approvals
42
+ state_manager: State manager for persisting state changes
43
+
44
+ Returns:
45
+ HookResponse if approval was handled, None otherwise.
46
+ """
47
+ # Get user prompt from event
48
+ prompt = event.data.get("prompt", "") if event.data else ""
49
+
50
+ # Check if we're waiting for approval
51
+ if state.approval_pending:
52
+ response = check_approval_response(prompt)
53
+
54
+ if response == "approved":
55
+ # Mark approval granted
56
+ condition_id = state.approval_condition_id
57
+ approved_var = f"_approval_{condition_id}_granted"
58
+ state.variables[approved_var] = True
59
+ state.approval_pending = False
60
+ state.approval_condition_id = None
61
+ state.approval_prompt = None
62
+ state.approval_requested_at = None
63
+ state_manager.save_state(state)
64
+
65
+ logger.info(f"User approved condition '{condition_id}' in step '{state.step}'")
66
+ return HookResponse(
67
+ decision="allow",
68
+ context=f"✓ Approval granted for: {state.approval_prompt or 'action'}",
69
+ )
70
+
71
+ elif response == "rejected":
72
+ # Mark approval rejected
73
+ condition_id = state.approval_condition_id
74
+ rejected_var = f"_approval_{condition_id}_rejected"
75
+ state.variables[rejected_var] = True
76
+ state.approval_pending = False
77
+ state.approval_condition_id = None
78
+ state.approval_prompt = None
79
+ state.approval_requested_at = None
80
+ state_manager.save_state(state)
81
+
82
+ logger.info(f"User rejected condition '{condition_id}' in step '{state.step}'")
83
+ return HookResponse(
84
+ decision="block",
85
+ reason="User rejected the approval request.",
86
+ )
87
+
88
+ else:
89
+ # User didn't respond with approval keyword - remind them
90
+ return HookResponse(
91
+ decision="allow",
92
+ context=(
93
+ f"⏳ **Waiting for approval:** {state.approval_prompt}\n\n"
94
+ f"Please respond with 'yes' or 'no' to continue."
95
+ ),
96
+ )
97
+
98
+ # Check if we need to request approval
99
+ approval_check = evaluator.check_pending_approval(current_step.exit_conditions, state)
100
+
101
+ if approval_check and approval_check.needs_approval:
102
+ # Set approval pending state
103
+ state.approval_pending = True
104
+ state.approval_condition_id = approval_check.condition_id
105
+ state.approval_prompt = approval_check.prompt
106
+ state.approval_requested_at = datetime.now(UTC)
107
+ state.approval_timeout_seconds = approval_check.timeout_seconds
108
+ state_manager.save_state(state)
109
+
110
+ logger.info(
111
+ f"Requesting approval for condition '{approval_check.condition_id}' "
112
+ f"in step '{state.step}'"
113
+ )
114
+ return HookResponse(
115
+ decision="allow",
116
+ context=(
117
+ f"🔔 **Approval Required**\n\n"
118
+ f"{approval_check.prompt}\n\n"
119
+ f"Please respond with 'yes' to approve or 'no' to reject."
120
+ ),
121
+ )
122
+
123
+ if approval_check and approval_check.is_timed_out:
124
+ # Timeout - treat as rejection
125
+ condition_id = approval_check.condition_id
126
+ rejected_var = f"_approval_{condition_id}_rejected"
127
+ state.variables[rejected_var] = True
128
+ state.approval_pending = False
129
+ state.approval_condition_id = None
130
+ state_manager.save_state(state)
131
+
132
+ logger.info(f"Approval timed out for condition '{condition_id}'")
133
+ return HookResponse(
134
+ decision="block",
135
+ reason=f"Approval request timed out after {approval_check.timeout_seconds} seconds.",
136
+ )
137
+
138
+ return None
@@ -0,0 +1,103 @@
1
+ """Artifact capture and read workflow actions.
2
+
3
+ Extracted from actions.py as part of strangler fig decomposition.
4
+ These functions handle file artifact capture and reading.
5
+ """
6
+
7
+ import glob
8
+ import logging
9
+ import os
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def capture_artifact(
16
+ state: Any,
17
+ pattern: str | None = None,
18
+ save_as: str | None = None,
19
+ ) -> dict[str, Any] | None:
20
+ """Capture an artifact (file) and store its path in state.
21
+
22
+ Args:
23
+ state: WorkflowState object with artifacts dict
24
+ pattern: Glob pattern to match files
25
+ save_as: Name to store the artifact under
26
+
27
+ Returns:
28
+ Dict with captured filepath, or None if no match
29
+ """
30
+ if not pattern:
31
+ return None
32
+
33
+ # Use iglob generator to avoid building entire list on deep trees
34
+ # Select the lexicographically smallest match for determinism
35
+ first_match: str | None = None
36
+ for match in glob.iglob(pattern, recursive=True):
37
+ if first_match is None or match < first_match:
38
+ first_match = match
39
+
40
+ if first_match is None:
41
+ return None
42
+
43
+ filepath = os.path.abspath(first_match)
44
+
45
+ if save_as:
46
+ if not state.artifacts:
47
+ state.artifacts = {}
48
+ state.artifacts[save_as] = filepath
49
+
50
+ return {"captured": filepath}
51
+
52
+
53
+ def read_artifact(
54
+ state: Any,
55
+ pattern: str | None = None,
56
+ variable_name: str | None = None,
57
+ ) -> dict[str, Any] | None:
58
+ """Read an artifact's content into a workflow variable.
59
+
60
+ Args:
61
+ state: WorkflowState object with artifacts and variables dicts
62
+ pattern: Glob pattern or artifact key to read
63
+ variable_name: Variable name to store content in
64
+
65
+ Returns:
66
+ Dict with read_artifact, variable, and length, or None on error
67
+ """
68
+ if not pattern:
69
+ return None
70
+
71
+ if not variable_name:
72
+ logger.warning("read_artifact: 'as' argument missing")
73
+ return None
74
+
75
+ # Check if pattern matches an existing artifact key first
76
+ filepath = None
77
+ if state.artifacts:
78
+ filepath = state.artifacts.get(pattern)
79
+
80
+ if not filepath:
81
+ # Try as glob pattern - use sorted() for deterministic selection
82
+ matches = sorted(glob.glob(pattern, recursive=True))
83
+ if matches:
84
+ filepath = os.path.abspath(matches[0])
85
+
86
+ if not filepath or not os.path.exists(filepath):
87
+ logger.warning(f"read_artifact: File not found for pattern '{pattern}'")
88
+ return None
89
+
90
+ try:
91
+ # Use explicit encoding and error handling for cross-platform safety
92
+ with open(filepath, encoding="utf-8", errors="replace") as f:
93
+ content = f.read()
94
+
95
+ if not state.variables:
96
+ state.variables = {}
97
+
98
+ state.variables[variable_name] = content
99
+ return {"read_artifact": True, "variable": variable_name, "length": len(content)}
100
+
101
+ except Exception as e:
102
+ logger.error(f"read_artifact: Failed to read {filepath}: {e}")
103
+ return None
@@ -0,0 +1,110 @@
1
+ """
2
+ Audit logging helper functions for workflow engine.
3
+
4
+ Extracted from engine.py to reduce complexity.
5
+ These are pure logging functions with no side effects beyond audit.
6
+ """
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from gobby.storage.workflow_audit import WorkflowAuditManager
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def log_tool_call(
18
+ audit_manager: "WorkflowAuditManager | None",
19
+ session_id: str,
20
+ step: str,
21
+ tool_name: str,
22
+ result: str,
23
+ reason: str | None = None,
24
+ context: dict[str, Any] | None = None,
25
+ ) -> None:
26
+ """Log a tool call permission check to the audit log."""
27
+ if audit_manager:
28
+ try:
29
+ audit_manager.log_tool_call(
30
+ session_id=session_id,
31
+ step=step,
32
+ tool_name=tool_name,
33
+ result=result,
34
+ reason=reason,
35
+ context=context,
36
+ )
37
+ except Exception as e:
38
+ logger.debug(f"Failed to log tool call audit: {e}")
39
+
40
+
41
+ def log_rule_eval(
42
+ audit_manager: "WorkflowAuditManager | None",
43
+ session_id: str,
44
+ step: str,
45
+ rule_id: str,
46
+ condition: str,
47
+ result: str,
48
+ reason: str | None = None,
49
+ context: dict[str, Any] | None = None,
50
+ ) -> None:
51
+ """Log a rule evaluation to the audit log."""
52
+ if audit_manager:
53
+ try:
54
+ audit_manager.log_rule_eval(
55
+ session_id=session_id,
56
+ step=step,
57
+ rule_id=rule_id,
58
+ condition=condition,
59
+ result=result,
60
+ reason=reason,
61
+ context=context,
62
+ )
63
+ except Exception as e:
64
+ logger.debug(f"Failed to log rule eval audit: {e}")
65
+
66
+
67
+ def log_transition(
68
+ audit_manager: "WorkflowAuditManager | None",
69
+ session_id: str,
70
+ from_step: str,
71
+ to_step: str,
72
+ reason: str | None = None,
73
+ context: dict[str, Any] | None = None,
74
+ ) -> None:
75
+ """Log a step transition to the audit log."""
76
+ if audit_manager:
77
+ try:
78
+ audit_manager.log_transition(
79
+ session_id=session_id,
80
+ from_step=from_step,
81
+ to_step=to_step,
82
+ reason=reason,
83
+ context=context,
84
+ )
85
+ except Exception as e:
86
+ logger.debug(f"Failed to log transition audit: {e}")
87
+
88
+
89
+ def log_approval(
90
+ audit_manager: "WorkflowAuditManager | None",
91
+ session_id: str,
92
+ step: str,
93
+ result: str,
94
+ condition_id: str | None = None,
95
+ prompt: str | None = None,
96
+ context: dict[str, Any] | None = None,
97
+ ) -> None:
98
+ """Log an approval gate event to the audit log."""
99
+ if audit_manager:
100
+ try:
101
+ audit_manager.log_approval(
102
+ session_id=session_id,
103
+ step=step,
104
+ result=result,
105
+ condition_id=condition_id,
106
+ prompt=prompt,
107
+ context=context,
108
+ )
109
+ except Exception as e:
110
+ logger.debug(f"Failed to log approval audit: {e}")
@@ -0,0 +1,286 @@
1
+ """Autonomous execution workflow actions.
2
+
3
+ Actions for managing autonomous loop execution including:
4
+ - Progress tracking (start, stop, record)
5
+ - Stuck detection (detect task loops, tool loops)
6
+ - Task selection recording
7
+ """
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from gobby.autonomous.progress_tracker import ProgressTracker, ProgressType
14
+ from gobby.autonomous.stuck_detector import StuckDetector
15
+ from gobby.workflows.definitions import WorkflowState
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def start_progress_tracking(
21
+ progress_tracker: "ProgressTracker | None",
22
+ session_id: str,
23
+ state: "WorkflowState",
24
+ ) -> dict[str, Any]:
25
+ """Start progress tracking for a session.
26
+
27
+ Marks the session as actively being tracked and clears any
28
+ previous progress data.
29
+
30
+ Args:
31
+ progress_tracker: ProgressTracker instance
32
+ session_id: The session to track
33
+ state: Current workflow state (updated with tracking info)
34
+
35
+ Returns:
36
+ Dict with success status
37
+ """
38
+ if not progress_tracker:
39
+ logger.warning("No progress_tracker available")
40
+ return {"success": False, "error": "Progress tracker not available"}
41
+
42
+ # Clear any existing progress data
43
+ progress_tracker.clear_session(session_id)
44
+
45
+ # Mark as tracking in workflow state
46
+ state.variables["_progress_tracking_active"] = True
47
+
48
+ logger.info(f"Started progress tracking for session {session_id}")
49
+ return {"success": True, "session_id": session_id}
50
+
51
+
52
+ def stop_progress_tracking(
53
+ progress_tracker: "ProgressTracker | None",
54
+ session_id: str,
55
+ state: "WorkflowState",
56
+ keep_data: bool = False,
57
+ ) -> dict[str, Any]:
58
+ """Stop progress tracking for a session.
59
+
60
+ Args:
61
+ progress_tracker: ProgressTracker instance
62
+ session_id: The session to stop tracking
63
+ state: Current workflow state
64
+ keep_data: If True, preserve progress data; otherwise clear it
65
+
66
+ Returns:
67
+ Dict with success status and final summary
68
+ """
69
+ if not progress_tracker:
70
+ return {"success": False, "error": "Progress tracker not available"}
71
+
72
+ # Get final summary before stopping
73
+ summary = progress_tracker.get_summary(session_id)
74
+
75
+ # Clear if requested
76
+ if not keep_data:
77
+ progress_tracker.clear_session(session_id)
78
+
79
+ # Mark as not tracking
80
+ state.variables["_progress_tracking_active"] = False
81
+
82
+ logger.info(f"Stopped progress tracking for session {session_id}")
83
+ return {
84
+ "success": True,
85
+ "session_id": session_id,
86
+ "final_summary": {
87
+ "total_events": summary.total_events,
88
+ "high_value_events": summary.high_value_events,
89
+ "was_stagnant": summary.is_stagnant,
90
+ },
91
+ }
92
+
93
+
94
+ def record_progress(
95
+ progress_tracker: "ProgressTracker | None",
96
+ session_id: str,
97
+ progress_type: "ProgressType | str",
98
+ tool_name: str | None = None,
99
+ details: dict[str, Any] | None = None,
100
+ ) -> dict[str, Any]:
101
+ """Record a progress event.
102
+
103
+ Args:
104
+ progress_tracker: ProgressTracker instance
105
+ session_id: The session to record for
106
+ progress_type: Type of progress (from ProgressType enum or string)
107
+ tool_name: Optional tool name that generated the event
108
+ details: Optional additional details
109
+
110
+ Returns:
111
+ Dict with success status and event info
112
+ """
113
+ if not progress_tracker:
114
+ return {"success": False, "error": "Progress tracker not available"}
115
+
116
+ from gobby.autonomous.progress_tracker import ProgressType
117
+
118
+ # Convert string to enum if needed
119
+ if isinstance(progress_type, str):
120
+ try:
121
+ progress_type = ProgressType(progress_type)
122
+ except ValueError:
123
+ progress_type = ProgressType.TOOL_CALL
124
+
125
+ event = progress_tracker.record_event(
126
+ session_id=session_id,
127
+ progress_type=progress_type,
128
+ tool_name=tool_name,
129
+ details=details,
130
+ )
131
+
132
+ return {
133
+ "success": True,
134
+ "event": {
135
+ "type": event.progress_type.value,
136
+ "is_high_value": event.is_high_value,
137
+ "timestamp": event.timestamp.isoformat(),
138
+ },
139
+ }
140
+
141
+
142
+ def detect_task_loop(
143
+ stuck_detector: "StuckDetector | None",
144
+ session_id: str,
145
+ state: "WorkflowState",
146
+ ) -> dict[str, Any]:
147
+ """Detect if the session is stuck in a task selection loop.
148
+
149
+ Args:
150
+ stuck_detector: StuckDetector instance
151
+ session_id: The session to check
152
+ state: Current workflow state (updated with detection results)
153
+
154
+ Returns:
155
+ Dict with detection results
156
+ """
157
+ if not stuck_detector:
158
+ return {"is_stuck": False, "error": "Stuck detector not available"}
159
+
160
+ result = stuck_detector.detect_task_loop(session_id)
161
+
162
+ # Update workflow state
163
+ state.variables["_task_loop_detected"] = result.is_stuck
164
+ if result.is_stuck:
165
+ state.variables["_task_loop_task_id"] = (
166
+ result.details.get("task_id") if result.details else None
167
+ )
168
+
169
+ return {
170
+ "is_stuck": result.is_stuck,
171
+ "reason": result.reason,
172
+ "layer": result.layer,
173
+ "details": result.details,
174
+ "suggested_action": result.suggested_action,
175
+ }
176
+
177
+
178
+ def detect_stuck(
179
+ stuck_detector: "StuckDetector | None",
180
+ session_id: str,
181
+ state: "WorkflowState",
182
+ ) -> dict[str, Any]:
183
+ """Run full stuck detection (all layers).
184
+
185
+ Args:
186
+ stuck_detector: StuckDetector instance
187
+ session_id: The session to check
188
+ state: Current workflow state (updated with detection results)
189
+
190
+ Returns:
191
+ Dict with detection results and optional inject_context
192
+ """
193
+ if not stuck_detector:
194
+ return {"is_stuck": False, "error": "Stuck detector not available"}
195
+
196
+ result = stuck_detector.is_stuck(session_id)
197
+
198
+ # Update workflow state
199
+ state.variables["_is_stuck"] = result.is_stuck
200
+ state.variables["_stuck_layer"] = result.layer
201
+ state.variables["_stuck_reason"] = result.reason
202
+
203
+ response: dict[str, Any] = {
204
+ "is_stuck": result.is_stuck,
205
+ "reason": result.reason,
206
+ "layer": result.layer,
207
+ "details": result.details,
208
+ "suggested_action": result.suggested_action,
209
+ }
210
+
211
+ # Add context injection if stuck
212
+ if result.is_stuck:
213
+ response["inject_context"] = (
214
+ f"⚠️ **Stuck Detected** ({result.layer})\n\n"
215
+ f"Reason: {result.reason}\n"
216
+ f"Suggested action: {result.suggested_action or 'Review approach'}\n\n"
217
+ f"Consider stopping or changing your approach."
218
+ )
219
+
220
+ return response
221
+
222
+
223
+ def record_task_selection(
224
+ stuck_detector: "StuckDetector | None",
225
+ session_id: str,
226
+ task_id: str,
227
+ context: dict[str, Any] | None = None,
228
+ ) -> dict[str, Any]:
229
+ """Record a task selection for loop detection.
230
+
231
+ Called when the autonomous loop selects a task to work on.
232
+
233
+ Args:
234
+ stuck_detector: StuckDetector instance
235
+ session_id: The session selecting the task
236
+ task_id: The task being selected
237
+ context: Optional context about the selection
238
+
239
+ Returns:
240
+ Dict with success status
241
+ """
242
+ if not stuck_detector:
243
+ return {"success": False, "error": "Stuck detector not available"}
244
+
245
+ event = stuck_detector.record_task_selection(
246
+ session_id=session_id,
247
+ task_id=task_id,
248
+ context=context,
249
+ )
250
+
251
+ return {
252
+ "success": True,
253
+ "task_id": event.task_id,
254
+ "recorded_at": event.selected_at.isoformat(),
255
+ }
256
+
257
+
258
+ def get_progress_summary(
259
+ progress_tracker: "ProgressTracker | None",
260
+ session_id: str,
261
+ ) -> dict[str, Any]:
262
+ """Get a summary of progress for a session.
263
+
264
+ Args:
265
+ progress_tracker: ProgressTracker instance
266
+ session_id: The session to get summary for
267
+
268
+ Returns:
269
+ Dict with progress summary
270
+ """
271
+ if not progress_tracker:
272
+ return {"error": "Progress tracker not available"}
273
+
274
+ summary = progress_tracker.get_summary(session_id)
275
+
276
+ return {
277
+ "total_events": summary.total_events,
278
+ "high_value_events": summary.high_value_events,
279
+ "is_stagnant": summary.is_stagnant,
280
+ "stagnation_duration_seconds": summary.stagnation_duration_seconds,
281
+ "last_high_value_at": (
282
+ summary.last_high_value_at.isoformat() if summary.last_high_value_at else None
283
+ ),
284
+ "last_event_at": (summary.last_event_at.isoformat() if summary.last_event_at else None),
285
+ "events_by_type": {k.value: v for k, v in summary.events_by_type.items()},
286
+ }