gobby 0.2.8__py3-none-any.whl → 0.2.11__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 (168) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +6 -0
  3. gobby/adapters/base.py +11 -2
  4. gobby/adapters/claude_code.py +5 -28
  5. gobby/adapters/codex_impl/adapter.py +38 -43
  6. gobby/adapters/copilot.py +324 -0
  7. gobby/adapters/cursor.py +373 -0
  8. gobby/adapters/gemini.py +2 -26
  9. gobby/adapters/windsurf.py +359 -0
  10. gobby/agents/definitions.py +162 -2
  11. gobby/agents/isolation.py +33 -1
  12. gobby/agents/pty_reader.py +192 -0
  13. gobby/agents/registry.py +10 -1
  14. gobby/agents/runner.py +24 -8
  15. gobby/agents/sandbox.py +8 -3
  16. gobby/agents/session.py +4 -0
  17. gobby/agents/spawn.py +9 -2
  18. gobby/agents/spawn_executor.py +49 -61
  19. gobby/agents/spawners/command_builder.py +4 -4
  20. gobby/app_context.py +64 -0
  21. gobby/cli/__init__.py +4 -0
  22. gobby/cli/install.py +259 -4
  23. gobby/cli/installers/__init__.py +12 -0
  24. gobby/cli/installers/copilot.py +242 -0
  25. gobby/cli/installers/cursor.py +244 -0
  26. gobby/cli/installers/shared.py +3 -0
  27. gobby/cli/installers/windsurf.py +242 -0
  28. gobby/cli/pipelines.py +639 -0
  29. gobby/cli/sessions.py +3 -1
  30. gobby/cli/skills.py +209 -0
  31. gobby/cli/tasks/crud.py +6 -5
  32. gobby/cli/tasks/search.py +1 -1
  33. gobby/cli/ui.py +116 -0
  34. gobby/cli/utils.py +5 -17
  35. gobby/cli/workflows.py +38 -17
  36. gobby/config/app.py +5 -0
  37. gobby/config/features.py +0 -20
  38. gobby/config/skills.py +23 -2
  39. gobby/config/tasks.py +4 -0
  40. gobby/hooks/broadcaster.py +9 -0
  41. gobby/hooks/event_handlers/__init__.py +155 -0
  42. gobby/hooks/event_handlers/_agent.py +175 -0
  43. gobby/hooks/event_handlers/_base.py +92 -0
  44. gobby/hooks/event_handlers/_misc.py +66 -0
  45. gobby/hooks/event_handlers/_session.py +487 -0
  46. gobby/hooks/event_handlers/_tool.py +196 -0
  47. gobby/hooks/events.py +48 -0
  48. gobby/hooks/hook_manager.py +27 -3
  49. gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
  50. gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
  51. gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
  52. gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
  53. gobby/llm/__init__.py +14 -1
  54. gobby/llm/claude.py +594 -43
  55. gobby/llm/service.py +149 -0
  56. gobby/mcp_proxy/importer.py +4 -41
  57. gobby/mcp_proxy/instructions.py +9 -27
  58. gobby/mcp_proxy/manager.py +13 -3
  59. gobby/mcp_proxy/models.py +1 -0
  60. gobby/mcp_proxy/registries.py +66 -5
  61. gobby/mcp_proxy/server.py +6 -2
  62. gobby/mcp_proxy/services/recommendation.py +2 -28
  63. gobby/mcp_proxy/services/tool_filter.py +7 -0
  64. gobby/mcp_proxy/services/tool_proxy.py +19 -1
  65. gobby/mcp_proxy/stdio.py +37 -21
  66. gobby/mcp_proxy/tools/agents.py +7 -0
  67. gobby/mcp_proxy/tools/artifacts.py +3 -3
  68. gobby/mcp_proxy/tools/hub.py +30 -1
  69. gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
  70. gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
  71. gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
  72. gobby/mcp_proxy/tools/orchestration/review.py +17 -4
  73. gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
  74. gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
  75. gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
  76. gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
  77. gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
  78. gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
  79. gobby/mcp_proxy/tools/skills/__init__.py +184 -30
  80. gobby/mcp_proxy/tools/spawn_agent.py +229 -14
  81. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  82. gobby/mcp_proxy/tools/tasks/_context.py +8 -0
  83. gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
  84. gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
  85. gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
  86. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
  87. gobby/mcp_proxy/tools/tasks/_search.py +1 -1
  88. gobby/mcp_proxy/tools/workflows/__init__.py +273 -0
  89. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  90. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  91. gobby/mcp_proxy/tools/workflows/_lifecycle.py +332 -0
  92. gobby/mcp_proxy/tools/workflows/_query.py +226 -0
  93. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  94. gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
  95. gobby/mcp_proxy/tools/worktrees.py +54 -15
  96. gobby/memory/components/__init__.py +0 -0
  97. gobby/memory/components/ingestion.py +98 -0
  98. gobby/memory/components/search.py +108 -0
  99. gobby/memory/context.py +5 -5
  100. gobby/memory/manager.py +16 -25
  101. gobby/paths.py +51 -0
  102. gobby/prompts/loader.py +1 -35
  103. gobby/runner.py +131 -16
  104. gobby/servers/http.py +193 -150
  105. gobby/servers/routes/__init__.py +2 -0
  106. gobby/servers/routes/admin.py +56 -0
  107. gobby/servers/routes/mcp/endpoints/execution.py +33 -32
  108. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  109. gobby/servers/routes/mcp/hooks.py +10 -1
  110. gobby/servers/routes/pipelines.py +227 -0
  111. gobby/servers/websocket.py +314 -1
  112. gobby/sessions/analyzer.py +89 -3
  113. gobby/sessions/manager.py +5 -5
  114. gobby/sessions/transcripts/__init__.py +3 -0
  115. gobby/sessions/transcripts/claude.py +5 -0
  116. gobby/sessions/transcripts/codex.py +5 -0
  117. gobby/sessions/transcripts/gemini.py +5 -0
  118. gobby/skills/hubs/__init__.py +25 -0
  119. gobby/skills/hubs/base.py +234 -0
  120. gobby/skills/hubs/claude_plugins.py +328 -0
  121. gobby/skills/hubs/clawdhub.py +289 -0
  122. gobby/skills/hubs/github_collection.py +465 -0
  123. gobby/skills/hubs/manager.py +263 -0
  124. gobby/skills/hubs/skillhub.py +342 -0
  125. gobby/skills/parser.py +23 -0
  126. gobby/skills/sync.py +5 -4
  127. gobby/storage/artifacts.py +19 -0
  128. gobby/storage/memories.py +4 -4
  129. gobby/storage/migrations.py +118 -3
  130. gobby/storage/pipelines.py +367 -0
  131. gobby/storage/sessions.py +23 -4
  132. gobby/storage/skills.py +48 -8
  133. gobby/storage/tasks/_aggregates.py +2 -2
  134. gobby/storage/tasks/_lifecycle.py +4 -4
  135. gobby/storage/tasks/_models.py +7 -1
  136. gobby/storage/tasks/_queries.py +3 -3
  137. gobby/sync/memories.py +4 -3
  138. gobby/tasks/commits.py +48 -17
  139. gobby/tasks/external_validator.py +4 -17
  140. gobby/tasks/validation.py +13 -87
  141. gobby/tools/summarizer.py +18 -51
  142. gobby/utils/status.py +13 -0
  143. gobby/workflows/actions.py +80 -0
  144. gobby/workflows/context_actions.py +265 -27
  145. gobby/workflows/definitions.py +119 -1
  146. gobby/workflows/detection_helpers.py +23 -11
  147. gobby/workflows/enforcement/__init__.py +11 -1
  148. gobby/workflows/enforcement/blocking.py +96 -0
  149. gobby/workflows/enforcement/handlers.py +35 -1
  150. gobby/workflows/enforcement/task_policy.py +18 -0
  151. gobby/workflows/engine.py +26 -4
  152. gobby/workflows/evaluator.py +8 -5
  153. gobby/workflows/lifecycle_evaluator.py +59 -27
  154. gobby/workflows/loader.py +567 -30
  155. gobby/workflows/lobster_compat.py +147 -0
  156. gobby/workflows/pipeline_executor.py +801 -0
  157. gobby/workflows/pipeline_state.py +172 -0
  158. gobby/workflows/pipeline_webhooks.py +206 -0
  159. gobby/workflows/premature_stop.py +5 -0
  160. gobby/worktrees/git.py +135 -20
  161. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
  162. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
  163. gobby/hooks/event_handlers.py +0 -1008
  164. gobby/mcp_proxy/tools/workflows.py +0 -1023
  165. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
  166. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
  167. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
  168. {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ from __future__ import annotations
14
14
  import logging
15
15
  import uuid
16
16
  from pathlib import Path
17
- from typing import TYPE_CHECKING, Any, Literal, cast
17
+ from typing import TYPE_CHECKING, Any, Literal
18
18
 
19
19
  from gobby.agents.definitions import AgentDefinition, AgentDefinitionLoader
20
20
  from gobby.agents.isolation import (
@@ -28,6 +28,7 @@ from gobby.mcp_proxy.tools.internal import InternalToolRegistry
28
28
  from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
29
29
  from gobby.utils.machine_id import get_machine_id
30
30
  from gobby.utils.project_context import get_project_context
31
+ from gobby.workflows.loader import WorkflowLoader
31
32
 
32
33
  if TYPE_CHECKING:
33
34
  from gobby.agents.runner import AgentRunner
@@ -36,6 +37,82 @@ if TYPE_CHECKING:
36
37
  logger = logging.getLogger(__name__)
37
38
 
38
39
 
40
+ async def _handle_self_mode(
41
+ workflow: str | None,
42
+ parent_session_id: str,
43
+ step_variables: dict[str, Any] | None,
44
+ initial_step: str | None,
45
+ workflow_loader: WorkflowLoader | None,
46
+ state_manager: Any | None,
47
+ session_manager: Any | None,
48
+ db: Any | None,
49
+ project_path: str | None,
50
+ ) -> dict[str, Any]:
51
+ """
52
+ Activate workflow on calling session instead of spawning a new agent.
53
+
54
+ This is the implementation for mode=self, which activates a workflow
55
+ on the parent session rather than creating a new child session.
56
+
57
+ Args:
58
+ workflow: Workflow name to activate
59
+ parent_session_id: Session to activate workflow on (the caller)
60
+ step_variables: Initial variables for the workflow
61
+ initial_step: Optional starting step (defaults to first step)
62
+ workflow_loader: WorkflowLoader instance
63
+ state_manager: WorkflowStateManager instance (or created from db if None)
64
+ session_manager: LocalSessionManager instance
65
+ db: Database instance
66
+ project_path: Project path for workflow lookup
67
+
68
+ Returns:
69
+ Dict with success status and activation details
70
+ """
71
+ if not workflow:
72
+ return {"success": False, "error": "mode: self requires a workflow to activate"}
73
+
74
+ # Create state_manager from db if not provided
75
+ effective_state_manager = state_manager
76
+ if effective_state_manager is None and db is not None:
77
+ from gobby.workflows.state_manager import WorkflowStateManager
78
+
79
+ effective_state_manager = WorkflowStateManager(db)
80
+
81
+ if not workflow_loader or not effective_state_manager or not session_manager or not db:
82
+ return {
83
+ "success": False,
84
+ "error": "mode: self requires workflow_loader, state_manager (or db), session_manager, and db",
85
+ }
86
+
87
+ # Import and call the existing activate_workflow function
88
+ from gobby.mcp_proxy.tools.workflows._lifecycle import activate_workflow
89
+
90
+ result = activate_workflow(
91
+ loader=workflow_loader,
92
+ state_manager=effective_state_manager,
93
+ session_manager=session_manager,
94
+ db=db,
95
+ name=workflow,
96
+ session_id=parent_session_id,
97
+ initial_step=initial_step,
98
+ variables=step_variables,
99
+ project_path=project_path,
100
+ )
101
+
102
+ if not result.get("success"):
103
+ return result
104
+
105
+ return {
106
+ "success": True,
107
+ "mode": "self",
108
+ "workflow_activated": workflow,
109
+ "session_id": parent_session_id,
110
+ "step": result.get("step"),
111
+ "steps": result.get("steps"),
112
+ "message": f"Workflow '{workflow}' activated on session {parent_session_id}",
113
+ }
114
+
115
+
39
116
  async def spawn_agent_impl(
40
117
  prompt: str,
41
118
  runner: AgentRunner,
@@ -53,7 +130,8 @@ async def spawn_agent_impl(
53
130
  clone_manager: Any | None = None,
54
131
  # Execution
55
132
  workflow: str | None = None,
56
- mode: Literal["terminal", "embedded", "headless"] | None = None,
133
+ mode: Literal["terminal", "embedded", "headless", "self"] | None = None,
134
+ initial_step: str | None = None, # For mode=self, start at specific step
57
135
  terminal: str = "auto",
58
136
  provider: str | None = None,
59
137
  model: str | None = None,
@@ -68,6 +146,11 @@ async def spawn_agent_impl(
68
146
  # Context
69
147
  parent_session_id: str | None = None,
70
148
  project_path: str | None = None,
149
+ # For mode=self (workflow activation on caller session)
150
+ workflow_loader: WorkflowLoader | None = None,
151
+ state_manager: Any | None = None, # WorkflowStateManager
152
+ session_manager: Any | None = None, # LocalSessionManager
153
+ db: Any | None = None, # DatabaseProtocol
71
154
  ) -> dict[str, Any]:
72
155
  """
73
156
  Core spawn_agent implementation that can be called directly.
@@ -91,7 +174,7 @@ async def spawn_agent_impl(
91
174
  workflow: Workflow to use
92
175
  mode: Execution mode (terminal/embedded/headless)
93
176
  terminal: Terminal type for terminal mode
94
- provider: AI provider (claude/gemini/codex)
177
+ provider: AI provider (claude/gemini/codex/cursor/windsurf/copilot)
95
178
  model: Model to use
96
179
  timeout: Timeout in seconds
97
180
  max_turns: Maximum conversation turns
@@ -116,18 +199,77 @@ async def spawn_agent_impl(
116
199
  effective_provider = agent_def.provider
117
200
  effective_provider = effective_provider or "claude"
118
201
 
119
- effective_mode: Literal["terminal", "embedded", "headless"] | None = mode
202
+ effective_mode: Literal["terminal", "embedded", "headless", "self"] | None = mode
120
203
  if effective_mode is None and agent_def:
121
- effective_mode = cast(Literal["terminal", "embedded", "headless"], agent_def.mode)
204
+ effective_mode = agent_def.get_effective_mode(workflow)
122
205
  effective_mode = effective_mode or "terminal"
123
206
 
124
- effective_workflow = workflow
125
- if effective_workflow is None and agent_def:
126
- effective_workflow = agent_def.workflow
207
+ # Resolve workflow using agent_def's named workflows map
208
+ # Resolution order: explicit param > agent's workflows map > legacy workflow field
209
+ effective_workflow: str | None = None
210
+ if agent_def:
211
+ effective_workflow = agent_def.get_effective_workflow(workflow)
212
+ elif workflow:
213
+ effective_workflow = workflow
214
+
215
+ # Handle mode=self: activate workflow on caller session instead of spawning
216
+ if effective_mode == "self":
217
+ # Validate constraints
218
+ if effective_isolation != "current":
219
+ return {
220
+ "success": False,
221
+ "error": "mode: self is incompatible with isolation (worktree/clone). "
222
+ "Self mode activates a workflow on the calling session.",
223
+ }
224
+ if not effective_workflow:
225
+ return {
226
+ "success": False,
227
+ "error": "mode: self requires a workflow to activate",
228
+ }
229
+ if not parent_session_id:
230
+ return {
231
+ "success": False,
232
+ "error": "mode: self requires parent_session_id (the session to activate on)",
233
+ }
234
+
235
+ # Resolve step_variables for workflow activation
236
+ self_step_variables: dict[str, Any] | None = None
237
+ if task_id and task_manager:
238
+ # Resolve project context first for task resolution
239
+ ctx = get_project_context(Path(project_path) if project_path else None)
240
+ self_project_id = ctx.get("id") if ctx else None
241
+ if self_project_id:
242
+ try:
243
+ self_task_id = resolve_task_id_for_mcp(task_manager, task_id, self_project_id)
244
+ task = task_manager.get_task(self_task_id)
245
+ if task:
246
+ self_step_variables = {
247
+ "assigned_task_id": f"#{task.seq_num}" if task.seq_num else self_task_id
248
+ }
249
+ except Exception as e:
250
+ logger.warning(f"Failed to resolve task_id {task_id}: {e}")
251
+
252
+ return await _handle_self_mode(
253
+ workflow=effective_workflow,
254
+ parent_session_id=parent_session_id,
255
+ step_variables=self_step_variables,
256
+ initial_step=initial_step,
257
+ workflow_loader=workflow_loader,
258
+ state_manager=state_manager,
259
+ session_manager=session_manager,
260
+ db=db,
261
+ project_path=project_path,
262
+ )
127
263
 
128
264
  effective_base_branch = base_branch
129
265
  if effective_base_branch is None and agent_def:
130
266
  effective_base_branch = agent_def.base_branch
267
+ # Auto-detect current branch if no base_branch specified
268
+ if effective_base_branch is None and git_manager:
269
+ try:
270
+ effective_base_branch = git_manager.get_current_branch()
271
+ except Exception: # nosec B110 - fallback to default branch is intentional
272
+ effective_base_branch = None
131
273
  effective_base_branch = effective_base_branch or "main"
132
274
 
133
275
  effective_branch_prefix = None
@@ -250,7 +392,14 @@ async def spawn_agent_impl(
250
392
  session_id = str(uuid.uuid4())
251
393
  run_id = str(uuid.uuid4())
252
394
 
253
- # 10. Execute spawn via SpawnExecutor
395
+ # 10. Build step_variables for workflow activation (e.g., assigned_task_id)
396
+ step_variables: dict[str, Any] | None = None
397
+ if resolved_task_id:
398
+ step_variables = {
399
+ "assigned_task_id": f"#{task_seq_num}" if task_seq_num else resolved_task_id
400
+ }
401
+
402
+ # 11. Execute spawn via SpawnExecutor
254
403
  spawn_request = SpawnRequest(
255
404
  prompt=enhanced_prompt,
256
405
  cwd=isolation_ctx.cwd,
@@ -262,8 +411,10 @@ async def spawn_agent_impl(
262
411
  parent_session_id=parent_session_id,
263
412
  project_id=project_id,
264
413
  workflow=effective_workflow,
414
+ step_variables=step_variables,
265
415
  worktree_id=isolation_ctx.worktree_id,
266
416
  clone_id=isolation_ctx.clone_id,
417
+ branch_name=isolation_ctx.branch_name,
267
418
  session_manager=runner._child_session_manager,
268
419
  machine_id=get_machine_id() or "unknown",
269
420
  sandbox_config=effective_sandbox_config,
@@ -314,6 +465,10 @@ def create_spawn_agent_registry(
314
465
  clone_storage: Any | None = None,
315
466
  clone_manager: Any | None = None,
316
467
  session_manager: Any | None = None,
468
+ workflow_loader: WorkflowLoader | None = None,
469
+ # For mode=self (workflow activation on caller session)
470
+ state_manager: Any | None = None, # WorkflowStateManager
471
+ db: Any | None = None, # DatabaseProtocol
317
472
  ) -> InternalToolRegistry:
318
473
  """
319
474
  Create a spawn_agent tool registry with the unified spawn_agent tool.
@@ -327,6 +482,9 @@ def create_spawn_agent_registry(
327
482
  clone_storage: Storage for clone records.
328
483
  clone_manager: Git manager for clone operations.
329
484
  session_manager: Session manager for resolving session references.
485
+ workflow_loader: Loader for workflow validation.
486
+ state_manager: WorkflowStateManager for mode=self activation.
487
+ db: Database instance for mode=self activation.
330
488
 
331
489
  Returns:
332
490
  InternalToolRegistry with spawn_agent tool registered.
@@ -345,8 +503,9 @@ def create_spawn_agent_registry(
345
503
  description="Unified agent spawning with isolation support",
346
504
  )
347
505
 
348
- # Use provided loader or create default
506
+ # Use provided loaders or create defaults
349
507
  loader = agent_loader or AgentDefinitionLoader()
508
+ wf_loader = workflow_loader or WorkflowLoader()
350
509
 
351
510
  @registry.tool(
352
511
  name="spawn_agent",
@@ -367,7 +526,8 @@ def create_spawn_agent_registry(
367
526
  base_branch: str | None = None,
368
527
  # Execution
369
528
  workflow: str | None = None,
370
- mode: Literal["terminal", "embedded", "headless"] | None = None,
529
+ mode: Literal["terminal", "embedded", "headless", "self"] | None = None,
530
+ initial_step: str | None = None,
371
531
  terminal: str = "auto",
372
532
  provider: str | None = None,
373
533
  model: str | None = None,
@@ -394,9 +554,11 @@ def create_spawn_agent_registry(
394
554
  branch_name: Git branch name (auto-generated from task if not provided)
395
555
  base_branch: Base branch for worktree/clone
396
556
  workflow: Workflow to use
397
- mode: Execution mode (terminal/embedded/headless)
557
+ mode: Execution mode (terminal/embedded/headless/self).
558
+ 'self' activates workflow on caller session instead of spawning.
559
+ initial_step: For mode=self, start at specific step (defaults to first)
398
560
  terminal: Terminal type for terminal mode
399
- provider: AI provider (claude/gemini/codex)
561
+ provider: AI provider (claude/gemini/codex/cursor/windsurf/copilot)
400
562
  model: Model to use
401
563
  timeout: Timeout in seconds
402
564
  max_turns: Maximum conversation turns
@@ -423,6 +585,53 @@ def create_spawn_agent_registry(
423
585
  if agent_def is None and agent != "generic":
424
586
  return {"success": False, "error": f"Agent '{agent}' not found"}
425
587
 
588
+ # Determine effective workflow using agent's named workflows map
589
+ # Resolution: explicit param > agent's workflows map > legacy workflow field
590
+ effective_workflow: str | None = None
591
+ inline_workflow_spec = None
592
+
593
+ if agent_def:
594
+ effective_workflow = agent_def.get_effective_workflow(workflow)
595
+
596
+ # Check if this is an inline workflow that needs registration
597
+ if workflow and agent_def.workflows and workflow in agent_def.workflows:
598
+ spec = agent_def.workflows[workflow]
599
+ if spec.is_inline():
600
+ inline_workflow_spec = spec
601
+ elif (
602
+ not workflow
603
+ and agent_def.default_workflow
604
+ and agent_def.workflows
605
+ and agent_def.default_workflow in agent_def.workflows
606
+ ):
607
+ spec = agent_def.workflows[agent_def.default_workflow]
608
+ if spec.is_inline():
609
+ inline_workflow_spec = spec
610
+ elif workflow:
611
+ effective_workflow = workflow
612
+
613
+ # Get project_path for workflow lookup
614
+ ctx = get_project_context(Path(project_path) if project_path else None)
615
+ wf_project_path = ctx.get("project_path") if ctx else None
616
+
617
+ # Register inline workflow if needed
618
+ if inline_workflow_spec and effective_workflow:
619
+ wf_loader.register_inline_workflow(
620
+ effective_workflow, inline_workflow_spec.model_dump(), project_path=wf_project_path
621
+ )
622
+
623
+ # Validate workflow exists if specified (skip for inline that we just registered)
624
+ if effective_workflow and not inline_workflow_spec:
625
+ loaded_workflow = wf_loader.load_workflow(
626
+ effective_workflow, project_path=wf_project_path
627
+ )
628
+ if loaded_workflow is None:
629
+ return {
630
+ "success": False,
631
+ "error": f"Workflow '{effective_workflow}' not found. "
632
+ f"Check available workflows with list_workflows().",
633
+ }
634
+
426
635
  # Delegate to spawn_agent_impl
427
636
  return await spawn_agent_impl(
428
637
  prompt=prompt,
@@ -437,8 +646,9 @@ def create_spawn_agent_registry(
437
646
  git_manager=git_manager,
438
647
  clone_storage=clone_storage,
439
648
  clone_manager=clone_manager,
440
- workflow=workflow,
649
+ workflow=effective_workflow,
441
650
  mode=mode,
651
+ initial_step=initial_step,
442
652
  terminal=terminal,
443
653
  provider=provider,
444
654
  model=model,
@@ -450,6 +660,11 @@ def create_spawn_agent_registry(
450
660
  sandbox_extra_paths=sandbox_extra_paths,
451
661
  parent_session_id=resolved_parent_session_id,
452
662
  project_path=project_path,
663
+ # For mode=self
664
+ workflow_loader=wf_loader,
665
+ state_manager=state_manager,
666
+ session_manager=session_manager,
667
+ db=db,
453
668
  )
454
669
 
455
670
  return registry
@@ -14,6 +14,7 @@ from collections.abc import Callable
14
14
  from typing import TYPE_CHECKING, Any
15
15
 
16
16
  from gobby.mcp_proxy.tools.internal import InternalToolRegistry
17
+ from gobby.storage.sessions import LocalSessionManager
17
18
  from gobby.storage.tasks import TaskNotFoundError
18
19
  from gobby.utils.project_context import get_project_context
19
20
  from gobby.workflows.state_manager import WorkflowStateManager
@@ -227,6 +228,7 @@ def create_readiness_registry(
227
228
 
228
229
  # Create workflow state manager for session_task scoping
229
230
  workflow_state_manager = WorkflowStateManager(task_manager.db)
231
+ session_manager = LocalSessionManager(task_manager.db)
230
232
 
231
233
  # --- list_ready_tasks ---
232
234
 
@@ -376,7 +378,16 @@ def create_readiness_registry(
376
378
 
377
379
  # Auto-scope to session_task if session_id is provided and parent_task_id is not set
378
380
  if session_id and not parent_task_id:
379
- workflow_state = workflow_state_manager.get_state(session_id)
381
+ # Resolve session_id from #N format to UUID
382
+ try:
383
+ resolved_session_id = session_manager.resolve_session_reference(
384
+ session_id, project_id
385
+ )
386
+ except Exception as e:
387
+ logger.warning(f"Could not resolve session_id '{session_id}': {e}")
388
+ resolved_session_id = session_id
389
+
390
+ workflow_state = workflow_state_manager.get_state(resolved_session_id)
380
391
  if workflow_state:
381
392
  session_task = workflow_state.variables.get("session_task")
382
393
  if session_task and session_task != "*":
@@ -395,6 +406,19 @@ def create_readiness_registry(
395
406
  ready_tasks = _get_ready_descendants(
396
407
  task_manager, parent_task_id, task_type, project_id
397
408
  )
409
+ # If no ready descendants, check if the parent task itself is ready
410
+ # This handles the case where session_task is a leaf task with no children
411
+ if not ready_tasks:
412
+ parent_task = task_manager.get_task(parent_task_id)
413
+ if parent_task and parent_task.status == "open":
414
+ # Check if it matches task_type filter
415
+ if task_type is None or parent_task.task_type == task_type:
416
+ # Check if task is ready by seeing if it appears in ready list
417
+ ready_check = task_manager.list_ready_tasks(
418
+ project_id=project_id, limit=200
419
+ )
420
+ if any(t.id == parent_task_id for t in ready_check):
421
+ ready_tasks = [parent_task]
398
422
  else:
399
423
  ready_tasks = task_manager.list_ready_tasks(
400
424
  task_type=task_type, limit=50, project_id=project_id
@@ -492,7 +516,7 @@ def create_readiness_registry(
492
516
  "score": best_score,
493
517
  "reason": f"Selected because: {', '.join(reasons) if reasons else 'best available option'}",
494
518
  "alternatives": [
495
- {"ref": t.to_brief()["ref"], "title": t.title, "score": s}
519
+ {"ref": t.to_brief().get("ref", t.id), "title": t.title, "score": s}
496
520
  for t, s, _, _ in scored[1:4] # Show top 3 alternatives
497
521
  ],
498
522
  "recommended_skills": recommended_skills,
@@ -525,10 +549,9 @@ def create_readiness_registry(
525
549
  },
526
550
  "session_id": {
527
551
  "type": "string",
528
- "description": "Your session ID (from system context). Used to auto-scope suggestions based on workflow's session_task variable.",
552
+ "description": "Your session ID (from system context). When provided, auto-scopes suggestions based on workflow's session_task variable.",
529
553
  },
530
554
  },
531
- "required": ["session_id"],
532
555
  },
533
556
  func=suggest_next_task,
534
557
  )
@@ -88,6 +88,14 @@ class RegistryContext:
88
88
  return project_id
89
89
  return None
90
90
 
91
+ def get_current_project_name(self) -> str | None:
92
+ """Get the current project name from context, or None if not in a project."""
93
+ ctx = get_project_context()
94
+ if ctx and ctx.get("name"):
95
+ name: str = ctx["name"]
96
+ return name
97
+ return None
98
+
91
99
  def get_workflow_state(self, session_id: str | None) -> WorkflowState | None:
92
100
  """Get workflow state for a session, if available."""
93
101
  if not session_id:
@@ -335,6 +335,32 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
335
335
  except ValueError as e:
336
336
  return {"error": str(e)}
337
337
 
338
+ # Block closing tasks via update_task - must use close_task for proper workflow
339
+ if status is not None and status.lower() == "closed":
340
+ return {
341
+ "error": "Cannot set status to 'closed' via update_task. "
342
+ "Use close_task(task_id, commit_sha='...') to properly close tasks with commit linking."
343
+ }
344
+
345
+ # Block claiming tasks via update_task - must use claim_task for proper workflow
346
+ if status is not None and status.lower() == "in_progress":
347
+ return {
348
+ "error": "Cannot set status to 'in_progress' via update_task. "
349
+ "Use claim_task(task_id, session_id='...') to properly claim tasks with session tracking."
350
+ }
351
+ if assignee is not None:
352
+ return {
353
+ "error": "Cannot set assignee via update_task. "
354
+ "Use claim_task(task_id, session_id='...') to properly claim tasks with session tracking."
355
+ }
356
+
357
+ # Block needs_review status via update_task - must use mark_task_for_review for proper workflow
358
+ if status is not None and status.lower() in ("review", "needs_review"):
359
+ return {
360
+ "error": "Cannot set status to 'needs_review' via update_task. "
361
+ "Use mark_task_for_review(task_id, session_id='...') to properly route tasks for review."
362
+ }
363
+
338
364
  # Build kwargs only for non-None values to avoid overwriting with NULL
339
365
  kwargs: dict[str, Any] = {}
340
366
  if title is not None:
@@ -395,7 +421,7 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
395
421
  },
396
422
  "status": {
397
423
  "type": "string",
398
- "description": "New status (open, in_progress, review, closed)",
424
+ "description": "New status (open, in_progress, needs_review, closed)",
399
425
  "default": None,
400
426
  },
401
427
  "priority": {"type": "integer", "description": "New priority", "default": None},
@@ -6,7 +6,7 @@ for task operations.
6
6
 
7
7
  # Reasons for which commit linking and validation are skipped when closing tasks
8
8
  SKIP_REASONS: frozenset[str] = frozenset(
9
- {"duplicate", "already_implemented", "wont_fix", "obsolete"}
9
+ {"duplicate", "already_implemented", "wont_fix", "obsolete", "out_of_repo"}
10
10
  )
11
11
 
12
12
  # Category inference patterns mapping category to keywords/phrases