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.
Files changed (125) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/claude_code.py +99 -61
  3. gobby/adapters/gemini.py +140 -38
  4. gobby/agents/isolation.py +130 -0
  5. gobby/agents/registry.py +11 -0
  6. gobby/agents/session.py +1 -0
  7. gobby/agents/spawn_executor.py +43 -13
  8. gobby/agents/spawners/macos.py +26 -1
  9. gobby/app_context.py +59 -0
  10. gobby/cli/__init__.py +0 -2
  11. gobby/cli/memory.py +185 -0
  12. gobby/cli/utils.py +5 -17
  13. gobby/clones/git.py +177 -0
  14. gobby/config/features.py +0 -20
  15. gobby/config/skills.py +31 -0
  16. gobby/config/tasks.py +4 -0
  17. gobby/hooks/event_handlers/__init__.py +155 -0
  18. gobby/hooks/event_handlers/_agent.py +175 -0
  19. gobby/hooks/event_handlers/_base.py +87 -0
  20. gobby/hooks/event_handlers/_misc.py +66 -0
  21. gobby/hooks/event_handlers/_session.py +573 -0
  22. gobby/hooks/event_handlers/_tool.py +196 -0
  23. gobby/hooks/hook_manager.py +21 -1
  24. gobby/install/gemini/hooks/hook_dispatcher.py +74 -15
  25. gobby/llm/claude.py +377 -42
  26. gobby/mcp_proxy/importer.py +4 -41
  27. gobby/mcp_proxy/instructions.py +2 -2
  28. gobby/mcp_proxy/manager.py +13 -3
  29. gobby/mcp_proxy/registries.py +35 -4
  30. gobby/mcp_proxy/services/recommendation.py +2 -28
  31. gobby/mcp_proxy/tools/agent_messaging.py +93 -44
  32. gobby/mcp_proxy/tools/agents.py +45 -9
  33. gobby/mcp_proxy/tools/artifacts.py +46 -12
  34. gobby/mcp_proxy/tools/sessions/_commits.py +31 -24
  35. gobby/mcp_proxy/tools/sessions/_crud.py +5 -5
  36. gobby/mcp_proxy/tools/sessions/_handoff.py +45 -41
  37. gobby/mcp_proxy/tools/sessions/_messages.py +35 -7
  38. gobby/mcp_proxy/tools/spawn_agent.py +44 -6
  39. gobby/mcp_proxy/tools/task_readiness.py +27 -4
  40. gobby/mcp_proxy/tools/tasks/_context.py +18 -0
  41. gobby/mcp_proxy/tools/tasks/_crud.py +13 -6
  42. gobby/mcp_proxy/tools/tasks/_lifecycle.py +29 -14
  43. gobby/mcp_proxy/tools/tasks/_session.py +22 -7
  44. gobby/mcp_proxy/tools/workflows/__init__.py +266 -0
  45. gobby/mcp_proxy/tools/workflows/_artifacts.py +225 -0
  46. gobby/mcp_proxy/tools/workflows/_import.py +112 -0
  47. gobby/mcp_proxy/tools/workflows/_lifecycle.py +321 -0
  48. gobby/mcp_proxy/tools/workflows/_query.py +207 -0
  49. gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
  50. gobby/mcp_proxy/tools/workflows/_terminal.py +139 -0
  51. gobby/mcp_proxy/tools/worktrees.py +32 -7
  52. gobby/memory/components/__init__.py +0 -0
  53. gobby/memory/components/ingestion.py +98 -0
  54. gobby/memory/components/search.py +108 -0
  55. gobby/memory/extractor.py +15 -1
  56. gobby/memory/manager.py +16 -25
  57. gobby/paths.py +51 -0
  58. gobby/prompts/loader.py +1 -35
  59. gobby/runner.py +36 -10
  60. gobby/servers/http.py +186 -149
  61. gobby/servers/routes/admin.py +12 -0
  62. gobby/servers/routes/mcp/endpoints/execution.py +15 -7
  63. gobby/servers/routes/mcp/endpoints/registry.py +8 -8
  64. gobby/servers/routes/mcp/hooks.py +50 -3
  65. gobby/servers/websocket.py +57 -1
  66. gobby/sessions/analyzer.py +4 -4
  67. gobby/sessions/manager.py +9 -0
  68. gobby/sessions/transcripts/gemini.py +100 -34
  69. gobby/skills/parser.py +23 -0
  70. gobby/skills/sync.py +5 -4
  71. gobby/storage/artifacts.py +19 -0
  72. gobby/storage/database.py +9 -2
  73. gobby/storage/memories.py +32 -21
  74. gobby/storage/migrations.py +46 -4
  75. gobby/storage/sessions.py +4 -2
  76. gobby/storage/skills.py +87 -7
  77. gobby/tasks/external_validator.py +4 -17
  78. gobby/tasks/validation.py +13 -87
  79. gobby/tools/summarizer.py +18 -51
  80. gobby/utils/status.py +13 -0
  81. gobby/workflows/actions.py +5 -0
  82. gobby/workflows/context_actions.py +21 -24
  83. gobby/workflows/detection_helpers.py +38 -24
  84. gobby/workflows/enforcement/__init__.py +11 -1
  85. gobby/workflows/enforcement/blocking.py +109 -1
  86. gobby/workflows/enforcement/handlers.py +35 -1
  87. gobby/workflows/engine.py +96 -0
  88. gobby/workflows/evaluator.py +110 -0
  89. gobby/workflows/hooks.py +41 -0
  90. gobby/workflows/lifecycle_evaluator.py +2 -1
  91. gobby/workflows/memory_actions.py +11 -0
  92. gobby/workflows/safe_evaluator.py +8 -0
  93. gobby/workflows/summary_actions.py +123 -50
  94. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/METADATA +1 -1
  95. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/RECORD +99 -107
  96. gobby/cli/tui.py +0 -34
  97. gobby/hooks/event_handlers.py +0 -909
  98. gobby/mcp_proxy/tools/workflows.py +0 -973
  99. gobby/tui/__init__.py +0 -5
  100. gobby/tui/api_client.py +0 -278
  101. gobby/tui/app.py +0 -329
  102. gobby/tui/screens/__init__.py +0 -25
  103. gobby/tui/screens/agents.py +0 -333
  104. gobby/tui/screens/chat.py +0 -450
  105. gobby/tui/screens/dashboard.py +0 -377
  106. gobby/tui/screens/memory.py +0 -305
  107. gobby/tui/screens/metrics.py +0 -231
  108. gobby/tui/screens/orchestrator.py +0 -903
  109. gobby/tui/screens/sessions.py +0 -412
  110. gobby/tui/screens/tasks.py +0 -440
  111. gobby/tui/screens/workflows.py +0 -289
  112. gobby/tui/screens/worktrees.py +0 -174
  113. gobby/tui/widgets/__init__.py +0 -21
  114. gobby/tui/widgets/chat.py +0 -210
  115. gobby/tui/widgets/conductor.py +0 -104
  116. gobby/tui/widgets/menu.py +0 -132
  117. gobby/tui/widgets/message_panel.py +0 -160
  118. gobby/tui/widgets/review_gate.py +0 -224
  119. gobby/tui/widgets/task_tree.py +0 -99
  120. gobby/tui/widgets/token_budget.py +0 -166
  121. gobby/tui/ws_client.py +0 -258
  122. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/WHEEL +0 -0
  123. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/entry_points.txt +0 -0
  124. {gobby-0.2.7.dist-info → gobby-0.2.9.dist-info}/licenses/LICENSE.md +0 -0
  125. {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=socket.gethostname(),
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. Return response with isolation metadata
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: 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=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
- 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
  )
@@ -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=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(session_id, task.id, "created")
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=session_id,
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(session_id, task.id, "claimed")
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(session_id)
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 (from system context). Required to track which session created the task.",
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 session_id and not should_skip:
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(session_id)
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 session_id:
196
+ if resolved_session_id:
189
197
  try:
190
- ctx.session_task_manager.link_task(session_id, resolved_id, "review")
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=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 session_id:
225
+ if resolved_session_id:
218
226
  try:
219
- ctx.session_task_manager.link_task(session_id, resolved_id, "closed")
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 session_id:
235
+ if resolved_session_id:
228
236
  try:
229
- state = ctx.workflow_state_manager.get_state(session_id)
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 (from system context). Pass this to track which session closed the task.",
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 != session_id and not force:
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=session_id,
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(session_id, resolved_id, "claimed")
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 (from system context). The session claiming the task.",
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.session_task_manager.link_task(session_id, resolved_id, action)
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 ID (optional, defaults to linking context if available)",
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
- tasks = ctx.session_task_manager.get_session_tasks(session_id)
78
- return {"session_id": session_id, "tasks": tasks}
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": {"type": "string", "description": "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
  },