gobby 0.2.5__py3-none-any.whl → 0.2.7__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 +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
Provides core task operations: create, get, update, list, and tree building.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import logging
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
9
10
|
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
10
11
|
from gobby.mcp_proxy.tools.tasks._helpers import _infer_category
|
|
11
12
|
from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
|
|
13
|
+
from gobby.storage.task_dependencies import DependencyCycleError
|
|
12
14
|
from gobby.storage.tasks import TaskNotFoundError
|
|
13
15
|
from gobby.utils.project_context import get_project_context
|
|
14
16
|
from gobby.utils.project_init import initialize_project
|
|
15
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
18
22
|
"""Create a registry with task CRUD tools.
|
|
@@ -36,9 +40,11 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
36
40
|
task_type: str = "task",
|
|
37
41
|
parent_task_id: str | None = None,
|
|
38
42
|
blocks: list[str] | None = None,
|
|
43
|
+
depends_on: list[str] | None = None,
|
|
39
44
|
labels: list[str] | None = None,
|
|
40
45
|
category: str | None = None,
|
|
41
46
|
validation_criteria: str | None = None,
|
|
47
|
+
claim: bool = False,
|
|
42
48
|
) -> dict[str, Any]:
|
|
43
49
|
"""Create a single task in the current project.
|
|
44
50
|
|
|
@@ -47,15 +53,17 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
47
53
|
|
|
48
54
|
Args:
|
|
49
55
|
title: Task title
|
|
50
|
-
session_id: Your session ID for tracking (REQUIRED)
|
|
56
|
+
session_id: Your session ID for tracking (REQUIRED).
|
|
51
57
|
description: Detailed description
|
|
52
58
|
priority: Priority level (1=High, 2=Medium, 3=Low)
|
|
53
59
|
task_type: Task type (task, bug, feature, epic)
|
|
54
60
|
parent_task_id: Optional parent task ID
|
|
55
61
|
blocks: List of task IDs that this new task blocks
|
|
62
|
+
depends_on: List of task IDs that this new task depends on (must complete first)
|
|
56
63
|
labels: List of labels
|
|
57
64
|
category: Task domain category (test, code, document, research, config, manual)
|
|
58
65
|
validation_criteria: Acceptance criteria for validating completion.
|
|
66
|
+
claim: If True, auto-claim the task (set assignee and status to in_progress).
|
|
59
67
|
|
|
60
68
|
Returns:
|
|
61
69
|
Created task dict with id (minimal) or full task details based on config.
|
|
@@ -98,10 +106,72 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
98
106
|
|
|
99
107
|
task = ctx.task_manager.get_task(create_result["task"]["id"])
|
|
100
108
|
|
|
109
|
+
# Link task to session (best-effort) - tracks which session created the task
|
|
110
|
+
try:
|
|
111
|
+
ctx.session_task_manager.link_task(session_id, task.id, "created")
|
|
112
|
+
except Exception:
|
|
113
|
+
pass # nosec B110 - best-effort linking
|
|
114
|
+
|
|
115
|
+
# Auto-claim if requested: set assignee and status to in_progress
|
|
116
|
+
if claim:
|
|
117
|
+
updated_task = ctx.task_manager.update_task(
|
|
118
|
+
task.id,
|
|
119
|
+
assignee=session_id,
|
|
120
|
+
status="in_progress",
|
|
121
|
+
)
|
|
122
|
+
if updated_task is None:
|
|
123
|
+
logger.warning(f"Failed to auto-claim task {task.id}: update_task returned None")
|
|
124
|
+
else:
|
|
125
|
+
task = updated_task
|
|
126
|
+
# Link task to session with "claimed" action (best-effort)
|
|
127
|
+
try:
|
|
128
|
+
ctx.session_task_manager.link_task(session_id, task.id, "claimed")
|
|
129
|
+
except Exception:
|
|
130
|
+
pass # nosec B110 - best-effort linking
|
|
131
|
+
|
|
132
|
+
# Set workflow state for Claude Code (CC doesn't include tool results in PostToolUse)
|
|
133
|
+
# This mirrors close_task behavior in _lifecycle.py:196-207
|
|
134
|
+
try:
|
|
135
|
+
state = ctx.workflow_state_manager.get_state(session_id)
|
|
136
|
+
if state:
|
|
137
|
+
state.variables["task_claimed"] = True
|
|
138
|
+
state.variables["claimed_task_id"] = task.id # Always use UUID
|
|
139
|
+
ctx.workflow_state_manager.save_state(state)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass # nosec B110 - best-effort state update
|
|
142
|
+
|
|
101
143
|
# Handle 'blocks' argument if provided (syntactic sugar)
|
|
144
|
+
# Collect errors consistently with depends_on handling below
|
|
145
|
+
dependency_errors: list[str] = []
|
|
102
146
|
if blocks:
|
|
103
147
|
for blocked_id in blocks:
|
|
104
|
-
|
|
148
|
+
try:
|
|
149
|
+
resolved_blocked = resolve_task_id_for_mcp(
|
|
150
|
+
ctx.task_manager, blocked_id, project_id
|
|
151
|
+
)
|
|
152
|
+
ctx.dep_manager.add_dependency(task.id, resolved_blocked, "blocks")
|
|
153
|
+
except TaskNotFoundError:
|
|
154
|
+
dependency_errors.append(f"Task '{blocked_id}' not found (blocks)")
|
|
155
|
+
except ValueError as e:
|
|
156
|
+
dependency_errors.append(f"Invalid ref '{blocked_id}' (blocks): {e}")
|
|
157
|
+
except DependencyCycleError:
|
|
158
|
+
dependency_errors.append(f"Cycle detected for '{blocked_id}' (blocks)")
|
|
159
|
+
|
|
160
|
+
# Handle 'depends_on' argument if provided
|
|
161
|
+
# The new task depends on resolved_blocker, meaning resolved_blocker blocks the new task
|
|
162
|
+
if depends_on:
|
|
163
|
+
for blocker_ref in depends_on:
|
|
164
|
+
try:
|
|
165
|
+
resolved_blocker = resolve_task_id_for_mcp(
|
|
166
|
+
ctx.task_manager, blocker_ref, project_id
|
|
167
|
+
)
|
|
168
|
+
ctx.dep_manager.add_dependency(resolved_blocker, task.id, "blocks")
|
|
169
|
+
except TaskNotFoundError:
|
|
170
|
+
dependency_errors.append(f"Task '{blocker_ref}' not found")
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
dependency_errors.append(f"Invalid ref '{blocker_ref}': {e}")
|
|
173
|
+
except DependencyCycleError:
|
|
174
|
+
dependency_errors.append(f"Cycle detected for '{blocker_ref}'")
|
|
105
175
|
|
|
106
176
|
# Return minimal or full result based on config
|
|
107
177
|
if ctx.show_result_on_create:
|
|
@@ -113,6 +183,11 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
113
183
|
"ref": f"#{task.seq_num}",
|
|
114
184
|
}
|
|
115
185
|
|
|
186
|
+
# Include dependency errors if any
|
|
187
|
+
if dependency_errors:
|
|
188
|
+
result["dependency_errors"] = dependency_errors
|
|
189
|
+
result["warning"] = f"Task created but {len(dependency_errors)} dependency(s) failed"
|
|
190
|
+
|
|
116
191
|
return result
|
|
117
192
|
|
|
118
193
|
registry.register(
|
|
@@ -148,6 +223,12 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
148
223
|
"description": "List of task IDs that this new task blocks (optional)",
|
|
149
224
|
"default": None,
|
|
150
225
|
},
|
|
226
|
+
"depends_on": {
|
|
227
|
+
"type": "array",
|
|
228
|
+
"items": {"type": "string"},
|
|
229
|
+
"description": "Tasks this new task depends on (must complete first): #N, N, path, or UUID",
|
|
230
|
+
"default": None,
|
|
231
|
+
},
|
|
151
232
|
"labels": {
|
|
152
233
|
"type": "array",
|
|
153
234
|
"items": {"type": "string"},
|
|
@@ -169,6 +250,11 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
169
250
|
"type": "string",
|
|
170
251
|
"description": "Your session ID (from system context). Required to track which session created the task.",
|
|
171
252
|
},
|
|
253
|
+
"claim": {
|
|
254
|
+
"type": "boolean",
|
|
255
|
+
"description": "If true, auto-claim the task (set assignee to session_id and status to in_progress). Default: false - task is created with status 'open' and no assignee.",
|
|
256
|
+
"default": False,
|
|
257
|
+
},
|
|
172
258
|
},
|
|
173
259
|
"required": ["title", "session_id"],
|
|
174
260
|
},
|
|
@@ -265,8 +351,9 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
265
351
|
try:
|
|
266
352
|
resolved_parent = resolve_task_id_for_mcp(ctx.task_manager, parent_task_id)
|
|
267
353
|
kwargs["parent_task_id"] = resolved_parent
|
|
268
|
-
except (TaskNotFoundError, ValueError):
|
|
269
|
-
|
|
354
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
355
|
+
logger.warning(f"Invalid parent_task_id '{parent_task_id}': {e}")
|
|
356
|
+
return {"error": f"Invalid parent_task_id '{parent_task_id}': {e}"}
|
|
270
357
|
else:
|
|
271
358
|
kwargs["parent_task_id"] = None
|
|
272
359
|
if category is not None:
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Expansion tools for skill-based task decomposition.
|
|
2
|
+
|
|
3
|
+
Provides tools for the /gobby-expand skill workflow:
|
|
4
|
+
1. save_expansion_spec - Save expansion spec to task for later execution
|
|
5
|
+
2. execute_expansion - Create subtasks atomically from saved spec
|
|
6
|
+
3. get_expansion_spec - Check for pending expansion (for resume)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
14
|
+
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
15
|
+
from gobby.mcp_proxy.tools.tasks._resolution import resolve_task_id_for_mcp
|
|
16
|
+
from gobby.storage.tasks import TaskNotFoundError
|
|
17
|
+
from gobby.utils.project_context import get_project_context
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_expansion_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
23
|
+
"""Create a registry with task expansion tools.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ctx: Shared registry context
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
InternalToolRegistry with expansion tools registered
|
|
30
|
+
"""
|
|
31
|
+
registry = InternalToolRegistry(
|
|
32
|
+
name="gobby-tasks-expansion",
|
|
33
|
+
description="Task expansion for skill-based decomposition",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def save_expansion_spec(
|
|
37
|
+
task_id: str,
|
|
38
|
+
spec: dict[str, Any],
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Save expansion spec to task.expansion_context for later execution.
|
|
41
|
+
|
|
42
|
+
Used by the /gobby-expand skill to persist the expansion plan before
|
|
43
|
+
creating subtasks. This ensures the spec survives session compaction.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
task_id: Task ID to expand (can be #N, path, or UUID)
|
|
47
|
+
spec: Expansion specification containing:
|
|
48
|
+
- subtasks: List of subtask definitions, each with:
|
|
49
|
+
- title: Subtask title (required)
|
|
50
|
+
- category: code, config, docs, research, planning, manual
|
|
51
|
+
- depends_on: List of indices of subtasks this depends on
|
|
52
|
+
- validation: Validation criteria string
|
|
53
|
+
- description: Optional description
|
|
54
|
+
- priority: Optional priority (default: 2)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
{"saved": True, "task_id": str, "subtask_count": int}
|
|
58
|
+
"""
|
|
59
|
+
# Get project context
|
|
60
|
+
project_ctx = get_project_context()
|
|
61
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
62
|
+
|
|
63
|
+
# Resolve task ID
|
|
64
|
+
try:
|
|
65
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id, project_id)
|
|
66
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
67
|
+
return {"error": f"Task not found: {e}"}
|
|
68
|
+
|
|
69
|
+
# Validate spec structure
|
|
70
|
+
if "subtasks" not in spec or not isinstance(spec["subtasks"], list):
|
|
71
|
+
return {"error": "Spec must contain 'subtasks' array"}
|
|
72
|
+
|
|
73
|
+
if len(spec["subtasks"]) == 0:
|
|
74
|
+
return {"error": "Spec must contain at least one subtask"}
|
|
75
|
+
|
|
76
|
+
# Validate subtask structure
|
|
77
|
+
for i, subtask in enumerate(spec["subtasks"]):
|
|
78
|
+
if "title" not in subtask:
|
|
79
|
+
return {"error": f"Subtask {i} missing required 'title' field"}
|
|
80
|
+
|
|
81
|
+
# Save spec to task
|
|
82
|
+
ctx.task_manager.update_task(
|
|
83
|
+
resolved_id,
|
|
84
|
+
expansion_context=json.dumps(spec),
|
|
85
|
+
expansion_status="pending",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
logger.info(
|
|
89
|
+
f"Saved expansion spec for task {task_id} with {len(spec['subtasks'])} subtasks"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"saved": True,
|
|
94
|
+
"task_id": resolved_id,
|
|
95
|
+
"subtask_count": len(spec["subtasks"]),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async def execute_expansion(
|
|
99
|
+
parent_task_id: str,
|
|
100
|
+
session_id: str,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Execute a saved expansion spec atomically.
|
|
103
|
+
|
|
104
|
+
Creates all subtasks from the saved spec and wires dependencies.
|
|
105
|
+
Call save_expansion_spec first to persist the spec.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
parent_task_id: Task ID with saved expansion spec
|
|
109
|
+
session_id: Session ID for tracking created tasks
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
{"created": ["#N", ...], "count": int} or {"error": str}
|
|
113
|
+
"""
|
|
114
|
+
# Get project context
|
|
115
|
+
project_ctx = get_project_context()
|
|
116
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
117
|
+
|
|
118
|
+
# Resolve task ID
|
|
119
|
+
try:
|
|
120
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, parent_task_id, project_id)
|
|
121
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
122
|
+
return {"error": f"Task not found: {e}"}
|
|
123
|
+
|
|
124
|
+
# Get task and check for pending spec
|
|
125
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
126
|
+
if not task:
|
|
127
|
+
return {"error": f"Task {parent_task_id} not found"}
|
|
128
|
+
|
|
129
|
+
if task.expansion_status != "pending":
|
|
130
|
+
return {
|
|
131
|
+
"error": f"Task has no pending expansion spec (status: {task.expansion_status})"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if not task.expansion_context:
|
|
135
|
+
return {"error": "Task has no expansion_context"}
|
|
136
|
+
|
|
137
|
+
# Parse spec
|
|
138
|
+
try:
|
|
139
|
+
spec = json.loads(task.expansion_context)
|
|
140
|
+
except json.JSONDecodeError as e:
|
|
141
|
+
return {"error": f"Invalid expansion_context JSON: {e}"}
|
|
142
|
+
|
|
143
|
+
subtasks = spec.get("subtasks", [])
|
|
144
|
+
if not subtasks:
|
|
145
|
+
return {"error": "No subtasks in spec"}
|
|
146
|
+
|
|
147
|
+
# Create subtasks atomically - clean up on failure
|
|
148
|
+
created_tasks = []
|
|
149
|
+
created_refs = []
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
for subtask in subtasks:
|
|
153
|
+
result = ctx.task_manager.create_task_with_decomposition(
|
|
154
|
+
project_id=task.project_id,
|
|
155
|
+
title=subtask["title"],
|
|
156
|
+
description=subtask.get("description"),
|
|
157
|
+
priority=subtask.get("priority", 2),
|
|
158
|
+
task_type=subtask.get("task_type", "task"),
|
|
159
|
+
parent_task_id=resolved_id,
|
|
160
|
+
category=subtask.get("category"),
|
|
161
|
+
validation_criteria=subtask.get("validation"),
|
|
162
|
+
created_in_session_id=session_id,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Get the task (create_task_with_decomposition returns dict with task dict)
|
|
166
|
+
subtask_id = result["task"]["id"]
|
|
167
|
+
created_task = ctx.task_manager.get_task(subtask_id)
|
|
168
|
+
created_tasks.append(created_task)
|
|
169
|
+
|
|
170
|
+
# Build ref
|
|
171
|
+
ref = f"#{created_task.seq_num}" if created_task.seq_num else created_task.id[:8]
|
|
172
|
+
created_refs.append(ref)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
# Clean up any tasks created before failure
|
|
175
|
+
logger.error(f"Expansion failed after creating {len(created_tasks)} tasks: {e}")
|
|
176
|
+
for task_to_delete in created_tasks:
|
|
177
|
+
try:
|
|
178
|
+
ctx.task_manager.delete_task(task_to_delete.id)
|
|
179
|
+
except Exception as delete_err:
|
|
180
|
+
logger.warning(f"Failed to clean up task {task_to_delete.id}: {delete_err}")
|
|
181
|
+
return {"error": f"Expansion failed: {e}", "cleaned_up": len(created_tasks)}
|
|
182
|
+
|
|
183
|
+
# Wire dependencies
|
|
184
|
+
for i, subtask in enumerate(subtasks):
|
|
185
|
+
depends_on = subtask.get("depends_on", [])
|
|
186
|
+
for dep_idx in depends_on:
|
|
187
|
+
if 0 <= dep_idx < len(created_tasks):
|
|
188
|
+
try:
|
|
189
|
+
ctx.dep_manager.add_dependency(
|
|
190
|
+
task_id=created_tasks[i].id,
|
|
191
|
+
depends_on=created_tasks[dep_idx].id,
|
|
192
|
+
dep_type="blocks",
|
|
193
|
+
)
|
|
194
|
+
except ValueError:
|
|
195
|
+
pass # Dependency already exists or invalid
|
|
196
|
+
|
|
197
|
+
# Wire parent blocked by all children
|
|
198
|
+
for created_task in created_tasks:
|
|
199
|
+
try:
|
|
200
|
+
ctx.dep_manager.add_dependency(
|
|
201
|
+
task_id=resolved_id,
|
|
202
|
+
depends_on=created_task.id,
|
|
203
|
+
dep_type="blocks",
|
|
204
|
+
)
|
|
205
|
+
except ValueError:
|
|
206
|
+
pass # Already exists
|
|
207
|
+
|
|
208
|
+
# Update parent task status
|
|
209
|
+
ctx.task_manager.update_task(
|
|
210
|
+
resolved_id,
|
|
211
|
+
is_expanded=True,
|
|
212
|
+
expansion_status="completed",
|
|
213
|
+
validation_criteria="All subtasks must be completed (status: closed).",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
logger.info(
|
|
217
|
+
f"Executed expansion for task {parent_task_id}: created {len(created_tasks)} subtasks"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"created": created_refs,
|
|
222
|
+
"count": len(created_refs),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async def get_expansion_spec(
|
|
226
|
+
task_id: str,
|
|
227
|
+
) -> dict[str, Any]:
|
|
228
|
+
"""Check for pending expansion spec (for resume after compaction).
|
|
229
|
+
|
|
230
|
+
Used by /gobby-expand skill to check if there's a pending expansion
|
|
231
|
+
that was interrupted and can be resumed.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
task_id: Task ID to check
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
{"pending": True, "spec": {...}} if pending expansion exists
|
|
238
|
+
{"pending": False} otherwise
|
|
239
|
+
"""
|
|
240
|
+
# Get project context
|
|
241
|
+
project_ctx = get_project_context()
|
|
242
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
243
|
+
|
|
244
|
+
# Resolve task ID
|
|
245
|
+
try:
|
|
246
|
+
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id, project_id)
|
|
247
|
+
except (TaskNotFoundError, ValueError) as e:
|
|
248
|
+
return {"error": f"Task not found: {e}"}
|
|
249
|
+
|
|
250
|
+
# Get task
|
|
251
|
+
task = ctx.task_manager.get_task(resolved_id)
|
|
252
|
+
if not task:
|
|
253
|
+
return {"error": f"Task {task_id} not found"}
|
|
254
|
+
|
|
255
|
+
# Check for pending expansion
|
|
256
|
+
if task.expansion_status == "pending" and task.expansion_context:
|
|
257
|
+
try:
|
|
258
|
+
spec = json.loads(task.expansion_context)
|
|
259
|
+
return {
|
|
260
|
+
"pending": True,
|
|
261
|
+
"spec": spec,
|
|
262
|
+
"subtask_count": len(spec.get("subtasks", [])),
|
|
263
|
+
}
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
return {"pending": False, "error": "Invalid expansion_context JSON"}
|
|
266
|
+
|
|
267
|
+
return {"pending": False}
|
|
268
|
+
|
|
269
|
+
# Register tools
|
|
270
|
+
registry.register(
|
|
271
|
+
name="save_expansion_spec",
|
|
272
|
+
description="Save expansion spec to task for later execution. Used by /gobby-expand skill.",
|
|
273
|
+
input_schema={
|
|
274
|
+
"type": "object",
|
|
275
|
+
"properties": {
|
|
276
|
+
"task_id": {
|
|
277
|
+
"type": "string",
|
|
278
|
+
"description": "Task ID to expand (can be #N, path, or UUID)",
|
|
279
|
+
},
|
|
280
|
+
"spec": {
|
|
281
|
+
"type": "object",
|
|
282
|
+
"description": "Expansion specification containing subtasks array",
|
|
283
|
+
"properties": {
|
|
284
|
+
"subtasks": {
|
|
285
|
+
"type": "array",
|
|
286
|
+
"description": "List of subtask definitions",
|
|
287
|
+
"items": {
|
|
288
|
+
"type": "object",
|
|
289
|
+
"properties": {
|
|
290
|
+
"title": {"type": "string"},
|
|
291
|
+
"category": {"type": "string"},
|
|
292
|
+
"depends_on": {
|
|
293
|
+
"type": "array",
|
|
294
|
+
"items": {"type": "integer"},
|
|
295
|
+
},
|
|
296
|
+
"validation": {"type": "string"},
|
|
297
|
+
"description": {"type": "string"},
|
|
298
|
+
"priority": {"type": "integer"},
|
|
299
|
+
},
|
|
300
|
+
"required": ["title"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
"required": ["subtasks"],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
"required": ["task_id", "spec"],
|
|
308
|
+
},
|
|
309
|
+
func=save_expansion_spec,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
registry.register(
|
|
313
|
+
name="execute_expansion",
|
|
314
|
+
description="Execute a saved expansion spec atomically. Creates subtasks with dependencies.",
|
|
315
|
+
input_schema={
|
|
316
|
+
"type": "object",
|
|
317
|
+
"properties": {
|
|
318
|
+
"parent_task_id": {
|
|
319
|
+
"type": "string",
|
|
320
|
+
"description": "Task ID with saved expansion spec",
|
|
321
|
+
},
|
|
322
|
+
"session_id": {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"description": "Session ID for tracking created tasks",
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
"required": ["parent_task_id", "session_id"],
|
|
328
|
+
},
|
|
329
|
+
func=execute_expansion,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
registry.register(
|
|
333
|
+
name="get_expansion_spec",
|
|
334
|
+
description="Check for pending expansion spec (for resume after session compaction).",
|
|
335
|
+
input_schema={
|
|
336
|
+
"type": "object",
|
|
337
|
+
"properties": {
|
|
338
|
+
"task_id": {
|
|
339
|
+
"type": "string",
|
|
340
|
+
"description": "Task ID to check",
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
"required": ["task_id"],
|
|
344
|
+
},
|
|
345
|
+
func=get_expansion_spec,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return registry
|
|
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
|
|
9
9
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
10
10
|
from gobby.mcp_proxy.tools.task_dependencies import create_dependency_registry
|
|
11
|
-
from gobby.mcp_proxy.tools.task_expansion import create_expansion_registry
|
|
12
11
|
from gobby.mcp_proxy.tools.task_github import create_github_sync_registry
|
|
13
12
|
from gobby.mcp_proxy.tools.task_orchestration import create_orchestration_registry
|
|
14
13
|
from gobby.mcp_proxy.tools.task_readiness import create_readiness_registry
|
|
@@ -16,13 +15,13 @@ from gobby.mcp_proxy.tools.task_sync import create_sync_registry
|
|
|
16
15
|
from gobby.mcp_proxy.tools.task_validation import create_validation_registry
|
|
17
16
|
from gobby.mcp_proxy.tools.tasks._context import RegistryContext
|
|
18
17
|
from gobby.mcp_proxy.tools.tasks._crud import create_crud_registry
|
|
18
|
+
from gobby.mcp_proxy.tools.tasks._expansion import create_expansion_registry
|
|
19
19
|
from gobby.mcp_proxy.tools.tasks._lifecycle import create_lifecycle_registry
|
|
20
20
|
from gobby.mcp_proxy.tools.tasks._search import create_search_registry
|
|
21
21
|
from gobby.mcp_proxy.tools.tasks._session import create_session_registry
|
|
22
22
|
from gobby.storage.tasks import LocalTaskManager
|
|
23
23
|
from gobby.storage.worktrees import LocalWorktreeManager
|
|
24
24
|
from gobby.sync.tasks import TaskSyncManager
|
|
25
|
-
from gobby.tasks.expansion import TaskExpander
|
|
26
25
|
from gobby.tasks.validation import TaskValidator
|
|
27
26
|
|
|
28
27
|
if TYPE_CHECKING:
|
|
@@ -35,7 +34,6 @@ if TYPE_CHECKING:
|
|
|
35
34
|
def create_task_registry(
|
|
36
35
|
task_manager: LocalTaskManager,
|
|
37
36
|
sync_manager: TaskSyncManager,
|
|
38
|
-
task_expander: TaskExpander | None = None,
|
|
39
37
|
task_validator: TaskValidator | None = None,
|
|
40
38
|
config: "DaemonConfig | None" = None,
|
|
41
39
|
agent_runner: "AgentRunner | None" = None,
|
|
@@ -50,7 +48,6 @@ def create_task_registry(
|
|
|
50
48
|
Args:
|
|
51
49
|
task_manager: LocalTaskManager instance
|
|
52
50
|
sync_manager: TaskSyncManager instance
|
|
53
|
-
task_expander: TaskExpander instance (optional)
|
|
54
51
|
task_validator: TaskValidator instance (optional)
|
|
55
52
|
config: DaemonConfig instance (optional)
|
|
56
53
|
agent_runner: AgentRunner instance for external validator agent mode (optional)
|
|
@@ -66,7 +63,6 @@ def create_task_registry(
|
|
|
66
63
|
ctx = RegistryContext(
|
|
67
64
|
task_manager=task_manager,
|
|
68
65
|
sync_manager=sync_manager,
|
|
69
|
-
task_expander=task_expander,
|
|
70
66
|
task_validator=task_validator,
|
|
71
67
|
agent_runner=agent_runner,
|
|
72
68
|
config=config,
|
|
@@ -98,6 +94,11 @@ def create_task_registry(
|
|
|
98
94
|
for tool_name, tool in search_registry._tools.items():
|
|
99
95
|
registry._tools[tool_name] = tool
|
|
100
96
|
|
|
97
|
+
# Merge expansion tools (skill-based task decomposition)
|
|
98
|
+
expansion_registry = create_expansion_registry(ctx)
|
|
99
|
+
for tool_name, tool in expansion_registry._tools.items():
|
|
100
|
+
registry._tools[tool_name] = tool
|
|
101
|
+
|
|
101
102
|
# Merge validation tools from extracted module (Strangler Fig pattern)
|
|
102
103
|
validation_registry = create_validation_registry(
|
|
103
104
|
task_manager=task_manager,
|
|
@@ -108,17 +109,6 @@ def create_task_registry(
|
|
|
108
109
|
for tool_name, tool in validation_registry._tools.items():
|
|
109
110
|
registry._tools[tool_name] = tool
|
|
110
111
|
|
|
111
|
-
# Merge expansion tools from extracted module (Strangler Fig pattern)
|
|
112
|
-
expansion_registry = create_expansion_registry(
|
|
113
|
-
task_manager=task_manager,
|
|
114
|
-
task_expander=task_expander,
|
|
115
|
-
task_validator=task_validator,
|
|
116
|
-
auto_generate_on_expand=ctx.auto_generate_on_expand,
|
|
117
|
-
resolve_tdd_mode=ctx.resolve_tdd_mode,
|
|
118
|
-
)
|
|
119
|
-
for tool_name, tool in expansion_registry._tools.items():
|
|
120
|
-
registry._tools[tool_name] = tool
|
|
121
|
-
|
|
122
112
|
# Merge dependency tools from extracted module (Strangler Fig pattern)
|
|
123
113
|
dependency_registry = create_dependency_registry(
|
|
124
114
|
task_manager=task_manager,
|