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
gobby/tui/screens/agents.py
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
"""Agents screen with running agents and spawn controls."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
from textual.app import ComposeResult
|
|
9
|
-
from textual.containers import Container, Horizontal, Vertical
|
|
10
|
-
from textual.reactive import reactive
|
|
11
|
-
from textual.widget import Widget
|
|
12
|
-
from textual.widgets import (
|
|
13
|
-
Button,
|
|
14
|
-
DataTable,
|
|
15
|
-
LoadingIndicator,
|
|
16
|
-
Select,
|
|
17
|
-
Static,
|
|
18
|
-
TextArea,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
from gobby.tui.api_client import GobbyAPIClient
|
|
22
|
-
from gobby.tui.ws_client import GobbyWebSocketClient
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class SpawnAgentDialog(Widget):
|
|
26
|
-
"""Dialog for spawning a new agent."""
|
|
27
|
-
|
|
28
|
-
DEFAULT_CSS = """
|
|
29
|
-
SpawnAgentDialog {
|
|
30
|
-
width: 100%;
|
|
31
|
-
height: auto;
|
|
32
|
-
padding: 1;
|
|
33
|
-
border: round #7c3aed;
|
|
34
|
-
background: #1e1e2e;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
SpawnAgentDialog .dialog-title {
|
|
38
|
-
text-style: bold;
|
|
39
|
-
color: #a78bfa;
|
|
40
|
-
padding-bottom: 1;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
SpawnAgentDialog .form-row {
|
|
44
|
-
height: auto;
|
|
45
|
-
margin-bottom: 1;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
SpawnAgentDialog .form-label {
|
|
49
|
-
color: #a6adc8;
|
|
50
|
-
margin-bottom: 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
SpawnAgentDialog #prompt-input {
|
|
54
|
-
height: 5;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
SpawnAgentDialog .button-row {
|
|
58
|
-
layout: horizontal;
|
|
59
|
-
height: 3;
|
|
60
|
-
margin-top: 1;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
SpawnAgentDialog .button-row Button {
|
|
64
|
-
margin-right: 1;
|
|
65
|
-
}
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
def compose(self) -> ComposeResult:
|
|
69
|
-
yield Static("🚀 Spawn New Agent", classes="dialog-title")
|
|
70
|
-
|
|
71
|
-
with Vertical(classes="form-row"):
|
|
72
|
-
yield Static("Prompt:", classes="form-label")
|
|
73
|
-
yield TextArea(id="prompt-input")
|
|
74
|
-
|
|
75
|
-
with Horizontal(classes="form-row"):
|
|
76
|
-
with Vertical():
|
|
77
|
-
yield Static("Mode:", classes="form-label")
|
|
78
|
-
yield Select(
|
|
79
|
-
[
|
|
80
|
-
(label, value)
|
|
81
|
-
for label, value in [
|
|
82
|
-
("Terminal", "terminal"),
|
|
83
|
-
("Embedded", "embedded"),
|
|
84
|
-
("Headless", "headless"),
|
|
85
|
-
]
|
|
86
|
-
],
|
|
87
|
-
value="terminal",
|
|
88
|
-
id="mode-select",
|
|
89
|
-
)
|
|
90
|
-
with Vertical():
|
|
91
|
-
yield Static("Workflow:", classes="form-label")
|
|
92
|
-
yield Select(
|
|
93
|
-
[
|
|
94
|
-
(label, value)
|
|
95
|
-
for label, value in [
|
|
96
|
-
("None", ""),
|
|
97
|
-
("Plan-Execute", "plan-execute"),
|
|
98
|
-
("Test-Driven", "test-driven"),
|
|
99
|
-
("Auto-Task", "auto-task"),
|
|
100
|
-
]
|
|
101
|
-
],
|
|
102
|
-
value="",
|
|
103
|
-
id="workflow-select",
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
with Horizontal(classes="button-row"):
|
|
107
|
-
yield Button("Spawn", variant="primary", id="btn-spawn")
|
|
108
|
-
yield Button("Cancel", id="btn-cancel-spawn")
|
|
109
|
-
|
|
110
|
-
def get_values(self) -> dict[str, Any]:
|
|
111
|
-
"""Get the form values."""
|
|
112
|
-
prompt = self.query_one("#prompt-input", TextArea).text
|
|
113
|
-
mode = str(self.query_one("#mode-select", Select).value)
|
|
114
|
-
workflow = str(self.query_one("#workflow-select", Select).value)
|
|
115
|
-
return {"prompt": prompt, "mode": mode, "workflow": workflow or None}
|
|
116
|
-
|
|
117
|
-
def clear(self) -> None:
|
|
118
|
-
"""Clear the form."""
|
|
119
|
-
self.query_one("#prompt-input", TextArea).clear()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
class AgentsScreen(Widget):
|
|
123
|
-
"""Agents screen showing running agents and spawn controls."""
|
|
124
|
-
|
|
125
|
-
DEFAULT_CSS = """
|
|
126
|
-
AgentsScreen {
|
|
127
|
-
width: 1fr;
|
|
128
|
-
height: 1fr;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
AgentsScreen .screen-header {
|
|
132
|
-
height: auto;
|
|
133
|
-
padding: 1;
|
|
134
|
-
background: #313244;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
AgentsScreen .header-row {
|
|
138
|
-
layout: horizontal;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
AgentsScreen .panel-title {
|
|
142
|
-
text-style: bold;
|
|
143
|
-
color: #a78bfa;
|
|
144
|
-
width: 1fr;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
AgentsScreen #agents-table {
|
|
148
|
-
height: 1fr;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
AgentsScreen #spawn-dialog {
|
|
152
|
-
display: none;
|
|
153
|
-
margin: 1;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
AgentsScreen #spawn-dialog.--visible {
|
|
157
|
-
display: block;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
AgentsScreen .loading-container {
|
|
161
|
-
width: 1fr;
|
|
162
|
-
height: 1fr;
|
|
163
|
-
content-align: center middle;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
AgentsScreen .empty-state {
|
|
167
|
-
content-align: center middle;
|
|
168
|
-
height: 1fr;
|
|
169
|
-
color: #a6adc8;
|
|
170
|
-
}
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
loading = reactive(True)
|
|
174
|
-
agents: reactive[list[dict[str, Any]]] = reactive(list)
|
|
175
|
-
show_spawn_dialog = reactive(False)
|
|
176
|
-
selected_agent_id: reactive[str | None] = reactive(None)
|
|
177
|
-
|
|
178
|
-
def __init__(
|
|
179
|
-
self,
|
|
180
|
-
api_client: GobbyAPIClient,
|
|
181
|
-
ws_client: GobbyWebSocketClient,
|
|
182
|
-
**kwargs: Any,
|
|
183
|
-
) -> None:
|
|
184
|
-
super().__init__(**kwargs)
|
|
185
|
-
self.api_client = api_client
|
|
186
|
-
self.ws_client = ws_client
|
|
187
|
-
|
|
188
|
-
def compose(self) -> ComposeResult:
|
|
189
|
-
with Vertical(classes="screen-header"):
|
|
190
|
-
with Horizontal(classes="header-row"):
|
|
191
|
-
yield Static("🤖 Agents", classes="panel-title")
|
|
192
|
-
yield Button("+ Spawn Agent", variant="primary", id="btn-show-spawn")
|
|
193
|
-
yield Button("Cancel Selected", id="btn-cancel-agent")
|
|
194
|
-
yield Button("Refresh", id="btn-refresh")
|
|
195
|
-
|
|
196
|
-
yield SpawnAgentDialog(id="spawn-dialog")
|
|
197
|
-
|
|
198
|
-
if self.loading:
|
|
199
|
-
with Container(classes="loading-container"):
|
|
200
|
-
yield LoadingIndicator()
|
|
201
|
-
else:
|
|
202
|
-
yield DataTable(id="agents-table")
|
|
203
|
-
|
|
204
|
-
async def on_mount(self) -> None:
|
|
205
|
-
"""Load data when mounted."""
|
|
206
|
-
await self.refresh_data()
|
|
207
|
-
|
|
208
|
-
async def refresh_data(self) -> None:
|
|
209
|
-
"""Refresh agent list."""
|
|
210
|
-
try:
|
|
211
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
212
|
-
agents = await client.list_agents()
|
|
213
|
-
self.agents = agents
|
|
214
|
-
except Exception as e:
|
|
215
|
-
self.notify(f"Failed to load agents: {e}", severity="error")
|
|
216
|
-
finally:
|
|
217
|
-
self.loading = False
|
|
218
|
-
await self._setup_table()
|
|
219
|
-
|
|
220
|
-
async def _setup_table(self) -> None:
|
|
221
|
-
"""Set up and populate the agents table."""
|
|
222
|
-
try:
|
|
223
|
-
table = self.query_one("#agents-table", DataTable)
|
|
224
|
-
table.clear(columns=True)
|
|
225
|
-
table.add_columns("ID", "Status", "Mode", "Prompt", "Duration")
|
|
226
|
-
table.cursor_type = "row"
|
|
227
|
-
|
|
228
|
-
for agent in self.agents:
|
|
229
|
-
run_id = agent.get("run_id", "")[:12]
|
|
230
|
-
status = agent.get("status", "unknown")
|
|
231
|
-
mode = agent.get("mode", "?")
|
|
232
|
-
prompt_val = agent.get("prompt") or ""
|
|
233
|
-
prompt = prompt_val[:40] + "..." if len(prompt_val) > 40 else prompt_val
|
|
234
|
-
|
|
235
|
-
# Calculate duration
|
|
236
|
-
started = agent.get("started_at", "")
|
|
237
|
-
if started and status == "running":
|
|
238
|
-
try:
|
|
239
|
-
started_dt = datetime.fromisoformat(started.replace("Z", "+00:00"))
|
|
240
|
-
duration = datetime.now(started_dt.tzinfo) - started_dt
|
|
241
|
-
duration_str = f"{duration.seconds // 60}m"
|
|
242
|
-
except Exception:
|
|
243
|
-
duration_str = "?"
|
|
244
|
-
else:
|
|
245
|
-
duration_str = "-"
|
|
246
|
-
|
|
247
|
-
table.add_row(run_id, status, mode, prompt, duration_str, key=agent.get("run_id"))
|
|
248
|
-
|
|
249
|
-
except Exception:
|
|
250
|
-
pass # nosec B110 - TUI update failure is non-critical
|
|
251
|
-
|
|
252
|
-
def watch_show_spawn_dialog(self, show: bool) -> None:
|
|
253
|
-
"""Toggle spawn dialog visibility."""
|
|
254
|
-
try:
|
|
255
|
-
dialog = self.query_one("#spawn-dialog", SpawnAgentDialog)
|
|
256
|
-
dialog.set_class(show, "--visible")
|
|
257
|
-
except Exception:
|
|
258
|
-
pass # nosec B110 - widget may not be mounted yet
|
|
259
|
-
|
|
260
|
-
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
261
|
-
"""Handle agent selection."""
|
|
262
|
-
self.selected_agent_id = str(event.row_key.value) if event.row_key else None
|
|
263
|
-
|
|
264
|
-
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
265
|
-
"""Handle button presses."""
|
|
266
|
-
button_id = event.button.id
|
|
267
|
-
|
|
268
|
-
if button_id == "btn-show-spawn":
|
|
269
|
-
self.show_spawn_dialog = True
|
|
270
|
-
|
|
271
|
-
elif button_id == "btn-cancel-spawn":
|
|
272
|
-
self.show_spawn_dialog = False
|
|
273
|
-
try:
|
|
274
|
-
dialog = self.query_one("#spawn-dialog", SpawnAgentDialog)
|
|
275
|
-
dialog.clear()
|
|
276
|
-
except Exception:
|
|
277
|
-
pass # nosec B110 - widget may not be mounted yet
|
|
278
|
-
|
|
279
|
-
elif button_id == "btn-spawn":
|
|
280
|
-
await self._spawn_agent()
|
|
281
|
-
|
|
282
|
-
elif button_id == "btn-cancel-agent":
|
|
283
|
-
await self._cancel_agent()
|
|
284
|
-
|
|
285
|
-
elif button_id == "btn-refresh":
|
|
286
|
-
self.loading = True
|
|
287
|
-
await self.refresh_data()
|
|
288
|
-
|
|
289
|
-
async def _spawn_agent(self) -> None:
|
|
290
|
-
"""Spawn a new agent."""
|
|
291
|
-
try:
|
|
292
|
-
dialog = self.query_one("#spawn-dialog", SpawnAgentDialog)
|
|
293
|
-
values = dialog.get_values()
|
|
294
|
-
|
|
295
|
-
if not values.get("prompt"):
|
|
296
|
-
self.notify("Prompt is required", severity="error")
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
300
|
-
result = await client.start_agent(
|
|
301
|
-
prompt=values["prompt"],
|
|
302
|
-
mode=values["mode"],
|
|
303
|
-
workflow=values.get("workflow"),
|
|
304
|
-
)
|
|
305
|
-
self.notify(f"Agent spawned: {result.get('run_id', 'unknown')[:12]}")
|
|
306
|
-
|
|
307
|
-
self.show_spawn_dialog = False
|
|
308
|
-
dialog.clear()
|
|
309
|
-
await self.refresh_data()
|
|
310
|
-
|
|
311
|
-
except Exception as e:
|
|
312
|
-
self.notify(f"Failed to spawn agent: {e}", severity="error")
|
|
313
|
-
|
|
314
|
-
async def _cancel_agent(self) -> None:
|
|
315
|
-
"""Cancel the selected agent."""
|
|
316
|
-
if not self.selected_agent_id:
|
|
317
|
-
self.notify("No agent selected", severity="warning")
|
|
318
|
-
return
|
|
319
|
-
|
|
320
|
-
try:
|
|
321
|
-
async with GobbyAPIClient(self.api_client.base_url) as client:
|
|
322
|
-
await client.cancel_agent(self.selected_agent_id)
|
|
323
|
-
self.notify(f"Agent cancelled: {self.selected_agent_id[:12]}")
|
|
324
|
-
|
|
325
|
-
await self.refresh_data()
|
|
326
|
-
|
|
327
|
-
except Exception as e:
|
|
328
|
-
self.notify(f"Failed to cancel agent: {e}", severity="error")
|
|
329
|
-
|
|
330
|
-
def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
|
|
331
|
-
"""Handle WebSocket events."""
|
|
332
|
-
if event_type == "agent_event":
|
|
333
|
-
self.run_worker(self.refresh_data(), name="refresh_data", exclusive=True)
|