gobby 0.2.9__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +2 -2
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +5 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/skills.py +23 -2
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/_base.py +6 -1
- gobby/hooks/event_handlers/_session.py +44 -130
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +25 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +217 -1
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +56 -9
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +9 -2
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +12 -1
- gobby/mcp_proxy/tools/workflows/_query.py +45 -26
- gobby/mcp_proxy/tools/workflows/_terminal.py +39 -3
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/context.py +5 -5
- gobby/runner.py +108 -6
- gobby/servers/http.py +7 -1
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +44 -0
- gobby/servers/routes/mcp/endpoints/execution.py +18 -25
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +87 -1
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +95 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +1 -1
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- gobby/workflows/actions.py +75 -0
- gobby/workflows/context_actions.py +246 -5
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +20 -1
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +57 -26
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/RECORD +134 -106
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.9.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
|
@@ -103,30 +103,42 @@ def detect_task_claim(
|
|
|
103
103
|
if isinstance(result, dict) and result.get("error"):
|
|
104
104
|
return
|
|
105
105
|
|
|
106
|
-
# Extract task_id based on tool type
|
|
106
|
+
# Extract task_id based on tool type - MUST resolve to UUID
|
|
107
|
+
# Refs like '#123' will fail comparison with task.id (UUID) in close_task logic
|
|
107
108
|
arguments = tool_input.get("arguments", {}) or {}
|
|
109
|
+
task_id: str | None = None
|
|
110
|
+
|
|
108
111
|
if inner_tool_name in ("update_task", "claim_task"):
|
|
109
|
-
|
|
110
|
-
#
|
|
111
|
-
if
|
|
112
|
+
raw_task_id = arguments.get("task_id")
|
|
113
|
+
# MUST resolve to UUID - refs like '#123' break comparisons in close_task
|
|
114
|
+
if raw_task_id and task_manager:
|
|
112
115
|
try:
|
|
113
|
-
task = task_manager.get_task(
|
|
116
|
+
task = task_manager.get_task(raw_task_id)
|
|
114
117
|
if task:
|
|
115
118
|
task_id = task.id # Use UUID
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
else:
|
|
120
|
+
logger.warning(
|
|
121
|
+
f"Cannot resolve task ref '{raw_task_id}' to UUID - task not found"
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.warning(f"Cannot resolve task ref '{raw_task_id}' to UUID: {e}")
|
|
125
|
+
elif raw_task_id and not task_manager:
|
|
126
|
+
logger.warning(f"Cannot resolve task ref '{raw_task_id}' to UUID - no task_manager")
|
|
118
127
|
elif inner_tool_name == "create_task":
|
|
119
|
-
# For create_task, the id is in the result
|
|
128
|
+
# For create_task, the id is in the result (already a UUID)
|
|
120
129
|
result = tool_output.get("result", {}) if isinstance(tool_output, dict) else {}
|
|
121
130
|
task_id = result.get("id") if isinstance(result, dict) else None
|
|
122
131
|
# Skip if we can't get the task ID (e.g., Claude Code doesn't include tool results)
|
|
123
132
|
# The MCP tool itself handles state updates in this case via _crud.py
|
|
124
133
|
if not task_id:
|
|
125
134
|
return
|
|
126
|
-
else:
|
|
127
|
-
task_id = None
|
|
128
135
|
|
|
129
|
-
#
|
|
136
|
+
# Only set claimed_task_id if we have a valid UUID
|
|
137
|
+
if not task_id:
|
|
138
|
+
logger.debug(f"Skipping task claim state update - no valid UUID for {inner_tool_name}")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# All conditions met - set task_claimed and claimed_task_id (UUID)
|
|
130
142
|
state.variables["task_claimed"] = True
|
|
131
143
|
state.variables["claimed_task_id"] = task_id
|
|
132
144
|
logger.info(
|
|
@@ -6,6 +6,7 @@ Provides actions that enforce task tracking and scoping requirements.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
+
import uuid
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
10
11
|
|
|
11
12
|
from gobby.mcp_proxy.tools.task_readiness import is_descendant_of
|
|
@@ -21,6 +22,15 @@ if TYPE_CHECKING:
|
|
|
21
22
|
logger = logging.getLogger(__name__)
|
|
22
23
|
|
|
23
24
|
|
|
25
|
+
def _is_uuid(value: str) -> bool:
|
|
26
|
+
"""Check if a string is a valid UUID (not a ref like #123)."""
|
|
27
|
+
try:
|
|
28
|
+
uuid.UUID(value)
|
|
29
|
+
return True
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
async def require_task_complete(
|
|
25
35
|
task_manager: LocalTaskManager | None,
|
|
26
36
|
session_id: str,
|
|
@@ -84,6 +94,14 @@ async def require_task_complete(
|
|
|
84
94
|
if workflow_state:
|
|
85
95
|
has_claimed_task = workflow_state.variables.get("task_claimed", False)
|
|
86
96
|
claimed_task_id = workflow_state.variables.get("claimed_task_id")
|
|
97
|
+
# Resolve claimed_task_id to UUID if it's a ref (backward compat)
|
|
98
|
+
if claimed_task_id and not _is_uuid(claimed_task_id):
|
|
99
|
+
try:
|
|
100
|
+
claimed_task = task_manager.get_task(claimed_task_id)
|
|
101
|
+
if claimed_task:
|
|
102
|
+
claimed_task_id = claimed_task.id
|
|
103
|
+
except Exception: # nosec B110 - keep original ID if resolution fails
|
|
104
|
+
claimed_task_id = claimed_task_id # explicit no-op
|
|
87
105
|
|
|
88
106
|
try:
|
|
89
107
|
# Collect incomplete tasks across all specified task IDs
|
gobby/workflows/engine.py
CHANGED
|
@@ -116,7 +116,11 @@ class WorkflowEngine:
|
|
|
116
116
|
if state.step != "reflect":
|
|
117
117
|
project_path = Path(event.cwd) if event.cwd else None
|
|
118
118
|
workflow = self.loader.load_workflow(state.workflow_name, project_path)
|
|
119
|
-
if
|
|
119
|
+
if (
|
|
120
|
+
workflow
|
|
121
|
+
and isinstance(workflow, WorkflowDefinition)
|
|
122
|
+
and workflow.get_step("reflect")
|
|
123
|
+
):
|
|
120
124
|
await self.transition_to(state, "reflect", workflow)
|
|
121
125
|
return HookResponse(
|
|
122
126
|
decision="modify",
|
|
@@ -145,6 +149,11 @@ class WorkflowEngine:
|
|
|
145
149
|
)
|
|
146
150
|
return HookResponse(decision="allow")
|
|
147
151
|
|
|
152
|
+
# Step handling only applies to WorkflowDefinition, not PipelineDefinition
|
|
153
|
+
if not isinstance(workflow, WorkflowDefinition):
|
|
154
|
+
logger.debug(f"Workflow '{workflow.name}' is a pipeline, skipping step handling")
|
|
155
|
+
return HookResponse(decision="allow")
|
|
156
|
+
|
|
148
157
|
# 4. Process event
|
|
149
158
|
# Logic matches WORKFLOWS.md "Evaluation Flow"
|
|
150
159
|
|
|
@@ -344,6 +353,8 @@ class WorkflowEngine:
|
|
|
344
353
|
memory_sync_manager=self.action_executor.memory_sync_manager,
|
|
345
354
|
task_sync_manager=self.action_executor.task_sync_manager,
|
|
346
355
|
session_task_manager=self.action_executor.session_task_manager,
|
|
356
|
+
pipeline_executor=self.action_executor.pipeline_executor,
|
|
357
|
+
workflow_loader=self.action_executor.workflow_loader,
|
|
347
358
|
)
|
|
348
359
|
|
|
349
360
|
for action_def in actions:
|
|
@@ -530,6 +541,14 @@ class WorkflowEngine:
|
|
|
530
541
|
"error": f"Workflow '{workflow_name}' is lifecycle type (auto-runs on events)",
|
|
531
542
|
}
|
|
532
543
|
|
|
544
|
+
# Only WorkflowDefinition can be activated as step workflows
|
|
545
|
+
if not isinstance(definition, WorkflowDefinition):
|
|
546
|
+
logger.debug(f"Workflow '{workflow_name}' is a pipeline, not a step workflow")
|
|
547
|
+
return {
|
|
548
|
+
"success": False,
|
|
549
|
+
"error": f"'{workflow_name}' is a pipeline. Use pipeline execution instead.",
|
|
550
|
+
}
|
|
551
|
+
|
|
533
552
|
# Check for existing step workflow
|
|
534
553
|
existing = self.state_manager.get_state(session_id)
|
|
535
554
|
if existing and existing.workflow_name != "__lifecycle__":
|
gobby/workflows/evaluator.py
CHANGED
|
@@ -23,7 +23,7 @@ def is_task_complete(task: Any) -> bool:
|
|
|
23
23
|
|
|
24
24
|
A task is complete if:
|
|
25
25
|
- status is 'closed', OR
|
|
26
|
-
- status is '
|
|
26
|
+
- status is 'needs_review' AND requires_user_review is False
|
|
27
27
|
(agent marked for visibility but doesn't need user sign-off)
|
|
28
28
|
|
|
29
29
|
Tasks in 'review' with requires_user_review=True are NOT complete
|
|
@@ -38,7 +38,7 @@ def is_task_complete(task: Any) -> bool:
|
|
|
38
38
|
if task.status == "closed":
|
|
39
39
|
return True
|
|
40
40
|
requires_user_review = getattr(task, "requires_user_review", False)
|
|
41
|
-
if task.status == "
|
|
41
|
+
if task.status == "needs_review" and not requires_user_review:
|
|
42
42
|
return True
|
|
43
43
|
return False
|
|
44
44
|
|
|
@@ -55,7 +55,7 @@ def task_needs_user_review(task_manager: Any, task_id: str | None) -> bool:
|
|
|
55
55
|
task_id: Task ID to check
|
|
56
56
|
|
|
57
57
|
Returns:
|
|
58
|
-
True if task is in '
|
|
58
|
+
True if task is in 'needs_review' status AND has requires_user_review=True.
|
|
59
59
|
Returns False if task_id is None or task not found.
|
|
60
60
|
"""
|
|
61
61
|
if not task_id or not task_manager:
|
|
@@ -65,7 +65,7 @@ def task_needs_user_review(task_manager: Any, task_id: str | None) -> bool:
|
|
|
65
65
|
if not task:
|
|
66
66
|
return False
|
|
67
67
|
|
|
68
|
-
return bool(task.status == "
|
|
68
|
+
return bool(task.status == "needs_review" and getattr(task, "requires_user_review", False))
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def task_tree_complete(task_manager: Any, task_id: str | list[str] | None) -> bool:
|
|
@@ -74,7 +74,7 @@ def task_tree_complete(task_manager: Any, task_id: str | list[str] | None) -> bo
|
|
|
74
74
|
|
|
75
75
|
A task is complete if:
|
|
76
76
|
- status is 'closed', OR
|
|
77
|
-
- status is '
|
|
77
|
+
- status is 'needs_review' AND requires_user_review is False
|
|
78
78
|
|
|
79
79
|
Used in workflow transition conditions like:
|
|
80
80
|
when: "task_tree_complete(variables.session_task)"
|
|
@@ -283,6 +283,9 @@ class ConditionEvaluator:
|
|
|
283
283
|
"None": None,
|
|
284
284
|
"True": True,
|
|
285
285
|
"False": False,
|
|
286
|
+
# YAML/JSON use lowercase booleans
|
|
287
|
+
"true": True,
|
|
288
|
+
"false": False,
|
|
286
289
|
}
|
|
287
290
|
|
|
288
291
|
# Add plugin conditions as callable functions
|
|
@@ -10,10 +10,10 @@ from datetime import UTC, datetime
|
|
|
10
10
|
from typing import TYPE_CHECKING, Any, Literal
|
|
11
11
|
|
|
12
12
|
from gobby.hooks.events import HookEvent, HookEventType, HookResponse
|
|
13
|
+
from gobby.workflows.definitions import WorkflowDefinition, WorkflowState
|
|
13
14
|
|
|
14
15
|
if TYPE_CHECKING:
|
|
15
16
|
from .actions import ActionExecutor
|
|
16
|
-
from .definitions import WorkflowDefinition, WorkflowState
|
|
17
17
|
from .evaluator import ConditionEvaluator
|
|
18
18
|
from .loader import WorkflowLoader
|
|
19
19
|
from .state_manager import WorkflowStateManager
|
|
@@ -123,28 +123,30 @@ async def evaluate_workflow_triggers(
|
|
|
123
123
|
session_id = event.metadata.get("_platform_session_id") or "global"
|
|
124
124
|
|
|
125
125
|
# Try to load existing state, or create new one
|
|
126
|
-
state
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
126
|
+
# Track whether we created a new state to determine save behavior later
|
|
127
|
+
existing_state = state_manager.get_state(session_id)
|
|
128
|
+
state_was_created = existing_state is None
|
|
129
|
+
state: WorkflowState = existing_state or WorkflowState(
|
|
130
|
+
session_id=session_id,
|
|
131
|
+
workflow_name=workflow.name,
|
|
132
|
+
step="global",
|
|
133
|
+
step_entered_at=datetime.now(UTC),
|
|
134
|
+
step_action_count=0,
|
|
135
|
+
total_action_count=0,
|
|
136
|
+
artifacts=event.data.get("artifacts", {}) if event.data else {},
|
|
137
|
+
observations=[],
|
|
138
|
+
reflection_pending=False,
|
|
139
|
+
context_injected=False,
|
|
140
|
+
variables={},
|
|
141
|
+
task_list=None,
|
|
142
|
+
current_task_index=0,
|
|
143
|
+
files_modified_this_task=0,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Merge context_data (workflow defaults) into state variables
|
|
147
|
+
# Persisted state values take precedence over workflow defaults
|
|
146
148
|
if context_data:
|
|
147
|
-
state.variables.
|
|
149
|
+
state.variables = {**context_data, **state.variables}
|
|
148
150
|
|
|
149
151
|
action_ctx = ActionContext(
|
|
150
152
|
session_id=session_id,
|
|
@@ -236,11 +238,28 @@ async def evaluate_workflow_triggers(
|
|
|
236
238
|
exc_info=True,
|
|
237
239
|
)
|
|
238
240
|
|
|
239
|
-
# Persist state changes (e.g., _injected_memory_ids from memory_recall_relevant
|
|
241
|
+
# Persist state changes (e.g., _injected_memory_ids from memory_recall_relevant,
|
|
242
|
+
# unlocked_tools from track_schema_lookup)
|
|
240
243
|
# Only save if we have a real session ID (not "global" fallback)
|
|
241
244
|
# The workflow_states table has a FK to sessions, so we can't save for non-existent sessions
|
|
242
245
|
if session_id != "global":
|
|
243
|
-
|
|
246
|
+
if state_was_created:
|
|
247
|
+
# We created a new lifecycle state - check for existing step workflow
|
|
248
|
+
# to avoid overwriting it with our new lifecycle state.
|
|
249
|
+
# Step workflows (activated via activate_workflow) have their own workflow_name.
|
|
250
|
+
current_state = state_manager.get_state(session_id)
|
|
251
|
+
is_step_workflow = (
|
|
252
|
+
current_state is not None
|
|
253
|
+
and current_state.workflow_name != "__lifecycle__"
|
|
254
|
+
and current_state.workflow_name != workflow.name
|
|
255
|
+
)
|
|
256
|
+
if not is_step_workflow:
|
|
257
|
+
state_manager.save_state(state)
|
|
258
|
+
else:
|
|
259
|
+
# We fetched an existing state (possibly a step workflow) and updated
|
|
260
|
+
# its variables. Safe to save since we're just persisting variable
|
|
261
|
+
# changes (like unlocked_tools), not changing workflow_name or step.
|
|
262
|
+
state_manager.save_state(state)
|
|
244
263
|
|
|
245
264
|
final_context = "\n\n".join(injected_context) if injected_context else None
|
|
246
265
|
logger.debug(
|
|
@@ -290,6 +309,11 @@ async def evaluate_lifecycle_triggers(
|
|
|
290
309
|
logger.warning(f"Workflow '{workflow_name}' not found in project_path={project_path}")
|
|
291
310
|
return HookResponse(decision="allow")
|
|
292
311
|
|
|
312
|
+
# Lifecycle triggers only apply to WorkflowDefinition, not PipelineDefinition
|
|
313
|
+
if not isinstance(workflow, WorkflowDefinition):
|
|
314
|
+
logger.debug(f"Workflow '{workflow_name}' is not a WorkflowDefinition, skipping triggers")
|
|
315
|
+
return HookResponse(decision="allow")
|
|
316
|
+
|
|
293
317
|
logger.debug(
|
|
294
318
|
f"Workflow '{workflow_name}' loaded, triggers={list(workflow.triggers.keys()) if workflow.triggers else []}"
|
|
295
319
|
)
|
|
@@ -533,6 +557,10 @@ async def evaluate_all_lifecycle_workflows(
|
|
|
533
557
|
for discovered in workflows:
|
|
534
558
|
workflow = discovered.definition
|
|
535
559
|
|
|
560
|
+
# Skip PipelineDefinition - lifecycle triggers only for WorkflowDefinition
|
|
561
|
+
if not isinstance(workflow, WorkflowDefinition):
|
|
562
|
+
continue
|
|
563
|
+
|
|
536
564
|
# Skip if this workflow+trigger has already been processed
|
|
537
565
|
key = (workflow.name, trigger_name)
|
|
538
566
|
if key in processed_triggers:
|
|
@@ -540,10 +568,11 @@ async def evaluate_all_lifecycle_workflows(
|
|
|
540
568
|
|
|
541
569
|
# Merge workflow definition's default variables (lower priority than session state)
|
|
542
570
|
# Precedence: session state > workflow YAML defaults
|
|
543
|
-
|
|
571
|
+
# Update context_data directly so workflow variables propagate to response metadata
|
|
572
|
+
context_data = {**workflow.variables, **context_data}
|
|
544
573
|
|
|
545
574
|
response = await evaluate_workflow_triggers(
|
|
546
|
-
workflow, event,
|
|
575
|
+
workflow, event, context_data, state_manager, action_executor, evaluator
|
|
547
576
|
)
|
|
548
577
|
|
|
549
578
|
# Accumulate context
|
|
@@ -594,6 +623,7 @@ async def evaluate_all_lifecycle_workflows(
|
|
|
594
623
|
)
|
|
595
624
|
detect_task_claim_fn(event, state)
|
|
596
625
|
detect_plan_mode_fn(event, state)
|
|
626
|
+
# Safe to save - we're updating variables on existing state, not changing workflow_name
|
|
597
627
|
state_manager.save_state(state)
|
|
598
628
|
|
|
599
629
|
# Detect plan mode from system reminders for BEFORE_AGENT events
|
|
@@ -609,6 +639,7 @@ async def evaluate_all_lifecycle_workflows(
|
|
|
609
639
|
step="",
|
|
610
640
|
)
|
|
611
641
|
detect_plan_mode_from_context_fn(event, state)
|
|
642
|
+
# Safe to save - we're updating variables on existing state, not changing workflow_name
|
|
612
643
|
state_manager.save_state(state)
|
|
613
644
|
|
|
614
645
|
# Check for premature stop in active step workflows on STOP events
|