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,591 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task expansion MCP tools module.
|
|
3
|
+
|
|
4
|
+
Provides tools for expanding tasks into subtasks with automatic TDD sandwich pattern:
|
|
5
|
+
- expand_task: Expand task into subtasks via AI (auto-applies TDD sandwich)
|
|
6
|
+
- analyze_complexity: Analyze task complexity
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
15
|
+
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
16
|
+
from gobby.storage.tasks import LocalTaskManager, Task, TaskNotFoundError
|
|
17
|
+
|
|
18
|
+
# Import shared TDD utilities
|
|
19
|
+
from gobby.tasks.tdd import (
|
|
20
|
+
TDD_CATEGORIES,
|
|
21
|
+
TDD_PREFIXES,
|
|
22
|
+
TDD_SKIP_PATTERNS,
|
|
23
|
+
apply_tdd_sandwich,
|
|
24
|
+
build_expansion_context,
|
|
25
|
+
should_skip_expansion,
|
|
26
|
+
should_skip_tdd,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from gobby.tasks.expansion import TaskExpander
|
|
31
|
+
from gobby.tasks.validation import TaskValidator
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Re-export for backwards compatibility
|
|
36
|
+
__all__ = [
|
|
37
|
+
"create_expansion_registry",
|
|
38
|
+
"should_skip_tdd",
|
|
39
|
+
"TDD_PREFIXES",
|
|
40
|
+
"TDD_SKIP_PATTERNS",
|
|
41
|
+
"TDD_CATEGORIES",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_expansion_registry(
|
|
46
|
+
task_manager: LocalTaskManager,
|
|
47
|
+
task_expander: "TaskExpander | None" = None,
|
|
48
|
+
task_validator: "TaskValidator | None" = None,
|
|
49
|
+
auto_generate_on_expand: bool = True,
|
|
50
|
+
resolve_tdd_mode: Callable[[str | None], bool] | None = None,
|
|
51
|
+
) -> InternalToolRegistry:
|
|
52
|
+
"""
|
|
53
|
+
Create a registry with task expansion tools.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
task_manager: LocalTaskManager instance
|
|
57
|
+
task_expander: TaskExpander instance (optional, required for AI expansion)
|
|
58
|
+
task_validator: TaskValidator instance (optional, for auto-generating criteria)
|
|
59
|
+
auto_generate_on_expand: Whether to auto-generate validation criteria on expand
|
|
60
|
+
resolve_tdd_mode: Function to resolve TDD mode from session (optional)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
InternalToolRegistry with expansion tools registered
|
|
64
|
+
"""
|
|
65
|
+
# Lazy import to avoid circular dependency
|
|
66
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
67
|
+
|
|
68
|
+
registry = InternalToolRegistry(
|
|
69
|
+
name="gobby-tasks-expansion",
|
|
70
|
+
description="Task expansion tools - AI and structured parsing",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Create helper managers
|
|
74
|
+
dep_manager = TaskDependencyManager(task_manager.db)
|
|
75
|
+
|
|
76
|
+
async def _apply_tdd_sandwich_wrapper(
|
|
77
|
+
parent_task_id: str,
|
|
78
|
+
impl_task_ids: list[str],
|
|
79
|
+
refactor_task_ids: list[str] | None = None,
|
|
80
|
+
) -> dict[str, Any]:
|
|
81
|
+
"""Wrapper to call shared apply_tdd_sandwich with local managers."""
|
|
82
|
+
return await apply_tdd_sandwich(
|
|
83
|
+
task_manager, dep_manager, parent_task_id, impl_task_ids, refactor_task_ids
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _find_unexpanded_epic(root_task_id: str) -> Task | None:
|
|
87
|
+
"""Depth-first search for first unexpanded epic in the task tree.
|
|
88
|
+
|
|
89
|
+
Traverses the task tree starting from root_task_id to find the first
|
|
90
|
+
epic that hasn't been expanded yet (is_expanded=False).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
root_task_id: Task ID to start search from
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
First unexpanded epic Task, or None if all epics are expanded
|
|
97
|
+
"""
|
|
98
|
+
task = task_manager.get_task(root_task_id)
|
|
99
|
+
if not task:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Check if this task itself is an unexpanded epic
|
|
103
|
+
if task.task_type == "epic" and not task.is_expanded:
|
|
104
|
+
return task
|
|
105
|
+
|
|
106
|
+
# Search children depth-first
|
|
107
|
+
children = task_manager.list_tasks(parent_task_id=root_task_id, limit=1000)
|
|
108
|
+
for child in children:
|
|
109
|
+
if child.task_type == "epic":
|
|
110
|
+
result = _find_unexpanded_epic(child.id)
|
|
111
|
+
if result:
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _count_unexpanded_epics(root_task_id: str) -> int:
|
|
117
|
+
"""Count unexpanded epics in the task tree.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
root_task_id: Task ID to start counting from
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Number of unexpanded epics in the tree
|
|
124
|
+
"""
|
|
125
|
+
count = 0
|
|
126
|
+
task = task_manager.get_task(root_task_id)
|
|
127
|
+
if not task:
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
# Count this task if it's an unexpanded epic
|
|
131
|
+
if task.task_type == "epic" and not task.is_expanded:
|
|
132
|
+
count += 1
|
|
133
|
+
|
|
134
|
+
# Count children recursively
|
|
135
|
+
children = task_manager.list_tasks(parent_task_id=root_task_id, limit=1000)
|
|
136
|
+
for child in children:
|
|
137
|
+
count += _count_unexpanded_epics(child.id)
|
|
138
|
+
|
|
139
|
+
return count
|
|
140
|
+
|
|
141
|
+
async def _expand_single_task(
|
|
142
|
+
single_task_id: str,
|
|
143
|
+
context: str | None,
|
|
144
|
+
enable_web_research: bool,
|
|
145
|
+
enable_code_context: bool,
|
|
146
|
+
should_generate_validation: bool,
|
|
147
|
+
skip_tdd: bool = False,
|
|
148
|
+
force: bool = False,
|
|
149
|
+
session_id: str | None = None,
|
|
150
|
+
iterative: bool = False,
|
|
151
|
+
) -> dict[str, Any]:
|
|
152
|
+
"""Internal helper to expand a single task.
|
|
153
|
+
|
|
154
|
+
When iterative=True, supports iterative expansion of epic trees:
|
|
155
|
+
- If the root task is already expanded, finds the next unexpanded epic
|
|
156
|
+
- Returns progress info (unexpanded_epics count, complete flag)
|
|
157
|
+
- Call repeatedly until complete=True
|
|
158
|
+
"""
|
|
159
|
+
# Resolve task reference
|
|
160
|
+
try:
|
|
161
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, single_task_id)
|
|
162
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
163
|
+
return {"error": f"Invalid task_id: {e}", "task_id": single_task_id}
|
|
164
|
+
|
|
165
|
+
if not task_expander:
|
|
166
|
+
return {"error": "Task expansion is not enabled", "task_id": single_task_id}
|
|
167
|
+
|
|
168
|
+
root_task = task_manager.get_task(resolved_task_id)
|
|
169
|
+
if not root_task:
|
|
170
|
+
return {"error": f"Task not found: {single_task_id}", "task_id": single_task_id}
|
|
171
|
+
|
|
172
|
+
# Auto-enable iterative mode for epics (timeout-safe cascade)
|
|
173
|
+
# Each call expands one epic and returns - caller loops until complete=True
|
|
174
|
+
if root_task.task_type == "epic" and not iterative:
|
|
175
|
+
iterative = True
|
|
176
|
+
|
|
177
|
+
# Iterative mode: find next unexpanded epic in tree
|
|
178
|
+
if iterative:
|
|
179
|
+
target_task = _find_unexpanded_epic(resolved_task_id)
|
|
180
|
+
if target_task is None:
|
|
181
|
+
# All epics expanded - tree is complete
|
|
182
|
+
return {
|
|
183
|
+
"complete": True,
|
|
184
|
+
"task_id": resolved_task_id,
|
|
185
|
+
"root_ref": f"#{root_task.seq_num}" if root_task.seq_num else root_task.id[:8],
|
|
186
|
+
"unexpanded_epics": 0,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Re-fetch to get latest state and verify task should be expanded
|
|
190
|
+
target_task = task_manager.get_task(target_task.id)
|
|
191
|
+
if target_task is None:
|
|
192
|
+
return {"error": "Task deleted during expansion", "task_id": resolved_task_id}
|
|
193
|
+
|
|
194
|
+
# Check if task should be skipped (TDD prefixes or already expanded)
|
|
195
|
+
skip, reason = should_skip_expansion(target_task.title, target_task.is_expanded, force)
|
|
196
|
+
if skip:
|
|
197
|
+
logger.info(f"Skipping task {target_task.id[:8]}: {reason}")
|
|
198
|
+
return {
|
|
199
|
+
"skipped": True,
|
|
200
|
+
"reason": reason,
|
|
201
|
+
"task_id": target_task.id,
|
|
202
|
+
"unexpanded_epics": _count_unexpanded_epics(resolved_task_id),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
task = target_task
|
|
206
|
+
else:
|
|
207
|
+
task = root_task
|
|
208
|
+
|
|
209
|
+
# Non-iterative mode: Check if task should be skipped
|
|
210
|
+
skip, reason = should_skip_expansion(task.title, task.is_expanded, force)
|
|
211
|
+
if skip:
|
|
212
|
+
if reason == "already expanded":
|
|
213
|
+
return {
|
|
214
|
+
"error": "Task already expanded (is_expanded=True). Use force=True to re-expand, or use iterative mode to expand child epics.",
|
|
215
|
+
"task_id": task.id,
|
|
216
|
+
"is_expanded": True,
|
|
217
|
+
}
|
|
218
|
+
else:
|
|
219
|
+
return {
|
|
220
|
+
"error": f"Cannot expand task: {reason}",
|
|
221
|
+
"task_id": task.id,
|
|
222
|
+
"skipped": True,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Check if task is a leaf (no children) - only in non-iterative mode
|
|
226
|
+
existing_children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
|
|
227
|
+
if existing_children:
|
|
228
|
+
return {
|
|
229
|
+
"error": "Task already has children. Only leaf tasks can be expanded. Use iterative mode or CLI with --cascade for parent tasks.",
|
|
230
|
+
"task_id": task.id,
|
|
231
|
+
"existing_children": len(existing_children),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Build context from any stored expansion_context + user context
|
|
235
|
+
merged_context = build_expansion_context(task.expansion_context, context)
|
|
236
|
+
|
|
237
|
+
# Note: TDD transformation is applied separately via apply_tdd command
|
|
238
|
+
result = await task_expander.expand_task(
|
|
239
|
+
task_id=task.id,
|
|
240
|
+
title=task.title,
|
|
241
|
+
description=task.description,
|
|
242
|
+
context=merged_context,
|
|
243
|
+
enable_web_research=enable_web_research,
|
|
244
|
+
enable_code_context=enable_code_context,
|
|
245
|
+
session_id=session_id,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Handle errors
|
|
249
|
+
if "error" in result:
|
|
250
|
+
return {"error": result["error"], "task_id": task.id}
|
|
251
|
+
|
|
252
|
+
# Extract subtask IDs (already created by agent via create_task tool calls)
|
|
253
|
+
subtask_ids = result.get("subtask_ids", [])
|
|
254
|
+
|
|
255
|
+
# Wire parent → subtask dependencies
|
|
256
|
+
for subtask_id in subtask_ids:
|
|
257
|
+
try:
|
|
258
|
+
dep_manager.add_dependency(
|
|
259
|
+
task_id=task.id, depends_on=subtask_id, dep_type="blocks"
|
|
260
|
+
)
|
|
261
|
+
except ValueError:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# Fetch created subtasks for the response (include seq_num for ergonomics)
|
|
265
|
+
created_subtasks = []
|
|
266
|
+
for sid in subtask_ids:
|
|
267
|
+
subtask = task_manager.get_task(sid)
|
|
268
|
+
if subtask:
|
|
269
|
+
subtask_info: dict[str, Any] = {
|
|
270
|
+
"ref": f"#{subtask.seq_num}" if subtask.seq_num else subtask.id[:8],
|
|
271
|
+
"title": subtask.title,
|
|
272
|
+
"seq_num": subtask.seq_num,
|
|
273
|
+
"id": subtask.id,
|
|
274
|
+
}
|
|
275
|
+
created_subtasks.append(subtask_info)
|
|
276
|
+
|
|
277
|
+
# Auto-generate validation criteria for each subtask (when enabled)
|
|
278
|
+
validation_generated = 0
|
|
279
|
+
validation_skipped_reason = None
|
|
280
|
+
if should_generate_validation and subtask_ids:
|
|
281
|
+
if not task_validator:
|
|
282
|
+
validation_skipped_reason = "task_validator not configured"
|
|
283
|
+
else:
|
|
284
|
+
for sid in subtask_ids:
|
|
285
|
+
subtask = task_manager.get_task(sid)
|
|
286
|
+
if subtask and not subtask.validation_criteria and subtask.task_type != "epic":
|
|
287
|
+
try:
|
|
288
|
+
criteria = await task_validator.generate_criteria(
|
|
289
|
+
title=subtask.title,
|
|
290
|
+
description=subtask.description,
|
|
291
|
+
)
|
|
292
|
+
if criteria:
|
|
293
|
+
task_manager.update_task(sid, validation_criteria=criteria)
|
|
294
|
+
validation_generated += 1
|
|
295
|
+
except Exception:
|
|
296
|
+
pass # nosec B110 - best-effort validation generation
|
|
297
|
+
|
|
298
|
+
# Update parent task: set is_expanded and validation criteria
|
|
299
|
+
task_manager.update_task(
|
|
300
|
+
task.id,
|
|
301
|
+
is_expanded=True,
|
|
302
|
+
validation_criteria="All child tasks must be completed (status: closed).",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Auto-apply TDD sandwich pattern (unless skip_tdd=True or expanding an epic)
|
|
306
|
+
# Creates ONE [TDD] task, renames impl tasks with [IMPL], creates ONE [REF] task
|
|
307
|
+
# IMPORTANT: Skip TDD when expanding epics - TDD is applied at feature task level
|
|
308
|
+
tdd_applied = False
|
|
309
|
+
tdd_categories = ("code", "config")
|
|
310
|
+
should_apply_tdd = not skip_tdd and subtask_ids and task.task_type != "epic"
|
|
311
|
+
logger.info(
|
|
312
|
+
f"TDD check: task={task.id[:8]} type={task.task_type} "
|
|
313
|
+
f"skip_tdd={skip_tdd} subtask_count={len(subtask_ids)} "
|
|
314
|
+
f"should_apply={should_apply_tdd}"
|
|
315
|
+
)
|
|
316
|
+
if should_apply_tdd:
|
|
317
|
+
# Collect code/config subtasks that should be wrapped in TDD sandwich
|
|
318
|
+
impl_task_ids = []
|
|
319
|
+
refactor_task_ids = []
|
|
320
|
+
for sid in subtask_ids:
|
|
321
|
+
subtask = task_manager.get_task(sid)
|
|
322
|
+
if not subtask:
|
|
323
|
+
continue
|
|
324
|
+
# Include code/config categories that aren't already TDD-formatted
|
|
325
|
+
if subtask.category in tdd_categories:
|
|
326
|
+
if not should_skip_tdd(subtask.title):
|
|
327
|
+
impl_task_ids.append(sid)
|
|
328
|
+
logger.debug(
|
|
329
|
+
f" TDD candidate: {subtask.id[:8]} category={subtask.category}"
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
logger.debug(
|
|
333
|
+
f" TDD skip (pattern): {subtask.id[:8]} title={subtask.title[:40]}"
|
|
334
|
+
)
|
|
335
|
+
elif subtask.category == "refactor":
|
|
336
|
+
# Refactor-category tasks get [REF] prefix and wire into dependency chain
|
|
337
|
+
if not should_skip_tdd(subtask.title):
|
|
338
|
+
refactor_task_ids.append(sid)
|
|
339
|
+
logger.debug(
|
|
340
|
+
f" TDD refactor candidate: {subtask.id[:8]} category={subtask.category}"
|
|
341
|
+
)
|
|
342
|
+
else:
|
|
343
|
+
logger.debug(
|
|
344
|
+
f" TDD skip (pattern): {subtask.id[:8]} title={subtask.title[:40]}"
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
logger.debug(
|
|
348
|
+
f" TDD skip (category): {subtask.id[:8]} category={subtask.category}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Apply TDD sandwich at parent level (wraps all impl tasks)
|
|
352
|
+
if impl_task_ids:
|
|
353
|
+
logger.info(
|
|
354
|
+
f"Applying TDD sandwich: {len(impl_task_ids)} code/config subtasks, "
|
|
355
|
+
f"{len(refactor_task_ids)} refactor subtasks"
|
|
356
|
+
)
|
|
357
|
+
tdd_result = await _apply_tdd_sandwich_wrapper(
|
|
358
|
+
resolved_task_id, impl_task_ids, refactor_task_ids or None
|
|
359
|
+
)
|
|
360
|
+
if tdd_result.get("success", False):
|
|
361
|
+
tdd_applied = True
|
|
362
|
+
logger.info(f"TDD sandwich applied successfully to {task.id[:8]}")
|
|
363
|
+
else:
|
|
364
|
+
logger.warning(f"TDD sandwich failed: {tdd_result}")
|
|
365
|
+
else:
|
|
366
|
+
logger.info(f"No code/config subtasks for TDD sandwich in {task.id[:8]}")
|
|
367
|
+
|
|
368
|
+
# Build response
|
|
369
|
+
response: dict[str, Any] = {
|
|
370
|
+
"task_id": task.id,
|
|
371
|
+
"tasks_created": len(subtask_ids),
|
|
372
|
+
"subtasks": created_subtasks,
|
|
373
|
+
"is_expanded": True,
|
|
374
|
+
}
|
|
375
|
+
# Include seq_num refs for ergonomics
|
|
376
|
+
if task.seq_num is not None:
|
|
377
|
+
response["expanded_ref"] = f"#{task.seq_num}"
|
|
378
|
+
# Keep legacy field for compatibility
|
|
379
|
+
response["parent_seq_num"] = task.seq_num
|
|
380
|
+
response["parent_ref"] = f"#{task.seq_num}"
|
|
381
|
+
|
|
382
|
+
# Iterative mode: include progress info
|
|
383
|
+
if iterative:
|
|
384
|
+
remaining = _count_unexpanded_epics(resolved_task_id)
|
|
385
|
+
response["unexpanded_epics"] = remaining
|
|
386
|
+
response["complete"] = remaining == 0
|
|
387
|
+
# Include root task ref for context
|
|
388
|
+
response["root_ref"] = (
|
|
389
|
+
f"#{root_task.seq_num}" if root_task.seq_num else root_task.id[:8]
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if validation_generated > 0:
|
|
393
|
+
response["validation_criteria_generated"] = validation_generated
|
|
394
|
+
if validation_skipped_reason:
|
|
395
|
+
response["validation_skipped_reason"] = validation_skipped_reason
|
|
396
|
+
if tdd_applied:
|
|
397
|
+
response["tdd_applied"] = True
|
|
398
|
+
return response
|
|
399
|
+
|
|
400
|
+
@registry.tool(
|
|
401
|
+
name="expand_task",
|
|
402
|
+
description="Expand a task into smaller subtasks using AI. Supports iterative expansion of epic trees.",
|
|
403
|
+
)
|
|
404
|
+
async def expand_task(
|
|
405
|
+
task_id: str | None = None,
|
|
406
|
+
task_ids: list[str] | None = None,
|
|
407
|
+
context: str | None = None,
|
|
408
|
+
enable_web_research: bool = False,
|
|
409
|
+
enable_code_context: bool = True,
|
|
410
|
+
generate_validation: bool | None = None,
|
|
411
|
+
skip_tdd: bool = False,
|
|
412
|
+
force: bool = False,
|
|
413
|
+
session_id: str | None = None,
|
|
414
|
+
iterative: bool = False,
|
|
415
|
+
) -> dict[str, Any]:
|
|
416
|
+
"""
|
|
417
|
+
Expand a task into subtasks using tool-based expansion.
|
|
418
|
+
|
|
419
|
+
The expansion agent calls create_task MCP tool directly to create subtasks,
|
|
420
|
+
wiring dependencies via the 'blocks' parameter.
|
|
421
|
+
|
|
422
|
+
## Iterative Mode (iterative=True)
|
|
423
|
+
|
|
424
|
+
For epic trees, call repeatedly on the root epic until complete=True:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
while True:
|
|
428
|
+
result = expand_task(task_id="#100", iterative=True)
|
|
429
|
+
if result.get("complete"):
|
|
430
|
+
break
|
|
431
|
+
print(f"Expanded {result['expanded_ref']}, {result['unexpanded_epics']} remaining")
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
- expanded_ref: The task that was expanded (may differ from input)
|
|
436
|
+
- unexpanded_epics: Count of remaining unexpanded epics
|
|
437
|
+
- complete: True when all epics in tree are expanded
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
task_id: ID of single task to expand (mutually exclusive with task_ids)
|
|
441
|
+
task_ids: List of task IDs for batch parallel expansion
|
|
442
|
+
context: Additional context for expansion
|
|
443
|
+
enable_web_research: Whether to enable web research (default: False)
|
|
444
|
+
enable_code_context: Whether to enable code context gathering (default: True)
|
|
445
|
+
generate_validation: Whether to auto-generate validation_criteria for subtasks.
|
|
446
|
+
Defaults to config setting (gobby_tasks.validation.auto_generate_on_expand).
|
|
447
|
+
skip_tdd: Skip automatic TDD transformation for code/config subtasks
|
|
448
|
+
force: Re-expand even if is_expanded=True
|
|
449
|
+
session_id: Session ID for TDD mode resolution (optional)
|
|
450
|
+
iterative: Enable iterative expansion mode for epic trees. When True, finds
|
|
451
|
+
the next unexpanded epic in the tree and expands it. Call repeatedly
|
|
452
|
+
until complete=True. (default: False)
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dictionary with expansion results. In iterative mode, includes:
|
|
456
|
+
- expanded_ref: Task that was expanded
|
|
457
|
+
- unexpanded_epics: Remaining unexpanded epics
|
|
458
|
+
- complete: True when tree is fully expanded
|
|
459
|
+
"""
|
|
460
|
+
# Use config default if not specified
|
|
461
|
+
should_generate_validation = (
|
|
462
|
+
generate_validation if generate_validation is not None else auto_generate_on_expand
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Validate parameters
|
|
466
|
+
if task_id and task_ids:
|
|
467
|
+
return {
|
|
468
|
+
"error": "task_id and task_ids are mutually exclusive. Provide one or the other."
|
|
469
|
+
}
|
|
470
|
+
if not task_id and not task_ids:
|
|
471
|
+
return {"error": "Either task_id or task_ids must be provided"}
|
|
472
|
+
if task_ids is not None and len(task_ids) == 0:
|
|
473
|
+
return {"error": "task_ids list cannot be empty"}
|
|
474
|
+
|
|
475
|
+
# Single task mode
|
|
476
|
+
if task_id:
|
|
477
|
+
return await _expand_single_task(
|
|
478
|
+
single_task_id=task_id,
|
|
479
|
+
context=context,
|
|
480
|
+
enable_web_research=enable_web_research,
|
|
481
|
+
enable_code_context=enable_code_context,
|
|
482
|
+
should_generate_validation=should_generate_validation,
|
|
483
|
+
skip_tdd=skip_tdd,
|
|
484
|
+
force=force,
|
|
485
|
+
session_id=session_id,
|
|
486
|
+
iterative=iterative,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# Batch mode - process tasks in parallel
|
|
490
|
+
# At this point, task_ids is guaranteed to be a non-empty list (validated above)
|
|
491
|
+
assert task_ids is not None # nosec B101 - Type narrowing for mypy
|
|
492
|
+
|
|
493
|
+
async def expand_one(tid: str) -> dict[str, Any]:
|
|
494
|
+
return await _expand_single_task(
|
|
495
|
+
single_task_id=tid,
|
|
496
|
+
context=context,
|
|
497
|
+
enable_web_research=enable_web_research,
|
|
498
|
+
enable_code_context=enable_code_context,
|
|
499
|
+
should_generate_validation=should_generate_validation,
|
|
500
|
+
skip_tdd=skip_tdd,
|
|
501
|
+
force=force,
|
|
502
|
+
session_id=session_id,
|
|
503
|
+
iterative=iterative,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
raw_results = await asyncio.gather(
|
|
507
|
+
*[expand_one(tid) for tid in task_ids], return_exceptions=True
|
|
508
|
+
)
|
|
509
|
+
# Convert exceptions to error dicts to preserve per-task success/error information
|
|
510
|
+
processed_results: list[dict[str, Any]] = []
|
|
511
|
+
for i, result in enumerate(raw_results):
|
|
512
|
+
if isinstance(result, BaseException):
|
|
513
|
+
processed_results.append(
|
|
514
|
+
{"error": str(result), "task_id": task_ids[i], "success": False}
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
processed_results.append(result)
|
|
518
|
+
return {"results": processed_results}
|
|
519
|
+
|
|
520
|
+
@registry.tool(
|
|
521
|
+
name="analyze_complexity",
|
|
522
|
+
description="Analyze task complexity based on existing subtasks or description.",
|
|
523
|
+
)
|
|
524
|
+
async def analyze_complexity(task_id: str) -> dict[str, Any]:
|
|
525
|
+
"""
|
|
526
|
+
Analyze task complexity.
|
|
527
|
+
|
|
528
|
+
With tool-based expansion, this now analyzes existing subtasks if present,
|
|
529
|
+
or estimates complexity from description length. For detailed breakdown,
|
|
530
|
+
use expand_task which creates subtasks directly.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Complexity analysis with score and reasoning
|
|
537
|
+
"""
|
|
538
|
+
# Resolve task reference
|
|
539
|
+
try:
|
|
540
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
541
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
542
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
543
|
+
|
|
544
|
+
task = task_manager.get_task(resolved_task_id)
|
|
545
|
+
if not task:
|
|
546
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
547
|
+
|
|
548
|
+
# Check for existing subtasks
|
|
549
|
+
subtasks = task_manager.list_tasks(parent_task_id=task.id, limit=100)
|
|
550
|
+
subtask_count = len(subtasks)
|
|
551
|
+
|
|
552
|
+
# Simple heuristic-based complexity
|
|
553
|
+
if subtask_count > 0:
|
|
554
|
+
# Complexity based on subtask count
|
|
555
|
+
score = min(10, 1 + subtask_count // 2)
|
|
556
|
+
reasoning = f"Task has {subtask_count} subtasks"
|
|
557
|
+
recommended = subtask_count
|
|
558
|
+
else:
|
|
559
|
+
# Estimate from description length
|
|
560
|
+
desc_len = len(task.description or "")
|
|
561
|
+
if desc_len < 100:
|
|
562
|
+
score = 2
|
|
563
|
+
reasoning = "Short description, likely simple task"
|
|
564
|
+
recommended = 2
|
|
565
|
+
elif desc_len < 500:
|
|
566
|
+
score = 5
|
|
567
|
+
reasoning = "Medium description, moderate complexity"
|
|
568
|
+
recommended = 5
|
|
569
|
+
else:
|
|
570
|
+
score = 8
|
|
571
|
+
reasoning = "Long description, likely complex task"
|
|
572
|
+
recommended = 10
|
|
573
|
+
|
|
574
|
+
# Update task with complexity score
|
|
575
|
+
task_manager.update_task(
|
|
576
|
+
task.id,
|
|
577
|
+
complexity_score=score,
|
|
578
|
+
estimated_subtasks=recommended,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
"task_id": task.id,
|
|
583
|
+
"title": task.title,
|
|
584
|
+
"complexity_score": score,
|
|
585
|
+
"reasoning": reasoning,
|
|
586
|
+
"recommended_subtasks": recommended,
|
|
587
|
+
"existing_subtasks": subtask_count,
|
|
588
|
+
"note": "For detailed breakdown, use expand_task to create subtasks",
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return registry
|