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.
- gobby/__init__.py +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Git utility functions for workflow actions.
|
|
2
|
+
|
|
3
|
+
Extracted from actions.py as part of strangler fig decomposition.
|
|
4
|
+
These are pure utility functions with no ActionContext dependency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess # nosec B404 - subprocess needed for git commands
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_git_status() -> str:
|
|
14
|
+
"""Get git status for current directory.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Short git status output, or error message if not a git repo.
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
21
|
+
["git", "status", "--short"],
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
timeout=5,
|
|
25
|
+
)
|
|
26
|
+
return result.stdout.strip() or "No changes"
|
|
27
|
+
except Exception:
|
|
28
|
+
return "Not a git repository or git not available"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_recent_git_commits(max_commits: int = 10) -> list[dict[str, str]]:
|
|
32
|
+
"""Get recent git commits with hash and message.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
max_commits: Maximum number of commits to return
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of dicts with 'hash' and 'message' keys
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
42
|
+
["git", "log", f"-{max_commits}", "--format=%H|%s"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=5,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
commits = []
|
|
51
|
+
for line in result.stdout.strip().split("\n"):
|
|
52
|
+
if "|" in line:
|
|
53
|
+
hash_part, message = line.split("|", 1)
|
|
54
|
+
commits.append({"hash": hash_part, "message": message})
|
|
55
|
+
return commits
|
|
56
|
+
except Exception:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_file_changes() -> str:
|
|
61
|
+
"""Get detailed file changes from git.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Formatted string with modified/deleted and untracked files.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
# Get changed files with status
|
|
68
|
+
diff_result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
69
|
+
["git", "diff", "HEAD", "--name-status"],
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
timeout=5,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Get untracked files
|
|
76
|
+
untracked_result = subprocess.run( # nosec B603 B607 - hardcoded git command
|
|
77
|
+
["git", "ls-files", "--others", "--exclude-standard"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=5,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Combine results
|
|
84
|
+
changes = []
|
|
85
|
+
if diff_result.stdout.strip():
|
|
86
|
+
changes.append("Modified/Deleted:")
|
|
87
|
+
changes.append(diff_result.stdout.strip())
|
|
88
|
+
|
|
89
|
+
if untracked_result.stdout.strip():
|
|
90
|
+
changes.append("\nUntracked:")
|
|
91
|
+
changes.append(untracked_result.stdout.strip())
|
|
92
|
+
|
|
93
|
+
return "\n".join(changes) if changes else "No changes"
|
|
94
|
+
|
|
95
|
+
except Exception:
|
|
96
|
+
return "Unable to determine file changes"
|
gobby/workflows/hooks.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from gobby.hooks.events import HookEvent, HookResponse
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .engine import WorkflowEngine
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorkflowHookHandler:
|
|
15
|
+
"""
|
|
16
|
+
Integrates WorkflowEngine into the HookManager.
|
|
17
|
+
Wraps the async engine to be callable from synchronous hooks.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
engine: "WorkflowEngine",
|
|
23
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
24
|
+
timeout: float = 30.0, # Timeout for workflow operations in seconds
|
|
25
|
+
enabled: bool = True,
|
|
26
|
+
):
|
|
27
|
+
self.engine = engine
|
|
28
|
+
self._loop = loop
|
|
29
|
+
# Convert 0 to None for asyncio (0 means no timeout)
|
|
30
|
+
self.timeout = timeout if timeout > 0 else None
|
|
31
|
+
self._enabled = enabled
|
|
32
|
+
|
|
33
|
+
# If no loop provided, try to get one or create one for this thread
|
|
34
|
+
if not self._loop:
|
|
35
|
+
try:
|
|
36
|
+
self._loop = asyncio.get_running_loop()
|
|
37
|
+
except RuntimeError:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def handle_all_lifecycles(self, event: HookEvent) -> HookResponse:
|
|
41
|
+
"""
|
|
42
|
+
Handle a hook event by discovering and evaluating all lifecycle workflows.
|
|
43
|
+
|
|
44
|
+
This is the preferred method - it automatically discovers all lifecycle
|
|
45
|
+
workflows and evaluates them in priority order. Replaces the need to
|
|
46
|
+
call handle_lifecycle() with a specific workflow name.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
event: The hook event to handle
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Merged HookResponse from all workflows
|
|
53
|
+
"""
|
|
54
|
+
if not self._enabled:
|
|
55
|
+
return HookResponse(decision="allow")
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if self._loop and self._loop.is_running():
|
|
59
|
+
if threading.current_thread() is threading.main_thread():
|
|
60
|
+
return HookResponse(decision="allow")
|
|
61
|
+
else:
|
|
62
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
63
|
+
self.engine.evaluate_all_lifecycle_workflows(event),
|
|
64
|
+
self._loop,
|
|
65
|
+
)
|
|
66
|
+
return future.result(timeout=self.timeout)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
asyncio.get_running_loop()
|
|
70
|
+
# If we get here, a loop is running
|
|
71
|
+
logger.warning("Could not run workflow engine: Event loop is already running.")
|
|
72
|
+
return HookResponse(decision="allow")
|
|
73
|
+
except RuntimeError:
|
|
74
|
+
# No loop running, safe to use asyncio.run
|
|
75
|
+
return asyncio.run(self.engine.evaluate_all_lifecycle_workflows(event))
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Error handling all lifecycle workflows: {e}", exc_info=True)
|
|
79
|
+
return HookResponse(decision="allow")
|
|
80
|
+
|
|
81
|
+
def handle(self, event: HookEvent) -> HookResponse:
|
|
82
|
+
"""
|
|
83
|
+
Handle a hook event by delegating to the workflow engine.
|
|
84
|
+
Handles the sync/async bridge.
|
|
85
|
+
"""
|
|
86
|
+
if not self._enabled:
|
|
87
|
+
return HookResponse(decision="allow")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# We need to run the async self.engine.handle_event(event) synchronously
|
|
91
|
+
|
|
92
|
+
# Case 1: We have a captured loop (main loop) and we are likely in a thread
|
|
93
|
+
# This is the common case for FastAPI sync endpoints
|
|
94
|
+
if self._loop and self._loop.is_running():
|
|
95
|
+
if threading.current_thread() is threading.main_thread():
|
|
96
|
+
# We are on the main thread and the loop is running.
|
|
97
|
+
# We cannot block here without deadlock if we use run_until_complete.
|
|
98
|
+
# But HookManager.handle is synchronous, so this is a tricky spot.
|
|
99
|
+
# Ideally, HookManager should await, but it's not async.
|
|
100
|
+
# For now, we return allow and log a warning if we can't run.
|
|
101
|
+
# OR we create a task and return allow (fire and forget), but we need the result.
|
|
102
|
+
|
|
103
|
+
# Actually, if we are here, we are blocking the event loop!
|
|
104
|
+
# This implementation assumes HookManager.handle is run in a threadpool (def handle vs async def handle).
|
|
105
|
+
# Pydantic/FastAPI runs sync def routes in threadpool.
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
# We are in a thread, loop is in another thread.
|
|
109
|
+
# Safe to block this thread waiting for loop.
|
|
110
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
111
|
+
self.engine.handle_event(event), self._loop
|
|
112
|
+
)
|
|
113
|
+
return future.result(timeout=self.timeout)
|
|
114
|
+
|
|
115
|
+
# Case 2: No loop running, or we just want to run it.
|
|
116
|
+
# Create a new loop or use asyncio.run if appropriate
|
|
117
|
+
try:
|
|
118
|
+
asyncio.get_running_loop()
|
|
119
|
+
# If we get here, a loop is running
|
|
120
|
+
logger.warning(
|
|
121
|
+
"Could not run workflow engine: Event loop is already running and we are blocking it."
|
|
122
|
+
)
|
|
123
|
+
return HookResponse(decision="allow")
|
|
124
|
+
except RuntimeError:
|
|
125
|
+
# No loop running, safe to use asyncio.run
|
|
126
|
+
return asyncio.run(self.engine.handle_event(event))
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Error handling workflow hook: {e}", exc_info=True)
|
|
130
|
+
return HookResponse(decision="allow")
|
|
131
|
+
|
|
132
|
+
def handle_lifecycle(
|
|
133
|
+
self, workflow_name: str, event: HookEvent, context_data: dict[str, Any] | None = None
|
|
134
|
+
) -> HookResponse:
|
|
135
|
+
"""
|
|
136
|
+
Handle a lifecycle workflow event.
|
|
137
|
+
"""
|
|
138
|
+
if not self._enabled:
|
|
139
|
+
return HookResponse(decision="allow")
|
|
140
|
+
|
|
141
|
+
logger.debug(
|
|
142
|
+
f"handle_lifecycle called: workflow={workflow_name}, event_type={event.event_type}"
|
|
143
|
+
)
|
|
144
|
+
try:
|
|
145
|
+
if self._loop and self._loop.is_running():
|
|
146
|
+
if threading.current_thread() is threading.main_thread():
|
|
147
|
+
# See comment in handle() about blocking main thread loop
|
|
148
|
+
return HookResponse(decision="allow")
|
|
149
|
+
else:
|
|
150
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
151
|
+
self.engine.evaluate_lifecycle_triggers(workflow_name, event, context_data),
|
|
152
|
+
self._loop,
|
|
153
|
+
)
|
|
154
|
+
return future.result(timeout=self.timeout)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
asyncio.get_running_loop()
|
|
158
|
+
# If we get here, a loop is running
|
|
159
|
+
logger.warning("Could not run workflow engine: Event loop is already running.")
|
|
160
|
+
return HookResponse(decision="allow")
|
|
161
|
+
except RuntimeError:
|
|
162
|
+
# No loop running, safe to use asyncio.run
|
|
163
|
+
return asyncio.run(
|
|
164
|
+
self.engine.evaluate_lifecycle_triggers(workflow_name, event, context_data)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Error handling lifecycle workflow: {e}", exc_info=True)
|
|
169
|
+
return HookResponse(decision="allow")
|