gobby 0.2.6__py3-none-any.whl → 0.2.7__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 (146) hide show
  1. gobby/__init__.py +1 -1
  2. gobby/adapters/__init__.py +2 -1
  3. gobby/adapters/codex_impl/__init__.py +28 -0
  4. gobby/adapters/codex_impl/adapter.py +722 -0
  5. gobby/adapters/codex_impl/client.py +679 -0
  6. gobby/adapters/codex_impl/protocol.py +20 -0
  7. gobby/adapters/codex_impl/types.py +68 -0
  8. gobby/agents/definitions.py +11 -1
  9. gobby/agents/isolation.py +395 -0
  10. gobby/agents/sandbox.py +261 -0
  11. gobby/agents/spawn.py +42 -287
  12. gobby/agents/spawn_executor.py +385 -0
  13. gobby/agents/spawners/__init__.py +24 -0
  14. gobby/agents/spawners/command_builder.py +189 -0
  15. gobby/agents/spawners/embedded.py +21 -2
  16. gobby/agents/spawners/headless.py +21 -2
  17. gobby/agents/spawners/prompt_manager.py +125 -0
  18. gobby/cli/install.py +4 -4
  19. gobby/cli/installers/claude.py +6 -0
  20. gobby/cli/installers/gemini.py +6 -0
  21. gobby/cli/installers/shared.py +103 -4
  22. gobby/cli/sessions.py +1 -1
  23. gobby/cli/utils.py +9 -2
  24. gobby/config/__init__.py +12 -97
  25. gobby/config/app.py +10 -94
  26. gobby/config/extensions.py +2 -2
  27. gobby/config/features.py +7 -130
  28. gobby/config/tasks.py +4 -28
  29. gobby/hooks/__init__.py +0 -13
  30. gobby/hooks/event_handlers.py +45 -2
  31. gobby/hooks/hook_manager.py +2 -2
  32. gobby/hooks/plugins.py +1 -1
  33. gobby/hooks/webhooks.py +1 -1
  34. gobby/llm/resolver.py +3 -2
  35. gobby/mcp_proxy/importer.py +62 -4
  36. gobby/mcp_proxy/instructions.py +2 -0
  37. gobby/mcp_proxy/registries.py +1 -4
  38. gobby/mcp_proxy/services/recommendation.py +43 -11
  39. gobby/mcp_proxy/tools/agents.py +31 -731
  40. gobby/mcp_proxy/tools/clones.py +0 -385
  41. gobby/mcp_proxy/tools/memory.py +2 -2
  42. gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
  43. gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
  44. gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
  45. gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
  46. gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
  47. gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
  48. gobby/mcp_proxy/tools/skills/__init__.py +14 -29
  49. gobby/mcp_proxy/tools/spawn_agent.py +417 -0
  50. gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
  51. gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
  52. gobby/mcp_proxy/tools/worktrees.py +0 -343
  53. gobby/memory/ingestion/__init__.py +5 -0
  54. gobby/memory/ingestion/multimodal.py +221 -0
  55. gobby/memory/manager.py +62 -283
  56. gobby/memory/search/__init__.py +10 -0
  57. gobby/memory/search/coordinator.py +248 -0
  58. gobby/memory/services/__init__.py +5 -0
  59. gobby/memory/services/crossref.py +142 -0
  60. gobby/prompts/loader.py +5 -2
  61. gobby/servers/http.py +1 -4
  62. gobby/servers/routes/admin.py +14 -0
  63. gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
  64. gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
  65. gobby/servers/routes/mcp/endpoints/execution.py +568 -0
  66. gobby/servers/routes/mcp/endpoints/registry.py +378 -0
  67. gobby/servers/routes/mcp/endpoints/server.py +304 -0
  68. gobby/servers/routes/mcp/hooks.py +1 -1
  69. gobby/servers/routes/mcp/tools.py +48 -1506
  70. gobby/sessions/lifecycle.py +1 -1
  71. gobby/sessions/processor.py +10 -0
  72. gobby/sessions/transcripts/base.py +1 -0
  73. gobby/sessions/transcripts/claude.py +15 -5
  74. gobby/skills/parser.py +30 -2
  75. gobby/storage/migrations.py +159 -372
  76. gobby/storage/sessions.py +43 -7
  77. gobby/storage/skills.py +37 -4
  78. gobby/storage/tasks/_lifecycle.py +18 -3
  79. gobby/sync/memories.py +1 -1
  80. gobby/tasks/external_validator.py +1 -1
  81. gobby/tasks/validation.py +22 -20
  82. gobby/tools/summarizer.py +91 -10
  83. gobby/utils/project_context.py +2 -3
  84. gobby/utils/status.py +13 -0
  85. gobby/workflows/actions.py +221 -1217
  86. gobby/workflows/artifact_actions.py +31 -0
  87. gobby/workflows/autonomous_actions.py +11 -0
  88. gobby/workflows/context_actions.py +50 -1
  89. gobby/workflows/enforcement/__init__.py +47 -0
  90. gobby/workflows/enforcement/blocking.py +269 -0
  91. gobby/workflows/enforcement/commit_policy.py +283 -0
  92. gobby/workflows/enforcement/handlers.py +269 -0
  93. gobby/workflows/enforcement/task_policy.py +542 -0
  94. gobby/workflows/git_utils.py +106 -0
  95. gobby/workflows/llm_actions.py +30 -0
  96. gobby/workflows/mcp_actions.py +20 -1
  97. gobby/workflows/memory_actions.py +80 -0
  98. gobby/workflows/safe_evaluator.py +183 -0
  99. gobby/workflows/session_actions.py +44 -0
  100. gobby/workflows/state_actions.py +60 -1
  101. gobby/workflows/stop_signal_actions.py +55 -0
  102. gobby/workflows/summary_actions.py +94 -1
  103. gobby/workflows/task_sync_actions.py +347 -0
  104. gobby/workflows/todo_actions.py +34 -1
  105. gobby/workflows/webhook_actions.py +185 -0
  106. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
  107. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
  108. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
  109. gobby/adapters/codex.py +0 -1332
  110. gobby/install/claude/commands/gobby/bug.md +0 -51
  111. gobby/install/claude/commands/gobby/chore.md +0 -51
  112. gobby/install/claude/commands/gobby/epic.md +0 -52
  113. gobby/install/claude/commands/gobby/eval.md +0 -235
  114. gobby/install/claude/commands/gobby/feat.md +0 -49
  115. gobby/install/claude/commands/gobby/nit.md +0 -52
  116. gobby/install/claude/commands/gobby/ref.md +0 -52
  117. gobby/mcp_proxy/tools/session_messages.py +0 -1055
  118. gobby/prompts/defaults/expansion/system.md +0 -119
  119. gobby/prompts/defaults/expansion/user.md +0 -48
  120. gobby/prompts/defaults/external_validation/agent.md +0 -72
  121. gobby/prompts/defaults/external_validation/external.md +0 -63
  122. gobby/prompts/defaults/external_validation/spawn.md +0 -83
  123. gobby/prompts/defaults/external_validation/system.md +0 -6
  124. gobby/prompts/defaults/features/import_mcp.md +0 -22
  125. gobby/prompts/defaults/features/import_mcp_github.md +0 -17
  126. gobby/prompts/defaults/features/import_mcp_search.md +0 -16
  127. gobby/prompts/defaults/features/recommend_tools.md +0 -32
  128. gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
  129. gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
  130. gobby/prompts/defaults/features/server_description.md +0 -20
  131. gobby/prompts/defaults/features/server_description_system.md +0 -6
  132. gobby/prompts/defaults/features/task_description.md +0 -31
  133. gobby/prompts/defaults/features/task_description_system.md +0 -6
  134. gobby/prompts/defaults/features/tool_summary.md +0 -17
  135. gobby/prompts/defaults/features/tool_summary_system.md +0 -6
  136. gobby/prompts/defaults/handoff/compact.md +0 -63
  137. gobby/prompts/defaults/handoff/session_end.md +0 -57
  138. gobby/prompts/defaults/memory/extract.md +0 -61
  139. gobby/prompts/defaults/research/step.md +0 -58
  140. gobby/prompts/defaults/validation/criteria.md +0 -47
  141. gobby/prompts/defaults/validation/validate.md +0 -38
  142. gobby/storage/migrations_legacy.py +0 -1359
  143. gobby/workflows/task_enforcement_actions.py +0 -1343
  144. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
  145. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
  146. {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ from pathlib import Path
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  from gobby.agents.constants import get_terminal_env_vars
12
+ from gobby.agents.sandbox import SandboxConfig, compute_sandbox_paths, get_sandbox_resolver
12
13
  from gobby.agents.spawners.base import HeadlessResult
13
14
 
14
15
  if TYPE_CHECKING:
@@ -26,11 +27,11 @@ def _get_spawn_utils() -> tuple[
26
27
  """Lazy import to avoid circular dependencies."""
27
28
  from gobby.agents.spawn import (
28
29
  MAX_ENV_PROMPT_LENGTH,
29
- _create_prompt_file,
30
30
  build_cli_command,
31
+ create_prompt_file,
31
32
  )
32
33
 
33
- return build_cli_command, _create_prompt_file, MAX_ENV_PROMPT_LENGTH
34
+ return build_cli_command, create_prompt_file, MAX_ENV_PROMPT_LENGTH
34
35
 
35
36
 
36
37
  class HeadlessSpawner:
@@ -169,6 +170,7 @@ class HeadlessSpawner:
169
170
  agent_depth: int = 1,
170
171
  max_agent_depth: int = 3,
171
172
  prompt: str | None = None,
173
+ sandbox_config: SandboxConfig | None = None,
172
174
  ) -> HeadlessResult:
173
175
  """
174
176
  Spawn a CLI agent in headless mode.
@@ -184,12 +186,24 @@ class HeadlessSpawner:
184
186
  agent_depth: Current nesting depth
185
187
  max_agent_depth: Maximum allowed depth
186
188
  prompt: Optional initial prompt
189
+ sandbox_config: Optional sandbox configuration
187
190
 
188
191
  Returns:
189
192
  HeadlessResult with process handle
190
193
  """
191
194
  build_cli_command, _create_prompt_file, max_env_prompt_length = _get_spawn_utils()
192
195
 
196
+ # Resolve sandbox configuration if enabled
197
+ sandbox_args: list[str] | None = None
198
+ sandbox_env: dict[str, str] = {}
199
+
200
+ if sandbox_config and sandbox_config.enabled:
201
+ # Compute sandbox paths based on cwd (workspace)
202
+ resolved_paths = compute_sandbox_paths(sandbox_config, str(cwd))
203
+ # Get CLI-specific resolver and generate args/env
204
+ resolver = get_sandbox_resolver(cli)
205
+ sandbox_args, sandbox_env = resolver.resolve(sandbox_config, resolved_paths)
206
+
193
207
  # Build command with prompt as CLI argument and auto-approve for autonomous work
194
208
  command = build_cli_command(
195
209
  cli,
@@ -198,6 +212,7 @@ class HeadlessSpawner:
198
212
  auto_approve=True, # Subagents need to work autonomously
199
213
  working_directory=str(cwd) if cli == "codex" else None,
200
214
  mode="headless", # Non-interactive headless mode
215
+ sandbox_args=sandbox_args,
201
216
  )
202
217
 
203
218
  # Handle prompt for environment variables (backup for hooks/context)
@@ -223,4 +238,8 @@ class HeadlessSpawner:
223
238
  prompt_file=prompt_file,
224
239
  )
