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,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task readiness MCP tools module.
|
|
3
|
+
|
|
4
|
+
Provides tools for task readiness management:
|
|
5
|
+
- list_ready_tasks: List tasks with no unresolved blocking dependencies
|
|
6
|
+
- list_blocked_tasks: List tasks that are blocked by dependencies
|
|
7
|
+
- suggest_next_task: Suggest the best next task based on scoring
|
|
8
|
+
|
|
9
|
+
Extracted from tasks.py using Strangler Fig pattern for code decomposition.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
16
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
17
|
+
from gobby.utils.project_context import get_project_context
|
|
18
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from gobby.storage.tasks import LocalTaskManager
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"create_readiness_registry",
|
|
25
|
+
"is_descendant_of",
|
|
26
|
+
"_get_ancestry_chain",
|
|
27
|
+
"_compute_proximity_boost",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_current_project_id() -> str | None:
|
|
32
|
+
"""Get the current project ID from context."""
|
|
33
|
+
context = get_project_context()
|
|
34
|
+
return context.get("id") if context else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_ready_descendants(
|
|
38
|
+
task_manager: "LocalTaskManager",
|
|
39
|
+
parent_task_id: str,
|
|
40
|
+
task_type: str | None = None,
|
|
41
|
+
project_id: str | None = None,
|
|
42
|
+
) -> list[Any]:
|
|
43
|
+
"""
|
|
44
|
+
Get all ready tasks that are descendants of the given parent task.
|
|
45
|
+
|
|
46
|
+
Traverses the task hierarchy to find all tasks under parent_task_id,
|
|
47
|
+
then filters to only those that are ready (open with no blockers).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
task_manager: LocalTaskManager instance
|
|
51
|
+
parent_task_id: ID of the ancestor task to filter by
|
|
52
|
+
task_type: Optional task type filter
|
|
53
|
+
project_id: Optional project ID filter
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of ready Task objects that are descendants of parent_task_id
|
|
57
|
+
"""
|
|
58
|
+
# Get all ready tasks first
|
|
59
|
+
all_ready = task_manager.list_ready_tasks(
|
|
60
|
+
task_type=task_type,
|
|
61
|
+
limit=200, # Get more since we'll filter
|
|
62
|
+
project_id=project_id,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if not all_ready:
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
# Build a set of all descendant IDs by traversing the hierarchy
|
|
69
|
+
descendant_ids = set()
|
|
70
|
+
to_check = [parent_task_id]
|
|
71
|
+
|
|
72
|
+
while to_check:
|
|
73
|
+
current_id = to_check.pop()
|
|
74
|
+
# Get direct children of this task
|
|
75
|
+
children = task_manager.list_tasks(parent_task_id=current_id, limit=100)
|
|
76
|
+
for child in children:
|
|
77
|
+
if child.id not in descendant_ids:
|
|
78
|
+
descendant_ids.add(child.id)
|
|
79
|
+
to_check.append(child.id)
|
|
80
|
+
|
|
81
|
+
# Filter ready tasks to only descendants
|
|
82
|
+
return [t for t in all_ready if t.id in descendant_ids]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_descendant_of(
|
|
86
|
+
task_manager: "LocalTaskManager",
|
|
87
|
+
task_id: str,
|
|
88
|
+
ancestor_id: str,
|
|
89
|
+
) -> bool:
|
|
90
|
+
"""
|
|
91
|
+
Check if a task is a descendant of another task.
|
|
92
|
+
|
|
93
|
+
Traverses up the parent chain from task_id to check if
|
|
94
|
+
ancestor_id appears in the ancestry.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
task_manager: LocalTaskManager instance
|
|
98
|
+
task_id: ID of the potential descendant task
|
|
99
|
+
ancestor_id: ID of the potential ancestor task
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if task_id is a descendant of ancestor_id
|
|
103
|
+
"""
|
|
104
|
+
if task_id == ancestor_id:
|
|
105
|
+
return True # A task is considered a descendant of itself
|
|
106
|
+
|
|
107
|
+
current_id: str | None = task_id
|
|
108
|
+
visited: set[str] = set()
|
|
109
|
+
|
|
110
|
+
while current_id and current_id not in visited:
|
|
111
|
+
visited.add(current_id)
|
|
112
|
+
task = task_manager.get_task(current_id)
|
|
113
|
+
if not task:
|
|
114
|
+
return False
|
|
115
|
+
if task.parent_task_id == ancestor_id:
|
|
116
|
+
return True
|
|
117
|
+
current_id = task.parent_task_id
|
|
118
|
+
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_ancestry_chain(
|
|
123
|
+
task_manager: "LocalTaskManager",
|
|
124
|
+
task_id: str,
|
|
125
|
+
) -> list[str]:
|
|
126
|
+
"""
|
|
127
|
+
Build the ancestry chain for a task, from task up to root.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
task_manager: LocalTaskManager instance
|
|
131
|
+
task_id: ID of the task to get ancestry for
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of task IDs starting with task_id and ending with root ancestor.
|
|
135
|
+
Returns empty list if task doesn't exist.
|
|
136
|
+
"""
|
|
137
|
+
chain: list[str] = []
|
|
138
|
+
current_id: str | None = task_id
|
|
139
|
+
visited: set[str] = set()
|
|
140
|
+
|
|
141
|
+
while current_id and current_id not in visited:
|
|
142
|
+
try:
|
|
143
|
+
task = task_manager.get_task(current_id)
|
|
144
|
+
except ValueError:
|
|
145
|
+
# Task doesn't exist - return chain so far or empty if just started
|
|
146
|
+
return chain if chain else []
|
|
147
|
+
visited.add(current_id)
|
|
148
|
+
chain.append(current_id)
|
|
149
|
+
current_id = task.parent_task_id
|
|
150
|
+
|
|
151
|
+
return chain
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _compute_proximity_boost(
|
|
155
|
+
task_ancestry: list[str],
|
|
156
|
+
active_ancestry: list[str],
|
|
157
|
+
) -> int:
|
|
158
|
+
"""
|
|
159
|
+
Compute proximity boost based on common ancestry.
|
|
160
|
+
|
|
161
|
+
The boost is higher for tasks closer to the active task in the hierarchy.
|
|
162
|
+
- If task is a descendant of the active task: max boost (50)
|
|
163
|
+
- Otherwise: max(0, 50 - (depth * 10)) where depth is distance to common ancestor
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
task_ancestry: Ancestry chain of the candidate task [task, parent, grandparent, ...]
|
|
167
|
+
active_ancestry: Ancestry chain of the active (in_progress) task
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Proximity boost score (0-50)
|
|
171
|
+
"""
|
|
172
|
+
if not task_ancestry or not active_ancestry:
|
|
173
|
+
return 0
|
|
174
|
+
|
|
175
|
+
# Convert to sets for O(1) lookup
|
|
176
|
+
active_set = set(active_ancestry)
|
|
177
|
+
active_task_id = active_ancestry[0]
|
|
178
|
+
|
|
179
|
+
# Find first common ancestor and its depth from the task
|
|
180
|
+
for depth, ancestor_id in enumerate(task_ancestry):
|
|
181
|
+
if ancestor_id in active_set:
|
|
182
|
+
# If common ancestor is the active task itself, task is a descendant
|
|
183
|
+
# of active work - give max boost
|
|
184
|
+
if ancestor_id == active_task_id:
|
|
185
|
+
return 50
|
|
186
|
+
# Otherwise, use depth from common ancestor
|
|
187
|
+
return max(0, 50 - (depth * 10))
|
|
188
|
+
|
|
189
|
+
# No common ancestor found
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ReadinessToolRegistry(InternalToolRegistry):
|
|
194
|
+
"""Registry for readiness tools with test-friendly get_tool method."""
|
|
195
|
+
|
|
196
|
+
def get_tool(self, name: str) -> Callable[..., Any] | None:
|
|
197
|
+
"""Get a tool function by name (for testing)."""
|
|
198
|
+
tool = self._tools.get(name)
|
|
199
|
+
return tool.func if tool else None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def create_readiness_registry(
|
|
203
|
+
task_manager: "LocalTaskManager | None" = None,
|
|
204
|
+
) -> ReadinessToolRegistry:
|
|
205
|
+
"""
|
|
206
|
+
Create a registry with task readiness tools.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
task_manager: LocalTaskManager instance (required)
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
ReadinessToolRegistry with readiness tools registered
|
|
213
|
+
"""
|
|
214
|
+
# Lazy import to avoid circular dependency
|
|
215
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
216
|
+
|
|
217
|
+
registry = ReadinessToolRegistry(
|
|
218
|
+
name="gobby-tasks-readiness",
|
|
219
|
+
description="Task readiness management tools",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
if task_manager is None:
|
|
223
|
+
raise ValueError("task_manager is required")
|
|
224
|
+
|
|
225
|
+
# Create workflow state manager for session_task scoping
|
|
226
|
+
workflow_state_manager = WorkflowStateManager(task_manager.db)
|
|
227
|
+
|
|
228
|
+
# --- list_ready_tasks ---
|
|
229
|
+
|
|
230
|
+
def list_ready_tasks(
|
|
231
|
+
priority: int | None = None,
|
|
232
|
+
task_type: str | None = None,
|
|
233
|
+
assignee: str | None = None,
|
|
234
|
+
parent_task_id: str | None = None,
|
|
235
|
+
limit: int = 10,
|
|
236
|
+
all_projects: bool = False,
|
|
237
|
+
) -> dict[str, Any]:
|
|
238
|
+
"""List tasks that are open and have no unresolved blocking dependencies."""
|
|
239
|
+
# Filter by current project unless all_projects is True
|
|
240
|
+
project_id = None if all_projects else get_current_project_id()
|
|
241
|
+
|
|
242
|
+
# Resolve parent_task_id if it's a reference format
|
|
243
|
+
if parent_task_id:
|
|
244
|
+
try:
|
|
245
|
+
parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
|
|
246
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
247
|
+
return {"error": f"Invalid parent_task_id: {e}", "tasks": [], "count": 0}
|
|
248
|
+
|
|
249
|
+
tasks = task_manager.list_ready_tasks(
|
|
250
|
+
priority=priority,
|
|
251
|
+
task_type=task_type,
|
|
252
|
+
assignee=assignee,
|
|
253
|
+
parent_task_id=parent_task_id,
|
|
254
|
+
limit=limit,
|
|
255
|
+
project_id=project_id,
|
|
256
|
+
)
|
|
257
|
+
return {"tasks": [t.to_brief() for t in tasks], "count": len(tasks)}
|
|
258
|
+
|
|
259
|
+
registry.register(
|
|
260
|
+
name="list_ready_tasks",
|
|
261
|
+
description="List tasks that are open and have no unresolved blocking dependencies.",
|
|
262
|
+
input_schema={
|
|
263
|
+
"type": "object",
|
|
264
|
+
"properties": {
|
|
265
|
+
"priority": {
|
|
266
|
+
"type": "integer",
|
|
267
|
+
"description": "Filter by priority",
|
|
268
|
+
"default": None,
|
|
269
|
+
},
|
|
270
|
+
"task_type": {"type": "string", "description": "Filter by type", "default": None},
|
|
271
|
+
"assignee": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "Filter by assignee",
|
|
274
|
+
"default": None,
|
|
275
|
+
},
|
|
276
|
+
"parent_task_id": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"description": "Filter by parent task (find ready subtasks): #N, N (seq_num), path (1.2.3), or UUID",
|
|
279
|
+
"default": None,
|
|
280
|
+
},
|
|
281
|
+
"limit": {"type": "integer", "description": "Max results", "default": 10},
|
|
282
|
+
"all_projects": {
|
|
283
|
+
"type": "boolean",
|
|
284
|
+
"description": "If true, list tasks from all projects instead of just the current project",
|
|
285
|
+
"default": False,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
func=list_ready_tasks,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# --- list_blocked_tasks ---
|
|
293
|
+
|
|
294
|
+
def list_blocked_tasks(
|
|
295
|
+
parent_task_id: str | None = None,
|
|
296
|
+
limit: int = 20,
|
|
297
|
+
all_projects: bool = False,
|
|
298
|
+
) -> dict[str, Any]:
|
|
299
|
+
"""List tasks that are currently blocked, including what blocks them."""
|
|
300
|
+
# Filter by current project unless all_projects is True
|
|
301
|
+
project_id = None if all_projects else get_current_project_id()
|
|
302
|
+
|
|
303
|
+
# Resolve parent_task_id if it's a reference format
|
|
304
|
+
if parent_task_id:
|
|
305
|
+
try:
|
|
306
|
+
parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
|
|
307
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
308
|
+
return {"error": f"Invalid parent_task_id: {e}", "tasks": [], "count": 0}
|
|
309
|
+
|
|
310
|
+
blocked_tasks = task_manager.list_blocked_tasks(
|
|
311
|
+
parent_task_id=parent_task_id,
|
|
312
|
+
limit=limit,
|
|
313
|
+
project_id=project_id,
|
|
314
|
+
)
|
|
315
|
+
return {"tasks": [t.to_brief() for t in blocked_tasks], "count": len(blocked_tasks)}
|
|
316
|
+
|
|
317
|
+
registry.register(
|
|
318
|
+
name="list_blocked_tasks",
|
|
319
|
+
description="List tasks that are currently blocked by external dependencies (excludes parent tasks blocked by their own children).",
|
|
320
|
+
input_schema={
|
|
321
|
+
"type": "object",
|
|
322
|
+
"properties": {
|
|
323
|
+
"parent_task_id": {
|
|
324
|
+
"type": "string",
|
|
325
|
+
"description": "Filter by parent task (find blocked subtasks): #N, N (seq_num), path (1.2.3), or UUID",
|
|
326
|
+
"default": None,
|
|
327
|
+
},
|
|
328
|
+
"limit": {"type": "integer", "description": "Max results", "default": 20},
|
|
329
|
+
"all_projects": {
|
|
330
|
+
"type": "boolean",
|
|
331
|
+
"description": "If true, list tasks from all projects instead of just the current project",
|
|
332
|
+
"default": False,
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
func=list_blocked_tasks,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# --- suggest_next_task ---
|
|
340
|
+
|
|
341
|
+
def suggest_next_task(
|
|
342
|
+
task_type: str | None = None,
|
|
343
|
+
prefer_subtasks: bool = True,
|
|
344
|
+
parent_task_id: str | None = None,
|
|
345
|
+
session_id: str | None = None,
|
|
346
|
+
) -> dict[str, Any]:
|
|
347
|
+
"""
|
|
348
|
+
Suggest the best next task to work on.
|
|
349
|
+
|
|
350
|
+
Uses a scoring algorithm considering:
|
|
351
|
+
- Task is ready (no blockers)
|
|
352
|
+
- Priority (higher priority = higher score)
|
|
353
|
+
- Is a leaf task (subtask with no children)
|
|
354
|
+
- Has clear scope (complexity_score if available)
|
|
355
|
+
- Proximity to current in_progress task (same branch preferred)
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
task_type: Filter by task type (optional)
|
|
359
|
+
prefer_subtasks: Prefer leaf tasks over parent tasks (default: True)
|
|
360
|
+
parent_task_id: Filter to descendants of this task (optional).
|
|
361
|
+
When set, only tasks under this parent hierarchy are considered.
|
|
362
|
+
Use this to explicitly scope suggestions to a specific epic/feature.
|
|
363
|
+
session_id: Your session ID (required for MCP callers, from system context).
|
|
364
|
+
When provided and parent_task_id is not set, checks workflow state
|
|
365
|
+
for session_task variable and auto-scopes suggestions to that task's
|
|
366
|
+
hierarchy. Function signature is optional for TUI/internal callers.
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
Suggested task with reasoning
|
|
370
|
+
"""
|
|
371
|
+
# Filter by current project
|
|
372
|
+
project_id = get_current_project_id()
|
|
373
|
+
|
|
374
|
+
# Auto-scope to session_task if session_id is provided and parent_task_id is not set
|
|
375
|
+
if session_id and not parent_task_id:
|
|
376
|
+
workflow_state = workflow_state_manager.get_state(session_id)
|
|
377
|
+
if workflow_state:
|
|
378
|
+
session_task = workflow_state.variables.get("session_task")
|
|
379
|
+
if session_task and session_task != "*":
|
|
380
|
+
# session_task is set, use it as parent_task_id for scoping
|
|
381
|
+
parent_task_id = session_task
|
|
382
|
+
|
|
383
|
+
# Resolve parent_task_id if it's a reference format
|
|
384
|
+
if parent_task_id:
|
|
385
|
+
try:
|
|
386
|
+
parent_task_id = resolve_task_id_for_mcp(task_manager, parent_task_id, project_id)
|
|
387
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
388
|
+
return {"error": f"Invalid parent_task_id: {e}", "suggestion": None}
|
|
389
|
+
|
|
390
|
+
# If parent_task_id is set, get all descendants of that task
|
|
391
|
+
if parent_task_id:
|
|
392
|
+
ready_tasks = _get_ready_descendants(
|
|
393
|
+
task_manager, parent_task_id, task_type, project_id
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
ready_tasks = task_manager.list_ready_tasks(
|
|
397
|
+
task_type=task_type, limit=50, project_id=project_id
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if not ready_tasks:
|
|
401
|
+
return {
|
|
402
|
+
"suggestion": None,
|
|
403
|
+
"reason": "No ready tasks found",
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# Find current in_progress task for proximity scoring
|
|
407
|
+
in_progress_tasks = task_manager.list_tasks(
|
|
408
|
+
status="in_progress", limit=1, project_id=project_id
|
|
409
|
+
)
|
|
410
|
+
active_ancestry: list[str] = []
|
|
411
|
+
active_task_id: str | None = None
|
|
412
|
+
if in_progress_tasks:
|
|
413
|
+
active_task_id = in_progress_tasks[0].id
|
|
414
|
+
active_ancestry = _get_ancestry_chain(task_manager, active_task_id)
|
|
415
|
+
|
|
416
|
+
# Filter out in_progress tasks - we want to suggest the NEXT task, not current
|
|
417
|
+
ready_tasks = [t for t in ready_tasks if t.status != "in_progress"]
|
|
418
|
+
if not ready_tasks:
|
|
419
|
+
return {
|
|
420
|
+
"suggestion": None,
|
|
421
|
+
"reason": "No ready tasks found (all tasks are in_progress)",
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
# Score each task
|
|
425
|
+
scored = []
|
|
426
|
+
for task in ready_tasks:
|
|
427
|
+
score = 0
|
|
428
|
+
proximity_boost = 0
|
|
429
|
+
|
|
430
|
+
# Priority boost - use weight of 110 per level to ensure priority dominates
|
|
431
|
+
# This makes the gap between priority levels (110) larger than max possible
|
|
432
|
+
# bonuses (leaf=25 + complexity=15 + category=10 + proximity=50 = 100)
|
|
433
|
+
# Priority 0 (critical): 440, Priority 1 (high): 330, etc.
|
|
434
|
+
score += (4 - task.priority) * 110
|
|
435
|
+
|
|
436
|
+
# Check if it's a leaf task (no children)
|
|
437
|
+
children = task_manager.list_tasks(parent_task_id=task.id, status="open", limit=1)
|
|
438
|
+
is_leaf = len(children) == 0
|
|
439
|
+
|
|
440
|
+
if prefer_subtasks and is_leaf:
|
|
441
|
+
score += 25 # Prefer actionable leaf tasks
|
|
442
|
+
|
|
443
|
+
# Bonus for tasks with clear complexity
|
|
444
|
+
if task.complexity_score and task.complexity_score <= 5:
|
|
445
|
+
score += 15 # Prefer lower complexity tasks
|
|
446
|
+
|
|
447
|
+
# Bonus for tasks with category defined
|
|
448
|
+
if task.category:
|
|
449
|
+
score += 10
|
|
450
|
+
|
|
451
|
+
# Proximity boost based on ancestry relationship to in_progress task
|
|
452
|
+
if active_ancestry:
|
|
453
|
+
task_ancestry = _get_ancestry_chain(task_manager, task.id)
|
|
454
|
+
proximity_boost = _compute_proximity_boost(task_ancestry, active_ancestry)
|
|
455
|
+
score += proximity_boost
|
|
456
|
+
|
|
457
|
+
scored.append((task, score, is_leaf, proximity_boost))
|
|
458
|
+
|
|
459
|
+
# Sort by score descending
|
|
460
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
461
|
+
best_task, best_score, is_leaf, best_proximity = scored[0]
|
|
462
|
+
|
|
463
|
+
reasons = []
|
|
464
|
+
if best_task.priority == 0:
|
|
465
|
+
reasons.append("critical priority")
|
|
466
|
+
elif best_task.priority == 1:
|
|
467
|
+
reasons.append("high priority")
|
|
468
|
+
if is_leaf:
|
|
469
|
+
reasons.append("actionable leaf task")
|
|
470
|
+
if best_task.complexity_score and best_task.complexity_score <= 5:
|
|
471
|
+
reasons.append("manageable complexity")
|
|
472
|
+
if best_task.category:
|
|
473
|
+
reasons.append(f"has category ({best_task.category})")
|
|
474
|
+
if best_proximity > 0:
|
|
475
|
+
reasons.append("same branch as current work")
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
"suggestion": best_task.to_brief(),
|
|
479
|
+
"score": best_score,
|
|
480
|
+
"reason": f"Selected because: {', '.join(reasons) if reasons else 'best available option'}",
|
|
481
|
+
"alternatives": [
|
|
482
|
+
{"ref": t.to_brief()["ref"], "title": t.title, "score": s}
|
|
483
|
+
for t, s, _, _ in scored[1:4] # Show top 3 alternatives
|
|
484
|
+
],
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
registry.register(
|
|
488
|
+
name="suggest_next_task",
|
|
489
|
+
description="Suggest the best next task to work on based on priority, readiness, and complexity. "
|
|
490
|
+
"Requires session_id to check workflow's session_task variable for automatic scoping. "
|
|
491
|
+
"Use parent_task_id to explicitly scope suggestions to a specific epic/feature hierarchy.",
|
|
492
|
+
input_schema={
|
|
493
|
+
"type": "object",
|
|
494
|
+
"properties": {
|
|
495
|
+
"task_type": {
|
|
496
|
+
"type": "string",
|
|
497
|
+
"description": "Filter by task type (optional)",
|
|
498
|
+
"default": None,
|
|
499
|
+
},
|
|
500
|
+
"prefer_subtasks": {
|
|
501
|
+
"type": "boolean",
|
|
502
|
+
"description": "Prefer leaf tasks over parent tasks (default: True)",
|
|
503
|
+
"default": True,
|
|
504
|
+
},
|
|
505
|
+
"parent_task_id": {
|
|
506
|
+
"type": "string",
|
|
507
|
+
"description": "Filter to descendants of this task (#N, N, path, or UUID). "
|
|
508
|
+
"When set, only tasks under this parent hierarchy are considered. "
|
|
509
|
+
"Use this to scope suggestions to a specific epic/feature.",
|
|
510
|
+
"default": None,
|
|
511
|
+
},
|
|
512
|
+
"session_id": {
|
|
513
|
+
"type": "string",
|
|
514
|
+
"description": "Your session ID (from system context). Used to auto-scope suggestions based on workflow's session_task variable.",
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
"required": ["session_id"],
|
|
518
|
+
},
|
|
519
|
+
func=suggest_next_task,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
return registry
|