gobby 0.2.5__py3-none-any.whl → 0.2.6__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 (148) hide show
  1. gobby/adapters/claude_code.py +13 -4
  2. gobby/adapters/codex.py +43 -3
  3. gobby/agents/runner.py +8 -0
  4. gobby/cli/__init__.py +6 -0
  5. gobby/cli/clones.py +419 -0
  6. gobby/cli/conductor.py +266 -0
  7. gobby/cli/installers/antigravity.py +3 -9
  8. gobby/cli/installers/claude.py +9 -9
  9. gobby/cli/installers/codex.py +2 -8
  10. gobby/cli/installers/gemini.py +2 -8
  11. gobby/cli/installers/shared.py +71 -8
  12. gobby/cli/skills.py +858 -0
  13. gobby/cli/tasks/ai.py +0 -440
  14. gobby/cli/tasks/crud.py +44 -6
  15. gobby/cli/tasks/main.py +0 -4
  16. gobby/cli/tui.py +2 -2
  17. gobby/cli/utils.py +3 -3
  18. gobby/clones/__init__.py +13 -0
  19. gobby/clones/git.py +547 -0
  20. gobby/conductor/__init__.py +16 -0
  21. gobby/conductor/alerts.py +135 -0
  22. gobby/conductor/loop.py +164 -0
  23. gobby/conductor/monitors/__init__.py +11 -0
  24. gobby/conductor/monitors/agents.py +116 -0
  25. gobby/conductor/monitors/tasks.py +155 -0
  26. gobby/conductor/pricing.py +234 -0
  27. gobby/conductor/token_tracker.py +160 -0
  28. gobby/config/app.py +63 -1
  29. gobby/config/search.py +110 -0
  30. gobby/config/servers.py +1 -1
  31. gobby/config/skills.py +43 -0
  32. gobby/config/tasks.py +6 -14
  33. gobby/hooks/event_handlers.py +145 -2
  34. gobby/hooks/hook_manager.py +48 -2
  35. gobby/hooks/skill_manager.py +130 -0
  36. gobby/install/claude/hooks/hook_dispatcher.py +4 -4
  37. gobby/install/codex/hooks/hook_dispatcher.py +1 -1
  38. gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
  39. gobby/llm/claude.py +22 -34
  40. gobby/llm/claude_executor.py +46 -256
  41. gobby/llm/codex_executor.py +59 -291
  42. gobby/llm/executor.py +21 -0
  43. gobby/llm/gemini.py +134 -110
  44. gobby/llm/litellm_executor.py +143 -6
  45. gobby/llm/resolver.py +95 -33
  46. gobby/mcp_proxy/instructions.py +54 -0
  47. gobby/mcp_proxy/models.py +15 -0
  48. gobby/mcp_proxy/registries.py +68 -5
  49. gobby/mcp_proxy/server.py +33 -3
  50. gobby/mcp_proxy/services/tool_proxy.py +81 -1
  51. gobby/mcp_proxy/stdio.py +2 -1
  52. gobby/mcp_proxy/tools/__init__.py +0 -2
  53. gobby/mcp_proxy/tools/agent_messaging.py +317 -0
  54. gobby/mcp_proxy/tools/clones.py +903 -0
  55. gobby/mcp_proxy/tools/memory.py +1 -24
  56. gobby/mcp_proxy/tools/metrics.py +65 -1
  57. gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
  58. gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
  59. gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
  60. gobby/mcp_proxy/tools/session_messages.py +1 -2
  61. gobby/mcp_proxy/tools/skills/__init__.py +631 -0
  62. gobby/mcp_proxy/tools/task_orchestration.py +7 -0
  63. gobby/mcp_proxy/tools/task_readiness.py +14 -0
  64. gobby/mcp_proxy/tools/task_sync.py +1 -1
  65. gobby/mcp_proxy/tools/tasks/_context.py +0 -20
  66. gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
  67. gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
  68. gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
  69. gobby/mcp_proxy/tools/tasks/_lifecycle.py +60 -29
  70. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
  71. gobby/mcp_proxy/tools/workflows.py +1 -1
  72. gobby/mcp_proxy/tools/worktrees.py +5 -0
  73. gobby/memory/backends/__init__.py +6 -1
  74. gobby/memory/backends/mem0.py +6 -1
  75. gobby/memory/extractor.py +477 -0
  76. gobby/memory/manager.py +11 -2
  77. gobby/prompts/defaults/handoff/compact.md +63 -0
  78. gobby/prompts/defaults/handoff/session_end.md +57 -0
  79. gobby/prompts/defaults/memory/extract.md +61 -0
  80. gobby/runner.py +37 -16
  81. gobby/search/__init__.py +48 -6
  82. gobby/search/backends/__init__.py +159 -0
  83. gobby/search/backends/embedding.py +225 -0
  84. gobby/search/embeddings.py +238 -0
  85. gobby/search/models.py +148 -0
  86. gobby/search/unified.py +496 -0
  87. gobby/servers/http.py +23 -8
  88. gobby/servers/routes/admin.py +280 -0
  89. gobby/servers/routes/mcp/tools.py +241 -52
  90. gobby/servers/websocket.py +2 -2
  91. gobby/sessions/analyzer.py +2 -0
  92. gobby/sessions/transcripts/base.py +1 -0
  93. gobby/sessions/transcripts/claude.py +64 -5
  94. gobby/skills/__init__.py +91 -0
  95. gobby/skills/loader.py +685 -0
  96. gobby/skills/manager.py +384 -0
  97. gobby/skills/parser.py +258 -0
  98. gobby/skills/search.py +463 -0
  99. gobby/skills/sync.py +119 -0
  100. gobby/skills/updater.py +385 -0
  101. gobby/skills/validator.py +368 -0
  102. gobby/storage/clones.py +378 -0
  103. gobby/storage/database.py +1 -1
  104. gobby/storage/memories.py +43 -13
  105. gobby/storage/migrations.py +180 -6
  106. gobby/storage/sessions.py +73 -0
  107. gobby/storage/skills.py +749 -0
  108. gobby/storage/tasks/_crud.py +4 -4
  109. gobby/storage/tasks/_lifecycle.py +41 -6
  110. gobby/storage/tasks/_manager.py +14 -5
  111. gobby/storage/tasks/_models.py +8 -3
  112. gobby/sync/memories.py +39 -4
  113. gobby/sync/tasks.py +83 -6
  114. gobby/tasks/__init__.py +1 -2
  115. gobby/tasks/validation.py +24 -15
  116. gobby/tui/api_client.py +4 -7
  117. gobby/tui/app.py +5 -3
  118. gobby/tui/screens/orchestrator.py +1 -2
  119. gobby/tui/screens/tasks.py +2 -4
  120. gobby/tui/ws_client.py +1 -1
  121. gobby/utils/daemon_client.py +2 -2
  122. gobby/workflows/actions.py +84 -2
  123. gobby/workflows/context_actions.py +43 -0
  124. gobby/workflows/detection_helpers.py +115 -31
  125. gobby/workflows/engine.py +13 -2
  126. gobby/workflows/lifecycle_evaluator.py +29 -1
  127. gobby/workflows/loader.py +19 -6
  128. gobby/workflows/memory_actions.py +74 -0
  129. gobby/workflows/summary_actions.py +17 -0
  130. gobby/workflows/task_enforcement_actions.py +448 -6
  131. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/METADATA +82 -21
  132. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/RECORD +136 -107
  133. gobby/install/codex/prompts/forget.md +0 -7
  134. gobby/install/codex/prompts/memories.md +0 -7
  135. gobby/install/codex/prompts/recall.md +0 -7
  136. gobby/install/codex/prompts/remember.md +0 -13
  137. gobby/llm/gemini_executor.py +0 -339
  138. gobby/mcp_proxy/tools/task_expansion.py +0 -591
  139. gobby/tasks/context.py +0 -747
  140. gobby/tasks/criteria.py +0 -342
  141. gobby/tasks/expansion.py +0 -626
  142. gobby/tasks/prompts/expand.py +0 -327
  143. gobby/tasks/research.py +0 -421
  144. gobby/tasks/tdd.py +0 -352
  145. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/WHEEL +0 -0
  146. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/entry_points.txt +0 -0
  147. {gobby-0.2.5.dist-info → gobby-0.2.6.dist-info}/licenses/LICENSE.md +0 -0
  148. {gobby-0.2.5.dist-info → gobby-0.2.6.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:
@@ -85,9 +82,7 @@ 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 {
93
88
  "error": commit_result.error_type,
@@ -141,7 +136,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
141
136
 
142
137
  # Determine close outcome
143
138
  route_to_review, store_override = determine_close_outcome(
144
- task, skip_validation, no_commit_needed, override_justification
139
+ task, skip_validation, override_justification
145
140
  )
146
141
 
147
142
  # Get git commit SHA (best-effort, dynamic short format for consistency)
@@ -194,6 +189,23 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
194
189
  except Exception:
195
190
  pass # nosec B110 - best-effort linking, don't fail the close
196
191
 
192
+ # Clear workflow task_claimed state if this was the claimed task
193
+ # Respects the clear_task_on_close variable (defaults to True if not set)
194
+ # This is done here because Claude Code's post-tool-use hook doesn't include
195
+ # the tool result, so the detection_helpers can't verify close succeeded
196
+ if session_id:
197
+ try:
198
+ state = ctx.workflow_state_manager.get_state(session_id)
199
+ if state and state.variables.get("claimed_task_id") == resolved_id:
200
+ # Check if clear_task_on_close is enabled (default: True)
201
+ clear_on_close = state.variables.get("clear_task_on_close", True)
202
+ if clear_on_close:
203
+ state.variables["task_claimed"] = False
204
+ state.variables["claimed_task_id"] = None
205
+ ctx.workflow_state_manager.save_state(state)
206
+ except Exception:
207
+ pass # nosec B110 - best-effort state update
208
+
197
209
  # Update worktree status based on closure reason (case-insensitive)
198
210
  try:
199
211
  reason_normalized = reason.lower()
@@ -216,7 +228,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
216
228
 
217
229
  registry.register(
218
230
  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.",
231
+ 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
232
  input_schema={
221
233
  "type": "object",
222
234
  "properties": {
@@ -251,24 +263,14 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
251
263
  "override_justification": {
252
264
  "type": "string",
253
265
  "description": (
254
- "Justification for bypassing validation or commit check. "
255
- "Required when skip_validation=True or no_commit_needed=True. "
266
+ "Justification for bypassing validation. Required when skip_validation=True. "
256
267
  "Example: 'Validation saw truncated diff - verified via git show that commit includes all changes'"
257
268
  ),
258
269
  "default": None,
259
270
  },
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
271
  "commit_sha": {
270
272
  "type": "string",
271
- "description": "Git commit SHA to link before closing. Convenience for commit + close in one call.",
273
+ "description": "RECOMMENDED: Git commit SHA to link and close in one call. Use this instead of separate link_commit + close_task calls.",
272
274
  "default": None,
273
275
  },
274
276
  },
@@ -334,8 +336,12 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
334
336
  func=reopen_task,
335
337
  )
336
338
 
337
- def delete_task(task_id: str, cascade: bool = True) -> dict[str, Any]:
338
- """Delete a task and its children by default."""
339
+ def delete_task(task_id: str, cascade: bool = True, unlink: bool = False) -> dict[str, Any]:
340
+ """Delete a task.
341
+
342
+ By default (cascade=True), deletes subtasks and dependent tasks.
343
+ Use unlink=True to remove dependency links but preserve dependent tasks.
344
+ """
339
345
  try:
340
346
  resolved_id = resolve_task_id_for_mcp(ctx.task_manager, task_id)
341
347
  except (TaskNotFoundError, ValueError) as e:
@@ -347,9 +353,26 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
347
353
  return {"error": f"Task {task_id} not found"}
348
354
  ref = f"#{task.seq_num}" if task.seq_num else resolved_id[:8]
349
355
 
350
- deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade)
351
- if not deleted:
352
- return {"error": f"Task {task_id} not found"}
356
+ try:
357
+ deleted = ctx.task_manager.delete_task(resolved_id, cascade=cascade, unlink=unlink)
358
+ if not deleted:
359
+ return {"error": f"Task {task_id} not found"}
360
+ except ValueError as e:
361
+ error_msg = str(e)
362
+ if "dependent task(s)" in error_msg:
363
+ return {
364
+ "error": "has_dependents",
365
+ "message": error_msg,
366
+ "suggestion": f"Use cascade=True to delete task {ref} and its dependents, "
367
+ f"or unlink=True to preserve dependent tasks.",
368
+ }
369
+ elif "has children" in error_msg:
370
+ return {
371
+ "error": "has_children",
372
+ "message": error_msg,
373
+ "suggestion": f"Use cascade=True to delete task {ref} and all its subtasks.",
374
+ }
375
+ return {"error": error_msg}
353
376
 
354
377
  return {
355
378
  "ref": ref,
@@ -358,7 +381,9 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
358
381
 
359
382
  registry.register(
360
383
  name="delete_task",
361
- description="Delete a task and its subtasks.",
384
+ description="Delete a task. By default (cascade=True), deletes subtasks and dependent tasks. "
385
+ "Set cascade=False to fail if task has children or dependents. "
386
+ "Use unlink=True to remove dependency links but preserve dependent tasks.",
362
387
  input_schema={
363
388
  "type": "object",
364
389
  "properties": {
@@ -368,9 +393,15 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
368
393
  },
369
394
  "cascade": {
370
395
  "type": "boolean",
371
- "description": "If True, delete all child tasks as well. Defaults to True.",
396
+ "description": "If True, delete subtasks and dependent tasks. Defaults to True.",
372
397
  "default": True,
373
398
  },
399
+ "unlink": {
400
+ "type": "boolean",
401
+ "description": "If True, remove dependency links but preserve dependent tasks. "
402
+ "Ignored if cascade=True.",
403
+ "default": False,
404
+ },
374
405
  },
375
406
  "required": ["task_id"],
376
407
  },
@@ -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
+ "A 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,
@@ -1014,6 +1014,11 @@ def create_worktrees_registry(
1014
1014
  if isinstance(terminal, str):
1015
1015
  terminal = terminal.lower()
1016
1016
 
1017
+ # Default to 'worktree-agent' workflow if not specified
1018
+ # This workflow restricts tools available to spawned agents in worktrees
1019
+ if workflow is None:
1020
+ workflow = "worktree-agent"
1021
+
1017
1022
  # Validate workflow (reject lifecycle workflows)
1018
1023
  if workflow:
1019
1024
  workflow_loader = WorkflowLoader()
@@ -76,7 +76,12 @@ def get_backend(backend_type: str, **kwargs: Any) -> MemoryBackendProtocol:
76
76
  return NullBackend()
77
77
 
78
78
  elif backend_type == "mem0":
79
- from gobby.memory.backends.mem0 import Mem0Backend
79
+ try:
80
+ from gobby.memory.backends.mem0 import Mem0Backend
81
+ except ImportError as e:
82
+ raise ImportError(
83
+ "mem0ai is not installed. Install with: pip install gobby[mem0]"
84
+ ) from e
80
85
 
81
86
  api_key: str | None = kwargs.get("api_key")
82
87
  if api_key is None:
@@ -60,7 +60,12 @@ class Mem0Backend:
60
60
  **kwargs: Additional MemoryClient configuration
61
61
  """
62
62
  # Lazy import to avoid requiring mem0ai when not used
63
- from mem0 import MemoryClient
63
+ try:
64
+ from mem0 import MemoryClient
65
+ except ImportError as e:
66
+ raise ImportError(
67
+ "Mem0 backend requires 'mem0ai' package. Install it with: pip install gobby[mem0]"
68
+ ) from e
64
69
 
65
70
  self._client: MemoryClient = MemoryClient(api_key=api_key, **kwargs)
66
71
  self._default_user_id = user_id