gobby 0.2.5__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.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/claude_code.py +13 -4
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/runner.py +8 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/__init__.py +6 -0
- gobby/cli/clones.py +419 -0
- gobby/cli/conductor.py +266 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/antigravity.py +3 -9
- gobby/cli/installers/claude.py +15 -9
- gobby/cli/installers/codex.py +2 -8
- gobby/cli/installers/gemini.py +8 -8
- gobby/cli/installers/shared.py +175 -13
- gobby/cli/sessions.py +1 -1
- gobby/cli/skills.py +858 -0
- gobby/cli/tasks/ai.py +0 -440
- gobby/cli/tasks/crud.py +44 -6
- gobby/cli/tasks/main.py +0 -4
- gobby/cli/tui.py +2 -2
- gobby/cli/utils.py +12 -5
- gobby/clones/__init__.py +13 -0
- gobby/clones/git.py +547 -0
- gobby/conductor/__init__.py +16 -0
- gobby/conductor/alerts.py +135 -0
- gobby/conductor/loop.py +164 -0
- gobby/conductor/monitors/__init__.py +11 -0
- gobby/conductor/monitors/agents.py +116 -0
- gobby/conductor/monitors/tasks.py +155 -0
- gobby/conductor/pricing.py +234 -0
- gobby/conductor/token_tracker.py +160 -0
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +69 -91
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/search.py +110 -0
- gobby/config/servers.py +1 -1
- gobby/config/skills.py +43 -0
- gobby/config/tasks.py +9 -41
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +188 -2
- gobby/hooks/hook_manager.py +50 -4
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/skill_manager.py +130 -0
- gobby/hooks/webhooks.py +1 -1
- gobby/install/claude/hooks/hook_dispatcher.py +4 -4
- gobby/install/codex/hooks/hook_dispatcher.py +1 -1
- gobby/install/gemini/hooks/hook_dispatcher.py +87 -12
- gobby/llm/claude.py +22 -34
- gobby/llm/claude_executor.py +46 -256
- gobby/llm/codex_executor.py +59 -291
- gobby/llm/executor.py +21 -0
- gobby/llm/gemini.py +134 -110
- gobby/llm/litellm_executor.py +143 -6
- gobby/llm/resolver.py +98 -35
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +56 -0
- gobby/mcp_proxy/models.py +15 -0
- gobby/mcp_proxy/registries.py +68 -8
- gobby/mcp_proxy/server.py +33 -3
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/services/tool_proxy.py +81 -1
- gobby/mcp_proxy/stdio.py +2 -1
- gobby/mcp_proxy/tools/__init__.py +0 -2
- gobby/mcp_proxy/tools/agent_messaging.py +317 -0
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +518 -0
- gobby/mcp_proxy/tools/memory.py +3 -26
- gobby/mcp_proxy/tools/metrics.py +65 -1
- gobby/mcp_proxy/tools/orchestration/__init__.py +3 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +151 -0
- gobby/mcp_proxy/tools/orchestration/wait.py +467 -0
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +616 -0
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/task_orchestration.py +7 -0
- gobby/mcp_proxy/tools/task_readiness.py +14 -0
- gobby/mcp_proxy/tools/task_sync.py +1 -1
- gobby/mcp_proxy/tools/tasks/_context.py +0 -20
- gobby/mcp_proxy/tools/tasks/_crud.py +91 -4
- gobby/mcp_proxy/tools/tasks/_expansion.py +348 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +6 -16
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +110 -45
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +18 -29
- gobby/mcp_proxy/tools/workflows.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -338
- gobby/memory/backends/__init__.py +6 -1
- gobby/memory/backends/mem0.py +6 -1
- gobby/memory/extractor.py +477 -0
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +73 -285
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/runner.py +37 -16
- gobby/search/__init__.py +48 -6
- gobby/search/backends/__init__.py +159 -0
- gobby/search/backends/embedding.py +225 -0
- gobby/search/embeddings.py +238 -0
- gobby/search/models.py +148 -0
- gobby/search/unified.py +496 -0
- gobby/servers/http.py +24 -12
- gobby/servers/routes/admin.py +294 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1317
- gobby/servers/websocket.py +2 -2
- gobby/sessions/analyzer.py +2 -0
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +2 -0
- gobby/sessions/transcripts/claude.py +79 -10
- gobby/skills/__init__.py +91 -0
- gobby/skills/loader.py +685 -0
- gobby/skills/manager.py +384 -0
- gobby/skills/parser.py +286 -0
- gobby/skills/search.py +463 -0
- gobby/skills/sync.py +119 -0
- gobby/skills/updater.py +385 -0
- gobby/skills/validator.py +368 -0
- gobby/storage/clones.py +378 -0
- gobby/storage/database.py +1 -1
- gobby/storage/memories.py +43 -13
- gobby/storage/migrations.py +162 -201
- gobby/storage/sessions.py +116 -7
- gobby/storage/skills.py +782 -0
- gobby/storage/tasks/_crud.py +4 -4
- gobby/storage/tasks/_lifecycle.py +57 -7
- gobby/storage/tasks/_manager.py +14 -5
- gobby/storage/tasks/_models.py +8 -3
- gobby/sync/memories.py +40 -5
- gobby/sync/tasks.py +83 -6
- gobby/tasks/__init__.py +1 -2
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +46 -35
- gobby/tools/summarizer.py +91 -10
- gobby/tui/api_client.py +4 -7
- gobby/tui/app.py +5 -3
- gobby/tui/screens/orchestrator.py +1 -2
- gobby/tui/screens/tasks.py +2 -4
- gobby/tui/ws_client.py +1 -1
- gobby/utils/daemon_client.py +2 -2
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1135
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +93 -1
- gobby/workflows/detection_helpers.py +115 -31
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/{task_enforcement_actions.py → enforcement/task_policy.py} +29 -388
- gobby/workflows/engine.py +13 -2
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/lifecycle_evaluator.py +29 -1
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/loader.py +19 -6
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +154 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +111 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/METADATA +87 -21
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/RECORD +201 -172
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1292
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/install/codex/prompts/forget.md +0 -7
- gobby/install/codex/prompts/memories.md +0 -7
- gobby/install/codex/prompts/recall.md +0 -7
- gobby/install/codex/prompts/remember.md +0 -13
- gobby/llm/gemini_executor.py +0 -339
- gobby/mcp_proxy/tools/session_messages.py +0 -1056
- gobby/mcp_proxy/tools/task_expansion.py +0 -591
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/tasks/context.py +0 -747
- gobby/tasks/criteria.py +0 -342
- gobby/tasks/expansion.py +0 -626
- gobby/tasks/prompts/expand.py +0 -327
- gobby/tasks/research.py +0 -421
- gobby/tasks/tdd.py +0 -352
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.5.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Commits and workflow tools for session management.
|
|
2
|
+
|
|
3
|
+
This module contains MCP tools for:
|
|
4
|
+
- Getting session commits (get_session_commits)
|
|
5
|
+
- Marking autonomous loop complete (mark_loop_complete)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import UTC
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
15
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_commits_tools(
|
|
19
|
+
registry: InternalToolRegistry,
|
|
20
|
+
session_manager: LocalSessionManager,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Register commits and workflow tools with a registry.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
registry: The InternalToolRegistry to register tools with
|
|
27
|
+
session_manager: LocalSessionManager instance for session operations
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@registry.tool(
|
|
31
|
+
name="get_session_commits",
|
|
32
|
+
description="Get git commits made during a session timeframe.",
|
|
33
|
+
)
|
|
34
|
+
def get_session_commits(
|
|
35
|
+
session_id: str,
|
|
36
|
+
max_commits: int = 20,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Get git commits made during a session's active timeframe.
|
|
40
|
+
|
|
41
|
+
Uses session.created_at and session.updated_at to filter
|
|
42
|
+
git log within that timeframe.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
session_id: Session ID
|
|
46
|
+
max_commits: Maximum commits to return (default 20)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Session ID, list of commits, and count
|
|
50
|
+
"""
|
|
51
|
+
import subprocess # nosec B404 - subprocess needed for git commands
|
|
52
|
+
from datetime import datetime
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
if session_manager is None:
|
|
56
|
+
return {"error": "Session manager not available"}
|
|
57
|
+
|
|
58
|
+
# Get session
|
|
59
|
+
session = session_manager.get(session_id)
|
|
60
|
+
if not session:
|
|
61
|
+
# Try prefix match
|
|
62
|
+
sessions = session_manager.list(limit=100)
|
|
63
|
+
matches = [s for s in sessions if s.id.startswith(session_id)]
|
|
64
|
+
if len(matches) == 1:
|
|
65
|
+
session = matches[0]
|
|
66
|
+
elif len(matches) > 1:
|
|
67
|
+
return {
|
|
68
|
+
"error": f"Ambiguous session ID prefix '{session_id}'",
|
|
69
|
+
"matches": [s.id for s in matches[:5]],
|
|
70
|
+
}
|
|
71
|
+
else:
|
|
72
|
+
return {"error": f"Session {session_id} not found"}
|
|
73
|
+
|
|
74
|
+
# Get working directory from transcript path or project
|
|
75
|
+
cwd = None
|
|
76
|
+
if session.jsonl_path:
|
|
77
|
+
cwd = str(Path(session.jsonl_path).parent)
|
|
78
|
+
|
|
79
|
+
# Format timestamps for git --since/--until
|
|
80
|
+
# Git expects ISO format or relative dates
|
|
81
|
+
# Session timestamps may be ISO strings or datetime objects
|
|
82
|
+
if isinstance(session.created_at, str):
|
|
83
|
+
since_time = datetime.fromisoformat(session.created_at.replace("Z", "+00:00"))
|
|
84
|
+
else:
|
|
85
|
+
since_time = session.created_at
|
|
86
|
+
|
|
87
|
+
if session.updated_at:
|
|
88
|
+
if isinstance(session.updated_at, str):
|
|
89
|
+
until_time = datetime.fromisoformat(session.updated_at.replace("Z", "+00:00"))
|
|
90
|
+
else:
|
|
91
|
+
until_time = session.updated_at
|
|
92
|
+
else:
|
|
93
|
+
until_time = datetime.now(UTC)
|
|
94
|
+
|
|
95
|
+
# Format as ISO 8601 for git
|
|
96
|
+
since_str = since_time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
97
|
+
until_str = until_time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Get commits within timeframe
|
|
101
|
+
cmd = [
|
|
102
|
+
"git",
|
|
103
|
+
"log",
|
|
104
|
+
f"--since={since_str}",
|
|
105
|
+
f"--until={until_str}",
|
|
106
|
+
f"-{max_commits}",
|
|
107
|
+
"--format=%H|%s|%aI", # hash|subject|author-date-iso
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
result = subprocess.run( # nosec B603 - cmd built from hardcoded git arguments
|
|
111
|
+
cmd,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=10,
|
|
115
|
+
cwd=cwd,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if result.returncode != 0:
|
|
119
|
+
return {
|
|
120
|
+
"session_id": session.id,
|
|
121
|
+
"error": "Git command failed",
|
|
122
|
+
"stderr": result.stderr.strip(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
commits = []
|
|
126
|
+
for line in result.stdout.strip().split("\n"):
|
|
127
|
+
if "|" in line:
|
|
128
|
+
parts = line.split("|", 2)
|
|
129
|
+
if len(parts) >= 2:
|
|
130
|
+
commit = {
|
|
131
|
+
"hash": parts[0],
|
|
132
|
+
"message": parts[1],
|
|
133
|
+
}
|
|
134
|
+
if len(parts) >= 3:
|
|
135
|
+
commit["timestamp"] = parts[2]
|
|
136
|
+
commits.append(commit)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"session_id": session.id,
|
|
140
|
+
"commits": commits,
|
|
141
|
+
"count": len(commits),
|
|
142
|
+
"timeframe": {
|
|
143
|
+
"since": since_str,
|
|
144
|
+
"until": until_str,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
return {
|
|
150
|
+
"session_id": session.id,
|
|
151
|
+
"error": "Git command timed out",
|
|
152
|
+
}
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
return {
|
|
155
|
+
"session_id": session.id,
|
|
156
|
+
"error": "Git not found or not a git repository",
|
|
157
|
+
}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {
|
|
160
|
+
"session_id": session.id,
|
|
161
|
+
"error": f"Failed to get commits: {e!s}",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@registry.tool(
|
|
165
|
+
name="mark_loop_complete",
|
|
166
|
+
description="""Mark the autonomous loop as complete, preventing session chaining.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
session_id: (REQUIRED) Your session ID. Get it from:
|
|
170
|
+
1. Your injected context (look for 'session_id: xxx')
|
|
171
|
+
2. Or call get_current(external_id, source) first""",
|
|
172
|
+
)
|
|
173
|
+
def mark_loop_complete(session_id: str) -> dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Mark the autonomous loop as complete for a session.
|
|
176
|
+
|
|
177
|
+
This sets stop_reason='completed' in the workflow state, which
|
|
178
|
+
signals the auto-loop workflow to NOT chain a new session
|
|
179
|
+
when this session ends.
|
|
180
|
+
|
|
181
|
+
Use this when:
|
|
182
|
+
- A task is fully complete and no more work is needed
|
|
183
|
+
- You want to exit the autonomous loop gracefully
|
|
184
|
+
- The user has explicitly asked to stop
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
session_id: Session ID (REQUIRED)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Success status and session details
|
|
191
|
+
"""
|
|
192
|
+
if not session_manager:
|
|
193
|
+
raise RuntimeError("Session manager not available")
|
|
194
|
+
|
|
195
|
+
# Find session - session_id is now required
|
|
196
|
+
session = session_manager.get(session_id)
|
|
197
|
+
|
|
198
|
+
if not session:
|
|
199
|
+
return {"error": f"Session {session_id} not found", "session_id": session_id}
|
|
200
|
+
|
|
201
|
+
# Load and update workflow state
|
|
202
|
+
from gobby.storage.database import LocalDatabase
|
|
203
|
+
from gobby.workflows.definitions import WorkflowState
|
|
204
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
205
|
+
|
|
206
|
+
db = LocalDatabase()
|
|
207
|
+
state_manager = WorkflowStateManager(db)
|
|
208
|
+
|
|
209
|
+
# Get or create state for session
|
|
210
|
+
state = state_manager.get_state(session.id)
|
|
211
|
+
if not state:
|
|
212
|
+
# Create minimal state just to hold the variable
|
|
213
|
+
state = WorkflowState(
|
|
214
|
+
session_id=session.id,
|
|
215
|
+
workflow_name="auto-loop",
|
|
216
|
+
step="active",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Mark loop complete using the action function
|
|
220
|
+
from gobby.workflows.state_actions import mark_loop_complete as action_mark_complete
|
|
221
|
+
|
|
222
|
+
action_mark_complete(state)
|
|
223
|
+
|
|
224
|
+
# Save updated state
|
|
225
|
+
state_manager.save_state(state)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"success": True,
|
|
229
|
+
"session_id": session.id,
|
|
230
|
+
"stop_reason": "completed",
|
|
231
|
+
"message": "Autonomous loop marked complete - session will not chain",
|
|
232
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Session CRUD tools for session management.
|
|
2
|
+
|
|
3
|
+
This module contains MCP tools for:
|
|
4
|
+
- Getting session details (get_session)
|
|
5
|
+
- Getting current session (get_current)
|
|
6
|
+
- Listing sessions (list_sessions)
|
|
7
|
+
- Session statistics (session_stats)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
16
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_crud_tools(
|
|
20
|
+
registry: InternalToolRegistry,
|
|
21
|
+
session_manager: LocalSessionManager,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register session CRUD tools with a registry.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
registry: The InternalToolRegistry to register tools with
|
|
28
|
+
session_manager: LocalSessionManager instance for session operations
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@registry.tool(
|
|
32
|
+
name="get_session",
|
|
33
|
+
description="Get session details by ID. Accepts #N (project-scoped ref), UUID, or prefix. Use the session_id from your injected context.",
|
|
34
|
+
)
|
|
35
|
+
def get_session(session_id: str) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Get session details by session reference.
|
|
38
|
+
|
|
39
|
+
Your session_id is injected into your context at session start.
|
|
40
|
+
Look for 'Session Ref: #N' or 'session_id: xxx' in your system reminders.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
session_id: Session reference - supports #N (project-scoped), UUID, or prefix
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Session dict with all fields, or error if not found
|
|
47
|
+
"""
|
|
48
|
+
from gobby.utils.project_context import get_project_context
|
|
49
|
+
|
|
50
|
+
# Support #N format, UUID, and prefix matching
|
|
51
|
+
if session_manager is None:
|
|
52
|
+
return {"error": "Session manager not available"}
|
|
53
|
+
|
|
54
|
+
# Get project_id for project-scoped resolution
|
|
55
|
+
project_ctx = get_project_context()
|
|
56
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
57
|
+
|
|
58
|
+
# Try to resolve session reference (#N, UUID, or prefix)
|
|
59
|
+
try:
|
|
60
|
+
resolved_id = session_manager.resolve_session_reference(session_id, project_id)
|
|
61
|
+
session = session_manager.get(resolved_id)
|
|
62
|
+
except ValueError:
|
|
63
|
+
session = None
|
|
64
|
+
|
|
65
|
+
if not session:
|
|
66
|
+
return {"error": f"Session {session_id} not found", "found": False}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"found": True,
|
|
70
|
+
**session.to_dict(),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@registry.tool(
|
|
74
|
+
name="get_current",
|
|
75
|
+
description="""Get YOUR current session ID - the CORRECT way to look up your session.
|
|
76
|
+
|
|
77
|
+
Use this when session_id wasn't in your injected context. Pass your external_id
|
|
78
|
+
(from transcript path or GOBBY_SESSION_ID env) and source (claude, gemini, codex).
|
|
79
|
+
|
|
80
|
+
DO NOT use list_sessions to find your session - it won't work with multiple active sessions.""",
|
|
81
|
+
)
|
|
82
|
+
def get_current(
|
|
83
|
+
external_id: str,
|
|
84
|
+
source: str,
|
|
85
|
+
) -> dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Look up your internal session_id from external_id and source.
|
|
88
|
+
|
|
89
|
+
The agent passes external_id (from injected context or GOBBY_SESSION_ID env var)
|
|
90
|
+
and source (claude, gemini, codex). project_id and machine_id are
|
|
91
|
+
auto-resolved from config files.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
external_id: Your CLI's session ID (from context or GOBBY_SESSION_ID env)
|
|
95
|
+
source: CLI source - "claude", "gemini", or "codex"
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
session_id: Internal Gobby session ID (use for parent_session_id, etc.)
|
|
99
|
+
Plus basic session metadata
|
|
100
|
+
"""
|
|
101
|
+
from gobby.utils.machine_id import get_machine_id
|
|
102
|
+
from gobby.utils.project_context import get_project_context
|
|
103
|
+
|
|
104
|
+
if session_manager is None:
|
|
105
|
+
return {"error": "Session manager not available"}
|
|
106
|
+
|
|
107
|
+
# Auto-resolve context
|
|
108
|
+
machine_id = get_machine_id()
|
|
109
|
+
project_ctx = get_project_context()
|
|
110
|
+
project_id = project_ctx.get("id") if project_ctx else None
|
|
111
|
+
|
|
112
|
+
if not machine_id:
|
|
113
|
+
return {"error": "Could not determine machine_id"}
|
|
114
|
+
if not project_id:
|
|
115
|
+
return {"error": "Could not determine project_id (not in a gobby project?)"}
|
|
116
|
+
|
|
117
|
+
# Use find_by_external_id with full composite key (safe lookup)
|
|
118
|
+
session = session_manager.find_by_external_id(
|
|
119
|
+
external_id=external_id,
|
|
120
|
+
machine_id=machine_id,
|
|
121
|
+
project_id=project_id,
|
|
122
|
+
source=source,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not session:
|
|
126
|
+
return {
|
|
127
|
+
"found": False,
|
|
128
|
+
"error": "Session not found",
|
|
129
|
+
"lookup": {
|
|
130
|
+
"external_id": external_id,
|
|
131
|
+
"source": source,
|
|
132
|
+
"project_id": project_id,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"found": True,
|
|
138
|
+
"session_id": session.id,
|
|
139
|
+
"project_id": session.project_id,
|
|
140
|
+
"status": session.status,
|
|
141
|
+
"agent_run_id": session.agent_run_id,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@registry.tool(
|
|
145
|
+
name="list_sessions",
|
|
146
|
+
description="""List sessions with optional filtering.
|
|
147
|
+
|
|
148
|
+
WARNING: Do NOT use this to find your own session_id!
|
|
149
|
+
- `list_sessions(status="active", limit=1)` will NOT reliably return YOUR session
|
|
150
|
+
- Multiple sessions can be active simultaneously (parallel agents, multiple terminals)
|
|
151
|
+
- Use `get_current(external_id, source)` instead - it uses your unique session key
|
|
152
|
+
|
|
153
|
+
This tool is for browsing/listing sessions, not for self-identification.""",
|
|
154
|
+
)
|
|
155
|
+
def list_sessions(
|
|
156
|
+
project_id: str | None = None,
|
|
157
|
+
status: str | None = None,
|
|
158
|
+
source: str | None = None,
|
|
159
|
+
limit: int = 20,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
List sessions with filters.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
project_id: Filter by project ID
|
|
166
|
+
status: Filter by status (active, paused, expired, archived, handoff_ready)
|
|
167
|
+
source: Filter by CLI source (claude, gemini, codex)
|
|
168
|
+
limit: Max results (default 20)
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
List of sessions and count
|
|
172
|
+
"""
|
|
173
|
+
if session_manager is None:
|
|
174
|
+
return {"error": "Session manager not available"}
|
|
175
|
+
|
|
176
|
+
sessions = session_manager.list(
|
|
177
|
+
project_id=project_id,
|
|
178
|
+
status=status,
|
|
179
|
+
source=source,
|
|
180
|
+
limit=limit,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
total = session_manager.count(
|
|
184
|
+
project_id=project_id,
|
|
185
|
+
status=status,
|
|
186
|
+
source=source,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Detect likely misuse pattern: trying to find own session
|
|
190
|
+
if status == "active" and limit == 1:
|
|
191
|
+
return {
|
|
192
|
+
"warning": (
|
|
193
|
+
"list_sessions(status='active', limit=1) will NOT reliably get YOUR session_id! "
|
|
194
|
+
"Multiple sessions can be active simultaneously. "
|
|
195
|
+
"Use get_current(external_id='<your-external-id>', source='claude') instead."
|
|
196
|
+
),
|
|
197
|
+
"hint": "Your external_id is in your transcript path: /path/to/<external_id>.jsonl",
|
|
198
|
+
"sessions": [s.to_dict() for s in sessions],
|
|
199
|
+
"count": len(sessions),
|
|
200
|
+
"total": total,
|
|
201
|
+
"limit": limit,
|
|
202
|
+
"filters": {
|
|
203
|
+
"project_id": project_id,
|
|
204
|
+
"status": status,
|
|
205
|
+
"source": source,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"sessions": [s.to_dict() for s in sessions],
|
|
211
|
+
"count": len(sessions),
|
|
212
|
+
"total": total,
|
|
213
|
+
"limit": limit,
|
|
214
|
+
"filters": {
|
|
215
|
+
"project_id": project_id,
|
|
216
|
+
"status": status,
|
|
217
|
+
"source": source,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@registry.tool(
|
|
222
|
+
name="session_stats",
|
|
223
|
+
description="Get session statistics for a project.",
|
|
224
|
+
)
|
|
225
|
+
def session_stats(project_id: str | None = None) -> dict[str, Any]:
|
|
226
|
+
"""
|
|
227
|
+
Get session statistics.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
project_id: Filter by project ID (optional)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Statistics including total, by_status, by_source
|
|
234
|
+
"""
|
|
235
|
+
if session_manager is None:
|
|
236
|
+
return {"error": "Session manager not available"}
|
|
237
|
+
|
|
238
|
+
total = session_manager.count(project_id=project_id)
|
|
239
|
+
by_status = session_manager.count_by_status()
|
|
240
|
+
|
|
241
|
+
# Count by source
|
|
242
|
+
by_source: dict[str, int] = {}
|
|
243
|
+
for src in ["claude_code", "gemini", "codex"]:
|
|
244
|
+
count = session_manager.count(project_id=project_id, source=src)
|
|
245
|
+
if count > 0:
|
|
246
|
+
by_source[src] = count
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"total": total,
|
|
250
|
+
"by_status": by_status,
|
|
251
|
+
"by_source": by_source,
|
|
252
|
+
"project_id": project_id,
|
|
253
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Factory function for creating the session messages tool registry.
|
|
2
|
+
|
|
3
|
+
Orchestrates the creation of all session tool sub-registries and merges them
|
|
4
|
+
into a unified registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
12
|
+
from gobby.mcp_proxy.tools.sessions._commits import register_commits_tools
|
|
13
|
+
from gobby.mcp_proxy.tools.sessions._crud import register_crud_tools
|
|
14
|
+
from gobby.mcp_proxy.tools.sessions._handoff import register_handoff_tools
|
|
15
|
+
from gobby.mcp_proxy.tools.sessions._messages import register_message_tools
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from gobby.storage.session_messages import LocalSessionMessageManager
|
|
19
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
20
|
+
|
|
21
|
+
__all__ = ["create_session_messages_registry"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_session_messages_registry(
|
|
25
|
+
message_manager: LocalSessionMessageManager | None = None,
|
|
26
|
+
session_manager: LocalSessionManager | None = None,
|
|
27
|
+
) -> InternalToolRegistry:
|
|
28
|
+
"""
|
|
29
|
+
Create a sessions tool registry with session and message tools.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message_manager: LocalSessionMessageManager instance for message operations
|
|
33
|
+
session_manager: LocalSessionManager instance for session CRUD
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
InternalToolRegistry with all session tools registered
|
|
37
|
+
"""
|
|
38
|
+
registry = InternalToolRegistry(
|
|
39
|
+
name="gobby-sessions",
|
|
40
|
+
description="Session management and message querying - CRUD, retrieval, search",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# --- Message Tools ---
|
|
44
|
+
# Only register if message_manager is available
|
|
45
|
+
if message_manager is not None:
|
|
46
|
+
register_message_tools(registry, message_manager)
|
|
47
|
+
|
|
48
|
+
# --- Handoff Tools ---
|
|
49
|
+
# Only register if session_manager is available
|
|
50
|
+
if session_manager is not None:
|
|
51
|
+
register_handoff_tools(registry, session_manager)
|
|
52
|
+
|
|
53
|
+
# --- Session CRUD Tools ---
|
|
54
|
+
# Only register if session_manager is available
|
|
55
|
+
if session_manager is not None:
|
|
56
|
+
register_crud_tools(registry, session_manager)
|
|
57
|
+
|
|
58
|
+
# --- Commits Tools ---
|
|
59
|
+
# Only register if session_manager is available
|
|
60
|
+
if session_manager is not None:
|
|
61
|
+
register_commits_tools(registry, session_manager)
|
|
62
|
+
|
|
63
|
+
return registry
|