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,52 @@
1
+ import logging
2
+ from typing import Any
3
+
4
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class TemplateEngine:
10
+ """
11
+ Engine for rendering Jinja2 templates in workflows.
12
+ """
13
+
14
+ def __init__(self, template_dirs: list[str] | None = None):
15
+ if template_dirs:
16
+ loader = FileSystemLoader(template_dirs)
17
+ else:
18
+ loader = None
19
+
20
+ self.env = Environment(
21
+ loader=loader,
22
+ # Disable autoescape for inline templates (default_for_string=False)
23
+ # We generate markdown, not HTML - escaping breaks apostrophes etc.
24
+ autoescape=select_autoescape(["html", "xml"], default_for_string=False),
25
+ trim_blocks=True,
26
+ lstrip_blocks=True,
27
+ )
28
+
29
+ def render(self, template_str: str, context: dict[str, Any]) -> str:
30
+ """
31
+ Render a template string with the given context.
32
+ """
33
+ try:
34
+ template = self.env.from_string(template_str)
35
+ return str(template.render(**context))
36
+ except Exception as e:
37
+ logger.error(f"Error rendering template: {e}", exc_info=True)
38
+ # Fallback to original string or raise?
39
+ # For workflows, it might be better to fail typically, but let's return error message in string for visibility if strict validation isn't on.
40
+ # actually, better to raise so the action fails and handles it.
41
+ raise e
42
+
43
+ def render_file(self, template_name: str, context: dict[str, Any]) -> str:
44
+ """
45
+ Render a template file with the given context.
46
+ """
47
+ try:
48
+ template = self.env.get_template(template_name)
49
+ return str(template.render(**context))
50
+ except Exception as e:
51
+ logger.error(f"Error rendering template file '{template_name}': {e}", exc_info=True)
52
+ raise e
@@ -0,0 +1,84 @@
1
+ """Todo file workflow actions.
2
+
3
+ Extracted from actions.py as part of strangler fig decomposition.
4
+ These functions handle TODO.md file operations.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ from typing import Any
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def write_todos(
15
+ todos: list[str],
16
+ filename: str = "TODO.md",
17
+ mode: str = "w",
18
+ ) -> dict[str, Any]:
19
+ """Write todos to a file (default TODO.md).
20
+
21
+ Args:
22
+ todos: List of todo strings
23
+ filename: Output filename (default: TODO.md)
24
+ mode: "w" to overwrite, "append" to append
25
+
26
+ Returns:
27
+ Dict with todos_written count and file path, or error
28
+ """
29
+ try:
30
+ formatted_todos = [f"- [ ] {todo}" for todo in todos]
31
+
32
+ if mode == "append" and os.path.exists(filename):
33
+ with open(filename, "a") as f:
34
+ f.write("\n" + "\n".join(formatted_todos) + "\n")
35
+ else:
36
+ with open(filename, "w") as f:
37
+ f.write("# TODOs\n\n" + "\n".join(formatted_todos) + "\n")
38
+
39
+ return {"todos_written": len(todos), "file": filename}
40
+ except Exception as e:
41
+ logger.error(f"write_todos: Failed: {e}")
42
+ return {"error": str(e)}
43
+
44
+
45
+ def mark_todo_complete(
46
+ todo_text: str,
47
+ filename: str = "TODO.md",
48
+ ) -> dict[str, Any]:
49
+ """Mark the first occurrence of a todo as complete in TODO.md.
50
+
51
+ Args:
52
+ todo_text: Text of the todo to mark complete
53
+ filename: Todo file path (default: TODO.md)
54
+
55
+ Returns:
56
+ Dict with todo_completed boolean and text, or error
57
+ """
58
+ if not todo_text:
59
+ return {"error": "Missing todo_text"}
60
+
61
+ if not os.path.exists(filename):
62
+ return {"error": "File not found"}
63
+
64
+ try:
65
+ with open(filename) as f:
66
+ lines = f.readlines()
67
+
68
+ updated = False
69
+ new_lines = []
70
+ for line in lines:
71
+ if not updated and todo_text in line and "- [ ]" in line:
72
+ new_lines.append(line.replace("- [ ]", "- [x]"))
73
+ updated = True
74
+ else:
75
+ new_lines.append(line)
76
+
77
+ if updated:
78
+ with open(filename, "w") as f:
79
+ f.writelines(new_lines)
80
+
81
+ return {"todo_completed": updated, "text": todo_text}
82
+ except Exception as e:
83
+ logger.error(f"mark_todo_complete: Failed: {e}")
84
+ return {"error": str(e)}
@@ -0,0 +1,223 @@
1
+ """
2
+ Webhook workflow action models.
3
+
4
+ Defines the WebhookAction class and related configuration models
5
+ for making HTTP requests from workflows.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ # Valid HTTP methods for webhook actions
15
+ VALID_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
16
+
17
+ # Default retry status codes (server errors and rate limiting)
18
+ DEFAULT_RETRY_STATUS_CODES = [429, 500, 502, 503, 504]
19
+
20
+
21
+ @dataclass
22
+ class RetryConfig:
23
+ """Configuration for webhook retry behavior."""
24
+
25
+ max_attempts: int = 3
26
+ backoff_seconds: int = 1
27
+ retry_on_status: list[int] = field(default_factory=lambda: DEFAULT_RETRY_STATUS_CODES.copy())
28
+
29
+ @classmethod
30
+ def from_dict(cls, data: dict[str, Any]) -> RetryConfig:
31
+ """Parse RetryConfig from a dict.
32
+
33
+ Args:
34
+ data: Dict with retry configuration fields.
35
+
36
+ Returns:
37
+ RetryConfig instance.
38
+
39
+ Raises:
40
+ ValueError: If max_attempts is outside 1-10 range.
41
+ """
42
+ max_attempts = data.get("max_attempts", 3)
43
+ if not (1 <= max_attempts <= 10):
44
+ raise ValueError(f"max_attempts must be between 1 and 10, got {max_attempts}")
45
+
46
+ backoff_seconds = data.get("backoff_seconds", 1)
47
+ retry_on_status = data.get("retry_on_status", DEFAULT_RETRY_STATUS_CODES.copy())
48
+
49
+ return cls(
50
+ max_attempts=max_attempts,
51
+ backoff_seconds=backoff_seconds,
52
+ retry_on_status=list(retry_on_status),
53
+ )
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Serialize to dict."""
57
+ return {
58
+ "max_attempts": self.max_attempts,
59
+ "backoff_seconds": self.backoff_seconds,
60
+ "retry_on_status": self.retry_on_status,
61
+ }
62
+
63
+
64
+ @dataclass
65
+ class CaptureConfig:
66
+ """Configuration for capturing webhook response data."""
67
+
68
+ status_var: str | None = None
69
+ body_var: str | None = None
70
+ headers_var: str | None = None
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: dict[str, Any]) -> CaptureConfig:
74
+ """Parse CaptureConfig from a dict.
75
+
76
+ Args:
77
+ data: Dict with capture configuration fields.
78
+
79
+ Returns:
80
+ CaptureConfig instance.
81
+ """
82
+ return cls(
83
+ status_var=data.get("status_var"),
84
+ body_var=data.get("body_var"),
85
+ headers_var=data.get("headers_var"),
86
+ )
87
+
88
+ def to_dict(self) -> dict[str, Any]:
89
+ """Serialize to dict."""
90
+ result: dict[str, Any] = {}
91
+ if self.status_var:
92
+ result["status_var"] = self.status_var
93
+ if self.body_var:
94
+ result["body_var"] = self.body_var
95
+ if self.headers_var:
96
+ result["headers_var"] = self.headers_var
97
+ return result
98
+
99
+
100
+ @dataclass
101
+ class WebhookAction:
102
+ """Webhook action definition for workflows.
103
+
104
+ Represents an HTTP request that can be made during workflow execution.
105
+ Either `url` or `webhook_id` must be provided, but not both.
106
+ """
107
+
108
+ url: str | None = None
109
+ webhook_id: str | None = None
110
+ method: str = "POST"
111
+ headers: dict[str, str] = field(default_factory=dict)
112
+ payload: str | dict[str, Any] | None = None
113
+ timeout: int = 30
114
+ retry: RetryConfig | None = None
115
+ on_success: str | None = None
116
+ on_failure: str | None = None
117
+ capture_response: CaptureConfig | None = None
118
+
119
+ @classmethod
120
+ def from_dict(cls, data: dict[str, Any]) -> WebhookAction:
121
+ """Parse WebhookAction from a dict (e.g., from YAML workflow).
122
+
123
+ Args:
124
+ data: Dict with webhook action fields.
125
+
126
+ Returns:
127
+ WebhookAction instance.
128
+
129
+ Raises:
130
+ ValueError: If validation fails (missing url/webhook_id, invalid method, etc.).
131
+ """
132
+ url = data.get("url")
133
+ webhook_id = data.get("webhook_id")
134
+
135
+ # Validate url/webhook_id exclusivity
136
+ if url and webhook_id:
137
+ raise ValueError(
138
+ "url and webhook_id are mutually exclusive - provide only one, not both"
139
+ )
140
+ if not url and not webhook_id:
141
+ raise ValueError("Either url or webhook_id is required")
142
+
143
+ # Validate URL scheme if url is provided
144
+ if url:
145
+ cls._validate_url(url)
146
+
147
+ # Validate method
148
+ method = data.get("method", "POST").upper()
149
+ if method not in VALID_METHODS:
150
+ raise ValueError(
151
+ f"Invalid HTTP method '{method}'. Must be one of: {', '.join(sorted(VALID_METHODS))}"
152
+ )
153
+
154
+ # Validate timeout
155
+ timeout = data.get("timeout", 30)
156
+ if not (1 <= timeout <= 300):
157
+ raise ValueError(f"timeout must be in range 1-300, got {timeout}")
158
+
159
+ # Parse nested configs
160
+ retry_data = data.get("retry")
161
+ retry = RetryConfig.from_dict(retry_data) if retry_data else None
162
+
163
+ capture_data = data.get("capture_response")
164
+ capture = CaptureConfig.from_dict(capture_data) if capture_data else None
165
+
166
+ return cls(
167
+ url=url,
168
+ webhook_id=webhook_id,
169
+ method=method,
170
+ headers=data.get("headers", {}),
171
+ payload=data.get("payload"),
172
+ timeout=timeout,
173
+ retry=retry,
174
+ on_success=data.get("on_success"),
175
+ on_failure=data.get("on_failure"),
176
+ capture_response=capture,
177
+ )
178
+
179
+ @staticmethod
180
+ def _validate_url(url: str) -> None:
181
+ """Validate URL scheme is http or https.
182
+
183
+ Args:
184
+ url: URL string to validate.
185
+
186
+ Raises:
187
+ ValueError: If URL scheme is not http or https.
188
+ """
189
+ parsed = urlparse(url)
190
+ if parsed.scheme not in ("http", "https"):
191
+ raise ValueError(
192
+ f"Invalid URL scheme '{parsed.scheme}'. Only http and https are allowed."
193
+ )
194
+
195
+ def to_dict(self) -> dict[str, Any]:
196
+ """Serialize to dict.
197
+
198
+ Returns:
199
+ Dict representation suitable for YAML serialization.
200
+ """
201
+ result: dict[str, Any] = {
202
+ "method": self.method,
203
+ "timeout": self.timeout,
204
+ }
205
+
206
+ if self.url:
207
+ result["url"] = self.url
208
+ if self.webhook_id:
209
+ result["webhook_id"] = self.webhook_id
210
+ if self.headers:
211
+ result["headers"] = self.headers
212
+ if self.payload is not None:
213
+ result["payload"] = self.payload
214
+ if self.retry:
215
+ result["retry"] = self.retry.to_dict()
216
+ if self.on_success:
217
+ result["on_success"] = self.on_success
218
+ if self.on_failure:
219
+ result["on_failure"] = self.on_failure
220
+ if self.capture_response:
221
+ result["capture_response"] = self.capture_response.to_dict()
222
+
223
+ return result