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,517 @@
|
|
|
1
|
+
"""Lifecycle operations for task management.
|
|
2
|
+
|
|
3
|
+
Provides task lifecycle tools: close, reopen, delete, and label management.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
9
|
+
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
10
|
+
from gobby.mcp_proxy.tools.tasks._helpers import SKIP_REASONS
|
|
11
|
+
from gobby.mcp_proxy.tools.tasks._lifecycle_validation import (
|
|
12
|
+
determine_close_outcome,
|
|
13
|
+
gather_validation_context,
|
|
14
|
+
validate_commit_requirements,
|
|
15
|
+
validate_leaf_task_with_llm,
|
|
16
|
+
validate_parent_task,
|
|
17
|
+
)
|
|
18
|
+
from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
|
|
19
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
20
|
+
from gobby.storage.worktrees import LocalWorktreeManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
24
|
+
"""Create a registry with task lifecycle tools.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ctx: Shared registry context
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
InternalToolRegistry with lifecycle tools registered
|
|
31
|
+
"""
|
|
32
|
+
registry = InternalToolRegistry(
|
|
33
|
+
name="gobby-tasks-lifecycle",
|
|
34
|
+
description="Task lifecycle operations",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
async def close_task(
|
|
38
|
+
task_id: str,
|
|
39
|
+
reason: str = "completed",
|
|
40
|
+
changes_summary: str | None = None,
|
|
41
|
+
skip_validation: bool = False,
|
|
42
|
+
session_id: str | None = None,
|
|
43
|
+
override_justification: str | None = None,
|
|
44
|
+
no_commit_needed: bool = False,
|
|
45
|
+
commit_sha: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Close a task with validation.
|
|
48
|
+
|
|
49
|
+
For parent tasks: automatically checks all children are closed.
|
|
50
|
+
For leaf tasks: optionally validates with LLM if changes_summary provided.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
task_id: Task reference (#N, path, or UUID)
|
|
54
|
+
reason: Reason for closing
|
|
55
|
+
changes_summary: Summary of changes (enables LLM validation for leaf tasks)
|
|
56
|
+
skip_validation: Skip all validation checks
|
|
57
|
+
session_id: Session ID where task is being closed (auto-links to session)
|
|
58
|
+
override_justification: Why agent bypassed validation (stored for audit).
|
|
59
|
+
Also used to explain why no commit was needed when no_commit_needed=True.
|
|
60
|
+
no_commit_needed: Set to True for tasks that don't produce code changes
|
|
61
|
+
(research, planning, documentation review). Requires override_justification.
|
|
62
|
+
commit_sha: Git commit SHA to link before closing. Convenience for link + close in one call.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Closed task or error with validation feedback
|
|
66
|
+
"""
|
|
67
|
+
# Resolve task reference (supports #N, path, UUID formats)
|
|
68
|
+
try:
|
|
69
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
70
|
+
except TaskNotFoundError as e:
|
|
71
|
+
return {"error": str(e)}
|
|
72
|
+
except ValueError as e:
|
|
73
|
+
return {"error": str(e)}
|
|
74
|
+
|
|
75
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
76
|
+
if not task:
|
|
77
|
+
return {"error": f"Task {task_id} not found"}
|
|
78
|
+
|
|
79
|
+
# Link commit if provided (convenience for link + close in one call)
|
|
80
|
+
if commit_sha:
|
|
81
|
+
task = ctx.task_manager.link_commit(resolved_id, commit_sha)
|
|
82
|
+
|
|
83
|
+
# Get project repo_path for git commands
|
|
84
|
+
repo_path = ctx.get_project_repo_path(task.project_id)
|
|
85
|
+
cwd = repo_path or "."
|
|
86
|
+
|
|
87
|
+
# Check for linked commits (unless task type doesn't require commits)
|
|
88
|
+
commit_result = validate_commit_requirements(
|
|
89
|
+
task, reason, no_commit_needed, override_justification
|
|
90
|
+
)
|
|
91
|
+
if not commit_result.can_close:
|
|
92
|
+
return {
|
|
93
|
+
"error": commit_result.error_type,
|
|
94
|
+
"message": commit_result.message,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Auto-skip validation for certain close reasons
|
|
98
|
+
should_skip = skip_validation or reason.lower() in SKIP_REASONS
|
|
99
|
+
|
|
100
|
+
if not should_skip:
|
|
101
|
+
# Check if task has children (is a parent task)
|
|
102
|
+
parent_result = validate_parent_task(ctx, resolved_id)
|
|
103
|
+
if not parent_result.can_close:
|
|
104
|
+
response = {
|
|
105
|
+
"error": parent_result.error_type,
|
|
106
|
+
"message": parent_result.message,
|
|
107
|
+
}
|
|
108
|
+
if parent_result.extra:
|
|
109
|
+
response.update(parent_result.extra)
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
# Check for leaf task with validation criteria
|
|
113
|
+
children = ctx.task_manager.list_tasks(parent_task_id=resolved_id, limit=1)
|
|
114
|
+
is_leaf = len(children) == 0
|
|
115
|
+
|
|
116
|
+
if is_leaf and ctx.task_validator and task.validation_criteria:
|
|
117
|
+
# Gather validation context
|
|
118
|
+
validation_context, raw_diff = gather_validation_context(
|
|
119
|
+
task, changes_summary, repo_path, ctx.task_manager
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if validation_context:
|
|
123
|
+
# Run LLM validation
|
|
124
|
+
llm_result = await validate_leaf_task_with_llm(
|
|
125
|
+
task=task,
|
|
126
|
+
task_validator=ctx.task_validator,
|
|
127
|
+
validation_context=validation_context,
|
|
128
|
+
raw_diff=raw_diff,
|
|
129
|
+
ctx=ctx,
|
|
130
|
+
resolved_id=resolved_id,
|
|
131
|
+
validation_config=ctx.validation_config,
|
|
132
|
+
)
|
|
133
|
+
if not llm_result.can_close:
|
|
134
|
+
response = {
|
|
135
|
+
"error": llm_result.error_type,
|
|
136
|
+
"message": llm_result.message,
|
|
137
|
+
}
|
|
138
|
+
if llm_result.extra:
|
|
139
|
+
response.update(llm_result.extra)
|
|
140
|
+
return response
|
|
141
|
+
|
|
142
|
+
# Determine close outcome
|
|
143
|
+
route_to_review, store_override = determine_close_outcome(
|
|
144
|
+
task, skip_validation, no_commit_needed, override_justification
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Get git commit SHA (best-effort, dynamic short format for consistency)
|
|
148
|
+
from gobby.utils.git import run_git_command
|
|
149
|
+
|
|
150
|
+
current_commit_sha = run_git_command(["git", "rev-parse", "--short", "HEAD"], cwd=cwd)
|
|
151
|
+
|
|
152
|
+
if route_to_review:
|
|
153
|
+
# Route to review status instead of closing
|
|
154
|
+
# Task stays in review until user explicitly closes
|
|
155
|
+
ctx.task_manager.update_task(
|
|
156
|
+
resolved_id,
|
|
157
|
+
status="review",
|
|
158
|
+
validation_override_reason=override_justification if store_override else None,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Auto-link session if provided
|
|
162
|
+
if session_id:
|
|
163
|
+
try:
|
|
164
|
+
ctx.session_task_manager.link_task(session_id, resolved_id, "review")
|
|
165
|
+
except Exception:
|
|
166
|
+
pass # nosec B110 - best-effort linking
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"routed_to_review": True,
|
|
170
|
+
"message": (
|
|
171
|
+
"Task routed to review status. "
|
|
172
|
+
+ (
|
|
173
|
+
"Reason: requires user review before closing."
|
|
174
|
+
if task.requires_user_review
|
|
175
|
+
else "Reason: validation was overridden, human review recommended."
|
|
176
|
+
)
|
|
177
|
+
),
|
|
178
|
+
"task_id": resolved_id,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# All checks passed - close the task with session and commit tracking
|
|
182
|
+
ctx.task_manager.close_task(
|
|
183
|
+
resolved_id,
|
|
184
|
+
reason=reason,
|
|
185
|
+
closed_in_session_id=session_id,
|
|
186
|
+
closed_commit_sha=current_commit_sha,
|
|
187
|
+
validation_override_reason=override_justification if store_override else None,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Auto-link session if provided
|
|
191
|
+
if session_id:
|
|
192
|
+
try:
|
|
193
|
+
ctx.session_task_manager.link_task(session_id, resolved_id, "closed")
|
|
194
|
+
except Exception:
|
|
195
|
+
pass # nosec B110 - best-effort linking, don't fail the close
|
|
196
|
+
|
|
197
|
+
# Update worktree status based on closure reason (case-insensitive)
|
|
198
|
+
try:
|
|
199
|
+
reason_normalized = reason.lower()
|
|
200
|
+
worktree_manager = LocalWorktreeManager(ctx.task_manager.db)
|
|
201
|
+
wt = worktree_manager.get_by_task(resolved_id)
|
|
202
|
+
if wt:
|
|
203
|
+
if reason_normalized in (
|
|
204
|
+
"wont_fix",
|
|
205
|
+
"obsolete",
|
|
206
|
+
"duplicate",
|
|
207
|
+
"already_implemented",
|
|
208
|
+
):
|
|
209
|
+
worktree_manager.mark_abandoned(wt.id)
|
|
210
|
+
elif reason_normalized == "completed":
|
|
211
|
+
worktree_manager.mark_merged(wt.id)
|
|
212
|
+
except Exception:
|
|
213
|
+
pass # nosec B110 - best-effort worktree update, don't fail the close
|
|
214
|
+
|
|
215
|
+
return {}
|
|
216
|
+
|
|
217
|
+
registry.register(
|
|
218
|
+
name="close_task",
|
|
219
|
+
description="Close a task. Commits should use [#N] format (e.g., git commit -m '[#42] feat: add feature'). Requires commits to be linked (auto-detected from commit message or use link_commit). Parent tasks require all children closed. Leaf tasks validate with LLM. Validation auto-skipped for: duplicate, already_implemented, wont_fix, obsolete.",
|
|
220
|
+
input_schema={
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": {
|
|
223
|
+
"task_id": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
226
|
+
},
|
|
227
|
+
"reason": {
|
|
228
|
+
"type": "string",
|
|
229
|
+
"description": 'Reason for closing. Use "duplicate", "already_implemented", "wont_fix", or "obsolete" to auto-skip validation and commit check.',
|
|
230
|
+
"default": "completed",
|
|
231
|
+
},
|
|
232
|
+
"changes_summary": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"description": "Summary of changes made. If provided for leaf tasks, triggers LLM validation before close.",
|
|
235
|
+
"default": None,
|
|
236
|
+
},
|
|
237
|
+
"skip_validation": {
|
|
238
|
+
"type": "boolean",
|
|
239
|
+
"description": (
|
|
240
|
+
"Skip LLM validation even when task has validation_criteria. "
|
|
241
|
+
"USE THIS when: validation fails due to truncated diff, validator misses context, "
|
|
242
|
+
"or you've manually verified completion. Provide override_justification explaining why."
|
|
243
|
+
),
|
|
244
|
+
"default": False,
|
|
245
|
+
},
|
|
246
|
+
"session_id": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"description": "Your session ID (from system context). Pass this to track which session closed the task.",
|
|
249
|
+
"default": None,
|
|
250
|
+
},
|
|
251
|
+
"override_justification": {
|
|
252
|
+
"type": "string",
|
|
253
|
+
"description": (
|
|
254
|
+
"Justification for bypassing validation or commit check. "
|
|
255
|
+
"Required when skip_validation=True or no_commit_needed=True. "
|
|
256
|
+
"Example: 'Validation saw truncated diff - verified via git show that commit includes all changes'"
|
|
257
|
+
),
|
|
258
|
+
"default": None,
|
|
259
|
+
},
|
|
260
|
+
"no_commit_needed": {
|
|
261
|
+
"type": "boolean",
|
|
262
|
+
"description": (
|
|
263
|
+
"ONLY for tasks with NO code changes (pure research, planning, documentation review). "
|
|
264
|
+
"Do NOT use this to bypass validation when a commit exists - use skip_validation instead. "
|
|
265
|
+
"Requires override_justification."
|
|
266
|
+
),
|
|
267
|
+
"default": False,
|
|
268
|
+
},
|
|
269
|
+
"commit_sha": {
|
|
270
|
+
"type": "string",
|
|
271
|
+
"description": "Git commit SHA to link before closing. Convenience for commit + close in one call.",
|
|
272
|
+
"default": None,
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
"required": ["task_id"],
|
|
276
|
+
},
|
|
277
|
+
func=close_task,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def reopen_task(task_id: str, reason: str | None = None) -> dict[str, Any]:
|
|
281
|
+
"""Reopen a closed or review task.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
task_id: Task reference (#N, path, or UUID)
|
|
285
|
+
reason: Optional reason for reopening
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Reopened task or error. Resets accepted_by_user to false.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
292
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
293
|
+
return {"error": str(e)}
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
ctx.task_manager.reopen_task(resolved_id, reason=reason)
|
|
297
|
+
|
|
298
|
+
# Reactivate any associated worktrees that were marked merged/abandoned
|
|
299
|
+
try:
|
|
300
|
+
from gobby.storage.worktrees import WorktreeStatus
|
|
301
|
+
|
|
302
|
+
worktree_manager = LocalWorktreeManager(ctx.task_manager.db)
|
|
303
|
+
wt = worktree_manager.get_by_task(resolved_id)
|
|
304
|
+
if wt and wt.status in (
|
|
305
|
+
WorktreeStatus.MERGED.value,
|
|
306
|
+
WorktreeStatus.ABANDONED.value,
|
|
307
|
+
):
|
|
308
|
+
worktree_manager.update(wt.id, status=WorktreeStatus.ACTIVE.value)
|
|
309
|
+
except Exception:
|
|
310
|
+
pass # nosec B110 - best-effort worktree update
|
|
311
|
+
|
|
312
|
+
return {}
|
|
313
|
+
except ValueError as e:
|
|
314
|
+
return {"error": str(e)}
|
|
315
|
+
|
|
316
|
+
registry.register(
|
|
317
|
+
name="reopen_task",
|
|
318
|
+
description="Reopen a closed task. Clears closed_at, closed_reason, and closed_in_session_id. Optionally appends a reopen reason to the description.",
|
|
319
|
+
input_schema={
|
|
320
|
+
"type": "object",
|
|
321
|
+
"properties": {
|
|
322
|
+
"task_id": {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"description": "Task reference to reopen: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
325
|
+
},
|
|
326
|
+
"reason": {
|
|
327
|
+
"type": "string",
|
|
328
|
+
"description": "Optional reason for reopening the task",
|
|
329
|
+
"default": None,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
"required": ["task_id"],
|
|
333
|
+
},
|
|
334
|
+
func=reopen_task,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def delete_task(task_id: str, cascade: bool = True) -> dict[str, Any]:
|
|
338
|
+
"""Delete a task and its children by default."""
|
|
339
|
+
try:
|
|
340
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
341
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
342
|
+
return {"error": str(e)}
|
|
343
|
+
|
|
344
|
+
# Get task before deleting to capture seq_num for ref
|
|
345
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
346
|
+
if not task:
|
|
347
|
+
return {"error": f"Task {task_id} not found"}
|
|
348
|
+
ref = f"#{task.seq_num}" if task.seq_num else resolved_id[:8]
|
|
349
|
+
|
|
350
|
+
deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade)
|
|
351
|
+
if not deleted:
|
|
352
|
+
return {"error": f"Task {task_id} not found"}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"ref": ref,
|
|
356
|
+
"deleted_task_id": resolved_id, # UUID at end
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
registry.register(
|
|
360
|
+
name="delete_task",
|
|
361
|
+
description="Delete a task and its subtasks.",
|
|
362
|
+
input_schema={
|
|
363
|
+
"type": "object",
|
|
364
|
+
"properties": {
|
|
365
|
+
"task_id": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
368
|
+
},
|
|
369
|
+
"cascade": {
|
|
370
|
+
"type": "boolean",
|
|
371
|
+
"description": "If True, delete all child tasks as well. Defaults to True.",
|
|
372
|
+
"default": True,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
"required": ["task_id"],
|
|
376
|
+
},
|
|
377
|
+
func=delete_task,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def add_label(task_id: str, label: str) -> dict[str, Any]:
|
|
381
|
+
"""Add a label to a task."""
|
|
382
|
+
try:
|
|
383
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
384
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
385
|
+
return {"error": str(e)}
|
|
386
|
+
task = ctx.task_manager.add_label(resolved_id, label)
|
|
387
|
+
if not task:
|
|
388
|
+
return {"error": f"Task {task_id} not found"}
|
|
389
|
+
return {}
|
|
390
|
+
|
|
391
|
+
registry.register(
|
|
392
|
+
name="add_label",
|
|
393
|
+
description="Add a label to a task.",
|
|
394
|
+
input_schema={
|
|
395
|
+
"type": "object",
|
|
396
|
+
"properties": {
|
|
397
|
+
"task_id": {
|
|
398
|
+
"type": "string",
|
|
399
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
400
|
+
},
|
|
401
|
+
"label": {"type": "string", "description": "Label to add"},
|
|
402
|
+
},
|
|
403
|
+
"required": ["task_id", "label"],
|
|
404
|
+
},
|
|
405
|
+
func=add_label,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def remove_label(task_id: str, label: str) -> dict[str, Any]:
|
|
409
|
+
"""Remove a label from a task."""
|
|
410
|
+
try:
|
|
411
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
412
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
413
|
+
return {"error": str(e)}
|
|
414
|
+
task = ctx.task_manager.remove_label(resolved_id, label)
|
|
415
|
+
if not task:
|
|
416
|
+
return {"error": f"Task {task_id} not found"}
|
|
417
|
+
return {}
|
|
418
|
+
|
|
419
|
+
registry.register(
|
|
420
|
+
name="remove_label",
|
|
421
|
+
description="Remove a label from a task.",
|
|
422
|
+
input_schema={
|
|
423
|
+
"type": "object",
|
|
424
|
+
"properties": {
|
|
425
|
+
"task_id": {
|
|
426
|
+
"type": "string",
|
|
427
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
428
|
+
},
|
|
429
|
+
"label": {"type": "string", "description": "Label to remove"},
|
|
430
|
+
},
|
|
431
|
+
"required": ["task_id", "label"],
|
|
432
|
+
},
|
|
433
|
+
func=remove_label,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
def claim_task(
|
|
437
|
+
task_id: str,
|
|
438
|
+
session_id: str,
|
|
439
|
+
force: bool = False,
|
|
440
|
+
) -> dict[str, Any]:
|
|
441
|
+
"""Claim a task for the current session.
|
|
442
|
+
|
|
443
|
+
Combines setting the assignee and marking as in_progress in a single
|
|
444
|
+
atomic operation. Detects conflicts when another session has already
|
|
445
|
+
claimed the task.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
task_id: Task reference (#N, path, or UUID)
|
|
449
|
+
session_id: Session ID claiming the task
|
|
450
|
+
force: Override existing claim by another session (default: False)
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Empty dict on success, or error dict with conflict information.
|
|
454
|
+
"""
|
|
455
|
+
# Resolve task reference (supports #N, path, UUID formats)
|
|
456
|
+
try:
|
|
457
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
458
|
+
except TaskNotFoundError as e:
|
|
459
|
+
return {"error": str(e)}
|
|
460
|
+
except ValueError as e:
|
|
461
|
+
return {"error": str(e)}
|
|
462
|
+
|
|
463
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
464
|
+
if not task:
|
|
465
|
+
return {"error": f"Task {task_id} not found"}
|
|
466
|
+
|
|
467
|
+
# Check if already claimed by another session
|
|
468
|
+
if task.assignee and task.assignee != session_id and not force:
|
|
469
|
+
return {
|
|
470
|
+
"error": "Task already claimed by another session",
|
|
471
|
+
"claimed_by": task.assignee,
|
|
472
|
+
"message": f"Task is already claimed by session '{task.assignee}'. Use force=True to override.",
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
# Update task with assignee and status in single atomic call
|
|
476
|
+
updated = ctx.task_manager.update_task(
|
|
477
|
+
resolved_id,
|
|
478
|
+
assignee=session_id,
|
|
479
|
+
status="in_progress",
|
|
480
|
+
)
|
|
481
|
+
if not updated:
|
|
482
|
+
return {"error": f"Failed to claim task {task_id}"}
|
|
483
|
+
|
|
484
|
+
# Link task to session (best-effort, don't fail the claim if this fails)
|
|
485
|
+
try:
|
|
486
|
+
ctx.session_task_manager.link_task(session_id, resolved_id, "claimed")
|
|
487
|
+
except Exception:
|
|
488
|
+
pass # nosec B110 - best-effort linking
|
|
489
|
+
|
|
490
|
+
return {}
|
|
491
|
+
|
|
492
|
+
registry.register(
|
|
493
|
+
name="claim_task",
|
|
494
|
+
description="Claim a task for your session. Sets assignee to session_id and status to in_progress. Detects conflicts if already claimed by another session.",
|
|
495
|
+
input_schema={
|
|
496
|
+
"type": "object",
|
|
497
|
+
"properties": {
|
|
498
|
+
"task_id": {
|
|
499
|
+
"type": "string",
|
|
500
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
501
|
+
},
|
|
502
|
+
"session_id": {
|
|
503
|
+
"type": "string",
|
|
504
|
+
"description": "Your session ID (from system context). The session claiming the task.",
|
|
505
|
+
},
|
|
506
|
+
"force": {
|
|
507
|
+
"type": "boolean",
|
|
508
|
+
"description": "Override existing claim by another session (default: False)",
|
|
509
|
+
"default": False,
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
"required": ["task_id", "session_id"],
|
|
513
|
+
},
|
|
514
|
+
func=claim_task,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
return registry
|