gobby 0.2.7__py3-none-any.whl → 0.2.9__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/claude_code.py +99 -61
- 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/app_context.py +59 -0
- gobby/cli/__init__.py +0 -2
- gobby/cli/memory.py +185 -0
- gobby/cli/utils.py +5 -17
- gobby/clones/git.py +177 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +31 -0
- gobby/config/tasks.py +4 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +87 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +573 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/hook_manager.py +21 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
- gobby/llm/claude.py +377 -42
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +2 -2
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/registries.py +35 -4
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/tools/agent_messaging.py +93 -44
- gobby/mcp_proxy/tools/agents.py +45 -9
- gobby/mcp_proxy/tools/artifacts.py +46 -12
- 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/task_readiness.py +27 -4
- 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/__init__.py +266 -0
- gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
- gobby/mcp_proxy/tools/workflows/_import.py +112 -0
- gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
- gobby/mcp_proxy/tools/workflows/_query.py +207 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
- gobby/mcp_proxy/tools/worktrees.py +32 -7
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/extractor.py +15 -1
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +36 -10
- gobby/servers/http.py +186 -149
- gobby/servers/routes/admin.py +12 -0
- gobby/servers/routes/mcp/endpoints/execution.py +15 -7
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +50 -3
- gobby/servers/websocket.py +57 -1
- gobby/sessions/analyzer.py +4 -4
- gobby/sessions/manager.py +9 -0
- gobby/sessions/transcripts/gemini.py +100 -34
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/database.py +9 -2
- gobby/storage/memories.py +32 -21
- gobby/storage/migrations.py +46 -4
- gobby/storage/sessions.py +4 -2
- gobby/storage/skills.py +87 -7
- gobby/tasks/external_validator.py +4 -17
- gobby/tasks/validation.py +13 -87
- gobby/tools/summarizer.py +18 -51
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +5 -0
- gobby/workflows/context_actions.py +21 -24
- gobby/workflows/detection_helpers.py +38 -24
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +109 -1
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/engine.py +96 -0
- gobby/workflows/evaluator.py +110 -0
- gobby/workflows/hooks.py +41 -0
- gobby/workflows/lifecycle_evaluator.py +2 -1
- 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.9.dist-info}/METADATA +1 -1
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
- gobby/cli/tui.py +0 -34
- gobby/hooks/event_handlers.py +0 -909
- gobby/mcp_proxy/tools/workflows.py +0 -973
- 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.9.dist-info}/WHEEL +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,6 @@ One tool: spawn_agent(prompt, agent="generic", isolation="current"|"worktree"|"c
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import logging
|
|
15
|
-
import socket
|
|
16
15
|
import uuid
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
@@ -22,10 +21,12 @@ from gobby.agents.isolation import (
|
|
|
22
21
|
SpawnConfig,
|
|
23
22
|
get_isolation_handler,
|
|
24
23
|
)
|
|
24
|
+
from gobby.agents.registry import RunningAgent, get_running_agent_registry
|
|
25
25
|
from gobby.agents.sandbox import SandboxConfig
|
|
26
26
|
from gobby.agents.spawn_executor import SpawnRequest, execute_spawn
|
|
27
27
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
28
28
|
from gobby.mcp_proxy.tools.tasks import resolve_task_id_for_mcp
|
|
29
|
+
from gobby.utils.machine_id import get_machine_id
|
|
29
30
|
from gobby.utils.project_context import get_project_context
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
@@ -264,13 +265,30 @@ async def spawn_agent_impl(
|
|
|
264
265
|
worktree_id=isolation_ctx.worktree_id,
|
|
265
266
|
clone_id=isolation_ctx.clone_id,
|
|
266
267
|
session_manager=runner._child_session_manager,
|
|
267
|
-
machine_id=
|
|
268
|
+
machine_id=get_machine_id() or "unknown",
|
|
268
269
|
sandbox_config=effective_sandbox_config,
|
|
269
270
|
)
|
|
270
271
|
|
|
271
272
|
spawn_result = await execute_spawn(spawn_request)
|
|
272
273
|
|
|
273
|
-
# 11.
|
|
274
|
+
# 11. Register with RunningAgentRegistry for send_to_parent/child messaging
|
|
275
|
+
# Only register if spawn succeeded and we have a valid child_session_id
|
|
276
|
+
if spawn_result.success and spawn_result.child_session_id is not None:
|
|
277
|
+
agent_registry = get_running_agent_registry()
|
|
278
|
+
agent_registry.add(
|
|
279
|
+
RunningAgent(
|
|
280
|
+
run_id=spawn_result.run_id,
|
|
281
|
+
session_id=spawn_result.child_session_id,
|
|
282
|
+
parent_session_id=parent_session_id,
|
|
283
|
+
mode=effective_mode,
|
|
284
|
+
pid=spawn_result.pid,
|
|
285
|
+
provider=effective_provider,
|
|
286
|
+
workflow_name=effective_workflow,
|
|
287
|
+
worktree_id=isolation_ctx.worktree_id,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# 12. Return response with isolation metadata
|
|
274
292
|
return {
|
|
275
293
|
"success": spawn_result.success,
|
|
276
294
|
"run_id": spawn_result.run_id,
|
|
@@ -295,6 +313,7 @@ def create_spawn_agent_registry(
|
|
|
295
313
|
git_manager: Any | None = None,
|
|
296
314
|
clone_storage: Any | None = None,
|
|
297
315
|
clone_manager: Any | None = None,
|
|
316
|
+
session_manager: Any | None = None,
|
|
298
317
|
) -> InternalToolRegistry:
|
|
299
318
|
"""
|
|
300
319
|
Create a spawn_agent tool registry with the unified spawn_agent tool.
|
|
@@ -307,10 +326,20 @@ def create_spawn_agent_registry(
|
|
|
307
326
|
git_manager: Git manager for worktree operations.
|
|
308
327
|
clone_storage: Storage for clone records.
|
|
309
328
|
clone_manager: Git manager for clone operations.
|
|
329
|
+
session_manager: Session manager for resolving session references.
|
|
310
330
|
|
|
311
331
|
Returns:
|
|
312
332
|
InternalToolRegistry with spawn_agent tool registered.
|
|
313
333
|
"""
|
|
334
|
+
|
|
335
|
+
def _resolve_session_id(ref: str) -> str:
|
|
336
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID."""
|
|
337
|
+
if session_manager is None:
|
|
338
|
+
return ref # No resolution available, return as-is
|
|
339
|
+
ctx = get_project_context()
|
|
340
|
+
project_id = ctx.get("id") if ctx else None
|
|
341
|
+
return str(session_manager.resolve_session_reference(ref, project_id))
|
|
342
|
+
|
|
314
343
|
registry = InternalToolRegistry(
|
|
315
344
|
name="gobby-spawn-agent",
|
|
316
345
|
description="Unified agent spawning with isolation support",
|
|
@@ -324,7 +353,8 @@ def create_spawn_agent_registry(
|
|
|
324
353
|
description=(
|
|
325
354
|
"Spawn a subagent to execute a task. Supports isolation modes: "
|
|
326
355
|
"'current' (work in current directory), 'worktree' (create git worktree), "
|
|
327
|
-
"'clone' (create shallow clone). Can use named agent definitions or raw parameters."
|
|
356
|
+
"'clone' (create shallow clone). Can use named agent definitions or raw parameters. "
|
|
357
|
+
"Accepts #N, N, UUID, or prefix for parent_session_id."
|
|
328
358
|
),
|
|
329
359
|
)
|
|
330
360
|
async def spawn_agent(
|
|
@@ -374,12 +404,20 @@ def create_spawn_agent_registry(
|
|
|
374
404
|
sandbox_mode: Sandbox mode (permissive/restrictive). Overrides agent_def.
|
|
375
405
|
sandbox_allow_network: Allow network access. Overrides agent_def.
|
|
376
406
|
sandbox_extra_paths: Extra paths for sandbox write access.
|
|
377
|
-
parent_session_id:
|
|
407
|
+
parent_session_id: Session reference (accepts #N, N, UUID, or prefix) for the parent session
|
|
378
408
|
project_path: Project path override
|
|
379
409
|
|
|
380
410
|
Returns:
|
|
381
411
|
Dict with success status, run_id, child_session_id, isolation metadata
|
|
382
412
|
"""
|
|
413
|
+
# Resolve parent_session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
414
|
+
resolved_parent_session_id = parent_session_id
|
|
415
|
+
if parent_session_id:
|
|
416
|
+
try:
|
|
417
|
+
resolved_parent_session_id = _resolve_session_id(parent_session_id)
|
|
418
|
+
except ValueError as e:
|
|
419
|
+
return {"success": False, "error": str(e)}
|
|
420
|
+
|
|
383
421
|
# Load agent definition (defaults to "generic")
|
|
384
422
|
agent_def = loader.load(agent)
|
|
385
423
|
if agent_def is None and agent != "generic":
|
|
@@ -410,7 +448,7 @@ def create_spawn_agent_registry(
|
|
|
410
448
|
sandbox_mode=sandbox_mode,
|
|
411
449
|
sandbox_allow_network=sandbox_allow_network,
|
|
412
450
|
sandbox_extra_paths=sandbox_extra_paths,
|
|
413
|
-
parent_session_id=
|
|
451
|
+
parent_session_id=resolved_parent_session_id,
|
|
414
452
|
project_path=project_path,
|
|
415
453
|
)
|
|
416
454
|
|
|
@@ -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
|
-
|
|
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()
|
|
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).
|
|
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
|
)
|
|
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING
|
|
|
9
9
|
|
|
10
10
|
from gobby.storage.projects import LocalProjectManager
|
|
11
11
|
from gobby.storage.session_tasks import SessionTaskManager
|
|
12
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
12
13
|
from gobby.storage.task_dependencies import TaskDependencyManager
|
|
13
14
|
from gobby.storage.tasks import LocalTaskManager
|
|
14
15
|
from gobby.utils.project_context import get_project_context
|
|
@@ -42,6 +43,7 @@ class RegistryContext:
|
|
|
42
43
|
# Derived managers (initialized in __post_init__)
|
|
43
44
|
dep_manager: TaskDependencyManager = field(init=False)
|
|
44
45
|
session_task_manager: SessionTaskManager = field(init=False)
|
|
46
|
+
session_manager: LocalSessionManager = field(init=False)
|
|
45
47
|
workflow_state_manager: WorkflowStateManager = field(init=False)
|
|
46
48
|
project_manager: LocalProjectManager = field(init=False)
|
|
47
49
|
|
|
@@ -56,6 +58,7 @@ class RegistryContext:
|
|
|
56
58
|
db = self.task_manager.db
|
|
57
59
|
self.dep_manager = TaskDependencyManager(db)
|
|
58
60
|
self.session_task_manager = SessionTaskManager(db)
|
|
61
|
+
self.session_manager = LocalSessionManager(db)
|
|
59
62
|
self.workflow_state_manager = WorkflowStateManager(db)
|
|
60
63
|
self.project_manager = LocalProjectManager(db)
|
|
61
64
|
|
|
@@ -90,3 +93,18 @@ class RegistryContext:
|
|
|
90
93
|
if not session_id:
|
|
91
94
|
return None
|
|
92
95
|
return self.workflow_state_manager.get_state(session_id)
|
|
96
|
+
|
|
97
|
+
def resolve_session_id(self, session_id: str) -> str:
|
|
98
|
+
"""Resolve session reference (#N, N, UUID, or prefix) to UUID.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
session_id: Session reference string
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Resolved UUID string
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: If session cannot be resolved
|
|
108
|
+
"""
|
|
109
|
+
project_id = self.get_current_project_id()
|
|
110
|
+
return self.session_manager.resolve_session_reference(session_id, project_id)
|
|
@@ -90,6 +90,13 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
90
90
|
if effective_category is None:
|
|
91
91
|
effective_category = _infer_category(title, description)
|
|
92
92
|
|
|
93
|
+
# Resolve session_id to UUID (accepts #N, N, UUID, or prefix)
|
|
94
|
+
resolved_session_id = session_id
|
|
95
|
+
try:
|
|
96
|
+
resolved_session_id = ctx.resolve_session_id(session_id)
|
|
97
|
+
except ValueError:
|
|
98
|
+
pass # Fall back to raw value if resolution fails
|
|
99
|
+
|
|
93
100
|
# Create task
|
|
94
101
|
create_result = ctx.task_manager.create_task_with_decomposition(
|
|
95
102
|
project_id=project_id,
|
|
@@ -101,14 +108,14 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
101
108
|
labels=labels,
|
|
102
109
|
category=effective_category,
|
|
103
110
|
validation_criteria=validation_criteria,
|
|
104
|
-
created_in_session_id=
|
|
111
|
+
created_in_session_id=resolved_session_id,
|
|
105
112
|
)
|
|
106
113
|
|
|
107
114
|
task = ctx.task_manager.get_task(create_result["task"]["id"])
|
|
108
115
|
|
|
109
116
|
# Link task to session (best-effort) - tracks which session created the task
|
|
110
117
|
try:
|
|
111
|
-
ctx.session_task_manager.link_task(
|
|
118
|
+
ctx.session_task_manager.link_task(resolved_session_id, task.id, "created")
|
|
112
119
|
except Exception:
|
|
113
120
|
pass # nosec B110 - best-effort linking
|
|
114
121
|
|
|
@@ -116,7 +123,7 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
116
123
|
if claim:
|
|
117
124
|
updated_task = ctx.task_manager.update_task(
|
|
118
125
|
task.id,
|
|
119
|
-
assignee=
|
|
126
|
+
assignee=resolved_session_id,
|
|
120
127
|
status="in_progress",
|
|
121
128
|
)
|
|
122
129
|
if updated_task is None:
|
|
@@ -125,14 +132,14 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
125
132
|
task = updated_task
|
|
126
133
|
# Link task to session with "claimed" action (best-effort)
|
|
127
134
|
try:
|
|
128
|
-
ctx.session_task_manager.link_task(
|
|
135
|
+
ctx.session_task_manager.link_task(resolved_session_id, task.id, "claimed")
|
|
129
136
|
except Exception:
|
|
130
137
|
pass # nosec B110 - best-effort linking
|
|
131
138
|
|
|
132
139
|
# Set workflow state for Claude Code (CC doesn't include tool results in PostToolUse)
|
|
133
140
|
# This mirrors close_task behavior in _lifecycle.py:196-207
|
|
134
141
|
try:
|
|
135
|
-
state = ctx.workflow_state_manager.get_state(
|
|
142
|
+
state = ctx.workflow_state_manager.get_state(resolved_session_id)
|
|
136
143
|
if state:
|
|
137
144
|
state.variables["task_claimed"] = True
|
|
138
145
|
state.variables["claimed_task_id"] = task.id # Always use UUID
|
|
@@ -248,7 +255,7 @@ def create_crud_registry(ctx: RegistryContext) -> InternalToolRegistry:
|
|
|
248
255
|
},
|
|
249
256
|
"session_id": {
|
|
250
257
|
"type": "string",
|
|
251
|
-
"description": "Your session ID (
|
|
258
|
+
"description": "Your session ID (accepts #N, N, UUID, or prefix). Required to track which session created the task.",
|
|
252
259
|
},
|
|
253
260
|
"claim": {
|
|
254
261
|
"type": "boolean",
|
|
@@ -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
|
},
|