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.
Files changed (244) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/claude_code.py +13 -4
  4. gobby/adapters/codex_impl/__init__.py +28 -0
  5. gobby/adapters/codex_impl/adapter.py +722 -0
  6. gobby/adapters/codex_impl/client.py +679 -0
  7. gobby/adapters/codex_impl/protocol.py +20 -0
  8. gobby/adapters/codex_impl/types.py +68 -0
  9. gobby/agents/definitions.py +11 -1
  10. gobby/agents/isolation.py +395 -0
  11. gobby/agents/runner.py +8 -0
  12. gobby/agents/sandbox.py +261 -0
  13. gobby/agents/spawn.py +42 -287
  14. gobby/agents/spawn_executor.py +385 -0
  15. gobby/agents/spawners/__init__.py +24 -0
  16. gobby/agents/spawners/command_builder.py +189 -0
  17. gobby/agents/spawners/embedded.py +21 -2
  18. gobby/agents/spawners/headless.py +21 -2
  19. gobby/agents/spawners/prompt_manager.py +125 -0
  20. gobby/cli/__init__.py +6 -0
  21. gobby/cli/clones.py +419 -0
  22. gobby/cli/conductor.py +266 -0
  23. gobby/cli/install.py +4 -4
  24. gobby/cli/installers/antigravity.py +3 -9
  25. gobby/cli/installers/claude.py +15 -9
  26. gobby/cli/installers/codex.py +2 -8
  27. gobby/cli/installers/gemini.py +8 -8
  28. gobby/cli/installers/shared.py +175 -13
  29. gobby/cli/sessions.py +1 -1
  30. gobby/cli/skills.py +858 -0
  31. gobby/cli/tasks/ai.py +0 -440
  32. gobby/cli/tasks/crud.py +44 -6
  33. gobby/cli/tasks/main.py +0 -4
  34. gobby/cli/tui.py +2 -2
  35. gobby/cli/utils.py +12 -5
  36. gobby/clones/__init__.py +13 -0
  37. gobby/clones/git.py +547 -0
  38. gobby/conductor/__init__.py +16 -0
  39. gobby/conductor/alerts.py +135 -0
  40. gobby/conductor/loop.py +164 -0
  41. gobby/conductor/monitors/__init__.py +11 -0
  42. gobby/conductor/monitors/agents.py +116 -0
  43. gobby/conductor/monitors/tasks.py +155 -0
  44. gobby/conductor/pricing.py +234 -0
  45. gobby/conductor/token_tracker.py +160 -0
  46. gobby/config/__init__.py +12 -97
  47. gobby/config/app.py +69 -91
  48. gobby/config/extensions.py +2 -2
  49. gobby/config/features.py +7 -130
  50. gobby/config/search.py +110 -0
  51. gobby/config/servers.py +1 -1
  52. gobby/config/skills.py +43 -0
  53. gobby/config/tasks.py +9 -41
  54. gobby/hooks/__init__.py +0 -13
  55. gobby/hooks/event_handlers.py +188 -2
  56. gobby/hooks/hook_manager.py +50 -4
  57. gobby/hooks/plugins.py +1 -1
  58. gobby/hooks/skill_manager.py +130 -0
  59. gobby/hooks/webhooks.py +1 -1
  60. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  61. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  62. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  63. gobby/llm/claude.py +22 -34
  64. gobby/llm/claude_executor.py +46 -256
  65. gobby/llm/codex_executor.py +59 -291
  66. gobby/llm/executor.py +21 -0
  67. gobby/llm/gemini.py +134 -110
  68. gobby/llm/litellm_executor.py +143 -6
  69. gobby/llm/resolver.py +98 -35
  70. gobby/mcp_proxy/importer.py +62 -4
  71. gobby/mcp_proxy/instructions.py +56 -0
  72. gobby/mcp_proxy/models.py +15 -0
  73. gobby/mcp_proxy/registries.py +68 -8
  74. gobby/mcp_proxy/server.py +33 -3
  75. gobby/mcp_proxy/services/recommendation.py +43 -11
  76. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  77. gobby/mcp_proxy/stdio.py +2 -1
  78. gobby/mcp_proxy/tools/__init__.py +0 -2
  79. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  80. gobby/mcp_proxy/tools/agents.py +31 -731
  81. gobby/mcp_proxy/tools/clones.py +518 -0
  82. gobby/mcp_proxy/tools/memory.py +3 -26
  83. gobby/mcp_proxy/tools/metrics.py +65 -1
  84. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  85. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  86. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  87. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  88. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  89. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  90. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  91. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  92. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  93. gobby/mcp_proxy/tools/skills/__init__.py +616 -0
  94. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  95. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  96. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  97. gobby/mcp_proxy/tools/task_sync.py +1 -1
  98. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  99. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  100. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  101. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  102. gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
  103. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  104. gobby/mcp_proxy/tools/workflows.py +1 -1
  105. gobby/mcp_proxy/tools/worktrees.py +0 -338
  106. gobby/memory/backends/__init__.py +6 -1
  107. gobby/memory/backends/mem0.py +6 -1
  108. gobby/memory/extractor.py +477 -0
  109. gobby/memory/ingestion/__init__.py +5 -0
  110. gobby/memory/ingestion/multimodal.py +221 -0
  111. gobby/memory/manager.py +73 -285
  112. gobby/memory/search/__init__.py +10 -0
  113. gobby/memory/search/coordinator.py +248 -0
  114. gobby/memory/services/__init__.py +5 -0
  115. gobby/memory/services/crossref.py +142 -0
  116. gobby/prompts/loader.py +5 -2
  117. gobby/runner.py +37 -16
  118. gobby/search/__init__.py +48 -6
  119. gobby/search/backends/__init__.py +159 -0
  120. gobby/search/backends/embedding.py +225 -0
  121. gobby/search/embeddings.py +238 -0
  122. gobby/search/models.py +148 -0
  123. gobby/search/unified.py +496 -0
  124. gobby/servers/http.py +24 -12
  125. gobby/servers/routes/admin.py +294 -0
  126. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  127. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  128. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  129. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  130. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  131. gobby/servers/routes/mcp/hooks.py +1 -1
  132. gobby/servers/routes/mcp/tools.py +48 -1317
  133. gobby/servers/websocket.py +2 -2
  134. gobby/sessions/analyzer.py +2 -0
  135. gobby/sessions/lifecycle.py +1 -1
  136. gobby/sessions/processor.py +10 -0
  137. gobby/sessions/transcripts/base.py +2 -0
  138. gobby/sessions/transcripts/claude.py +79 -10
  139. gobby/skills/__init__.py +91 -0
  140. gobby/skills/loader.py +685 -0
  141. gobby/skills/manager.py +384 -0
  142. gobby/skills/parser.py +286 -0
  143. gobby/skills/search.py +463 -0
  144. gobby/skills/sync.py +119 -0
  145. gobby/skills/updater.py +385 -0
  146. gobby/skills/validator.py +368 -0
  147. gobby/storage/clones.py +378 -0
  148. gobby/storage/database.py +1 -1
  149. gobby/storage/memories.py +43 -13
  150. gobby/storage/migrations.py +162 -201
  151. gobby/storage/sessions.py +116 -7
  152. gobby/storage/skills.py +782 -0
  153. gobby/storage/tasks/_crud.py +4 -4
  154. gobby/storage/tasks/_lifecycle.py +57 -7
  155. gobby/storage/tasks/_manager.py +14 -5
  156. gobby/storage/tasks/_models.py +8 -3
  157. gobby/sync/memories.py +40 -5
  158. gobby/sync/tasks.py +83 -6
  159. gobby/tasks/__init__.py +1 -2
  160. gobby/tasks/external_validator.py +1 -1
  161. gobby/tasks/validation.py +46 -35
  162. gobby/tools/summarizer.py +91 -10
  163. gobby/tui/api_client.py +4 -7
  164. gobby/tui/app.py +5 -3
  165. gobby/tui/screens/orchestrator.py +1 -2
  166. gobby/tui/screens/tasks.py +2 -4
  167. gobby/tui/ws_client.py +1 -1
  168. gobby/utils/daemon_client.py +2 -2
  169. gobby/utils/project_context.py +2 -3
  170. gobby/utils/status.py +13 -0
  171. gobby/workflows/actions.py +221 -1135
  172. gobby/workflows/artifact_actions.py +31 -0
  173. gobby/workflows/autonomous_actions.py +11 -0
  174. gobby/workflows/context_actions.py +93 -1
  175. gobby/workflows/detection_helpers.py +115 -31
  176. gobby/workflows/enforcement/__init__.py +47 -0
  177. gobby/workflows/enforcement/blocking.py +269 -0
  178. gobby/workflows/enforcement/commit_policy.py +283 -0
  179. gobby/workflows/enforcement/handlers.py +269 -0
  180. gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
  181. gobby/workflows/engine.py +13 -2
  182. gobby/workflows/git_utils.py +106 -0
  183. gobby/workflows/lifecycle_evaluator.py +29 -1
  184. gobby/workflows/llm_actions.py +30 -0
  185. gobby/workflows/loader.py +19 -6
  186. gobby/workflows/mcp_actions.py +20 -1
  187. gobby/workflows/memory_actions.py +154 -0
  188. gobby/workflows/safe_evaluator.py +183 -0
  189. gobby/workflows/session_actions.py +44 -0
  190. gobby/workflows/state_actions.py +60 -1
  191. gobby/workflows/stop_signal_actions.py +55 -0
  192. gobby/workflows/summary_actions.py +111 -1
  193. gobby/workflows/task_sync_actions.py +347 -0
  194. gobby/workflows/todo_actions.py +34 -1
  195. gobby/workflows/webhook_actions.py +185 -0
  196. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
  197. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
  198. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  199. gobby/adapters/codex.py +0 -1292
  200. gobby/install/claude/commands/gobby/bug.md +0 -51
  201. gobby/install/claude/commands/gobby/chore.md +0 -51
  202. gobby/install/claude/commands/gobby/epic.md +0 -52
  203. gobby/install/claude/commands/gobby/eval.md +0 -235
  204. gobby/install/claude/commands/gobby/feat.md +0 -49
  205. gobby/install/claude/commands/gobby/nit.md +0 -52
  206. gobby/install/claude/commands/gobby/ref.md +0 -52
  207. gobby/install/codex/prompts/forget.md +0 -7
  208. gobby/install/codex/prompts/memories.md +0 -7
  209. gobby/install/codex/prompts/recall.md +0 -7
  210. gobby/install/codex/prompts/remember.md +0 -13
  211. gobby/llm/gemini_executor.py +0 -339
  212. gobby/mcp_proxy/tools/session_messages.py +0 -1056
  213. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  214. gobby/prompts/defaults/expansion/system.md +0 -119
  215. gobby/prompts/defaults/expansion/user.md +0 -48
  216. gobby/prompts/defaults/external_validation/agent.md +0 -72
  217. gobby/prompts/defaults/external_validation/external.md +0 -63
  218. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  219. gobby/prompts/defaults/external_validation/system.md +0 -6
  220. gobby/prompts/defaults/features/import_mcp.md +0 -22
  221. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  222. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  223. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  224. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  225. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  226. gobby/prompts/defaults/features/server_description.md +0 -20
  227. gobby/prompts/defaults/features/server_description_system.md +0 -6
  228. gobby/prompts/defaults/features/task_description.md +0 -31
  229. gobby/prompts/defaults/features/task_description_system.md +0 -6
  230. gobby/prompts/defaults/features/tool_summary.md +0 -17
  231. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  232. gobby/prompts/defaults/research/step.md +0 -58
  233. gobby/prompts/defaults/validation/criteria.md +0 -47
  234. gobby/prompts/defaults/validation/validate.md +0 -38
  235. gobby/storage/migrations_legacy.py +0 -1359
  236. gobby/tasks/context.py +0 -747
  237. gobby/tasks/criteria.py +0 -342
  238. gobby/tasks/expansion.py +0 -626
  239. gobby/tasks/prompts/expand.py +0 -327
  240. gobby/tasks/research.py +0 -421
  241. gobby/tasks/tdd.py +0 -352
  242. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  243. {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  244. {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, no_commit_needed, override_justification
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. Commits should use [#N] format (e.g., git commit -m '[#42] feat: add feature'). Requires commits to be linked (auto-detected from commit message or use link_commit). Parent tasks require all children closed. Leaf tasks validate with LLM. Validation auto-skipped for: duplicate, already_implemented, wont_fix, obsolete.",
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 or commit check. "
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 before closing. Convenience for commit + close in one call.",
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 and its children by default."""
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
- deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade)
351
- if not deleted:
352
- return {"error": f"Task {task_id} not found"}
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 its subtasks.",
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 all child tasks as well. Defaults to True.",
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
- no_commit_needed: bool,
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
- no_commit_needed: If True, allow closing without commits
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
- # No commits linked - require explicit acknowledgment
55
- if no_commit_needed:
56
- if not override_justification:
57
- return ValidationResult(
58
- can_close=False,
59
- error_type="justification_required",
60
- message=(
61
- "When no_commit_needed=True, you must provide "
62
- "override_justification explaining why no commit was needed."
63
- ),
64
- )
65
- # Allowed to proceed - agent confirmed no commit needed
66
- else:
67
- return ValidationResult(
68
- can_close=False,
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 or no_commit_needed
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
@@ -808,7 +808,7 @@ def create_workflows_registry(
808
808
  shutil.copy(source, dest_path)
809
809
 
810
810
  # Clear loader cache so new workflow is discoverable
811
- _loader.clear_discovery_cache()
811
+ _loader.clear_cache()
812
812
 
813
813
  return {
814
814
  "success": True,