225
240
 
241
+ # Merge sandbox environment variables if present
242
+ if sandbox_env:
243
+ env.update(sandbox_env)
244
+
226
245
  return self.spawn(command, cwd, env)
@@ -0,0 +1,125 @@
1
+ """Prompt file management for agent spawning.
2
+
3
+ Handles creation, cleanup, and tracking of temporary prompt files
4
+ used to pass long prompts to spawned CLI agents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import atexit
10
+ import logging
11
+ import os
12
+ import re
13
+ import tempfile
14
+ import threading
15
+ import uuid
16
+ from pathlib import Path
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Maximum prompt length to pass via environment variable
21
+ # Longer prompts will be written to a temp file
22
+ MAX_ENV_PROMPT_LENGTH = 4096
23
+
24
+ # Module-level set for tracking prompt files to clean up on exit
25
+ # This avoids registering a new atexit handler for each prompt file
26
+ _prompt_files_to_cleanup: set[Path] = set()
27
+ _atexit_registered = False
28
+ _atexit_lock = threading.Lock()
29
+
30
+
31
+ def cleanup_all_prompt_files() -> None:
32
+ """Clean up all tracked prompt files on process exit."""
33
+ with _atexit_lock:
34
+ files_to_cleanup = list(_prompt_files_to_cleanup)
35
+ _prompt_files_to_cleanup.clear()
36
+ for prompt_path in files_to_cleanup:
37
+ try:
38
+ if prompt_path.exists():
39
+ prompt_path.unlink()
40
+ except OSError:
41
+ pass
42
+
43
+
44
+ def create_prompt_file(prompt: str, session_id: str) -> str:
45
+ """
46
+ Create a prompt file with secure permissions.
47
+
48
+ The file is created in the system temp directory with restrictive
49
+ permissions (owner read/write only) and tracked for cleanup on exit.
50
+
51
+ Args:
52
+ prompt: The prompt content to write
53
+ session_id: Session ID for naming the file
54
+
55
+ Returns:
56
+ Path to the created temp file
57
+ """
58
+ global _atexit_registered
59
+
60
+ # Create temp directory with restrictive permissions
61
+ temp_dir = Path(tempfile.gettempdir()) / "gobby-prompts"
62
+ temp_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
63
+
64
+ # Sanitize session_id to prevent path traversal attacks
65
+ # Strip path separators and limit to alphanumeric, hyphens, underscores
66
+ safe_session_id = re.sub(r"[^a-zA-Z0-9_-]", "", session_id)
67
+ if not safe_session_id or len(safe_session_id) > 128:
68
+ safe_session_id = str(uuid.uuid4())
69
+
70
+ # Create the prompt file path
71
+ prompt_path = temp_dir / f"prompt-{safe_session_id}.txt"
72
+
73
+ # Write with secure permissions atomically - create with mode 0o600 from the start
74
+ # This avoids the TOCTOU window between write_text and chmod
75
+ fd = os.open(str(prompt_path), os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600)
76
+ try:
77
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
78
+ f.write(prompt)
79
+ f.flush()
80
+ os.fsync(f.fileno())
81
+ except Exception:
82
+ # fd is closed by fdopen, but if fdopen fails we need to close it
83
+ try:
84
+ os.close(fd)
85
+ except OSError:
86
+ pass
87
+ raise
88
+
89
+ # Track for cleanup and register handler (thread-safe)
90
+ with _atexit_lock:
91
+ _prompt_files_to_cleanup.add(prompt_path)
92
+ if not _atexit_registered:
93
+ atexit.register(cleanup_all_prompt_files)
94
+ _atexit_registered = True
95
+
96
+ logger.debug(f"Created secure prompt file: {prompt_path}")
97
+ return str(prompt_path)
98
+
99
+
100
+ def read_prompt_from_env() -> str | None:
101
+ """
102
+ Read initial prompt from environment variables.
103
+
104
+ Checks GOBBY_PROMPT_FILE first (for long prompts),
105
+ then falls back to GOBBY_PROMPT (for short prompts).
106
+
107
+ Returns:
108
+ Prompt string or None if not set
109
+ """
110
+ from gobby.agents.constants import GOBBY_PROMPT, GOBBY_PROMPT_FILE
111
+
112
+ # Check for prompt file first
113
+ prompt_file = os.environ.get(GOBBY_PROMPT_FILE)
114
+ if prompt_file:
115
+ try:
116
+ prompt_path = Path(prompt_file)
117
+ if prompt_path.exists():
118
+ return prompt_path.read_text(encoding="utf-8")
119
+ else:
120
+ logger.warning(f"Prompt file not found: {prompt_file}")
121
+ except Exception as e:
122
+ logger.error(f"Error reading prompt file: {e}")
123
+
124
+ # Fall back to inline prompt
125
+ return os.environ.get(GOBBY_PROMPT)
gobby/cli/install.py CHANGED
@@ -250,7 +250,7 @@ def install(
250
250
  click.echo(f" - {cmd}")
251
251
  if result.get("plugins_installed"):
252
252
  click.echo(
253
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
253
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
254
254
  )
255
255
  for plugin in result["plugins_installed"]:
256
256
  click.echo(f" - {plugin}")
@@ -287,7 +287,7 @@ def install(
287
287
  click.echo(f" - {cmd}")
288
288
  if result.get("plugins_installed"):
289
289
  click.echo(
290
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
290
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
291
291
  )
292
292
  for plugin in result["plugins_installed"]:
293
293
  click.echo(f" - {plugin}")
@@ -334,7 +334,7 @@ def install(
334
334
  click.echo(f" - {cmd}")
335
335
  if result.get("plugins_installed"):
336
336
  click.echo(
337
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
337
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
338
338
  )
339
339
  for plugin in result["plugins_installed"]:
340
340
  click.echo(f" - {plugin}")
@@ -395,7 +395,7 @@ def install(
395
395
  click.echo(f" - {cmd}")
396
396
  if result.get("plugins_installed"):
397
397
  click.echo(
398
- f"Installed {len(result['plugins_installed'])} plugins to ~/.gobby/plugins/"
398
+ f"Installed {len(result['plugins_installed'])} plugins to .gobby/plugins/"
399
399
  )
400
400
  for plugin in result["plugins_installed"]:
401
401
  click.echo(f" - {plugin}")
@@ -20,6 +20,7 @@ from .shared import (
20
20
  backup_gobby_skills,
21
21
  configure_mcp_server_json,
22
22
  install_cli_content,
23
+ install_router_skills_as_commands,
23
24
  install_shared_content,
24
25
  remove_mcp_server_json,
25
26
  )
@@ -125,6 +126,11 @@ def install_claude(project_path: Path) -> dict[str, Any]:
125
126
  result["commands_installed"] = cli.get("commands", [])
126
127
  result["plugins_installed"] = shared.get("plugins", [])
127
128
 
129
+ # Install router skills (gobby, g) as flattened commands
130
+ commands_dir = claude_path / "commands"
131
+ router_commands = install_router_skills_as_commands(commands_dir)
132
+ result["commands_installed"].extend(router_commands)
133
+
128
134
  # Skills are now auto-synced to database on daemon startup (sync_bundled_skills)
129
135
  # No longer need to copy to .claude/skills/
130
136
 
@@ -17,6 +17,7 @@ from gobby.cli.utils import get_install_dir
17
17
  from .shared import (
18
18
  configure_mcp_server_json,
19
19
  install_cli_content,
20
+ install_router_skills_as_gemini_skills,
20
21
  install_shared_content,
21
22
  remove_mcp_server_json,
22
23
  )
@@ -87,6 +88,11 @@ def install_gemini(project_path: Path) -> dict[str, Any]:
87
88
  result["commands_installed"] = cli.get("commands", [])
88
89
  result["plugins_installed"] = shared.get("plugins", [])
89
90
 
91
+ # Install router skills (gobby, g) as Gemini skills
92
+ skills_dir = gemini_path / "skills"
93
+ router_skills = install_router_skills_as_gemini_skills(skills_dir)
94
+ result["commands_installed"].extend(router_skills)
95
+
90
96
  # Backup existing settings.json if it exists
91
97
  if settings_file.exists():
92
98
  timestamp = int(time.time())
@@ -23,7 +23,8 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
23
23
  """Install shared content from src/install/shared/.
