gobby 0.2.7__py3-none-any.whl → 0.2.8__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/adapters/claude_code.py +96 -35
- gobby/adapters/gemini.py +140 -38
- gobby/agents/isolation.py +130 -0
- gobby/agents/registry.py +11 -0
- gobby/agents/session.py +1 -0
- gobby/agents/spawn_executor.py +43 -13
- gobby/agents/spawners/macos.py +26 -1
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/clones/git.py +177 -0
- gobby/config/skills.py +31 -0
- gobby/hooks/event_handlers.py +109 -10
- gobby/hooks/hook_manager.py +19 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/registries.py +21 -4
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +43 -9
- gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
- gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
- gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
- gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
- gobby/mcp_proxy/tools/spawn_agent.py +44 -6
- gobby/mcp_proxy/tools/tasks/_context.py +18 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
- gobby/mcp_proxy/tools/tasks/_session.py +22 -7
- gobby/mcp_proxy/tools/workflows.py +84 -34
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/extractor.py +15 -1
- gobby/runner.py +13 -0
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +2 -2
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +23 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +43 -3
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/blocking.py +13 -1
- gobby/workflows/engine.py +93 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/memory_actions.py +11 -0
- gobby/workflows/safe_evaluator.py +8 -0
- gobby/workflows/summary_actions.py +123 -50
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/RECORD +56 -80
- gobby/cli/tui.py +0 -34
- gobby/tui/__init__.py +0 -5
- gobby/tui/api_client.py +0 -278
- gobby/tui/app.py +0 -329
- gobby/tui/screens/__init__.py +0 -25
- gobby/tui/screens/agents.py +0 -333
- gobby/tui/screens/chat.py +0 -450
- gobby/tui/screens/dashboard.py +0 -377
- gobby/tui/screens/memory.py +0 -305
- gobby/tui/screens/metrics.py +0 -231
- gobby/tui/screens/orchestrator.py +0 -903
- gobby/tui/screens/sessions.py +0 -412
- gobby/tui/screens/tasks.py +0 -440
- gobby/tui/screens/workflows.py +0 -289
- gobby/tui/screens/worktrees.py +0 -174
- gobby/tui/widgets/__init__.py +0 -21
- gobby/tui/widgets/chat.py +0 -210
- gobby/tui/widgets/conductor.py +0 -104
- gobby/tui/widgets/menu.py +0 -132
- gobby/tui/widgets/message_panel.py +0 -160
- gobby/tui/widgets/review_gate.py +0 -224
- gobby/tui/widgets/task_tree.py +0 -99
- gobby/tui/widgets/token_budget.py +0 -166
- gobby/tui/ws_client.py +0 -258
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.8.dist-info}/top_level.txt +0 -0
|
@@ -93,13 +93,21 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
93
93
|
# Auto-skip validation for certain close reasons
|
|
94
94
|
should_skip = skip_validation or reason.lower() in SKIP_REASONS
|
|
95
95
|
|
|
96
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
97
|
+
resolved_session_id = session_id
|
|
98
|
+
if session_id:
|
|
99
|
+
try:
|
|
100
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
101
|
+
except ValueError:
|
|
102
|
+
pass # Fall back to raw value if resolution fails
|
|
103
|
+
|
|
96
104
|
# Enforce commits if session had edits
|
|
97
|
-
if
|
|
105
|
+
if resolved_session_id and not should_skip:
|
|
98
106
|
try:
|
|
99
107
|
from gobby.storage.sessions import LocalSessionManager
|
|
100
108
|
|
|
101
109
|
session_manager = LocalSessionManager(ctx.task_manager.db)
|
|
102
|
-
session = session_manager.get(
|
|
110
|
+
session = session_manager.get(resolved_session_id)
|
|
103
111
|
|
|
104
112
|
# Check if task has commits (including the one being linked right now)
|
|
105
113
|
has_commits = bool(task.commits) or bool(commit_sha)
|
|
@@ -185,9 +193,9 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
185
193
|
)
|
|
186
194
|
|
|
187
195
|
# Auto-link session if provided
|
|
188
|
-
if
|
|
196
|
+
if resolved_session_id:
|
|
189
197
|
try:
|
|
190
|
-
ctx.session_task_manager.link_task(
|
|
198
|
+
ctx.session_task_manager.link_task(resolved_session_id, resolved_id, "review")
|
|
191
199
|
except Exception:
|
|
192
200
|
pass # nosec B110 - best-effort linking
|
|
193
201
|
|
|
@@ -208,15 +216,15 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
208
216
|
ctx.task_manager.close_task(
|
|
209
217
|
resolved_id,
|
|
210
218
|
reason=reason,
|
|
211
|
-
closed_in_session_id=
|
|
219
|
+
closed_in_session_id=resolved_session_id,
|
|
212
220
|
closed_commit_sha=current_commit_sha,
|
|
213
221
|
validation_override_reason=override_justification if store_override else None,
|
|
214
222
|
)
|
|
215
223
|
|
|
216
224
|
# Auto-link session if provided
|
|
217
|
-
if
|
|
225
|
+
if resolved_session_id:
|
|
218
226
|
try:
|
|
219
|
-
ctx.session_task_manager.link_task(
|
|
227
|
+
ctx.session_task_manager.link_task(resolved_session_id, resolved_id, "closed")
|
|
220
228
|
except Exception:
|
|
221
229
|
pass # nosec B110 - best-effort linking, don't fail the close
|
|
222
230
|
|
|
@@ -224,9 +232,9 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
224
232
|
# Respects the clear_task_on_close variable (defaults to True if not set)
|
|
225
233
|
# This is done here because Claude Code's post-tool-use hook doesn't include
|
|
226
234
|
# the tool result, so the detection_helpers can't verify close succeeded
|
|
227
|
-
if
|
|
235
|
+
if resolved_session_id:
|
|
228
236
|
try:
|
|
229
|
-
state = ctx.workflow_state_manager.get_state(
|
|
237
|
+
state = ctx.workflow_state_manager.get_state(resolved_session_id)
|
|
230
238
|
if state and state.variables.get("claimed_task_id") == resolved_id:
|
|
231
239
|
# Check if clear_task_on_close is enabled (default: True)
|
|
232
240
|
clear_on_close = state.variables.get("clear_task_on_close", True)
|
|
@@ -288,7 +296,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
288
296
|
},
|
|
289
297
|
"session_id": {
|
|
290
298
|
"type": "string",
|
|
291
|
-
"description": "Your session ID (
|
|
299
|
+
"description": "Your session ID (accepts #N, N, UUID, or prefix). Pass this to track which session closed the task.",
|
|
292
300
|
"default": None,
|
|
293
301
|
},
|
|
294
302
|
"override_justification": {
|
|
@@ -528,8 +536,15 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
528
536
|
if not task:
|
|
529
537
|
return {"success": False, "error": f"Task {task_id} not found"}
|
|
530
538
|
|
|
539
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
540
|
+
resolved_session_id = session_id
|
|
541
|
+
try:
|
|
542
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
543
|
+
except ValueError:
|
|
544
|
+
pass # Fall back to raw value if resolution fails
|
|
545
|
+
|
|
531
546
|
# Check if already claimed by another session
|
|
532
|
-
if task.assignee and task.assignee !=
|
|
547
|
+
if task.assignee and task.assignee != resolved_session_id and not force:
|
|
533
548
|
return {
|
|
534
549
|
"success": False,
|
|
535
550
|
"error": "Task already claimed by another session",
|
|
@@ -540,7 +555,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
540
555
|
# Update task with assignee and status in single atomic call
|
|
541
556
|
updated = ctx.task_manager.update_task(
|
|
542
557
|
resolved_id,
|
|
543
|
-
assignee=
|
|
558
|
+
assignee=resolved_session_id,
|
|
544
559
|
status="in_progress",
|
|
545
560
|
)
|
|
546
561
|
if not updated:
|
|
@@ -548,7 +563,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
548
563
|
|
|
549
564
|
# Link task to session (best-effort, don't fail the claim if this fails)
|
|
550
565
|
try:
|
|
551
|
-
ctx.session_task_manager.link_task(
|
|
566
|
+
ctx.session_task_manager.link_task(resolved_session_id, resolved_id, "claimed")
|
|
552
567
|
except Exception:
|
|
553
568
|
pass # nosec B110 - best-effort linking
|
|
554
569
|
|
|
@@ -566,7 +581,7 @@ def create_lifecycle_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
566
581
|
},
|
|
567
582
|
"session_id": {
|
|
568
583
|
"type": "string",
|
|
569
|
-
"description": "Your session ID (
|
|
584
|
+
"description": "Your session ID (accepts #N, N, UUID, or prefix). The session claiming the task.",
|
|
570
585
|
},
|
|
571
586
|
"force": {
|
|
572
587
|
"type": "boolean",
|
|
@@ -40,15 +40,21 @@ def create_session_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
40
40
|
except (TaskNotFoundError, ValueError) as e:
|
|
41
41
|
return {"error": str(e)}
|
|
42
42
|
|
|
43
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
43
44
|
try:
|
|
44
|
-
ctx.
|
|
45
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
46
|
+
except ValueError as e:
|
|
47
|
+
return {"error": f"Invalid session_id '{session_id}': {e}"}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
ctx.session_task_manager.link_task(resolved_session_id, resolved_id, action)
|
|
45
51
|
return {}
|
|
46
52
|
except ValueError as e:
|
|
47
53
|
return {"error": str(e)}
|
|
48
54
|
|
|
49
55
|
registry.register(
|
|
50
56
|
name="link_task_to_session",
|
|
51
|
-
description="Link a task to a session.",
|
|
57
|
+
description="Link a task to a session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
52
58
|
input_schema={
|
|
53
59
|
"type": "object",
|
|
54
60
|
"properties": {
|
|
@@ -58,7 +64,7 @@ def create_session_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
58
64
|
},
|
|
59
65
|
"session_id": {
|
|
60
66
|
"type": "string",
|
|
61
|
-
"description": "Session
|
|
67
|
+
"description": "Session reference (accepts #N, N, UUID, or prefix)",
|
|
62
68
|
"default": None,
|
|
63
69
|
},
|
|
64
70
|
"action": {
|
|
@@ -74,16 +80,25 @@ def create_session_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
74
80
|
|
|
75
81
|
def get_session_tasks(session_id: str) -> dict[str, Any]:
|
|
76
82
|
"""Get all tasks associated with a session."""
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
84
|
+
try:
|
|
85
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
86
|
+
except ValueError as e:
|
|
87
|
+
return {"error": f"Invalid session_id '{session_id}': {e}"}
|
|
88
|
+
|
|
89
|
+
tasks = ctx.session_task_manager.get_session_tasks(resolved_session_id)
|
|
90
|
+
return {"session_id": resolved_session_id, "tasks": tasks}
|
|
79
91
|
|
|
80
92
|
registry.register(
|
|
81
93
|
name="get_session_tasks",
|
|
82
|
-
description="Get all tasks associated with a session.",
|
|
94
|
+
description="Get all tasks associated with a session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
83
95
|
input_schema={
|
|
84
96
|
"type": "object",
|
|
85
97
|
"properties": {
|
|
86
|
-
"session_id": {
|
|
98
|
+
"session_id": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Session reference (accepts #N, N, UUID, or prefix)",
|
|
101
|
+
},
|
|
87
102
|
},
|
|
88
103
|
"required": ["session_id"],
|
|
89
104
|
},
|
|
@@ -113,12 +113,20 @@ def create_workflows_registry(
|
|
|
113
113
|
Returns:
|
|
114
114
|
InternalToolRegistry with workflow tools registered
|
|
115
115
|
"""
|
|
116
|
+
from gobby.utils.project_context import get_project_context
|
|
117
|
+
|
|
116
118
|
# Create defaults if not provided
|
|
117
119
|
_db = db or LocalDatabase()
|
|
118
120
|
_loader = loader or WorkflowLoader()
|
|
119
121
|
_state_manager = state_manager or WorkflowStateManager(_db)
|
|
120
122
|
_session_manager = session_manager or LocalSessionManager(_db)
|
|
121
123
|
|
|
124
|
+
def _resolve_session_id(ref: str) -> str:
|
|
125
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
126
|
+
project_ctx = get_project_context()
|
|
127
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
128
|
+
return _session_manager.resolve_session_reference(ref, project_id)
|
|
129
|
+
|
|
122
130
|
registry = InternalToolRegistry(
|
|
123
131
|
name="gobby-workflows",
|
|
124
132
|
description="Workflow management - list, activate, status, transition, end",
|
|
@@ -264,7 +272,7 @@ def create_workflows_registry(
|
|
|
264
272
|
|
|
265
273
|
@registry.tool(
|
|
266
274
|
name="activate_workflow",
|
|
267
|
-
description="Activate a step-based workflow for the current session.",
|
|
275
|
+
description="Activate a step-based workflow for the current session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
268
276
|
)
|
|
269
277
|
def activate_workflow(
|
|
270
278
|
name: str,
|
|
@@ -278,7 +286,7 @@ def create_workflows_registry(
|
|
|
278
286
|
|
|
279
287
|
Args:
|
|
280
288
|
name: Workflow name (e.g., "plan-act-reflect", "auto-task")
|
|
281
|
-
session_id:
|
|
289
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
282
290
|
initial_step: Optional starting step (defaults to first step)
|
|
283
291
|
variables: Optional initial variables to set (merged with workflow defaults)
|
|
284
292
|
project_path: Project directory path. Auto-discovered from cwd if not provided.
|
|
@@ -290,7 +298,7 @@ def create_workflows_registry(
|
|
|
290
298
|
activate_workflow(
|
|
291
299
|
name="auto-task",
|
|
292
300
|
variables={"session_task": "#47"},
|
|
293
|
-
session_id="
|
|
301
|
+
session_id="#5"
|
|
294
302
|
)
|
|
295
303
|
|
|
296
304
|
Errors if:
|
|
@@ -325,12 +333,18 @@ def create_workflows_registry(
|
|
|
325
333
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
326
334
|
}
|
|
327
335
|
|
|
336
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
337
|
+
try:
|
|
338
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
339
|
+
except ValueError as e:
|
|
340
|
+
return {"success": False, "error": str(e)}
|
|
341
|
+
|
|
328
342
|
# Check for existing workflow
|
|
329
343
|
# Allow if:
|
|
330
344
|
# - No existing state
|
|
331
345
|
# - Existing is __lifecycle__ placeholder
|
|
332
346
|
# - Existing is a lifecycle-type workflow (they run concurrently with step workflows)
|
|
333
|
-
existing = _state_manager.get_state(
|
|
347
|
+
existing = _state_manager.get_state(resolved_session_id)
|
|
334
348
|
if existing and existing.workflow_name != "__lifecycle__":
|
|
335
349
|
# Check if existing workflow is a lifecycle type
|
|
336
350
|
existing_def = _loader.load_workflow(existing.workflow_name, proj)
|
|
@@ -366,12 +380,12 @@ def create_workflows_registry(
|
|
|
366
380
|
session_task_val = merged_variables["session_task"]
|
|
367
381
|
if isinstance(session_task_val, str):
|
|
368
382
|
merged_variables["session_task"] = _resolve_session_task_value(
|
|
369
|
-
session_task_val,
|
|
383
|
+
session_task_val, resolved_session_id, _session_manager, _db
|
|
370
384
|
)
|
|
371
385
|
|
|
372
386
|
# Create state
|
|
373
387
|
state = WorkflowState(
|
|
374
|
-
session_id=
|
|
388
|
+
session_id=resolved_session_id,
|
|
375
389
|
workflow_name=name,
|
|
376
390
|
step=step,
|
|
377
391
|
step_entered_at=datetime.now(UTC),
|
|
@@ -391,7 +405,7 @@ def create_workflows_registry(
|
|
|
391
405
|
|
|
392
406
|
return {
|
|
393
407
|
"success": True,
|
|
394
|
-
"session_id":
|
|
408
|
+
"session_id": resolved_session_id,
|
|
395
409
|
"workflow": name,
|
|
396
410
|
"step": step,
|
|
397
411
|
"steps": [s.name for s in definition.steps],
|
|
@@ -400,7 +414,7 @@ def create_workflows_registry(
|
|
|
400
414
|
|
|
401
415
|
@registry.tool(
|
|
402
416
|
name="end_workflow",
|
|
403
|
-
description="End the currently active step-based workflow.",
|
|
417
|
+
description="End the currently active step-based workflow. Accepts #N, N, UUID, or prefix for session_id.",
|
|
404
418
|
)
|
|
405
419
|
def end_workflow(
|
|
406
420
|
session_id: str | None = None,
|
|
@@ -413,7 +427,7 @@ def create_workflows_registry(
|
|
|
413
427
|
Does not affect lifecycle workflows (they continue running).
|
|
414
428
|
|
|
415
429
|
Args:
|
|
416
|
-
session_id:
|
|
430
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
417
431
|
reason: Optional reason for ending
|
|
418
432
|
|
|
419
433
|
Returns:
|
|
@@ -426,24 +440,30 @@ def create_workflows_registry(
|
|
|
426
440
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
427
441
|
}
|
|
428
442
|
|
|
429
|
-
|
|
443
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
444
|
+
try:
|
|
445
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
446
|
+
except ValueError as e:
|
|
447
|
+
return {"success": False, "error": str(e)}
|
|
448
|
+
|
|
449
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
430
450
|
if not state:
|
|
431
451
|
return {"error": "No workflow active for session"}
|
|
432
452
|
|
|
433
|
-
_state_manager.delete_state(
|
|
453
|
+
_state_manager.delete_state(resolved_session_id)
|
|
434
454
|
|
|
435
455
|
return {}
|
|
436
456
|
|
|
437
457
|
@registry.tool(
|
|
438
458
|
name="get_workflow_status",
|
|
439
|
-
description="Get current workflow step and state.",
|
|
459
|
+
description="Get current workflow step and state. Accepts #N, N, UUID, or prefix for session_id.",
|
|
440
460
|
)
|
|
441
461
|
def get_workflow_status(session_id: str | None = None) -> dict[str, Any]:
|
|
442
462
|
"""
|
|
443
463
|
Get current workflow step and state.
|
|
444
464
|
|
|
445
465
|
Args:
|
|
446
|
-
session_id:
|
|
466
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
447
467
|
|
|
448
468
|
Returns:
|
|
449
469
|
Workflow state including step, action counts, artifacts
|
|
@@ -455,13 +475,19 @@ def create_workflows_registry(
|
|
|
455
475
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
456
476
|
}
|
|
457
477
|
|
|
458
|
-
|
|
478
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
479
|
+
try:
|
|
480
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
481
|
+
except ValueError as e:
|
|
482
|
+
return {"has_workflow": False, "error": str(e)}
|
|
483
|
+
|
|
484
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
459
485
|
if not state:
|
|
460
|
-
return {"has_workflow": False, "session_id":
|
|
486
|
+
return {"has_workflow": False, "session_id": resolved_session_id}
|
|
461
487
|
|
|
462
488
|
return {
|
|
463
489
|
"has_workflow": True,
|
|
464
|
-
"session_id":
|
|
490
|
+
"session_id": resolved_session_id,
|
|
465
491
|
"workflow_name": state.workflow_name,
|
|
466
492
|
"step": state.step,
|
|
467
493
|
"step_action_count": state.step_action_count,
|
|
@@ -479,7 +505,7 @@ def create_workflows_registry(
|
|
|
479
505
|
|
|
480
506
|
@registry.tool(
|
|
481
507
|
name="request_step_transition",
|
|
482
|
-
description="Request transition to a different step.",
|
|
508
|
+
description="Request transition to a different step. Accepts #N, N, UUID, or prefix for session_id.",
|
|
483
509
|
)
|
|
484
510
|
def request_step_transition(
|
|
485
511
|
to_step: str,
|
|
@@ -494,7 +520,7 @@ def create_workflows_registry(
|
|
|
494
520
|
Args:
|
|
495
521
|
to_step: Target step name
|
|
496
522
|
reason: Reason for transition
|
|
497
|
-
session_id:
|
|
523
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
498
524
|
force: Skip exit condition checks
|
|
499
525
|
project_path: Project directory path. Auto-discovered from cwd if not provided.
|
|
500
526
|
|
|
@@ -516,7 +542,13 @@ def create_workflows_registry(
|
|
|
516
542
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
517
543
|
}
|
|
518
544
|
|
|
519
|
-
|
|
545
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
546
|
+
try:
|
|
547
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
548
|
+
except ValueError as e:
|
|
549
|
+
return {"success": False, "error": str(e)}
|
|
550
|
+
|
|
551
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
520
552
|
if not state:
|
|
521
553
|
return {"success": False, "error": "No workflow active for session"}
|
|
522
554
|
|
|
@@ -565,7 +597,7 @@ def create_workflows_registry(
|
|
|
565
597
|
|
|
566
598
|
@registry.tool(
|
|
567
599
|
name="mark_artifact_complete",
|
|
568
|
-
description="Register an artifact as complete (plan, spec, etc.).",
|
|
600
|
+
description="Register an artifact as complete (plan, spec, etc.). Accepts #N, N, UUID, or prefix for session_id.",
|
|
569
601
|
)
|
|
570
602
|
def mark_artifact_complete(
|
|
571
603
|
artifact_type: str,
|
|
@@ -578,7 +610,7 @@ def create_workflows_registry(
|
|
|
578
610
|
Args:
|
|
579
611
|
artifact_type: Type of artifact (e.g., "plan", "spec", "test")
|
|
580
612
|
file_path: Path to the artifact file
|
|
581
|
-
session_id:
|
|
613
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
582
614
|
|
|
583
615
|
Returns:
|
|
584
616
|
Success status
|
|
@@ -590,7 +622,13 @@ def create_workflows_registry(
|
|
|
590
622
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
591
623
|
}
|
|
592
624
|
|
|
593
|
-
|
|
625
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
626
|
+
try:
|
|
627
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
628
|
+
except ValueError as e:
|
|
629
|
+
return {"success": False, "error": str(e)}
|
|
630
|
+
|
|
631
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
594
632
|
if not state:
|
|
595
633
|
return {"error": "No workflow active for session"}
|
|
596
634
|
|
|
@@ -602,7 +640,7 @@ def create_workflows_registry(
|
|
|
602
640
|
|
|
603
641
|
@registry.tool(
|
|
604
642
|
name="set_variable",
|
|
605
|
-
description="Set a workflow variable for the current session (session-scoped, not persisted to YAML).",
|
|
643
|
+
description="Set a workflow variable for the current session (session-scoped, not persisted to YAML). Accepts #N, N, UUID, or prefix for session_id.",
|
|
606
644
|
)
|
|
607
645
|
def set_variable(
|
|
608
646
|
name: str,
|
|
@@ -623,7 +661,7 @@ def create_workflows_registry(
|
|
|
623
661
|
Args:
|
|
624
662
|
name: Variable name (e.g., "session_epic", "is_worktree")
|
|
625
663
|
value: Variable value (string, number, boolean, or null)
|
|
626
|
-
session_id:
|
|
664
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
627
665
|
|
|
628
666
|
Returns:
|
|
629
667
|
Success status and updated variables
|
|
@@ -635,12 +673,18 @@ def create_workflows_registry(
|
|
|
635
673
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
636
674
|
}
|
|
637
675
|
|
|
676
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
677
|
+
try:
|
|
678
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
679
|
+
except ValueError as e:
|
|
680
|
+
return {"success": False, "error": str(e)}
|
|
681
|
+
|
|
638
682
|
# Get or create state
|
|
639
|
-
state = _state_manager.get_state(
|
|
683
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
640
684
|
if not state:
|
|
641
685
|
# Create a minimal lifecycle state for variable storage
|
|
642
686
|
state = WorkflowState(
|
|
643
|
-
session_id=
|
|
687
|
+
session_id=resolved_session_id,
|
|
644
688
|
workflow_name="__lifecycle__",
|
|
645
689
|
step="",
|
|
646
690
|
step_entered_at=datetime.now(UTC),
|
|
@@ -664,7 +708,7 @@ def create_workflows_registry(
|
|
|
664
708
|
# Resolve session_task references (#N or N) to UUIDs upfront
|
|
665
709
|
# This prevents repeated resolution failures in condition evaluation
|
|
666
710
|
if name == "session_task" and isinstance(value, str):
|
|
667
|
-
value = _resolve_session_task_value(value,
|
|
711
|
+
value = _resolve_session_task_value(value, resolved_session_id, _session_manager, _db)
|
|
668
712
|
|
|
669
713
|
# Set the variable
|
|
670
714
|
state.variables[name] = value
|
|
@@ -684,7 +728,7 @@ def create_workflows_registry(
|
|
|
684
728
|
|
|
685
729
|
@registry.tool(
|
|
686
730
|
name="get_variable",
|
|
687
|
-
description="Get workflow variable(s) for the current session.",
|
|
731
|
+
description="Get workflow variable(s) for the current session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
688
732
|
)
|
|
689
733
|
def get_variable(
|
|
690
734
|
name: str | None = None,
|
|
@@ -695,7 +739,7 @@ def create_workflows_registry(
|
|
|
695
739
|
|
|
696
740
|
Args:
|
|
697
741
|
name: Variable name to get (if None, returns all variables)
|
|
698
|
-
session_id:
|
|
742
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) - required to prevent cross-session bleed
|
|
699
743
|
|
|
700
744
|
Returns:
|
|
701
745
|
Variable value(s) and session info
|
|
@@ -707,19 +751,25 @@ def create_workflows_registry(
|
|
|
707
751
|
"error": "session_id is required. Pass the session ID explicitly to prevent cross-session variable bleed.",
|
|
708
752
|
}
|
|
709
753
|
|
|
710
|
-
|
|
754
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
755
|
+
try:
|
|
756
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
757
|
+
except ValueError as e:
|
|
758
|
+
return {"success": False, "error": str(e)}
|
|
759
|
+
|
|
760
|
+
state = _state_manager.get_state(resolved_session_id)
|
|
711
761
|
if not state:
|
|
712
762
|
if name:
|
|
713
763
|
return {
|
|
714
764
|
"success": True,
|
|
715
|
-
"session_id":
|
|
765
|
+
"session_id": resolved_session_id,
|
|
716
766
|
"variable": name,
|
|
717
767
|
"value": None,
|
|
718
768
|
"exists": False,
|
|
719
769
|
}
|
|
720
770
|
return {
|
|
721
771
|
"success": True,
|
|
722
|
-
"session_id":
|
|
772
|
+
"session_id": resolved_session_id,
|
|
723
773
|
"variables": {},
|
|
724
774
|
}
|
|
725
775
|
|
|
@@ -727,7 +777,7 @@ def create_workflows_registry(
|
|
|
727
777
|
value = state.variables.get(name)
|
|
728
778
|
return {
|
|
729
779
|
"success": True,
|
|
730
|
-
"session_id":
|
|
780
|
+
"session_id": resolved_session_id,
|
|
731
781
|
"variable": name,
|
|
732
782
|
"value": value,
|
|
733
783
|
"exists": name in state.variables,
|
|
@@ -735,7 +785,7 @@ def create_workflows_registry(
|
|
|
735
785
|
|
|
736
786
|
return {
|
|
737
787
|
"success": True,
|
|
738
|
-
"session_id":
|
|
788
|
+
"session_id": resolved_session_id,
|
|
739
789
|
"variables": state.variables,
|
|
740
790
|
}
|
|
741
791
|
|
|
@@ -287,6 +287,7 @@ def create_worktrees_registry(
|
|
|
287
287
|
worktree_storage: LocalWorktreeManager,
|
|
288
288
|
git_manager: WorktreeGitManager | None = None,
|
|
289
289
|
project_id: str | None = None,
|
|
290
|
+
session_manager: Any | None = None,
|
|
290
291
|
) -> InternalToolRegistry:
|
|
291
292
|
"""
|
|
292
293
|
Create a worktree tool registry with all worktree-related tools.
|
|
@@ -295,10 +296,20 @@ def create_worktrees_registry(
|
|
|
295
296
|
worktree_storage: LocalWorktreeManager for database operations.
|
|
296
297
|
git_manager: WorktreeGitManager for git operations.
|
|
297
298
|
project_id: Default project ID for operations.
|
|
299
|
+
session_manager: Session manager for resolving session references.
|
|
298
300
|
|
|
299
301
|
Returns:
|
|
300
302
|
InternalToolRegistry with all worktree tools registered.
|
|
301
303
|
"""
|
|
304
|
+
|
|
305
|
+
def _resolve_session_id(ref: str) -> str:
|
|
306
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
307
|
+
if session_manager is None:
|
|
308
|
+
return ref # No resolution available, return as-is
|
|
309
|
+
ctx = get_project_context()
|
|
310
|
+
proj_id = ctx.get("id") if ctx else project_id
|
|
311
|
+
return str(session_manager.resolve_session_reference(ref, proj_id))
|
|
312
|
+
|
|
302
313
|
registry = InternalToolRegistry(
|
|
303
314
|
name="gobby-worktrees",
|
|
304
315
|
description="Git worktree management - create, manage, and cleanup isolated development directories",
|
|
@@ -435,7 +446,7 @@ def create_worktrees_registry(
|
|
|
435
446
|
|
|
436
447
|
@registry.tool(
|
|
437
448
|
name="list_worktrees",
|
|
438
|
-
description="List worktrees with optional filters.",
|
|
449
|
+
description="List worktrees with optional filters. Accepts #N, N, UUID, or prefix for agent_session_id.",
|
|
439
450
|
)
|
|
440
451
|
async def list_worktrees(
|
|
441
452
|
status: str | None = None,
|
|
@@ -447,16 +458,24 @@ def create_worktrees_registry(
|
|
|
447
458
|
|
|
448
459
|
Args:
|
|
449
460
|
status: Filter by status (active, stale, merged, abandoned).
|
|
450
|
-
agent_session_id:
|
|
461
|
+
agent_session_id: Session reference (accepts #N, N, UUID, or prefix) to filter by owning session.
|
|
451
462
|
limit: Maximum results (default: 50).
|
|
452
463
|
|
|
453
464
|
Returns:
|
|
454
465
|
Dict with list of worktrees.
|
|
455
466
|
"""
|
|
467
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
468
|
+
resolved_session_id = agent_session_id
|
|
469
|
+
if agent_session_id:
|
|
470
|
+
try:
|
|
471
|
+
resolved_session_id = _resolve_session_id(agent_session_id)
|
|
472
|
+
except ValueError as e:
|
|
473
|
+
return {"success": False, "error": str(e)}
|
|
474
|
+
|
|
456
475
|
worktrees = worktree_storage.list_worktrees(
|
|
457
476
|
project_id=project_id,
|
|
458
477
|
status=status,
|
|
459
|
-
agent_session_id=
|
|
478
|
+
agent_session_id=resolved_session_id,
|
|
460
479
|
limit=limit,
|
|
461
480
|
)
|
|
462
481
|
|
|
@@ -479,7 +498,7 @@ def create_worktrees_registry(
|
|
|
479
498
|
|
|
480
499
|
@registry.tool(
|
|
481
500
|
name="claim_worktree",
|
|
482
|
-
description="Claim ownership of a worktree for an agent session.",
|
|
501
|
+
description="Claim ownership of a worktree for an agent session. Accepts #N, N, UUID, or prefix for session_id.",
|
|
483
502
|
)
|
|
484
503
|
async def claim_worktree(
|
|
485
504
|
worktree_id: str,
|
|
@@ -490,11 +509,17 @@ def create_worktrees_registry(
|
|
|
490
509
|
|
|
491
510
|
Args:
|
|
492
511
|
worktree_id: The worktree ID to claim.
|
|
493
|
-
session_id:
|
|
512
|
+
session_id: Session reference (accepts #N, N, UUID, or prefix) claiming ownership.
|
|
494
513
|
|
|
495
514
|
Returns:
|
|
496
515
|
Dict with success status.
|
|
497
516
|
"""
|
|
517
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
518
|
+
try:
|
|
519
|
+
resolved_session_id = _resolve_session_id(session_id)
|
|
520
|
+
except ValueError as e:
|
|
521
|
+
return {"success": False, "error": str(e)}
|
|
522
|
+
|
|
498
523
|
worktree = worktree_storage.get(worktree_id)
|
|
499
524
|
if not worktree:
|
|
500
525
|
return {
|
|
@@ -502,13 +527,13 @@ def create_worktrees_registry(
|
|
|
502
527
|
"error": f"Worktree '{worktree_id}' not found",
|
|
503
528
|
}
|
|
504
529
|
|
|
505
|
-
if worktree.agent_session_id and worktree.agent_session_id !=
|
|
530
|
+
if worktree.agent_session_id and worktree.agent_session_id != resolved_session_id:
|
|
506
531
|
return {
|
|
507
532
|
"success": False,
|
|
508
533
|
"error": f"Worktree already claimed by session '{worktree.agent_session_id}'",
|
|
509
534
|
}
|
|
510
535
|
|
|
511
|
-
updated = worktree_storage.claim(worktree_id,
|
|
536
|
+
updated = worktree_storage.claim(worktree_id, resolved_session_id)
|
|
512
537
|
if not updated:
|
|
513
538
|
return {"error": "Failed to claim worktree"}
|
|
514
539
|
|
gobby/memory/extractor.py
CHANGED
|
@@ -153,10 +153,15 @@ class SessionMemoryExtractor:
|
|
|
153
153
|
"""
|
|
154
154
|
session = self.session_manager.get(session_id)
|
|
155
155
|
if not session:
|
|
156
|
+
logger.warning(f"Session not found for memory extraction: {session_id}")
|
|
156
157
|
return None
|
|
157
158
|
|
|
158
|
-
# Get project info
|
|
159
|
+
# Get project info - log for debugging NULL project_id issues
|
|
159
160
|
project_id = session.project_id
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"Memory extraction context: session={session_id}, "
|
|
163
|
+
f"project_id={project_id!r} (type={type(project_id).__name__})"
|
|
164
|
+
)
|
|
160
165
|
project_name = "Unknown Project"
|
|
161
166
|
|
|
162
167
|
if project_id:
|
|
@@ -461,6 +466,15 @@ class SessionMemoryExtractor:
|
|
|
461
466
|
session_id: Source session ID
|
|
462
467
|
project_id: Project ID for the memories
|
|
463
468
|
"""
|
|
469
|
+
# Log project_id for debugging NULL project_id issues
|
|
470
|
+
if project_id is None:
|
|
471
|
+
logger.warning(
|
|
472
|
+
f"Storing memories with NULL project_id for session {session_id}. "
|
|
473
|
+
"This may cause duplicate detection issues."
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
logger.debug(f"Storing {len(candidates)} memories with project_id={project_id}")
|
|
477
|
+
|
|
464
478
|
for candidate in candidates:
|
|
465
479
|
try:
|
|
466
480
|
await self.memory_manager.remember(
|