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,843 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation MCP tools for Gobby Task System.
|
|
3
|
+
|
|
4
|
+
Extracted from tasks.py using Strangler Fig pattern.
|
|
5
|
+
|
|
6
|
+
Exposes functionality for:
|
|
7
|
+
- Task validation (validate_task, generate_validation_criteria)
|
|
8
|
+
- Validation status (get_validation_status, reset_validation_count)
|
|
9
|
+
- Validation history (get_validation_history, get_recurring_issues, clear_validation_history)
|
|
10
|
+
- De-escalation (de_escalate_task)
|
|
11
|
+
- QA loop (validate_and_fix, run_fix_attempt)
|
|
12
|
+
|
|
13
|
+
These tools are registered with the InternalToolRegistry and accessed
|
|
14
|
+
via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
21
|
+
from gobby.storage.tasks import LocalTaskManager, TaskNotFoundError
|
|
22
|
+
from gobby.tasks.validation import TaskValidator
|
|
23
|
+
from gobby.tasks.validation_history import ValidationHistoryManager
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from gobby.agents.runner import AgentRunner
|
|
27
|
+
from gobby.storage.projects import LocalProjectManager
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def create_validation_registry(
|
|
33
|
+
task_manager: LocalTaskManager,
|
|
34
|
+
task_validator: TaskValidator | None = None,
|
|
35
|
+
project_manager: "LocalProjectManager | None" = None,
|
|
36
|
+
get_project_repo_path: Any = None,
|
|
37
|
+
agent_runner: "AgentRunner | None" = None,
|
|
38
|
+
) -> InternalToolRegistry:
|
|
39
|
+
"""
|
|
40
|
+
Create a validation tool registry with all validation-related tools.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
task_manager: LocalTaskManager instance
|
|
44
|
+
task_validator: TaskValidator instance (optional, enables LLM validation)
|
|
45
|
+
project_manager: LocalProjectManager instance (optional)
|
|
46
|
+
get_project_repo_path: Callable to get repo path from project ID (optional)
|
|
47
|
+
agent_runner: AgentRunner instance (optional, enables fix agent spawning)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
InternalToolRegistry with all validation tools registered
|
|
51
|
+
"""
|
|
52
|
+
# Lazy import to avoid circular dependency
|
|
53
|
+
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
54
|
+
|
|
55
|
+
registry = InternalToolRegistry(
|
|
56
|
+
name="gobby-tasks-validation",
|
|
57
|
+
description="Task validation tools - validate, criteria, history",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Create helper managers
|
|
61
|
+
validation_history_manager = ValidationHistoryManager(task_manager.db)
|
|
62
|
+
|
|
63
|
+
@registry.tool(
|
|
64
|
+
name="validate_task",
|
|
65
|
+
description="Validate if a task is completed. Auto-gathers context from recent commits and relevant files if changes_summary not provided.",
|
|
66
|
+
)
|
|
67
|
+
async def validate_task(
|
|
68
|
+
task_id: str,
|
|
69
|
+
changes_summary: str | None = None,
|
|
70
|
+
context_files: list[str] | None = None,
|
|
71
|
+
) -> dict[str, Any]:
|
|
72
|
+
"""
|
|
73
|
+
Validate task completion.
|
|
74
|
+
|
|
75
|
+
For parent tasks (tasks with children), validation checks if all children are closed.
|
|
76
|
+
For leaf tasks, uses LLM-based validation against criteria.
|
|
77
|
+
|
|
78
|
+
If changes_summary is not provided for leaf tasks, uses smart context gathering:
|
|
79
|
+
1. Current uncommitted changes (staged + unstaged)
|
|
80
|
+
2. Multi-commit window (last 10 commits)
|
|
81
|
+
3. File-based analysis (reads files mentioned in criteria)
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
85
|
+
changes_summary: Summary of changes made (optional - auto-gathered if not provided)
|
|
86
|
+
context_files: List of file paths to read for context (optional)
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Validation result
|
|
90
|
+
"""
|
|
91
|
+
# Resolve task reference
|
|
92
|
+
try:
|
|
93
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
94
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
95
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
96
|
+
|
|
97
|
+
task = task_manager.get_task(resolved_task_id)
|
|
98
|
+
if not task:
|
|
99
|
+
return {"error": f"Task not found: {task_id}"}
|
|
100
|
+
|
|
101
|
+
# Check if task has children (is a parent task)
|
|
102
|
+
children = task_manager.list_tasks(parent_task_id=task.id, limit=1000)
|
|
103
|
+
|
|
104
|
+
if children:
|
|
105
|
+
# Parent task: validate based on child completion
|
|
106
|
+
open_children = [c for c in children if c.status != "closed"]
|
|
107
|
+
all_closed = len(open_children) == 0
|
|
108
|
+
|
|
109
|
+
from gobby.tasks.validation import ValidationResult
|
|
110
|
+
|
|
111
|
+
if all_closed:
|
|
112
|
+
result = ValidationResult(
|
|
113
|
+
status="valid",
|
|
114
|
+
feedback=f"All {len(children)} child tasks are completed.",
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
open_titles = [f"- {c.id}: {c.title}" for c in open_children[:5]]
|
|
118
|
+
remaining = len(open_children) - 5 if len(open_children) > 5 else 0
|
|
119
|
+
feedback = f"{len(open_children)} of {len(children)} child tasks still open:\n"
|
|
120
|
+
feedback += "\n".join(open_titles)
|
|
121
|
+
if remaining > 0:
|
|
122
|
+
feedback += f"\n... and {remaining} more"
|
|
123
|
+
result = ValidationResult(status="invalid", feedback=feedback)
|
|
124
|
+
else:
|
|
125
|
+
# Leaf task: use LLM-based validation
|
|
126
|
+
if not task_validator:
|
|
127
|
+
raise RuntimeError("Task validation is not enabled")
|
|
128
|
+
|
|
129
|
+
# Use provided changes_summary or auto-gather via smart context
|
|
130
|
+
validation_context = changes_summary
|
|
131
|
+
if not validation_context:
|
|
132
|
+
from gobby.tasks.validation import get_validation_context_smart
|
|
133
|
+
|
|
134
|
+
# Get project repo_path for git commands
|
|
135
|
+
repo_path = None
|
|
136
|
+
if get_project_repo_path and task.project_id:
|
|
137
|
+
repo_path = get_project_repo_path(task.project_id)
|
|
138
|
+
|
|
139
|
+
smart_context = get_validation_context_smart(
|
|
140
|
+
task_title=task.title,
|
|
141
|
+
validation_criteria=task.validation_criteria,
|
|
142
|
+
task_description=task.description,
|
|
143
|
+
cwd=repo_path,
|
|
144
|
+
)
|
|
145
|
+
if smart_context:
|
|
146
|
+
validation_context = f"Validation context:\n\n{smart_context}"
|
|
147
|
+
|
|
148
|
+
if not validation_context:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
"No changes found for validation. Either provide changes_summary "
|
|
151
|
+
"or ensure there are uncommitted changes or recent commits."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = await task_validator.validate_task(
|
|
155
|
+
task_id=task.id,
|
|
156
|
+
title=task.title,
|
|
157
|
+
description=task.description,
|
|
158
|
+
changes_summary=validation_context,
|
|
159
|
+
validation_criteria=task.validation_criteria,
|
|
160
|
+
context_files=context_files,
|
|
161
|
+
category=task.category,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Record validation iteration to history
|
|
165
|
+
# Calculate iteration number based on fail count (current fail count + 1 for this attempt)
|
|
166
|
+
current_fail_count = task.validation_fail_count or 0
|
|
167
|
+
iteration_number = current_fail_count + 1
|
|
168
|
+
|
|
169
|
+
# Determine validator type and context type
|
|
170
|
+
validator_type = "parent_completion" if children else "llm"
|
|
171
|
+
context_type = "child_status" if children else "smart_context"
|
|
172
|
+
context_summary = (
|
|
173
|
+
f"{len(children)} children checked" if children else "Auto-gathered from git/files"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
validation_history_manager.record_iteration(
|
|
177
|
+
task_id=task.id,
|
|
178
|
+
iteration=iteration_number,
|
|
179
|
+
status=result.status,
|
|
180
|
+
feedback=result.feedback,
|
|
181
|
+
issues=None, # ValidationResult from validation.py doesn't have issues
|
|
182
|
+
context_type=context_type,
|
|
183
|
+
context_summary=context_summary,
|
|
184
|
+
validator_type=validator_type,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Update validation status
|
|
188
|
+
updates: dict[str, Any] = {
|
|
189
|
+
"validation_status": result.status,
|
|
190
|
+
"validation_feedback": result.feedback,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
MAX_RETRIES = 3
|
|
194
|
+
|
|
195
|
+
if result.status == "valid":
|
|
196
|
+
# Success: Close task
|
|
197
|
+
task_manager.close_task(task.id, reason="Completed via validation")
|
|
198
|
+
elif result.status == "invalid":
|
|
199
|
+
# Failure: Increment fail count
|
|
200
|
+
current_fail_count = task.validation_fail_count or 0
|
|
201
|
+
new_fail_count = current_fail_count + 1
|
|
202
|
+
updates["validation_fail_count"] = new_fail_count
|
|
203
|
+
|
|
204
|
+
feedback_str = result.feedback or "Validation failed (no feedback provided)."
|
|
205
|
+
|
|
206
|
+
if new_fail_count < MAX_RETRIES:
|
|
207
|
+
# Create subtask to fix issues
|
|
208
|
+
fix_task = task_manager.create_task(
|
|
209
|
+
project_id=task.project_id,
|
|
210
|
+
title=f"Fix validation failures for {task.title}",
|
|
211
|
+
description=f"Validation failed with feedback:\n{feedback_str}\n\nPlease fix the issues and re-validate.",
|
|
212
|
+
parent_task_id=task.id,
|
|
213
|
+
priority=1, # High priority fix
|
|
214
|
+
task_type="bug",
|
|
215
|
+
)
|
|
216
|
+
updates["validation_feedback"] = (
|
|
217
|
+
feedback_str + f"\n\nCreated fix task: {fix_task.id}"
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
# Exceeded retries: Mark as failed
|
|
221
|
+
updates["status"] = "failed"
|
|
222
|
+
updates["validation_feedback"] = (
|
|
223
|
+
feedback_str + f"\n\nExceeded max retries ({MAX_RETRIES}). Marked as failed."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
task_manager.update_task(task.id, **updates)
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
"is_valid": result.status == "valid",
|
|
230
|
+
"feedback": result.feedback,
|
|
231
|
+
"status": result.status,
|
|
232
|
+
"fail_count": updates.get("validation_fail_count", task.validation_fail_count),
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@registry.tool(
|
|
236
|
+
name="get_validation_status",
|
|
237
|
+
description="Get validation details for a task.",
|
|
238
|
+
)
|
|
239
|
+
def get_validation_status(task_id: str) -> dict[str, Any]:
|
|
240
|
+
"""
|
|
241
|
+
Get validation details.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Validation details
|
|
248
|
+
"""
|
|
249
|
+
# Resolve task reference
|
|
250
|
+
try:
|
|
251
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
252
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
253
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
254
|
+
|
|
255
|
+
task = task_manager.get_task(resolved_task_id)
|
|
256
|
+
if not task:
|
|
257
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"task_id": task.id,
|
|
261
|
+
"validation_status": task.validation_status,
|
|
262
|
+
"validation_feedback": task.validation_feedback,
|
|
263
|
+
"validation_criteria": task.validation_criteria,
|
|
264
|
+
"validation_fail_count": task.validation_fail_count,
|
|
265
|
+
"use_external_validator": task.use_external_validator,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@registry.tool(
|
|
269
|
+
name="reset_validation_count",
|
|
270
|
+
description="Reset validation failure count for a task.",
|
|
271
|
+
)
|
|
272
|
+
def reset_validation_count(task_id: str) -> dict[str, Any]:
|
|
273
|
+
"""
|
|
274
|
+
Reset validation failure count.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Updated task details
|
|
281
|
+
"""
|
|
282
|
+
# Resolve task reference
|
|
283
|
+
try:
|
|
284
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
285
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
286
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
287
|
+
|
|
288
|
+
task = task_manager.get_task(resolved_task_id)
|
|
289
|
+
if not task:
|
|
290
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
291
|
+
|
|
292
|
+
updated_task = task_manager.update_task(task.id, validation_fail_count=0)
|
|
293
|
+
return {
|
|
294
|
+
"task_id": updated_task.id,
|
|
295
|
+
"validation_fail_count": updated_task.validation_fail_count,
|
|
296
|
+
"message": "Validation failure count reset to 0",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@registry.tool(
|
|
300
|
+
name="get_validation_history",
|
|
301
|
+
description="Get full validation history for a task, including all iterations, feedback, and issues.",
|
|
302
|
+
)
|
|
303
|
+
def get_validation_history(task_id: str) -> dict[str, Any]:
|
|
304
|
+
"""
|
|
305
|
+
Get validation history for a task.
|
|
306
|
+
|
|
307
|
+
Returns all validation iterations with their status, feedback, and issues.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Validation history with all iterations
|
|
314
|
+
"""
|
|
315
|
+
# Resolve task reference
|
|
316
|
+
try:
|
|
317
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
318
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
319
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
320
|
+
|
|
321
|
+
task = task_manager.get_task(resolved_task_id)
|
|
322
|
+
if not task:
|
|
323
|
+
raise ValueError(f"Task {task_id} not found")
|
|
324
|
+
|
|
325
|
+
history = validation_history_manager.get_iteration_history(task.id)
|
|
326
|
+
|
|
327
|
+
# Convert iterations to serializable format
|
|
328
|
+
history_dicts = []
|
|
329
|
+
for iteration in history:
|
|
330
|
+
iter_dict: dict[str, Any] = {
|
|
331
|
+
"iteration": iteration.iteration,
|
|
332
|
+
"status": iteration.status,
|
|
333
|
+
"feedback": iteration.feedback,
|
|
334
|
+
"issues": [i.to_dict() for i in (iteration.issues or [])],
|
|
335
|
+
"context_type": iteration.context_type,
|
|
336
|
+
"context_summary": iteration.context_summary,
|
|
337
|
+
"validator_type": iteration.validator_type,
|
|
338
|
+
"created_at": iteration.created_at,
|
|
339
|
+
}
|
|
340
|
+
history_dicts.append(iter_dict)
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
"task_id": task_id,
|
|
344
|
+
"history": history_dicts,
|
|
345
|
+
"total_iterations": len(history_dicts),
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@registry.tool(
|
|
349
|
+
name="get_recurring_issues",
|
|
350
|
+
description="Analyze validation history for recurring issues that keep appearing across iterations.",
|
|
351
|
+
)
|
|
352
|
+
def get_recurring_issues(
|
|
353
|
+
task_id: str,
|
|
354
|
+
threshold: int = 2,
|
|
355
|
+
) -> dict[str, Any]:
|
|
356
|
+
"""
|
|
357
|
+
Get recurring issues analysis for a task.
|
|
358
|
+
|
|
359
|
+
Finds issues that appear multiple times across validation iterations.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
363
|
+
threshold: Minimum occurrences to consider an issue recurring (default: 2)
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Recurring issues analysis with grouped issues and counts
|
|
367
|
+
"""
|
|
368
|
+
# Resolve task reference
|
|
369
|
+
try:
|
|
370
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
371
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
372
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
373
|
+
|
|
374
|
+
task = task_manager.get_task(resolved_task_id)
|
|
375
|
+
if not task:
|
|
376
|
+
return {"error": f"Task {task_id} not found"}
|
|
377
|
+
|
|
378
|
+
summary = validation_history_manager.get_recurring_issue_summary(
|
|
379
|
+
task.id, threshold=threshold
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
has_recurring = validation_history_manager.has_recurring_issues(
|
|
383
|
+
task.id, threshold=threshold
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"task_id": task.id,
|
|
388
|
+
"recurring_issues": summary["recurring_issues"],
|
|
389
|
+
"total_iterations": summary["total_iterations"],
|
|
390
|
+
"has_recurring": has_recurring,
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@registry.tool(
|
|
394
|
+
name="clear_validation_history",
|
|
395
|
+
description="Clear all validation history for a task. Use after major changes that invalidate previous feedback.",
|
|
396
|
+
)
|
|
397
|
+
def clear_validation_history(
|
|
398
|
+
task_id: str,
|
|
399
|
+
reason: str | None = None,
|
|
400
|
+
) -> dict[str, Any]:
|
|
401
|
+
"""
|
|
402
|
+
Clear validation history for a fresh start.
|
|
403
|
+
|
|
404
|
+
Removes all validation iterations and resets the fail count.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
408
|
+
reason: Optional reason for clearing history
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Confirmation of cleared history
|
|
412
|
+
"""
|
|
413
|
+
# Resolve task reference
|
|
414
|
+
try:
|
|
415
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
416
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
417
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
418
|
+
|
|
419
|
+
task = task_manager.get_task(resolved_task_id)
|
|
420
|
+
if not task:
|
|
421
|
+
return {"error": f"Task {task_id} not found"}
|
|
422
|
+
|
|
423
|
+
# Get count before clearing for response
|
|
424
|
+
history = validation_history_manager.get_iteration_history(task.id)
|
|
425
|
+
iterations_count = len(history)
|
|
426
|
+
|
|
427
|
+
# Clear history
|
|
428
|
+
validation_history_manager.clear_history(task.id)
|
|
429
|
+
|
|
430
|
+
# Also reset validation fail count
|
|
431
|
+
task_manager.update_task(task.id, validation_fail_count=0)
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"task_id": task.id,
|
|
435
|
+
"cleared": True,
|
|
436
|
+
"iterations_cleared": iterations_count,
|
|
437
|
+
"reason": reason,
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@registry.tool(
|
|
441
|
+
name="de_escalate_task",
|
|
442
|
+
description="Return an escalated task to open status after human intervention resolves the issue.",
|
|
443
|
+
)
|
|
444
|
+
def de_escalate_task(
|
|
445
|
+
task_id: str,
|
|
446
|
+
reason: str,
|
|
447
|
+
reset_validation: bool = False,
|
|
448
|
+
) -> dict[str, Any]:
|
|
449
|
+
"""
|
|
450
|
+
De-escalate a task back to open status.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
454
|
+
reason: Reason for de-escalation (required)
|
|
455
|
+
reset_validation: Also reset validation fail count (default: False)
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Updated task details
|
|
459
|
+
"""
|
|
460
|
+
# Resolve task reference
|
|
461
|
+
try:
|
|
462
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
463
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
464
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
465
|
+
|
|
466
|
+
task = task_manager.get_task(resolved_task_id)
|
|
467
|
+
if not task:
|
|
468
|
+
return {"error": f"Task {task_id} not found"}
|
|
469
|
+
|
|
470
|
+
if task.status != "escalated":
|
|
471
|
+
return {"error": f"Task {task_id} is not escalated (current status: {task.status})"}
|
|
472
|
+
|
|
473
|
+
# Build update kwargs
|
|
474
|
+
update_kwargs: dict[str, Any] = {
|
|
475
|
+
"status": "open",
|
|
476
|
+
"escalated_at": None,
|
|
477
|
+
"escalation_reason": None,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if reset_validation:
|
|
481
|
+
update_kwargs["validation_fail_count"] = 0
|
|
482
|
+
|
|
483
|
+
updated_task = task_manager.update_task(task.id, **update_kwargs)
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
"task_id": updated_task.id,
|
|
487
|
+
"status": updated_task.status,
|
|
488
|
+
"escalated_at": updated_task.escalated_at,
|
|
489
|
+
"escalation_reason": updated_task.escalation_reason,
|
|
490
|
+
"de_escalation_reason": reason,
|
|
491
|
+
"validation_reset": reset_validation,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@registry.tool(
|
|
495
|
+
name="generate_validation_criteria",
|
|
496
|
+
description="Generate validation criteria for a task using AI. Updates the task with the generated criteria.",
|
|
497
|
+
)
|
|
498
|
+
async def generate_validation_criteria(task_id: str) -> dict[str, Any]:
|
|
499
|
+
"""
|
|
500
|
+
Generate validation criteria for a task using AI.
|
|
501
|
+
|
|
502
|
+
For parent tasks (tasks with children), sets criteria to "All child tasks completed".
|
|
503
|
+
For leaf tasks, uses LLM to generate criteria from title/description.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Generated criteria and updated task info
|
|
510
|
+
"""
|
|
511
|
+
# Resolve task reference
|
|
512
|
+
try:
|
|
513
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
514
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
515
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
516
|
+
|
|
517
|
+
task = task_manager.get_task(resolved_task_id)
|
|
518
|
+
if not task:
|
|
519
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
520
|
+
|
|
521
|
+
if task.validation_criteria:
|
|
522
|
+
return {
|
|
523
|
+
"task_id": task.id,
|
|
524
|
+
"validation_criteria": task.validation_criteria,
|
|
525
|
+
"generated": False,
|
|
526
|
+
"message": "Task already has validation criteria",
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Check if task has children (is a parent task)
|
|
530
|
+
children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
|
|
531
|
+
criteria: str | None
|
|
532
|
+
|
|
533
|
+
if children:
|
|
534
|
+
# Parent task: criteria is child completion
|
|
535
|
+
criteria = "All child tasks must be completed (status: closed)."
|
|
536
|
+
else:
|
|
537
|
+
# Leaf task: use LLM to generate criteria
|
|
538
|
+
if not task_validator:
|
|
539
|
+
raise RuntimeError("Task validation is not enabled")
|
|
540
|
+
|
|
541
|
+
criteria = await task_validator.generate_criteria(
|
|
542
|
+
title=task.title,
|
|
543
|
+
description=task.description,
|
|
544
|
+
labels=task.labels,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
if not criteria:
|
|
548
|
+
return {
|
|
549
|
+
"task_id": task.id,
|
|
550
|
+
"validation_criteria": None,
|
|
551
|
+
"generated": False,
|
|
552
|
+
"error": "Failed to generate criteria",
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Update task with generated criteria
|
|
556
|
+
task_manager.update_task(task.id, validation_criteria=criteria)
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
"task_id": task.id,
|
|
560
|
+
"validation_criteria": criteria,
|
|
561
|
+
"generated": True,
|
|
562
|
+
"is_parent_task": len(children) > 0,
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
@registry.tool(
|
|
566
|
+
name="run_fix_attempt",
|
|
567
|
+
description="Spawn a fix agent to address validation issues. Returns when the fix attempt completes.",
|
|
568
|
+
)
|
|
569
|
+
async def run_fix_attempt(
|
|
570
|
+
task_id: str,
|
|
571
|
+
issues: list[str] | None = None,
|
|
572
|
+
timeout: float = 120.0,
|
|
573
|
+
max_turns: int = 10,
|
|
574
|
+
) -> dict[str, Any]:
|
|
575
|
+
"""
|
|
576
|
+
Spawn an agent to fix validation issues for a task.
|
|
577
|
+
|
|
578
|
+
The fix agent is given the original task context plus validation
|
|
579
|
+
failure details to guide its fixes.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
583
|
+
issues: List of specific issues to fix (uses validation_feedback if not provided)
|
|
584
|
+
timeout: Max time for fix attempt in seconds (default: 120)
|
|
585
|
+
max_turns: Max agent turns (default: 10)
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Dict with success status and fix details
|
|
589
|
+
"""
|
|
590
|
+
# Resolve task reference
|
|
591
|
+
try:
|
|
592
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
593
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
594
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
595
|
+
|
|
596
|
+
if not agent_runner:
|
|
597
|
+
return {
|
|
598
|
+
"success": False,
|
|
599
|
+
"error": "Agent runner not configured - cannot spawn fix agent",
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
task = task_manager.get_task(resolved_task_id)
|
|
603
|
+
if not task:
|
|
604
|
+
return {"success": False, "error": f"Task not found: {task_id}"}
|
|
605
|
+
|
|
606
|
+
# Get issues from parameter or task validation feedback
|
|
607
|
+
issues_text = ""
|
|
608
|
+
if issues:
|
|
609
|
+
issues_text = "\n".join(f"- {issue}" for issue in issues)
|
|
610
|
+
elif task.validation_feedback:
|
|
611
|
+
issues_text = task.validation_feedback
|
|
612
|
+
else:
|
|
613
|
+
return {
|
|
614
|
+
"success": False,
|
|
615
|
+
"error": "No issues provided and no validation feedback on task",
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
# Get repo path for context
|
|
619
|
+
repo_path = None
|
|
620
|
+
if get_project_repo_path and task.project_id:
|
|
621
|
+
repo_path = get_project_repo_path(task.project_id)
|
|
622
|
+
|
|
623
|
+
# Build fix prompt
|
|
624
|
+
fix_prompt = f"""You are fixing validation failures for a task.
|
|
625
|
+
|
|
626
|
+
## Original Task
|
|
627
|
+
**Title:** {task.title}
|
|
628
|
+
**Description:** {task.description or "No description provided."}
|
|
629
|
+
|
|
630
|
+
## Validation Criteria
|
|
631
|
+
{task.validation_criteria or "No specific criteria - use task description."}
|
|
632
|
+
|
|
633
|
+
## Validation Failures
|
|
634
|
+
{issues_text}
|
|
635
|
+
|
|
636
|
+
## Instructions
|
|
637
|
+
1. Read the relevant files to understand the current state
|
|
638
|
+
2. Fix the issues listed above
|
|
639
|
+
3. Ensure all validation criteria pass after your fixes
|
|
640
|
+
4. Do NOT create new tasks - fix the issues directly
|
|
641
|
+
|
|
642
|
+
Focus on fixing ONLY the listed issues. Do not make unrelated changes.
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
from gobby.agents.runner import AgentConfig
|
|
647
|
+
|
|
648
|
+
config = AgentConfig(
|
|
649
|
+
prompt=fix_prompt,
|
|
650
|
+
parent_session_id=None, # No parent session for fix agent
|
|
651
|
+
project_id=task.project_id,
|
|
652
|
+
machine_id=None, # Will be inferred
|
|
653
|
+
source="claude",
|
|
654
|
+
workflow=None, # No workflow - direct execution
|
|
655
|
+
task=None, # Don't claim the task
|
|
656
|
+
mode="headless",
|
|
657
|
+
timeout=timeout,
|
|
658
|
+
max_turns=max_turns,
|
|
659
|
+
project_path=repo_path,
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# Run the fix agent
|
|
663
|
+
result = await agent_runner.run(config)
|
|
664
|
+
|
|
665
|
+
# Record the fix attempt
|
|
666
|
+
iteration = (task.validation_fail_count or 0) + 1
|
|
667
|
+
validation_history_manager.record_iteration(
|
|
668
|
+
task_id=task.id,
|
|
669
|
+
iteration=iteration,
|
|
670
|
+
status="fix_attempted",
|
|
671
|
+
feedback=f"Fix agent completed with status: {result.status}",
|
|
672
|
+
issues=None,
|
|
673
|
+
context_type="fix_agent",
|
|
674
|
+
context_summary=f"Fix attempt {iteration}",
|
|
675
|
+
validator_type="fix_agent",
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
"success": True,
|
|
680
|
+
"task_id": task_id,
|
|
681
|
+
"fix_status": result.status,
|
|
682
|
+
"agent_output": result.output,
|
|
683
|
+
"agent_turns": result.turns_used,
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
except Exception as e:
|
|
687
|
+
logger.exception(f"Fix attempt failed for task {task_id}")
|
|
688
|
+
return {
|
|
689
|
+
"success": False,
|
|
690
|
+
"error": f"Fix agent failed: {e!s}",
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
@registry.tool(
|
|
694
|
+
name="validate_and_fix",
|
|
695
|
+
description="Run validation loop with automatic fix attempts. Validates, spawns fix agent if needed, re-validates.",
|
|
696
|
+
)
|
|
697
|
+
async def validate_and_fix(
|
|
698
|
+
task_id: str,
|
|
699
|
+
max_retries: int = 3,
|
|
700
|
+
auto_fix: bool = True,
|
|
701
|
+
fix_timeout: float = 120.0,
|
|
702
|
+
) -> dict[str, Any]:
|
|
703
|
+
"""
|
|
704
|
+
Run validation loop with automatic fix attempts.
|
|
705
|
+
|
|
706
|
+
1. Validate task completion
|
|
707
|
+
2. If failed and auto_fix=True:
|
|
708
|
+
- Spawn fix agent to address issues
|
|
709
|
+
- Re-validate after fix
|
|
710
|
+
- Repeat up to max_retries
|
|
711
|
+
3. If still failing after retries:
|
|
712
|
+
- Create fix subtask with failure details
|
|
713
|
+
- Mark task status = 'failed'
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
task_id: Task reference: #N, N (seq_num), path (1.2.3), or UUID
|
|
717
|
+
max_retries: Maximum fix attempts before giving up (default: 3)
|
|
718
|
+
auto_fix: Whether to attempt automatic fixes (default: True)
|
|
719
|
+
fix_timeout: Timeout per fix attempt in seconds (default: 120)
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
Validation result with loop history
|
|
723
|
+
"""
|
|
724
|
+
# Resolve task reference
|
|
725
|
+
try:
|
|
726
|
+
resolved_task_id = resolve_task_id_for_mcp(task_manager, task_id)
|
|
727
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
728
|
+
return {"error": f"Invalid task_id: {e}"}
|
|
729
|
+
|
|
730
|
+
task = task_manager.get_task(resolved_task_id)
|
|
731
|
+
if not task:
|
|
732
|
+
return {"success": False, "error": f"Task not found: {task_id}"}
|
|
733
|
+
|
|
734
|
+
# Check if task has children (parent tasks use child completion validation)
|
|
735
|
+
children = task_manager.list_tasks(parent_task_id=task.id, limit=1)
|
|
736
|
+
if children:
|
|
737
|
+
# For parent tasks, just run regular validation (no fix loop)
|
|
738
|
+
result = await validate_task(task.id)
|
|
739
|
+
return {
|
|
740
|
+
"success": True,
|
|
741
|
+
"task_id": task.id,
|
|
742
|
+
"is_parent_task": True,
|
|
743
|
+
"validation_result": result,
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
loop_history: list[dict[str, Any]] = []
|
|
747
|
+
current_retry = 0
|
|
748
|
+
|
|
749
|
+
while current_retry < max_retries:
|
|
750
|
+
# Run validation
|
|
751
|
+
validation_result = await validate_task(task.id)
|
|
752
|
+
loop_history.append(
|
|
753
|
+
{
|
|
754
|
+
"iteration": current_retry + 1,
|
|
755
|
+
"action": "validate",
|
|
756
|
+
"result": validation_result,
|
|
757
|
+
}
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
if validation_result.get("is_valid"):
|
|
761
|
+
# Success! Task is closed by validate_task
|
|
762
|
+
return {
|
|
763
|
+
"success": True,
|
|
764
|
+
"task_id": task.id,
|
|
765
|
+
"is_valid": True,
|
|
766
|
+
"iterations": current_retry + 1,
|
|
767
|
+
"loop_history": loop_history,
|
|
768
|
+
"message": "Task validated successfully",
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# Validation failed - attempt fix if enabled and agent runner available
|
|
772
|
+
if not auto_fix:
|
|
773
|
+
break
|
|
774
|
+
|
|
775
|
+
if not agent_runner:
|
|
776
|
+
loop_history.append(
|
|
777
|
+
{
|
|
778
|
+
"iteration": current_retry + 1,
|
|
779
|
+
"action": "fix_skipped",
|
|
780
|
+
"reason": "Agent runner not configured",
|
|
781
|
+
}
|
|
782
|
+
)
|
|
783
|
+
break
|
|
784
|
+
|
|
785
|
+
# Spawn fix agent
|
|
786
|
+
fix_result = await run_fix_attempt(
|
|
787
|
+
task_id=task.id,
|
|
788
|
+
timeout=fix_timeout,
|
|
789
|
+
)
|
|
790
|
+
loop_history.append(
|
|
791
|
+
{
|
|
792
|
+
"iteration": current_retry + 1,
|
|
793
|
+
"action": "fix_attempt",
|
|
794
|
+
"result": fix_result,
|
|
795
|
+
}
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
if not fix_result.get("success"):
|
|
799
|
+
# Fix agent failed to run
|
|
800
|
+
logger.warning(f"Fix attempt {current_retry + 1} failed: {fix_result.get('error')}")
|
|
801
|
+
|
|
802
|
+
current_retry += 1
|
|
803
|
+
|
|
804
|
+
# All retries exhausted - mark as failed
|
|
805
|
+
final_feedback = f"QA loop exhausted after {current_retry} fix attempts."
|
|
806
|
+
if loop_history:
|
|
807
|
+
last_validation = next(
|
|
808
|
+
(h for h in reversed(loop_history) if h.get("action") == "validate"),
|
|
809
|
+
None,
|
|
810
|
+
)
|
|
811
|
+
if last_validation and last_validation.get("result", {}).get("feedback"):
|
|
812
|
+
final_feedback += (
|
|
813
|
+
f"\n\nLast validation feedback:\n{last_validation['result']['feedback']}"
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
# Create fix subtask for manual intervention
|
|
817
|
+
fix_task = task_manager.create_task(
|
|
818
|
+
project_id=task.project_id,
|
|
819
|
+
title=f"[Manual Fix] {task.title}",
|
|
820
|
+
description=f"Automatic fix attempts failed.\n\n{final_feedback}",
|
|
821
|
+
parent_task_id=task.id,
|
|
822
|
+
priority=1,
|
|
823
|
+
task_type="bug",
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Mark task as failed
|
|
827
|
+
task_manager.update_task(
|
|
828
|
+
task.id,
|
|
829
|
+
status="failed",
|
|
830
|
+
validation_feedback=final_feedback,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
"success": False,
|
|
835
|
+
"task_id": task.id,
|
|
836
|
+
"is_valid": False,
|
|
837
|
+
"iterations": current_retry,
|
|
838
|
+
"loop_history": loop_history,
|
|
839
|
+
"fix_subtask_id": fix_task.id,
|
|
840
|
+
"message": f"Validation failed after {current_retry} fix attempts. Created fix subtask: {fix_task.id}",
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return registry
|