24
24
 
25
25
  Workflows are cross-CLI and go to {project_path}/.gobby/workflows/.
26
- Plugins are global and go to ~/.gobby/plugins/.
26
+ Plugins are project-scoped and go to {project_path}/.gobby/plugins/.
27
+ Prompts are project-scoped and go to {project_path}/.gobby/prompts/.
27
28
  Docs are project-local and go to {project_path}/.gobby/docs/.
28
29
 
29
30
  Args:
@@ -34,7 +35,7 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
34
35
  Dict with lists of installed items by type
35
36
  """
36
37
  shared_dir = get_install_dir() / "shared"
37
- installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "docs": []}
38
+ installed: dict[str, list[str]] = {"workflows": [], "plugins": [], "prompts": [], "docs": []}
38
39
 
39
40
  # Install shared workflows to .gobby/workflows/ (cross-CLI)
40
41
  shared_workflows = shared_dir / "workflows"
@@ -53,16 +54,33 @@ def install_shared_content(cli_path: Path, project_path: Path) -> dict[str, list
53
54
  copytree(item, target_subdir)
54
55
  installed["workflows"].append(f"{item.name}/")
55
56
 
56
- # Install shared plugins to ~/.gobby/plugins/ (global)
57
+ # Install shared plugins to .gobby/plugins/ (project-scoped)
57
58
  shared_plugins = shared_dir / "plugins"
58
59
  if shared_plugins.exists():
59
- target_plugins = Path("~/.gobby/plugins").expanduser()
60
+ target_plugins = project_path / ".gobby" / "plugins"
60
61
  target_plugins.mkdir(parents=True, exist_ok=True)
61
62
  for plugin_file in shared_plugins.iterdir():
62
63
  if plugin_file.is_file() and plugin_file.suffix == ".py":
63
64
  copy2(plugin_file, target_plugins / plugin_file.name)
64
65
  installed["plugins"].append(plugin_file.name)
65
66
 
67
+ # Install shared prompts to .gobby/prompts/ (project-scoped)
68
+ shared_prompts = shared_dir / "prompts"
69
+ if shared_prompts.exists():
70
+ target_prompts = project_path / ".gobby" / "prompts"
71
+ target_prompts.mkdir(parents=True, exist_ok=True)
72
+ for item in shared_prompts.iterdir():
73
+ if item.is_file():
74
+ copy2(item, target_prompts / item.name)
75
+ installed["prompts"].append(item.name)
76
+ elif item.is_dir():
77
+ # Copy subdirectories (e.g., expansion/, validation/)
78
+ target_subdir = target_prompts / item.name
79
+ if target_subdir.exists():
80
+ shutil.rmtree(target_subdir)
81
+ copytree(item, target_subdir)
82
+ installed["prompts"].append(f"{item.name}/")
83
+
66
84
  # Install shared docs to .gobby/docs/ (project-local)
67
85
  shared_docs = shared_dir / "docs"
68
86
  if shared_docs.exists():
@@ -185,6 +203,87 @@ def install_shared_skills(target_dir: Path) -> list[str]:
185
203
  return installed
186
204
 
187
205
 
206
+ def install_router_skills_as_commands(target_commands_dir: Path) -> list[str]:
207
+ """Install router skills (gobby, g) as flattened Claude commands.
208
+
209
+ Claude Code uses .claude/commands/name.md format for slash commands.
210
+ This function copies the gobby router skills from shared/skills/ to
211
+ commands/ as flattened .md files.
212
+
213
+ Args:
214
+ target_commands_dir: Path to commands directory (e.g., .claude/commands)
215
+
216
+ Returns:
217
+ List of installed command names
218
+ """
219
+ shared_skills_dir = get_install_dir() / "shared" / "skills"
220
+ installed: list[str] = []
221
+
222
+ # Router skills to install as commands
223
+ router_skills = ["gobby", "g"]
224
+
225
+ target_commands_dir.mkdir(parents=True, exist_ok=True)
226
+
227
+ for skill_name in router_skills:
228
+ source_skill_md = shared_skills_dir / skill_name / "SKILL.md"
229
+ if not source_skill_md.exists():
230
+ logger.warning(f"Router skill not found: {source_skill_md}")
231
+ continue
232
+
233
+ # Flatten: copy SKILL.md to commands/name.md
234
+ target_cmd = target_commands_dir / f"{skill_name}.md"
235
+
236
+ try:
237
+ copy2(source_skill_md, target_cmd)
238
+ installed.append(f"{skill_name}.md")
239
+ except OSError as e:
240
+ logger.error(f"Failed to copy router skill {skill_name}: {e}")
241
+
242
+ return installed
243
+
244
+
245
+ def install_router_skills_as_gemini_skills(target_skills_dir: Path) -> list[str]:
246
+ """Install router skills (gobby, g) as Gemini skills (directory structure).
247
+
248
+ Gemini CLI uses .gemini/skills/name/SKILL.md format for skills.
249
+ This function copies the gobby router skills from shared/skills/ to
250
+ the target skills directory preserving the directory structure.
251
+
252
+ Args:
253
+ target_skills_dir: Path to skills directory (e.g., .gemini/skills)
254
+
255
+ Returns:
256
+ List of installed skill names
257
+ """
258
+ shared_skills_dir = get_install_dir() / "shared" / "skills"
259
+ installed: list[str] = []
260
+
261
+ # Router skills to install
262
+ router_skills = ["gobby", "g"]
263
+
264
+ target_skills_dir.mkdir(parents=True, exist_ok=True)
265
+
266
+ for skill_name in router_skills:
267
+ source_skill_dir = shared_skills_dir / skill_name
268
+ source_skill_md = source_skill_dir / "SKILL.md"
269
+ if not source_skill_md.exists():
270
+ logger.warning(f"Router skill not found: {source_skill_md}")
271
+ continue
272
+
273
+ # Create skill directory and copy SKILL.md
274
+ target_skill_dir = target_skills_dir / skill_name
275
+ target_skill_dir.mkdir(parents=True, exist_ok=True)
276
+ target_skill_md = target_skill_dir / "SKILL.md"
277
+
278
+ try:
279
+ copy2(source_skill_md, target_skill_md)
280
+ installed.append(f"{skill_name}/")
281
+ except OSError as e:
282
+ logger.error(f"Failed to copy router skill {skill_name}: {e}")
283
+
284
+ return installed
285
+
286
+
188
287
  def install_cli_content(cli_name: str, target_path: Path) -> dict[str, list[str]]:
189
288
  """Install CLI-specific workflows/commands (layered on top of shared).
