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
|
@@ -41,7 +41,6 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
41
41
|
skip_validation: bool = False,
|
|
42
42
|
session_id: str | None = None,
|
|
43
43
|
override_justification: str | None = None,
|
|
44
|
-
no_commit_needed: bool = False,
|
|
45
44
|
commit_sha: str | None = None,
|
|
46
45
|
) -> dict[str, Any]:
|
|
47
46
|
"""Close a task with validation.
|
|
@@ -51,14 +50,12 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
51
50
|
|
|
52
51
|
Args:
|
|
53
52
|
task_id: Task reference (#N, path, or UUID)
|
|
54
|
-
reason: Reason for closing
|
|
53
|
+
reason: Reason for closing. Use "duplicate", "already_implemented", "wont_fix",
|
|
54
|
+
or "obsolete" to auto-skip commit check (these imply no work was done).
|
|
55
55
|
changes_summary: Summary of changes (enables LLM validation for leaf tasks)
|
|
56
56
|
skip_validation: Skip all validation checks
|
|
57
57
|
session_id: Session ID where task is being closed (auto-links to session)
|
|
58
58
|
override_justification: Why agent bypassed validation (stored for audit).
|
|
59
|
-
Also used to explain why no commit was needed when no_commit_needed=True.
|
|
60
|
-
no_commit_needed: Set to True for tasks that don't produce code changes
|
|
61
|
-
(research, planning, documentation review). Requires override_justification.
|
|
62
59
|
commit_sha: Git commit SHA to link before closing. Convenience for link + close in one call.
|
|
63
60
|
|
|
64
61
|
Returns:
|
|
@@ -68,13 +65,13 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
68
65
|
try:
|
|
69
66
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
70
67
|
except TaskNotFoundError as e:
|
|
71
|
-
return {"error": str(e)}
|
|
68
|
+
return {"success": False, "error": str(e)}
|
|
72
69
|
except ValueError as e:
|
|
73
|
-
return {"error": str(e)}
|
|
70
|
+
return {"success": False, "error": str(e)}
|
|
74
71
|
|
|
75
72
|
task = ctx.task_manager.get_task(resolved_id)
|
|
76
73
|
if not task:
|
|
77
|
-
return {"error": f"Task {task_id} not found"}
|
|
74
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
78
75
|
|
|
79
76
|
# Link commit if provided (convenience for link + close in one call)
|
|
80
77
|
if commit_sha:
|
|
@@ -85,11 +82,10 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
85
82
|
cwd = repo_path or "."
|
|
86
83
|
|
|
87
84
|
# Check for linked commits (unless task type doesn't require commits)
|
|
88
|
-
commit_result = validate_commit_requirements(
|
|
89
|
-
task, reason, no_commit_needed, override_justification
|
|
90
|
-
)
|
|
85
|
+
commit_result = validate_commit_requirements(task, reason, repo_path)
|
|
91
86
|
if not commit_result.can_close:
|
|
92
87
|
return {
|
|
88
|
+
"success": False,
|
|
93
89
|
"error": commit_result.error_type,
|
|
94
90
|
"message": commit_result.message,
|
|
95
91
|
}
|
|
@@ -97,11 +93,40 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
97
93
|
# Auto-skip validation for certain close reasons
|
|
98
94
|
should_skip = skip_validation or reason.lower() in SKIP_REASONS
|
|
99
95
|
|
|
96
|
+
# Enforce commits if session had edits
|
|
97
|
+
if session_id and not should_skip:
|
|
98
|
+
try:
|
|
99
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
100
|
+
|
|
101
|
+
session_manager = LocalSessionManager(ctx.task_manager.db)
|
|
102
|
+
session = session_manager.get(session_id)
|
|
103
|
+
|
|
104
|
+
# Check if task has commits (including the one being linked right now)
|
|
105
|
+
has_commits = bool(task.commits) or bool(commit_sha)
|
|
106
|
+
|
|
107
|
+
if session and session.had_edits and not has_commits:
|
|
108
|
+
return {
|
|
109
|
+
"success": False,
|
|
110
|
+
"error": "missing_commits_for_edits",
|
|
111
|
+
"message": (
|
|
112
|
+
"This session made edits but no commits are linked to the task. "
|
|
113
|
+
"You must commit your changes and link them to the task before closing."
|
|
114
|
+
),
|
|
115
|
+
"suggestion": (
|
|
116
|
+
"Commit your changes with `[#task_id]` in the message, "
|
|
117
|
+
"or pass `commit_sha` to `close_task`."
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
except Exception:
|
|
121
|
+
# Don't block close on internal error
|
|
122
|
+
pass # nosec B110 - best-effort session edit check
|
|
123
|
+
|
|
100
124
|
if not should_skip:
|
|
101
125
|
# Check if task has children (is a parent task)
|
|
102
126
|
parent_result = validate_parent_task(ctx, resolved_id)
|
|
103
127
|
if not parent_result.can_close:
|
|
104
|
-
response = {
|
|
128
|
+
response: dict[str, Any] = {
|
|
129
|
+
"success": False,
|
|
105
130
|
"error": parent_result.error_type,
|
|
106
131
|
"message": parent_result.message,
|
|
107
132
|
}
|
|
@@ -132,6 +157,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
132
157
|
)
|
|
133
158
|
if not llm_result.can_close:
|
|
134
159
|
response = {
|
|
160
|
+
"success": False,
|
|
135
161
|
"error": llm_result.error_type,
|
|
136
162
|
"message": llm_result.message,
|
|
137
163
|
}
|
|
@@ -141,7 +167,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
141
167
|
|
|
142
168
|
# Determine close outcome
|
|
143
169
|
route_to_review, store_override = determine_close_outcome(
|
|
144
|
-
task, skip_validation,
|
|
170
|
+
task, skip_validation, override_justification
|
|
145
171
|
)
|
|
146
172
|
|
|
147
173
|
# Get git commit SHA (best-effort, dynamic short format for consistency)
|
|
@@ -194,6 +220,23 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
194
220
|
except Exception:
|
|
195
221
|
pass # nosec B110 - best-effort linking, don't fail the close
|
|
196
222
|
|
|
223
|
+
# Clear workflow task_claimed state if this was the claimed task
|
|
224
|
+
# Respects the clear_task_on_close variable (defaults to True if not set)
|
|
225
|
+
# This is done here because Claude Code's post-tool-use hook doesn't include
|
|
226
|
+
# the tool result, so the detection_helpers can't verify close succeeded
|
|
227
|
+
if session_id:
|
|
228
|
+
try:
|
|
229
|
+
state = ctx.workflow_state_manager.get_state(session_id)
|
|
230
|
+
if state and state.variables.get("claimed_task_id") == resolved_id:
|
|
231
|
+
# Check if clear_task_on_close is enabled (default: True)
|
|
232
|
+
clear_on_close = state.variables.get("clear_task_on_close", True)
|
|
233
|
+
if clear_on_close:
|
|
234
|
+
state.variables["task_claimed"] = False
|
|
235
|
+
state.variables["claimed_task_id"] = None
|
|
236
|
+
ctx.workflow_state_manager.save_state(state)
|
|
237
|
+
except Exception:
|
|
238
|
+
pass # nosec B110 - best-effort state update
|
|
239
|
+
|
|
197
240
|
# Update worktree status based on closure reason (case-insensitive)
|
|
198
241
|
try:
|
|
199
242
|
reason_normalized = reason.lower()
|
|
@@ -216,7 +259,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
216
259
|
|
|
217
260
|
registry.register(
|
|
218
261
|
name="close_task",
|
|
219
|
-
description="Close a task.
|
|
262
|
+
description="Close a task. Pass commit_sha to link and close in one call: close_task(task_id, commit_sha='abc123'). Or include [#N] in commit message for auto-linking. Parent tasks require all children closed. Validation auto-skipped for: duplicate, already_implemented, wont_fix, obsolete.",
|
|
220
263
|
input_schema={
|
|
221
264
|
"type": "object",
|
|
222
265
|
"properties": {
|
|
@@ -251,24 +294,14 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
251
294
|
"override_justification": {
|
|
252
295
|
"type": "string",
|
|
253
296
|
"description": (
|
|
254
|
-
"Justification for bypassing validation
|
|
255
|
-
"Required when skip_validation=True or no_commit_needed=True. "
|
|
297
|
+
"Justification for bypassing validation. Required when skip_validation=True. "
|
|
256
298
|
"Example: 'Validation saw truncated diff - verified via git show that commit includes all changes'"
|
|
257
299
|
),
|
|
258
300
|
"default": None,
|
|
259
301
|
},
|
|
260
|
-
"no_commit_needed": {
|
|
261
|
-
"type": "boolean",
|
|
262
|
-
"description": (
|
|
263
|
-
"ONLY for tasks with NO code changes (pure research, planning, documentation review). "
|
|
264
|
-
"Do NOT use this to bypass validation when a commit exists - use skip_validation instead. "
|
|
265
|
-
"Requires override_justification."
|
|
266
|
-
),
|
|
267
|
-
"default": False,
|
|
268
|
-
},
|
|
269
302
|
"commit_sha": {
|
|
270
303
|
"type": "string",
|
|
271
|
-
"description": "Git commit SHA to link
|
|
304
|
+
"description": "RECOMMENDED: Git commit SHA to link and close in one call. Use this instead of separate link_commit + close_task calls.",
|
|
272
305
|
"default": None,
|
|
273
306
|
},
|
|
274
307
|
},
|
|
@@ -290,7 +323,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
290
323
|
try:
|
|
291
324
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
292
325
|
except (TaskNotFoundError, ValueError) as e:
|
|
293
|
-
return {"error": str(e)}
|
|
326
|
+
return {"success": False, "error": str(e)}
|
|
294
327
|
|
|
295
328
|
try:
|
|
296
329
|
ctx.task_manager.reopen_task(resolved_id, reason=reason)
|
|
@@ -311,7 +344,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
311
344
|
|
|
312
345
|
return {}
|
|
313
346
|
except ValueError as e:
|
|
314
|
-
return {"error": str(e)}
|
|
347
|
+
return {"success": False, "error": str(e)}
|
|
315
348
|
|
|
316
349
|
registry.register(
|
|
317
350
|
name="reopen_task",
|
|
@@ -334,22 +367,45 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
334
367
|
func=reopen_task,
|
|
335
368
|
)
|
|
336
369
|
|
|
337
|
-
def delete_task(task_id: str, cascade: bool = True) -> dict[str, Any]:
|
|
338
|
-
"""Delete a task
|
|
370
|
+
def delete_task(task_id: str, cascade: bool = True, unlink: bool = False) -> dict[str, Any]:
|
|
371
|
+
"""Delete a task.
|
|
372
|
+
|
|
373
|
+
By default (cascade=True), deletes subtasks and dependent tasks.
|
|
374
|
+
Use unlink=True to remove dependency links but preserve dependent tasks.
|
|
375
|
+
"""
|
|
339
376
|
try:
|
|
340
377
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
341
378
|
except (TaskNotFoundError, ValueError) as e:
|
|
342
|
-
return {"error": str(e)}
|
|
379
|
+
return {"success": False, "error": str(e)}
|
|
343
380
|
|
|
344
381
|
# Get task before deleting to capture seq_num for ref
|
|
345
382
|
task = ctx.task_manager.get_task(resolved_id)
|
|
346
383
|
if not task:
|
|
347
|
-
return {"error": f"Task {task_id} not found"}
|
|
384
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
348
385
|
ref = f"#{task.seq_num}" if task.seq_num else resolved_id[:8]
|
|
349
386
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
387
|
+
try:
|
|
388
|
+
deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade, unlink=unlink)
|
|
389
|
+
if not deleted:
|
|
390
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
391
|
+
except ValueError as e:
|
|
392
|
+
error_msg = str(e)
|
|
393
|
+
if "dependent task(s)" in error_msg:
|
|
394
|
+
return {
|
|
395
|
+
"success": False,
|
|
396
|
+
"error": "has_dependents",
|
|
397
|
+
"message": error_msg,
|
|
398
|
+
"suggestion": f"Use cascade=True to delete task {ref} and its dependents, "
|
|
399
|
+
f"or unlink=True to preserve dependent tasks.",
|
|
400
|
+
}
|
|
401
|
+
elif "has children" in error_msg:
|
|
402
|
+
return {
|
|
403
|
+
"success": False,
|
|
404
|
+
"error": "has_children",
|
|
405
|
+
"message": error_msg,
|
|
406
|
+
"suggestion": f"Use cascade=True to delete task {ref} and all its subtasks.",
|
|
407
|
+
}
|
|
408
|
+
return {"success": False, "error": error_msg}
|
|
353
409
|
|
|
354
410
|
return {
|
|
355
411
|
"ref": ref,
|
|
@@ -358,7 +414,9 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
358
414
|
|
|
359
415
|
registry.register(
|
|
360
416
|
name="delete_task",
|
|
361
|
-
description="Delete a task and
|
|
417
|
+
description="Delete a task. By default (cascade=True), deletes subtasks and dependent tasks. "
|
|
418
|
+
"Set cascade=False to fail if task has children or dependents. "
|
|
419
|
+
"Use unlink=True to remove dependency links but preserve dependent tasks.",
|
|
362
420
|
input_schema={
|
|
363
421
|
"type": "object",
|
|
364
422
|
"properties": {
|
|
@@ -368,9 +426,15 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
368
426
|
},
|
|
369
427
|
"cascade": {
|
|
370
428
|
"type": "boolean",
|
|
371
|
-
"description": "If True, delete
|
|
429
|
+
"description": "If True, delete subtasks and dependent tasks. Defaults to True.",
|
|
372
430
|
"default": True,
|
|
373
431
|
},
|
|
432
|
+
"unlink": {
|
|
433
|
+
"type": "boolean",
|
|
434
|
+
"description": "If True, remove dependency links but preserve dependent tasks. "
|
|
435
|
+
"Ignored if cascade=True.",
|
|
436
|
+
"default": False,
|
|
437
|
+
},
|
|
374
438
|
},
|
|
375
439
|
"required": ["task_id"],
|
|
376
440
|
},
|
|
@@ -382,10 +446,10 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
382
446
|
try:
|
|
383
447
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
384
448
|
except (TaskNotFoundError, ValueError) as e:
|
|
385
|
-
return {"error": str(e)}
|
|
449
|
+
return {"success": False, "error": str(e)}
|
|
386
450
|
task = ctx.task_manager.add_label(resolved_id, label)
|
|
387
451
|
if not task:
|
|
388
|
-
return {"error": f"Task {task_id} not found"}
|
|
452
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
389
453
|
return {}
|
|
390
454
|
|
|
391
455
|
registry.register(
|
|
@@ -410,10 +474,10 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
410
474
|
try:
|
|
411
475
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
412
476
|
except (TaskNotFoundError, ValueError) as e:
|
|
413
|
-
return {"error": str(e)}
|
|
477
|
+
return {"success": False, "error": str(e)}
|
|
414
478
|
task = ctx.task_manager.remove_label(resolved_id, label)
|
|
415
479
|
if not task:
|
|
416
|
-
return {"error": f"Task {task_id} not found"}
|
|
480
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
417
481
|
return {}
|
|
418
482
|
|
|
419
483
|
registry.register(
|
|
@@ -456,17 +520,18 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
456
520
|
try:
|
|
457
521
|
resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
|
|
458
522
|
except TaskNotFoundError as e:
|
|
459
|
-
return {"error": str(e)}
|
|
523
|
+
return {"success": False, "error": str(e)}
|
|
460
524
|
except ValueError as e:
|
|
461
|
-
return {"error": str(e)}
|
|
525
|
+
return {"success": False, "error": str(e)}
|
|
462
526
|
|
|
463
527
|
task = ctx.task_manager.get_task(resolved_id)
|
|
464
528
|
if not task:
|
|
465
|
-
return {"error": f"Task {task_id} not found"}
|
|
529
|
+
return {"success": False, "error": f"Task {task_id} not found"}
|
|
466
530
|
|
|
467
531
|
# Check if already claimed by another session
|
|
468
532
|
if task.assignee and task.assignee != session_id and not force:
|
|
469
533
|
return {
|
|
534
|
+
"success": False,
|
|
470
535
|
"error": "Task already claimed by another session",
|
|
471
536
|
"claimed_by": task.assignee,
|
|
472
537
|
"message": f"Task is already claimed by session '{task.assignee}'. Use force=True to override.",
|
|
@@ -479,7 +544,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
479
544
|
status="in_progress",
|
|
480
545
|
)
|
|
481
546
|
if not updated:
|
|
482
|
-
return {"error": f"Failed to claim task {task_id}"}
|
|
547
|
+
return {"success": False, "error": f"Failed to claim task {task_id}"}
|
|
483
548
|
|
|
484
549
|
# Link task to session (best-effort, don't fail the claim if this fails)
|
|
485
550
|
try:
|
|
@@ -33,16 +33,14 @@ class ValidationResult:
|
|
|
33
33
|
def validate_commit_requirements(
|
|
34
34
|
task: Task,
|
|
35
35
|
reason: str,
|
|
36
|
-
|
|
37
|
-
override_justification: str | None,
|
|
36
|
+
repo_path: str | None = None,
|
|
38
37
|
) -> ValidationResult:
|
|
39
38
|
"""Check if task meets commit requirements for closing.
|
|
40
39
|
|
|
41
40
|
Args:
|
|
42
41
|
task: The task to validate
|
|
43
42
|
reason: Reason for closing
|
|
44
|
-
|
|
45
|
-
override_justification: Justification for skipping commit check
|
|
43
|
+
repo_path: Path to the repository for git operations
|
|
46
44
|
|
|
47
45
|
Returns:
|
|
48
46
|
ValidationResult indicating if task can be closed
|
|
@@ -51,28 +49,21 @@ def validate_commit_requirements(
|
|
|
51
49
|
requires_commit_check = reason.lower() not in SKIP_REASONS
|
|
52
50
|
|
|
53
51
|
if requires_commit_check and not task.commits:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
error_type="no_commits_linked",
|
|
70
|
-
message=(
|
|
71
|
-
"Cannot close task: no commits are linked. Either:\n"
|
|
72
|
-
"1. Commit your changes and use link_commit() or include [task_id] in commit message\n"
|
|
73
|
-
"2. Set no_commit_needed=True with override_justification if this task didn't require code changes"
|
|
74
|
-
),
|
|
75
|
-
)
|
|
52
|
+
return ValidationResult(
|
|
53
|
+
can_close=False,
|
|
54
|
+
error_type="no_commits_linked",
|
|
55
|
+
message=(
|
|
56
|
+
"\nA commit is required before closing this task.\n\n"
|
|
57
|
+
"**Normal flow:**\n"
|
|
58
|
+
'1. Commit your changes: git commit -m "[#N] description"\n'
|
|
59
|
+
'2. Close with commit_sha: close_task(task_id="#N", commit_sha="<sha>")\n\n'
|
|
60
|
+
"**Edge cases (no work done):**\n"
|
|
61
|
+
'- Task was already done: reason="already_implemented"\n'
|
|
62
|
+
'- Task is no longer needed: reason="obsolete"\n'
|
|
63
|
+
'- Task duplicates another: reason="duplicate"\n'
|
|
64
|
+
'- Decided not to do it: reason="wont_fix"'
|
|
65
|
+
),
|
|
66
|
+
)
|
|
76
67
|
|
|
77
68
|
return ValidationResult(can_close=True)
|
|
78
69
|
|
|
@@ -277,7 +268,6 @@ async def validate_leaf_task_with_llm(
|
|
|
277
268
|
def determine_close_outcome(
|
|
278
269
|
task: Task,
|
|
279
270
|
skip_validation: bool,
|
|
280
|
-
no_commit_needed: bool,
|
|
281
271
|
override_justification: str | None,
|
|
282
272
|
) -> tuple[bool, bool]:
|
|
283
273
|
"""Determine the close outcome for a task.
|
|
@@ -285,14 +275,13 @@ def determine_close_outcome(
|
|
|
285
275
|
Args:
|
|
286
276
|
task: The task being closed
|
|
287
277
|
skip_validation: Whether validation was skipped
|
|
288
|
-
no_commit_needed: Whether commit was not needed
|
|
289
278
|
override_justification: Justification for override
|
|
290
279
|
|
|
291
280
|
Returns:
|
|
292
281
|
Tuple of (route_to_review, store_override)
|
|
293
282
|
"""
|
|
294
283
|
# Determine if override should be stored
|
|
295
|
-
store_override = skip_validation
|
|
284
|
+
store_override = skip_validation
|
|
296
285
|
|
|
297
286
|
# Route to review if task requires user review OR override was used
|
|
298
287
|
# This ensures tasks with HITL flag or skipped validation go through human review
|