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,301 @@
|
|
|
1
|
+
"""Validation helpers for task lifecycle operations.
|
|
2
|
+
|
|
3
|
+
Provides validation functions used by close_task to verify tasks
|
|
4
|
+
can be closed (commit checks, child completion, LLM validation).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from gobby.mcp_proxy.tools.tasks._helpers import SKIP_REASONS
|
|
12
|
+
from gobby.storage.tasks import Task
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.config.tasks import TaskValidationConfig
|
|
16
|
+
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
17
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
18
|
+
from gobby.tasks.validation import TaskValidator
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ValidationResult:
|
|
25
|
+
"""Result of validation checks."""
|
|
26
|
+
|
|
27
|
+
can_close: bool
|
|
28
|
+
error_type: str | None = None
|
|
29
|
+
message: str | None = None
|
|
30
|
+
extra: dict[str, Any] | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def validate_commit_requirements(
|
|
34
|
+
task: Task,
|
|
35
|
+
reason: str,
|
|
36
|
+
no_commit_needed: bool,
|
|
37
|
+
override_justification: str | None,
|
|
38
|
+
) -> ValidationResult:
|
|
39
|
+
"""Check if task meets commit requirements for closing.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
task: The task to validate
|
|
43
|
+
reason: Reason for closing
|
|
44
|
+
no_commit_needed: If True, allow closing without commits
|
|
45
|
+
override_justification: Justification for skipping commit check
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
ValidationResult indicating if task can be closed
|
|
49
|
+
"""
|
|
50
|
+
# Skip commit check for certain close reasons that imply no work was done
|
|
51
|
+
requires_commit_check = reason.lower() not in SKIP_REASONS
|
|
52
|
+
|
|
53
|
+
if requires_commit_check and not task.commits:
|
|
54
|
+
# No commits linked - require explicit acknowledgment
|
|
55
|
+
if no_commit_needed:
|
|
56
|
+
if not override_justification:
|
|
57
|
+
return ValidationResult(
|
|
58
|
+
can_close=False,
|
|
59
|
+
error_type="justification_required",
|
|
60
|
+
message=(
|
|
61
|
+
"When no_commit_needed=True, you must provide "
|
|
62
|
+
"override_justification explaining why no commit was needed."
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
# Allowed to proceed - agent confirmed no commit needed
|
|
66
|
+
else:
|
|
67
|
+
return ValidationResult(
|
|
68
|
+
can_close=False,
|
|
69
|
+
error_type="no_commits_linked",
|
|
70
|
+
message=(
|
|
71
|
+
"Cannot close task: no commits are linked. Either:\n"
|
|
72
|
+
"1. Commit your changes and use link_commit() or include [task_id] in commit message\n"
|
|
73
|
+
"2. Set no_commit_needed=True with override_justification if this task didn't require code changes"
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return ValidationResult(can_close=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def validate_parent_task(
|
|
81
|
+
ctx: "RegistryContext",
|
|
82
|
+
task_id: str,
|
|
83
|
+
) -> ValidationResult:
|
|
84
|
+
"""Check if a parent task's children are all closed.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: Registry context
|
|
88
|
+
task_id: The parent task ID
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
ValidationResult indicating if parent can be closed
|
|
92
|
+
"""
|
|
93
|
+
children = ctx.task_manager.list_tasks(parent_task_id=task_id, limit=1000)
|
|
94
|
+
|
|
95
|
+
if children:
|
|
96
|
+
open_children = [c for c in children if c.status != "closed"]
|
|
97
|
+
if open_children:
|
|
98
|
+
open_titles = [f"- {c.id}: {c.title}" for c in open_children[:5]]
|
|
99
|
+
remaining = len(open_children) - 5 if len(open_children) > 5 else 0
|
|
100
|
+
feedback = f"Cannot close: {len(open_children)} child tasks still open:\n"
|
|
101
|
+
feedback += "\n".join(open_titles)
|
|
102
|
+
if remaining > 0:
|
|
103
|
+
feedback += f"\n... and {remaining} more"
|
|
104
|
+
return ValidationResult(
|
|
105
|
+
can_close=False,
|
|
106
|
+
error_type="validation_failed",
|
|
107
|
+
message=feedback,
|
|
108
|
+
extra={"open_children": [c.id for c in open_children]},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return ValidationResult(can_close=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def gather_validation_context(
|
|
115
|
+
task: Task,
|
|
116
|
+
changes_summary: str | None,
|
|
117
|
+
repo_path: str | None,
|
|
118
|
+
task_manager: "LocalTaskManager",
|
|
119
|
+
) -> tuple[str | None, str | None]:
|
|
120
|
+
"""Gather context for LLM validation.
|
|
121
|
+
|
|
122
|
+
Uses provided changes_summary or auto-fetches via smart context gathering.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
task: The task to validate
|
|
126
|
+
changes_summary: Optional user-provided summary
|
|
127
|
+
repo_path: Path to the repository
|
|
128
|
+
task_manager: LocalTaskManager for fetching task diff
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Tuple of (validation_context, raw_diff)
|
|
132
|
+
"""
|
|
133
|
+
from gobby.tasks.commits import get_task_diff, summarize_diff_for_validation
|
|
134
|
+
|
|
135
|
+
validation_context = changes_summary
|
|
136
|
+
raw_diff = None
|
|
137
|
+
|
|
138
|
+
if not validation_context:
|
|
139
|
+
# First try commit-based diff if task has linked commits
|
|
140
|
+
if task.commits:
|
|
141
|
+
try:
|
|
142
|
+
# Don't include uncommitted changes - they're likely unrelated to this task
|
|
143
|
+
# The linked commits ARE the work for this task
|
|
144
|
+
diff_result = get_task_diff(
|
|
145
|
+
task_id=task.id,
|
|
146
|
+
task_manager=task_manager,
|
|
147
|
+
include_uncommitted=False,
|
|
148
|
+
cwd=repo_path,
|
|
149
|
+
)
|
|
150
|
+
if diff_result.diff:
|
|
151
|
+
raw_diff = diff_result.diff
|
|
152
|
+
# Use smart summarization to ensure all files are visible
|
|
153
|
+
summarized_diff = summarize_diff_for_validation(raw_diff)
|
|
154
|
+
validation_context = (
|
|
155
|
+
f"Commit-based diff ({len(diff_result.commits)} commits, "
|
|
156
|
+
f"{diff_result.file_count} files):\n\n{summarized_diff}"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
logger.warning(
|
|
160
|
+
f"get_task_diff returned empty for task {task.id} "
|
|
161
|
+
f"with commits {task.commits}"
|
|
162
|
+
)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning(f"get_task_diff failed for task {task.id}: {e}")
|
|
165
|
+
|
|
166
|
+
# Fall back to smart context ONLY if no linked commits
|
|
167
|
+
# (uncommitted changes are unrelated if we have specific commits linked)
|
|
168
|
+
if not validation_context and not task.commits:
|
|
169
|
+
from gobby.tasks.validation import get_validation_context_smart
|
|
170
|
+
|
|
171
|
+
# Smart context gathering: uncommitted changes + multi-commit window + file analysis
|
|
172
|
+
smart_context = get_validation_context_smart(
|
|
173
|
+
task_title=task.title,
|
|
174
|
+
validation_criteria=task.validation_criteria,
|
|
175
|
+
task_description=task.description,
|
|
176
|
+
cwd=repo_path,
|
|
177
|
+
)
|
|
178
|
+
if smart_context:
|
|
179
|
+
validation_context = f"Validation context:\n\n{smart_context}"
|
|
180
|
+
|
|
181
|
+
return validation_context, raw_diff
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def validate_leaf_task_with_llm(
|
|
185
|
+
task: Task,
|
|
186
|
+
task_validator: "TaskValidator",
|
|
187
|
+
validation_context: str,
|
|
188
|
+
raw_diff: str | None,
|
|
189
|
+
ctx: "RegistryContext",
|
|
190
|
+
resolved_id: str,
|
|
191
|
+
validation_config: "TaskValidationConfig | None",
|
|
192
|
+
) -> ValidationResult:
|
|
193
|
+
"""Run LLM validation on a leaf task.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
task: The task to validate
|
|
197
|
+
task_validator: The validator instance
|
|
198
|
+
validation_context: Context for validation
|
|
199
|
+
raw_diff: Raw diff for doc-only check
|
|
200
|
+
ctx: Registry context
|
|
201
|
+
resolved_id: Resolved task ID
|
|
202
|
+
validation_config: Validation configuration
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
ValidationResult indicating if task can be closed
|
|
206
|
+
"""
|
|
207
|
+
from gobby.tasks.commits import is_doc_only_diff
|
|
208
|
+
|
|
209
|
+
# Auto-skip LLM validation for doc-only changes
|
|
210
|
+
if raw_diff and is_doc_only_diff(raw_diff):
|
|
211
|
+
logger.info(f"Skipping LLM validation for task {task.id}: doc-only changes")
|
|
212
|
+
ctx.task_manager.update_task(
|
|
213
|
+
resolved_id,
|
|
214
|
+
validation_status="valid",
|
|
215
|
+
validation_feedback="Auto-validated: documentation-only changes",
|
|
216
|
+
)
|
|
217
|
+
return ValidationResult(can_close=True)
|
|
218
|
+
|
|
219
|
+
# Run LLM validation
|
|
220
|
+
result = await task_validator.validate_task(
|
|
221
|
+
task_id=task.id,
|
|
222
|
+
title=task.title,
|
|
223
|
+
description=task.description,
|
|
224
|
+
changes_summary=validation_context,
|
|
225
|
+
validation_criteria=task.validation_criteria,
|
|
226
|
+
category=task.category,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Store validation result regardless of pass/fail
|
|
230
|
+
ctx.task_manager.update_task(
|
|
231
|
+
resolved_id,
|
|
232
|
+
validation_status=result.status,
|
|
233
|
+
validation_feedback=result.feedback,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if result.status != "valid":
|
|
237
|
+
# Block closing on invalid or pending (error during validation)
|
|
238
|
+
return ValidationResult(
|
|
239
|
+
can_close=False,
|
|
240
|
+
error_type="validation_failed",
|
|
241
|
+
message=result.feedback or "Validation did not pass",
|
|
242
|
+
extra={"validation_status": result.status},
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Run external validation if enabled (after internal validation passes)
|
|
246
|
+
if validation_config and validation_config.use_external_validator:
|
|
247
|
+
from gobby.tasks.external_validator import run_external_validation
|
|
248
|
+
|
|
249
|
+
external_result = await run_external_validation(
|
|
250
|
+
config=validation_config,
|
|
251
|
+
llm_service=task_validator.llm_service,
|
|
252
|
+
task={
|
|
253
|
+
"id": task.id,
|
|
254
|
+
"title": task.title,
|
|
255
|
+
"description": task.description,
|
|
256
|
+
"validation_criteria": task.validation_criteria,
|
|
257
|
+
},
|
|
258
|
+
changes_context=validation_context,
|
|
259
|
+
agent_runner=ctx.agent_runner,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if external_result.status not in ("valid", "skipped"):
|
|
263
|
+
# Block closing on external validation failure
|
|
264
|
+
return ValidationResult(
|
|
265
|
+
can_close=False,
|
|
266
|
+
error_type="external_validation_failed",
|
|
267
|
+
message=external_result.summary,
|
|
268
|
+
extra={
|
|
269
|
+
"validation_status": external_result.status,
|
|
270
|
+
"issues": [issue.to_dict() for issue in external_result.issues],
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return ValidationResult(can_close=True)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def determine_close_outcome(
|
|
278
|
+
task: Task,
|
|
279
|
+
skip_validation: bool,
|
|
280
|
+
no_commit_needed: bool,
|
|
281
|
+
override_justification: str | None,
|
|
282
|
+
) -> tuple[bool, bool]:
|
|
283
|
+
"""Determine the close outcome for a task.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
task: The task being closed
|
|
287
|
+
skip_validation: Whether validation was skipped
|
|
288
|
+
no_commit_needed: Whether commit was not needed
|
|
289
|
+
override_justification: Justification for override
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Tuple of (route_to_review, store_override)
|
|
293
|
+
"""
|
|
294
|
+
# Determine if override should be stored
|
|
295
|
+
store_override = skip_validation or no_commit_needed
|
|
296
|
+
|
|
297
|
+
# Route to review if task requires user review OR override was used
|
|
298
|
+
# This ensures tasks with HITL flag or skipped validation go through human review
|
|
299
|
+
route_to_review = bool(task.requires_user_review or (override_justification and store_override))
|
|
300
|
+
|
|
301
|
+
return route_to_review, store_override
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Task ID resolution for MCP tools.
|
|
2
|
+
|
|
3
|
+
Provides resolve_task_id_for_mcp() which resolves various task reference
|
|
4
|
+
formats (#N, N, path, UUID) to task UUIDs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from gobby.mcp_proxy.tools.tasks._helpers import _is_path_format
|
|
8
|
+
from gobby.storage.tasks import LocalTaskManager, TaskNotFoundError
|
|
9
|
+
from gobby.utils.project_context import get_project_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_task_id_for_mcp(
|
|
13
|
+
task_manager: LocalTaskManager,
|
|
14
|
+
task_id: str,
|
|
15
|
+
project_id: str | None = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Resolve a task reference to its UUID for MCP tools.
|
|
18
|
+
|
|
19
|
+
Supports multiple reference formats:
|
|
20
|
+
- #N: Project-scoped seq_num (e.g., #1, #47) - requires project_id
|
|
21
|
+
- N: Bare numeric seq_num (e.g., 1, 47) - requires project_id
|
|
22
|
+
- 1.2.3: Path cache format - requires project_id
|
|
23
|
+
- UUID: Direct UUID lookup (validated to exist)
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
task_manager: The task manager
|
|
27
|
+
task_id: Task reference in any supported format
|
|
28
|
+
project_id: Project ID for scoped lookups (#N and path formats).
|
|
29
|
+
If not provided, will try to get from project context.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
The resolved task UUID
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
TaskNotFoundError: If the reference cannot be resolved
|
|
36
|
+
ValueError: If the format is invalid
|
|
37
|
+
"""
|
|
38
|
+
# Get project_id from context if not provided
|
|
39
|
+
if project_id is None:
|
|
40
|
+
ctx = get_project_context()
|
|
41
|
+
project_id = ctx.get("id") if ctx else None
|
|
42
|
+
|
|
43
|
+
# Check for #N format or path format (requires project_id)
|
|
44
|
+
if project_id and (task_id.startswith("#") or _is_path_format(task_id)):
|
|
45
|
+
return task_manager.resolve_task_reference(task_id, project_id)
|
|
46
|
+
|
|
47
|
+
# Check for bare numeric string (seq_num without #)
|
|
48
|
+
if project_id and task_id.isdigit():
|
|
49
|
+
return task_manager.resolve_task_reference(f"#{task_id}", project_id)
|
|
50
|
+
|
|
51
|
+
# UUID format: validate it exists by trying to get it
|
|
52
|
+
task = task_manager.get_task(task_id)
|
|
53
|
+
if task is None:
|
|
54
|
+
raise TaskNotFoundError(f"Task with UUID '{task_id}' not found")
|
|
55
|
+
return task_id
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Search operations for task management.
|
|
2
|
+
|
|
3
|
+
Provides semantic search tools for tasks using TF-IDF.
|
|
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._resolution import resolve_task_id_for_mcp
|
|
11
|
+
from gobby.utils.project_context import get_project_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_search_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
15
|
+
"""Create a registry with task search tools.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
ctx: Shared registry context
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
InternalToolRegistry with search tools registered
|
|
22
|
+
"""
|
|
23
|
+
registry = InternalToolRegistry(
|
|
24
|
+
name="gobby-tasks-search",
|
|
25
|
+
description="Task search operations",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def search_tasks(
|
|
29
|
+
query: str,
|
|
30
|
+
status: str | list[str] | None = None,
|
|
31
|
+
task_type: str | None = None,
|
|
32
|
+
priority: int | None = None,
|
|
33
|
+
parent_task_id: str | None = None,
|
|
34
|
+
category: str | None = None,
|
|
35
|
+
limit: int = 20,
|
|
36
|
+
min_score: float = 0.0,
|
|
37
|
+
all_projects: bool = False,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Search tasks using semantic TF-IDF search.
|
|
40
|
+
|
|
41
|
+
Performs semantic search on task title, description, labels, and type.
|
|
42
|
+
Results are ranked by relevance and can be filtered by status, type, etc.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
query: Search query text (required). Natural language query.
|
|
46
|
+
status: Filter by status (open, in_progress, review, closed).
|
|
47
|
+
Can be a single status or comma-separated list.
|
|
48
|
+
task_type: Filter by task type (task, bug, feature, epic)
|
|
49
|
+
priority: Filter by priority (1=High, 2=Medium, 3=Low)
|
|
50
|
+
parent_task_id: Filter by parent task ID (UUID, #N, or N format)
|
|
51
|
+
category: Filter by task category
|
|
52
|
+
limit: Maximum number of results (default: 20)
|
|
53
|
+
min_score: Minimum similarity score 0.0-1.0 (default: 0.0)
|
|
54
|
+
all_projects: If true, search all projects instead of current project
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dict with matching tasks and their similarity scores
|
|
58
|
+
"""
|
|
59
|
+
if not query or not query.strip():
|
|
60
|
+
return {"error": "Query is required", "tasks": [], "count": 0}
|
|
61
|
+
|
|
62
|
+
# Get current project context unless all_projects
|
|
63
|
+
project_id = None
|
|
64
|
+
if not all_projects:
|
|
65
|
+
project_ctx = get_project_context()
|
|
66
|
+
if project_ctx and project_ctx.get("id"):
|
|
67
|
+
project_id = project_ctx["id"]
|
|
68
|
+
|
|
69
|
+
# Handle comma-separated status string
|
|
70
|
+
status_filter: str | list[str] | None = status
|
|
71
|
+
if isinstance(status, str) and "," in status:
|
|
72
|
+
status_filter = [s.strip() for s in status.split(",")]
|
|
73
|
+
|
|
74
|
+
# Resolve parent_task_id if provided (#N, N, or UUID -> UUID)
|
|
75
|
+
resolved_parent_id = None
|
|
76
|
+
if parent_task_id:
|
|
77
|
+
try:
|
|
78
|
+
resolved_parent_id = resolve_task_id_for_mcp(
|
|
79
|
+
ctx.task_manager, parent_task_id, project_id
|
|
80
|
+
)
|
|
81
|
+
except Exception:
|
|
82
|
+
return {
|
|
83
|
+
"error": f"Invalid parent_task_id: {parent_task_id}",
|
|
84
|
+
"tasks": [],
|
|
85
|
+
"count": 0,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Perform search
|
|
89
|
+
results = ctx.task_manager.search_tasks(
|
|
90
|
+
query=query.strip(),
|
|
91
|
+
project_id=project_id,
|
|
92
|
+
status=status_filter,
|
|
93
|
+
task_type=task_type,
|
|
94
|
+
priority=priority,
|
|
95
|
+
parent_task_id=resolved_parent_id,
|
|
96
|
+
category=category,
|
|
97
|
+
limit=limit,
|
|
98
|
+
min_score=min_score,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"tasks": [
|
|
103
|
+
{
|
|
104
|
+
**task.to_brief(),
|
|
105
|
+
"score": round(score, 4),
|
|
106
|
+
}
|
|
107
|
+
for task, score in results
|
|
108
|
+
],
|
|
109
|
+
"count": len(results),
|
|
110
|
+
"query": query.strip(),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
registry.register(
|
|
114
|
+
name="search_tasks",
|
|
115
|
+
description="Search tasks using semantic TF-IDF search. Returns tasks ranked by relevance to the query.",
|
|
116
|
+
input_schema={
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"query": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "Search query text. Natural language query to find matching tasks.",
|
|
122
|
+
},
|
|
123
|
+
"status": {
|
|
124
|
+
"oneOf": [
|
|
125
|
+
{"type": "string"},
|
|
126
|
+
{"type": "array", "items": {"type": "string"}},
|
|
127
|
+
],
|
|
128
|
+
"description": "Filter by status. Can be single status or comma-separated list (e.g., 'open,in_progress')",
|
|
129
|
+
"default": None,
|
|
130
|
+
},
|
|
131
|
+
"task_type": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Filter by task type (task, bug, feature, epic)",
|
|
134
|
+
"default": None,
|
|
135
|
+
},
|
|
136
|
+
"priority": {
|
|
137
|
+
"type": "integer",
|
|
138
|
+
"description": "Filter by priority (1=High, 2=Medium, 3=Low)",
|
|
139
|
+
"default": None,
|
|
140
|
+
},
|
|
141
|
+
"parent_task_id": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": "Filter by parent task ID (UUID, #N, or N format)",
|
|
144
|
+
"default": None,
|
|
145
|
+
},
|
|
146
|
+
"category": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Filter by task category",
|
|
149
|
+
"default": None,
|
|
150
|
+
},
|
|
151
|
+
"limit": {
|
|
152
|
+
"type": "integer",
|
|
153
|
+
"description": "Maximum number of results to return",
|
|
154
|
+
"default": 20,
|
|
155
|
+
},
|
|
156
|
+
"min_score": {
|
|
157
|
+
"type": "number",
|
|
158
|
+
"description": "Minimum similarity score threshold (0.0-1.0)",
|
|
159
|
+
"default": 0.0,
|
|
160
|
+
},
|
|
161
|
+
"all_projects": {
|
|
162
|
+
"type": "boolean",
|
|
163
|
+
"description": "If true, search all projects instead of just the current project",
|
|
164
|
+
"default": False,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
"required": ["query"],
|
|
168
|
+
},
|
|
169
|
+
func=search_tasks,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def reindex_tasks(all_projects: bool = False) -> dict[str, Any]:
|
|
173
|
+
"""Force rebuild of the task search index.
|
|
174
|
+
|
|
175
|
+
Use this to refresh the search index after bulk operations
|
|
176
|
+
or if search results seem stale.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
all_projects: If true, reindex all projects instead of current project
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dict with index statistics
|
|
183
|
+
"""
|
|
184
|
+
# Get current project context unless all_projects
|
|
185
|
+
project_id = None
|
|
186
|
+
if not all_projects:
|
|
187
|
+
project_ctx = get_project_context()
|
|
188
|
+
if project_ctx and project_ctx.get("id"):
|
|
189
|
+
project_id = project_ctx["id"]
|
|
190
|
+
|
|
191
|
+
stats = ctx.task_manager.reindex_search(project_id)
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"success": True,
|
|
195
|
+
"message": f"Search index rebuilt with {stats.get('item_count', 0)} tasks",
|
|
196
|
+
"stats": stats,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
registry.register(
|
|
200
|
+
name="reindex_tasks",
|
|
201
|
+
description="Force rebuild of the task search index. Use after bulk operations or if search seems stale.",
|
|
202
|
+
input_schema={
|
|
203
|
+
"type": "object",
|
|
204
|
+
"properties": {
|
|
205
|
+
"all_projects": {
|
|
206
|
+
"type": "boolean",
|
|
207
|
+
"description": "If true, reindex all projects instead of just the current project",
|
|
208
|
+
"default": False,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
func=reindex_tasks,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return registry
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Session integration tools for task management.
|
|
2
|
+
|
|
3
|
+
Provides tools for linking tasks to sessions and querying task-session
|
|
4
|
+
relationships.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
10
|
+
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
11
|
+
from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
|
|
12
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_session_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
16
|
+
"""Create a registry with session-task integration tools.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
ctx: Shared registry context
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
InternalToolRegistry with session tools registered
|
|
23
|
+
"""
|
|
24
|
+
registry = InternalToolRegistry(
|
|
25
|
+
name="gobby-tasks-session",
|
|
26
|
+
description="Task-session integration tools",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def link_task_to_session(
|
|
30
|
+
task_id: str,
|
|
31
|
+
session_id: str | None = None,
|
|
32
|
+
action: str = "worked_on",
|
|
33
|
+
) -> dict[str, Any]:
|
|
34
|
+
"""Link a task to a session."""
|
|
35
|
+
if not session_id:
|
|
36
|
+
return {"error": "session_id is required"}
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
40
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
41
|
+
return {"error": str(e)}
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
ctx.session_task_manager.link_task(session_id, resolved_id, action)
|
|
45
|
+
return {}
|
|
46
|
+
except ValueError as e:
|
|
47
|
+
return {"error": str(e)}
|
|
48
|
+
|
|
49
|
+
registry.register(
|
|
50
|
+
name="link_task_to_session",
|
|
51
|
+
description="Link a task to a session.",
|
|
52
|
+
input_schema={
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"task_id": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
58
|
+
},
|
|
59
|
+
"session_id": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "Session ID (optional, defaults to linking context if available)",
|
|
62
|
+
"default": None,
|
|
63
|
+
},
|
|
64
|
+
"action": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Relationship type (worked_on, discovered, mentioned, closed)",
|
|
67
|
+
"default": "worked_on",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"required": ["task_id"],
|
|
71
|
+
},
|
|
72
|
+
func=link_task_to_session,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def get_session_tasks(session_id: str) -> dict[str, Any]:
|
|
76
|
+
"""Get all tasks associated with a session."""
|
|
77
|
+
tasks = ctx.session_task_manager.get_session_tasks(session_id)
|
|
78
|
+
return {"session_id": session_id, "tasks": tasks}
|
|
79
|
+
|
|
80
|
+
registry.register(
|
|
81
|
+
name="get_session_tasks",
|
|
82
|
+
description="Get all tasks associated with a session.",
|
|
83
|
+
input_schema={
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"session_id": {"type": "string", "description": "Session ID"},
|
|
87
|
+
},
|
|
88
|
+
"required": ["session_id"],
|
|
89
|
+
},
|
|
90
|
+
func=get_session_tasks,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def get_task_sessions(task_id: str) -> dict[str, Any]:
|
|
94
|
+
"""Get all sessions that touched a task."""
|
|
95
|
+
try:
|
|
96
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
97
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
98
|
+
return {"error": str(e)}
|
|
99
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
100
|
+
sessions = ctx.session_task_manager.get_task_sessions(resolved_id)
|
|
101
|
+
# Handle case where task is not found (shouldn't happen after resolve, but be defensive)
|
|
102
|
+
ref = f"#{task.seq_num}" if task and task.seq_num else resolved_id[:8]
|
|
103
|
+
return {
|
|
104
|
+
"ref": ref,
|
|
105
|
+
"task_id": resolved_id,
|
|
106
|
+
"sessions": sessions,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
registry.register(
|
|
110
|
+
name="get_task_sessions",
|
|
111
|
+
description="Get all sessions that touched a task.",
|
|
112
|
+
input_schema={
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {
|
|
115
|
+
"task_id": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Task reference: #N (e.g., #1, #47), path (e.g., 1.2.3), or UUID",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
"required": ["task_id"],
|
|
121
|
+
},
|
|
122
|
+
func=get_task_sessions,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return registry
|