190
289
 
gobby/cli/sessions.py CHANGED
@@ -387,7 +387,7 @@ def create_handoff(
387
387
  import time
388
388
  from pathlib import Path
389
389
 
390
- from gobby.mcp_proxy.tools.session_messages import _format_handoff_markdown
390
+ from gobby.mcp_proxy.tools.sessions._handoff import _format_handoff_markdown
391
391
  from gobby.sessions.analyzer import TranscriptAnalyzer
392
392
 
393
393
  manager = get_session_manager()
gobby/cli/utils.py CHANGED
@@ -118,7 +118,7 @@ def get_active_session_id(db: LocalDatabase | None = None) -> str | None:
118
118
  db.close()
119
119
 
120
120
 
121
- def resolve_session_id(session_ref: str | None) -> str:
121
+ def resolve_session_id(session_ref: str | None, project_id: str | None = None) -> str:
122
122
  """
123
123
  Resolve session reference to UUID.
124
124
 
@@ -126,6 +126,8 @@ def resolve_session_id(session_ref: str | None) -> str:
126
126
 
127
127
  Args:
128
128
  session_ref: User input string (UUID, #N, N, prefix) or None
129
+ project_id: Project ID for project-scoped #N lookup.
130
+ If not provided, auto-detected from current project context.
129
131
 
130
132
  Returns:
131
133
  Resolved UUID string
@@ -142,10 +144,15 @@ def resolve_session_id(session_ref: str | None) -> str:
142
144
  raise click.ClickException("No active session found. Specify --session.")
143
145
  return active_id
144
146
 
147
+ # Get project_id from context if not provided
148
+ if not project_id:
149
+ ctx = get_project_context()
150
+ project_id = ctx.get("id") if ctx else None
151
+
145
152
  # Use SessionManager for resolution logic
146
153
  manager = LocalSessionManager(db)
147
154
  try:
148
- return manager.resolve_session_reference(session_ref)
155
+ return manager.resolve_session_reference(session_ref, project_id)
149
156
  except ValueError as e:
150
157
  raise click.ClickException(str(e)) from None
151
158
  finally:
gobby/config/__init__.py CHANGED
@@ -14,116 +14,31 @@ Module structure:
14
14
  - extensions.py: Hook extension configs (webhooks, plugins)
15
15
  - sessions.py: Session lifecycle and tracking configs
16
16
  - features.py: MCP proxy feature configs (code execution, tool recommendation)
17
+
18
+ Import from submodules directly for specific configs:
19
+ from gobby.config.tasks import TaskValidationConfig
20
+ from gobby.config.extensions import WebhooksConfig
21
+
22
+ Import from this package for app-level items:
23
+ from gobby.config import DaemonConfig, load_config
17
24
  """
18
25
 
19
26
  # Core configuration and utilities from app.py
20
27
  from gobby.config.app import (
21
28
  DaemonConfig,
22
29
  expand_env_vars,
30
+ generate_default_config,
23
31
  load_config,
32
+ load_yaml,
24
33
  save_config,
25
34
  )
26
35
 
27
- # Extension configs
28
- from gobby.config.extensions import (
29
- HookExtensionsConfig,
30
- PluginItemConfig,
31
- PluginsConfig,
32
- WebhookEndpointConfig,
33
- WebhooksConfig,
34
- WebSocketBroadcastConfig,
35
- )
36
-
37
- # Feature configs
38
- from gobby.config.features import (
39
- ImportMCPServerConfig,
40
- MetricsConfig,
41
- ProjectVerificationConfig,
42
- RecommendToolsConfig,
43
- ToolSummarizerConfig,
44
- )
45
-
46
- # LLM provider configs
47
- from gobby.config.llm_providers import (
48
- LLMProviderConfig,
49
- LLMProvidersConfig,
50
- )
51
-
52
- # Logging configs
53
- from gobby.config.logging import LoggingSettings
54
-
55
- # Persistence configs
56
- from gobby.config.persistence import (
57
- MemoryConfig,
58
- MemorySyncConfig,
59
- )
60
-
61
- # Server configs
62
- from gobby.config.servers import (
63
- MCPClientProxyConfig,
64
- WebSocketSettings,
65
- )
66
-
67
- # Session configs
68
- from gobby.config.sessions import (
69
- ContextInjectionConfig,
70
- MessageTrackingConfig,
71
- SessionLifecycleConfig,
72
- SessionSummaryConfig,
73
- TitleSynthesisConfig,
74
- )
75
-
76
- # Task configs
77
- from gobby.config.tasks import (
78
- CompactHandoffConfig,
79
- GobbyTasksConfig,
80
- PatternCriteriaConfig,
81
- TaskExpansionConfig,
82
- TaskValidationConfig,
83
- WorkflowConfig,
84
- )
85
-
86
36
  __all__ = [
87
- # Core
37
+ # Core app-level exports only
88
38
  "DaemonConfig",
89
39
  "expand_env_vars",
40
+ "generate_default_config",
90
41
  "load_config",
42
+ "load_yaml",
91
43
  "save_config",
92
- # Extension configs
93
- "HookExtensionsConfig",
94
- "PluginItemConfig",
95
- "PluginsConfig",
96
- "WebhookEndpointConfig",
97
- "WebhooksConfig",
98
- "WebSocketBroadcastConfig",
99
- # Feature configs
100
- "ImportMCPServerConfig",
101
- "MetricsConfig",
102
- "ProjectVerificationConfig",
103
- "RecommendToolsConfig",
104
- "ToolSummarizerConfig",
105
- # LLM provider configs
106
- "LLMProviderConfig",
107
- "LLMProvidersConfig",
108
- # Logging configs
109
- "LoggingSettings",
110
- # Persistence configs
111
- "MemoryConfig",
112
- "MemorySyncConfig",
113
- # Server configs
114
- "MCPClientProxyConfig",
115
- "WebSocketSettings",
116
- # Session configs
117
- "ContextInjectionConfig",
118
- "MessageTrackingConfig",
119
- "SessionLifecycleConfig",
120
- "SessionSummaryConfig",
121
- "TitleSynthesisConfig",
122
- # Task configs
123
- "CompactHandoffConfig",
124
- "GobbyTasksConfig",
125
- "PatternCriteriaConfig",
126
- "TaskExpansionConfig",
127
- "TaskValidationConfig",
128
- "WorkflowConfig",
129
44
  ]