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
gobby/sync/github.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""GitHub sync service that orchestrates between gobby tasks and GitHub.
|
|
2
|
+
|
|
3
|
+
This service delegates all GitHub operations to the official GitHub MCP server
|
|
4
|
+
(@modelcontextprotocol/server-github), avoiding custom API client code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
from gobby.integrations.github import GitHubIntegration
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.mcp_proxy.manager import MCPClientManager
|
|
16
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"GitHubSyncService",
|
|
20
|
+
"GitHubSyncError",
|
|
21
|
+
"GitHubRateLimitError",
|
|
22
|
+
"GitHubNotFoundError",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GitHubSyncError(Exception):
|
|
29
|
+
"""Base exception for GitHub sync errors."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GitHubRateLimitError(GitHubSyncError):
|
|
35
|
+
"""Raised when GitHub API rate limit is exceeded.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
reset_at: Unix timestamp when rate limit resets.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, reset_at: int | None = None) -> None:
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.reset_at = reset_at
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GitHubNotFoundError(GitHubSyncError):
|
|
47
|
+
"""Raised when a GitHub resource is not found.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
resource: Type of resource (e.g., "issue", "repo", "pr").
|
|
51
|
+
resource_id: Identifier of the missing resource.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
resource: str | None = None,
|
|
58
|
+
resource_id: int | str | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
self.resource = resource
|
|
62
|
+
self.resource_id = resource_id
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class GitHubSyncService:
|
|
66
|
+
"""Service for syncing gobby tasks with GitHub issues and PRs.
|
|
67
|
+
|
|
68
|
+
This service orchestrates bidirectional sync between gobby tasks and GitHub:
|
|
69
|
+
- Import GitHub issues as gobby tasks
|
|
70
|
+
- Sync task updates back to GitHub issues
|
|
71
|
+
- Create PRs from completed tasks
|
|
72
|
+
|
|
73
|
+
All GitHub operations are delegated to the official GitHub MCP server.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
mcp_manager: MCPClientManager for accessing GitHub MCP server.
|
|
77
|
+
task_manager: LocalTaskManager for gobby task CRUD.
|
|
78
|
+
project_id: Gobby project ID for creating tasks.
|
|
79
|
+
github_repo: Default GitHub repo in "owner/repo" format.
|
|
80
|
+
github: GitHubIntegration instance for availability checks.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
mcp_manager: MCPClientManager,
|
|
86
|
+
task_manager: LocalTaskManager,
|
|
87
|
+
project_id: str,
|
|
88
|
+
github_repo: str | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Initialize GitHubSyncService.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
mcp_manager: MCPClientManager for GitHub MCP server access.
|
|
94
|
+
task_manager: LocalTaskManager for gobby task operations.
|
|
95
|
+
project_id: Gobby project ID for creating tasks.
|
|
96
|
+
github_repo: Default GitHub repo in "owner/repo" format.
|
|
97
|
+
"""
|
|
98
|
+
self.mcp_manager = mcp_manager
|
|
99
|
+
self.task_manager = task_manager
|
|
100
|
+
self.project_id = project_id
|
|
101
|
+
self.github_repo = github_repo
|
|
102
|
+
self.github = GitHubIntegration(mcp_manager)
|
|
103
|
+
|
|
104
|
+
def is_available(self) -> bool:
|
|
105
|
+
"""Check if GitHub MCP server is available.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if GitHub MCP server is available, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
return self.github.is_available()
|
|
111
|
+
|
|
112
|
+
async def import_github_issues(
|
|
113
|
+
self,
|
|
114
|
+
repo: str,
|
|
115
|
+
labels: list[str] | None = None,
|
|
116
|
+
state: str = "open",
|
|
117
|
+
) -> list[dict[str, Any]]:
|
|
118
|
+
"""Import GitHub issues as gobby tasks.
|
|
119
|
+
|
|
120
|
+
Fetches issues from GitHub via the MCP server and creates corresponding
|
|
121
|
+
gobby tasks with linked github_issue_number and github_repo fields.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
repo: GitHub repo in "owner/repo" format.
|
|
125
|
+
labels: Optional list of labels to filter issues.
|
|
126
|
+
state: Issue state to filter ("open", "closed", "all").
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of created task dictionaries.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
RuntimeError: If GitHub MCP server is unavailable.
|
|
133
|
+
"""
|
|
134
|
+
self.github.require_available()
|
|
135
|
+
|
|
136
|
+
# Call GitHub MCP to list issues
|
|
137
|
+
args: dict[str, Any] = {"owner": repo.split("/")[0], "repo": repo.split("/")[1]}
|
|
138
|
+
if labels:
|
|
139
|
+
args["labels"] = ",".join(labels)
|
|
140
|
+
if state:
|
|
141
|
+
args["state"] = state
|
|
142
|
+
|
|
143
|
+
result = await self.mcp_manager.call_tool(
|
|
144
|
+
server_name="github",
|
|
145
|
+
tool_name="list_issues",
|
|
146
|
+
arguments=args,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
issues = result.get("issues", [])
|
|
150
|
+
created_tasks = []
|
|
151
|
+
|
|
152
|
+
for issue in issues:
|
|
153
|
+
# Create gobby task linked to GitHub issue
|
|
154
|
+
task = self.task_manager.create_task(
|
|
155
|
+
project_id=self.project_id,
|
|
156
|
+
title=issue.get("title", "Untitled Issue"),
|
|
157
|
+
description=issue.get("body", ""),
|
|
158
|
+
github_issue_number=issue.get("number"),
|
|
159
|
+
github_repo=repo,
|
|
160
|
+
)
|
|
161
|
+
created_tasks.append(task.to_dict())
|
|
162
|
+
|
|
163
|
+
logger.info(f"Imported {len(created_tasks)} issues from {repo}")
|
|
164
|
+
return created_tasks
|
|
165
|
+
|
|
166
|
+
async def sync_task_to_github(self, task_id: str) -> dict[str, Any]:
|
|
167
|
+
"""Sync a gobby task to its linked GitHub issue.
|
|
168
|
+
|
|
169
|
+
Updates the GitHub issue title and body to match the task.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
task_id: ID of the task to sync.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Result from GitHub MCP update_issue call.
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
RuntimeError: If GitHub MCP server is unavailable.
|
|
179
|
+
ValueError: If task has no linked GitHub issue.
|
|
180
|
+
"""
|
|
181
|
+
self.github.require_available()
|
|
182
|
+
|
|
183
|
+
task = self.task_manager.get_task(task_id)
|
|
184
|
+
|
|
185
|
+
if not task.github_issue_number:
|
|
186
|
+
raise ValueError(
|
|
187
|
+
f"Task {task_id} has no linked GitHub issue. Set github_issue_number to sync."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
repo = task.github_repo or self.github_repo
|
|
191
|
+
if not repo:
|
|
192
|
+
raise ValueError(
|
|
193
|
+
f"Task {task_id} has no github_repo set and no default repo configured."
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
owner, repo_name = repo.split("/")
|
|
197
|
+
|
|
198
|
+
result = await self.mcp_manager.call_tool(
|
|
199
|
+
server_name="github",
|
|
200
|
+
tool_name="update_issue",
|
|
201
|
+
arguments={
|
|
202
|
+
"owner": owner,
|
|
203
|
+
"repo": repo_name,
|
|
204
|
+
"issue_number": task.github_issue_number,
|
|
205
|
+
"title": task.title,
|
|
206
|
+
"body": task.description or "",
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Validate response
|
|
211
|
+
if result is None or not isinstance(result, dict):
|
|
212
|
+
raise GitHubSyncError(
|
|
213
|
+
f"Invalid response from GitHub MCP when updating issue "
|
|
214
|
+
f"#{task.github_issue_number}: expected dict, got {type(result).__name__}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
logger.info(f"Synced task {task_id} to GitHub issue #{task.github_issue_number}")
|
|
218
|
+
return cast(dict[str, Any], result)
|
|
219
|
+
|
|
220
|
+
async def create_pr_for_task(
|
|
221
|
+
self,
|
|
222
|
+
task_id: str,
|
|
223
|
+
head_branch: str,
|
|
224
|
+
base_branch: str = "main",
|
|
225
|
+
draft: bool = False,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Create a GitHub PR for a task.
|
|
228
|
+
|
|
229
|
+
Creates a pull request on GitHub and links it to the task.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
task_id: ID of the task to create PR for.
|
|
233
|
+
head_branch: Branch containing the changes.
|
|
234
|
+
base_branch: Branch to merge into (default: "main").
|
|
235
|
+
draft: Whether to create as draft PR.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Result from GitHub MCP create_pull_request call.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
RuntimeError: If GitHub MCP server is unavailable.
|
|
242
|
+
"""
|
|
243
|
+
self.github.require_available()
|
|
244
|
+
|
|
245
|
+
task = self.task_manager.get_task(task_id)
|
|
246
|
+
|
|
247
|
+
repo = task.github_repo or self.github_repo
|
|
248
|
+
if not repo:
|
|
249
|
+
raise ValueError(
|
|
250
|
+
f"Task {task_id} has no github_repo set and no default repo configured."
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
owner, repo_name = repo.split("/")
|
|
254
|
+
|
|
255
|
+
# Create PR via GitHub MCP
|
|
256
|
+
result = await self.mcp_manager.call_tool(
|
|
257
|
+
server_name="github",
|
|
258
|
+
tool_name="create_pull_request",
|
|
259
|
+
arguments={
|
|
260
|
+
"owner": owner,
|
|
261
|
+
"repo": repo_name,
|
|
262
|
+
"title": task.title,
|
|
263
|
+
"body": task.description or "",
|
|
264
|
+
"head": head_branch,
|
|
265
|
+
"base": base_branch,
|
|
266
|
+
"draft": draft,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Update task with PR number if available
|
|
271
|
+
result_dict = cast(dict[str, Any], result)
|
|
272
|
+
pr_number = result_dict.get("number")
|
|
273
|
+
if pr_number:
|
|
274
|
+
self.task_manager.update_task(
|
|
275
|
+
task_id,
|
|
276
|
+
github_pr_number=pr_number,
|
|
277
|
+
github_repo=repo,
|
|
278
|
+
)
|
|
279
|
+
logger.info(f"Created PR #{pr_number} for task {task_id}")
|
|
280
|
+
|
|
281
|
+
return result_dict
|
|
282
|
+
|
|
283
|
+
def map_gobby_labels_to_github(
|
|
284
|
+
self,
|
|
285
|
+
gobby_labels: list[str],
|
|
286
|
+
prefix: str = "",
|
|
287
|
+
) -> list[str]:
|
|
288
|
+
"""Map gobby labels to GitHub label format.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
gobby_labels: List of gobby label strings.
|
|
292
|
+
prefix: Optional prefix to add to each label.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
List of GitHub-formatted labels.
|
|
296
|
+
"""
|
|
297
|
+
if not gobby_labels:
|
|
298
|
+
return []
|
|
299
|
+
|
|
300
|
+
github_labels = []
|
|
301
|
+
for label in gobby_labels:
|
|
302
|
+
if prefix:
|
|
303
|
+
github_labels.append(f"{prefix}{label}")
|
|
304
|
+
else:
|
|
305
|
+
github_labels.append(label)
|
|
306
|
+
|
|
307
|
+
return github_labels
|
|
308
|
+
|
|
309
|
+
def map_github_labels_to_gobby(
|
|
310
|
+
self,
|
|
311
|
+
github_labels: list[str],
|
|
312
|
+
strip_prefix: str = "",
|
|
313
|
+
) -> list[str]:
|
|
314
|
+
"""Map GitHub labels to gobby label format.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
github_labels: List of GitHub label strings.
|
|
318
|
+
strip_prefix: Optional prefix to strip from each label.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
List of gobby-formatted labels.
|
|
322
|
+
"""
|
|
323
|
+
if not github_labels:
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
gobby_labels = []
|
|
327
|
+
for label in github_labels:
|
|
328
|
+
if strip_prefix and label.startswith(strip_prefix):
|
|
329
|
+
gobby_labels.append(label[len(strip_prefix) :])
|
|
330
|
+
else:
|
|
331
|
+
gobby_labels.append(label)
|
|
332
|
+
|
|
333
|
+
return gobby_labels
|
gobby/sync/linear.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Linear sync service that orchestrates between gobby tasks and Linear.
|
|
2
|
+
|
|
3
|
+
This service delegates all Linear operations to the official Linear MCP server,
|
|
4
|
+
avoiding custom API client code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
11
|
+
|
|
12
|
+
from gobby.integrations.linear import LinearIntegration
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.mcp_proxy.manager import MCPClientManager
|
|
16
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"LinearSyncService",
|
|
20
|
+
"LinearSyncError",
|
|
21
|
+
"LinearRateLimitError",
|
|
22
|
+
"LinearNotFoundError",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LinearSyncError(Exception):
|
|
29
|
+
"""Base exception for Linear sync errors."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LinearRateLimitError(LinearSyncError):
|
|
35
|
+
"""Raised when Linear API rate limit is exceeded.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
reset_at: Unix timestamp when rate limit resets.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str, reset_at: int | None = None) -> None:
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.reset_at = reset_at
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LinearNotFoundError(LinearSyncError):
|
|
47
|
+
"""Raised when a Linear resource is not found.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
resource: Type of resource (e.g., "issue", "team", "project").
|
|
51
|
+
resource_id: Identifier of the missing resource.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
message: str,
|
|
57
|
+
resource: str | None = None,
|
|
58
|
+
resource_id: str | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
self.resource = resource
|
|
62
|
+
self.resource_id = resource_id
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LinearSyncService:
|
|
66
|
+
"""Service for syncing gobby tasks with Linear issues.
|
|
67
|
+
|
|
68
|
+
This service orchestrates bidirectional sync between gobby tasks and Linear:
|
|
69
|
+
- Import Linear issues as gobby tasks
|
|
70
|
+
- Sync task updates back to Linear issues
|
|
71
|
+
- Create new issues from gobby tasks
|
|
72
|
+
|
|
73
|
+
All Linear operations are delegated to the official Linear MCP server.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
mcp_manager: MCPClientManager for accessing Linear MCP server.
|
|
77
|
+
task_manager: LocalTaskManager for gobby task CRUD.
|
|
78
|
+
project_id: Gobby project ID for creating tasks.
|
|
79
|
+
linear_team_id: Default Linear team ID for creating issues.
|
|
80
|
+
linear: LinearIntegration instance for availability checks.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
mcp_manager: MCPClientManager,
|
|
86
|
+
task_manager: LocalTaskManager,
|
|
87
|
+
project_id: str,
|
|
88
|
+
linear_team_id: str | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Initialize LinearSyncService.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
mcp_manager: MCPClientManager for Linear MCP server access.
|
|
94
|
+
task_manager: LocalTaskManager for gobby task operations.
|
|
95
|
+
project_id: Gobby project ID for creating tasks.
|
|
96
|
+
linear_team_id: Default Linear team ID for creating issues.
|
|
97
|
+
"""
|
|
98
|
+
self.mcp_manager = mcp_manager
|
|
99
|
+
self.task_manager = task_manager
|
|
100
|
+
self.project_id = project_id
|
|
101
|
+
self.linear_team_id = linear_team_id
|
|
102
|
+
self.linear = LinearIntegration(mcp_manager)
|
|
103
|
+
|
|
104
|
+
def is_available(self) -> bool:
|
|
105
|
+
"""Check if Linear MCP server is available.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if Linear MCP server is available, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
return self.linear.is_available()
|
|
111
|
+
|
|
112
|
+
async def import_linear_issues(
|
|
113
|
+
self,
|
|
114
|
+
team_id: str | None = None,
|
|
115
|
+
state: str | None = None,
|
|
116
|
+
labels: list[str] | None = None,
|
|
117
|
+
) -> list[dict[str, Any]]:
|
|
118
|
+
"""Import Linear issues as gobby tasks.
|
|
119
|
+
|
|
120
|
+
Fetches issues from Linear via the MCP server and creates corresponding
|
|
121
|
+
gobby tasks with linked linear_issue_id and linear_team_id fields.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
team_id: Linear team ID to filter issues. Uses default if not provided.
|
|
125
|
+
state: Issue state to filter (e.g., "In Progress", "Todo").
|
|
126
|
+
labels: Optional list of labels to filter issues.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of created task dictionaries.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
RuntimeError: If Linear MCP server is unavailable.
|
|
133
|
+
ValueError: If no team_id is provided and no default configured.
|
|
134
|
+
"""
|
|
135
|
+
self.linear.require_available()
|
|
136
|
+
|
|
137
|
+
effective_team_id = team_id or self.linear_team_id
|
|
138
|
+
if not effective_team_id:
|
|
139
|
+
raise ValueError("No team_id provided and no default linear_team_id configured.")
|
|
140
|
+
|
|
141
|
+
# Build filter arguments for Linear MCP
|
|
142
|
+
args: dict[str, Any] = {"teamId": effective_team_id}
|
|
143
|
+
if state:
|
|
144
|
+
args["state"] = state
|
|
145
|
+
if labels:
|
|
146
|
+
args["labels"] = labels
|
|
147
|
+
|
|
148
|
+
result = await self.mcp_manager.call_tool(
|
|
149
|
+
server_name="linear",
|
|
150
|
+
tool_name="list_issues",
|
|
151
|
+
arguments=args,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
issues = result.get("issues", [])
|
|
155
|
+
created_tasks = []
|
|
156
|
+
|
|
157
|
+
for issue in issues:
|
|
158
|
+
# Create gobby task linked to Linear issue
|
|
159
|
+
task = self.task_manager.create_task(
|
|
160
|
+
project_id=self.project_id,
|
|
161
|
+
title=issue.get("title", "Untitled Issue"),
|
|
162
|
+
description=issue.get("description", ""),
|
|
163
|
+
linear_issue_id=issue.get("id"),
|
|
164
|
+
linear_team_id=effective_team_id,
|
|
165
|
+
)
|
|
166
|
+
created_tasks.append(task.to_dict())
|
|
167
|
+
|
|
168
|
+
logger.info(f"Imported {len(created_tasks)} issues from Linear team {effective_team_id}")
|
|
169
|
+
return created_tasks
|
|
170
|
+
|
|
171
|
+
async def sync_task_to_linear(self, task_id: str) -> dict[str, Any]:
|
|
172
|
+
"""Sync a gobby task to its linked Linear issue.
|
|
173
|
+
|
|
174
|
+
Updates the Linear issue title and description to match the task.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
task_id: ID of the task to sync.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Result from Linear MCP update_issue call.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: If Linear MCP server is unavailable.
|
|
184
|
+
ValueError: If task has no linked Linear issue.
|
|
185
|
+
"""
|
|
186
|
+
self.linear.require_available()
|
|
187
|
+
|
|
188
|
+
task = self.task_manager.get_task(task_id)
|
|
189
|
+
|
|
190
|
+
if not task.linear_issue_id:
|
|
191
|
+
raise ValueError(
|
|
192
|
+
f"Task {task_id} has no linked Linear issue. Set linear_issue_id to sync."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
result = await self.mcp_manager.call_tool(
|
|
196
|
+
server_name="linear",
|
|
197
|
+
tool_name="update_issue",
|
|
198
|
+
arguments={
|
|
199
|
+
"issueId": task.linear_issue_id,
|
|
200
|
+
"title": task.title,
|
|
201
|
+
"description": task.description or "",
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Validate response
|
|
206
|
+
if result is None or not isinstance(result, dict):
|
|
207
|
+
raise LinearSyncError(
|
|
208
|
+
f"Invalid response from Linear MCP when updating issue "
|
|
209
|
+
f"{task.linear_issue_id}: expected dict, got {type(result).__name__}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.info(f"Synced task {task_id} to Linear issue {task.linear_issue_id}")
|
|
213
|
+
return cast(dict[str, Any], result)
|
|
214
|
+
|
|
215
|
+
async def create_issue_for_task(
|
|
216
|
+
self,
|
|
217
|
+
task_id: str,
|
|
218
|
+
team_id: str | None = None,
|
|
219
|
+
) -> dict[str, Any]:
|
|
220
|
+
"""Create a Linear issue from a gobby task.
|
|
221
|
+
|
|
222
|
+
Creates an issue on Linear and links it to the task.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
task_id: ID of the task to create issue for.
|
|
226
|
+
team_id: Linear team ID for the issue. Uses default if not provided.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Result from Linear MCP create_issue call.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
RuntimeError: If Linear MCP server is unavailable.
|
|
233
|
+
ValueError: If no team_id is provided and no default configured.
|
|
234
|
+
"""
|
|
235
|
+
self.linear.require_available()
|
|
236
|
+
|
|
237
|
+
task = self.task_manager.get_task(task_id)
|
|
238
|
+
|
|
239
|
+
effective_team_id = team_id or task.linear_team_id or self.linear_team_id
|
|
240
|
+
if not effective_team_id:
|
|
241
|
+
raise ValueError(f"Task {task_id} has no linear_team_id set and no default configured.")
|
|
242
|
+
|
|
243
|
+
# Create issue via Linear MCP
|
|
244
|
+
result = await self.mcp_manager.call_tool(
|
|
245
|
+
server_name="linear",
|
|
246
|
+
tool_name="create_issue",
|
|
247
|
+
arguments={
|
|
248
|
+
"teamId": effective_team_id,
|
|
249
|
+
"title": task.title,
|
|
250
|
+
"description": task.description or "",
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Update task with Linear issue ID if available
|
|
255
|
+
result_dict = cast(dict[str, Any], result)
|
|
256
|
+
issue_id = result_dict.get("id")
|
|
257
|
+
if issue_id:
|
|
258
|
+
self.task_manager.update_task(
|
|
259
|
+
task_id,
|
|
260
|
+
linear_issue_id=issue_id,
|
|
261
|
+
linear_team_id=effective_team_id,
|
|
262
|
+
)
|
|
263
|
+
logger.info(f"Created Linear issue {issue_id} for task {task_id}")
|
|
264
|
+
|
|
265
|
+
return result_dict
|
|
266
|
+
|
|
267
|
+
def map_gobby_status_to_linear(self, gobby_status: str) -> str:
|
|
268
|
+
"""Map gobby task status to Linear issue state.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
gobby_status: Gobby task status.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Linear issue state name.
|
|
275
|
+
"""
|
|
276
|
+
status_map = {
|
|
277
|
+
"open": "Todo",
|
|
278
|
+
"in_progress": "In Progress",
|
|
279
|
+
"closed": "Done",
|
|
280
|
+
"failed": "Canceled",
|
|
281
|
+
"escalated": "In Review",
|
|
282
|
+
"needs_decomposition": "Backlog",
|
|
283
|
+
}
|
|
284
|
+
return status_map.get(gobby_status, "Todo")
|
|
285
|
+
|
|
286
|
+
def map_linear_status_to_gobby(self, linear_state: str) -> str:
|
|
287
|
+
"""Map Linear issue state to gobby task status.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
linear_state: Linear issue state name.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Gobby task status.
|
|
294
|
+
"""
|
|
295
|
+
state_map = {
|
|
296
|
+
"Todo": "open",
|
|
297
|
+
"In Progress": "in_progress",
|
|
298
|
+
"Done": "closed",
|
|
299
|
+
"Canceled": "closed",
|
|
300
|
+
"In Review": "in_progress",
|
|
301
|
+
"Backlog": "open",
|
|
302
|
+
"Triage": "open",
|
|
303
|
+
}
|
|
304
|
+
return state_map.get(linear_state, "open")
|