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
@@ -1,440 +0,0 @@
1
- """Tasks screen with tree view and detail panel."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
- from typing import Any
7
-
8
- from textual.app import ComposeResult
9
- from textual.containers import Container, Horizontal, Vertical
10
- from textual.message import Message
11
- from textual.reactive import reactive
12
- from textual.widget import Widget
13
- from textual.widgets import (
14
- Button,
15
- LoadingIndicator,
16
- Select,
17
- Static,
18
- Tree,
19
- )
20
- from textual.widgets.tree import TreeNode
21
-
22
- from gobby.tui.api_client import GobbyAPIClient
23
- from gobby.tui.ws_client import GobbyWebSocketClient
24
-
25
-
26
- class TaskTreePanel(Widget):
27
- """Panel displaying task tree with filtering."""
28
-
29
- DEFAULT_CSS = """
30
- TaskTreePanel {
31
- width: 1fr;
32
- height: 1fr;
33
- border-right: solid #45475a;
34
- }
35
-
36
- TaskTreePanel .panel-header {
37
- height: auto;
38
- padding: 1;
39
- background: #313244;
40
- }
41
-
42
- TaskTreePanel .filter-row {
43
- layout: horizontal;
44
- height: 3;
45
- }
46
-
47
- TaskTreePanel .filter-select {
48
- width: 1fr;
49
- margin-right: 1;
50
- }
51
-
52
- TaskTreePanel #task-tree {
53
- height: 1fr;
54
- }
55
- """
56
-
57
- def compose(self) -> ComposeResult:
58
- with Vertical(classes="panel-header"):
59
- yield Static("📋 Tasks", classes="panel-title")
60
- with Horizontal(classes="filter-row"):
61
- yield Select(
62
- [
63
- ("All Status", "all"),
64
- ("Open", "open"),
65
- ("In Progress", "in_progress"),
66
- ("Review", "review"),
67
- ("Closed", "closed"),
68
- ],
69
- value="all",
70
- id="status-filter",
71
- classes="filter-select",
72
- )
73
- yield Select(
74
- [
75
- ("All Types", "all"),
76
- ("Task", "task"),
77
- ("Bug", "bug"),
78
- ("Feature", "feature"),
79
- ("Epic", "epic"),
80
- ],
81
- value="all",
82
- id="type-filter",
83
- classes="filter-select",
84
- )
85
- yield Tree("Tasks", id="task-tree")
86
-
87
- def on_select_changed(self, event: Select.Changed) -> None:
88
- """Handle filter changes."""
89
- status_val = self.query_one("#status-filter", Select).value
90
- type_val = self.query_one("#type-filter", Select).value
91
- self.post_message(
92
- TasksScreen.FilterChanged(
93
- status=str(status_val) if status_val is not Select.BLANK else "all",
94
- task_type=str(type_val) if type_val is not Select.BLANK else "all",
95
- )
96
- )
97
-
98
-
99
- class TaskDetailPanel(Widget):
100
- """Panel displaying task details."""
101
-
102
- DEFAULT_CSS = """
103
- TaskDetailPanel {
104
- width: 1fr;
105
- height: 1fr;
106
- padding: 1;
107
- }
108
-
109
- TaskDetailPanel .detail-header {
110
- height: auto;
111
- padding-bottom: 1;
112
- }
113
-
114
- TaskDetailPanel .detail-title {
115
- text-style: bold;
116
- color: #a78bfa;
117
- }
118
-
119
- TaskDetailPanel .detail-ref {
120
- color: #06b6d4;
121
- }
122
-
123
- TaskDetailPanel .detail-section {
124
- padding: 1 0;
125
- }
126
-
127
- TaskDetailPanel .detail-label {
128
- color: #a6adc8;
129
- width: 12;
130
- }
131
-
132
- TaskDetailPanel .detail-value {
133
- width: 1fr;
134
- }
135
-
136
- TaskDetailPanel .detail-description {
137
- padding: 1;
138
- border: round #45475a;
139
- height: auto;
140
- max-height: 10;
141
- }
142
-
143
- TaskDetailPanel .action-buttons {
144
- layout: horizontal;
145
- height: 3;
146
- padding-top: 1;
147
- }
148
-
149
- TaskDetailPanel .action-buttons Button {
150
- margin-right: 1;
151
- }
152
-
153
- TaskDetailPanel .empty-state {
154
- content-align: center middle;
155
- height: 1fr;
156
- color: #a6adc8;
157
- }
158
- """
159
-
160
- task_data: reactive[dict[str, Any] | None] = reactive(None)
161
-
162
- def compose(self) -> ComposeResult:
163
- if self.task_data is None:
164
- yield Static("Select a task to view details", classes="empty-state")
165
- else:
166
- with Vertical(classes="detail-header"):
167
- yield Static(self.task_data.get("title", "Untitled"), classes="detail-title")
168
- yield Static(self.task_data.get("ref", ""), classes="detail-ref")
169
-
170
- with Vertical(classes="detail-section"):
171
- with Horizontal():
172
- yield Static("Status:", classes="detail-label")
173
- yield Static(
174
- self.task_data.get("status", "unknown"),
175
- classes="detail-value",
176
- id="detail-status",
177
- )
178
- with Horizontal():
179
- yield Static("Type:", classes="detail-label")
180
- yield Static(self.task_data.get("task_type", "task"), classes="detail-value")
181
- with Horizontal():
182
- yield Static("Priority:", classes="detail-label")
183
- yield Static(str(self.task_data.get("priority", 3)), classes="detail-value")
184
- with Horizontal():
185
- yield Static("Assignee:", classes="detail-label")
186
- yield Static(
187
- self.task_data.get("assignee", "Unassigned"), classes="detail-value"
188
- )
189
-
190
- if self.task_data.get("description"):
191
- yield Static("Description:", classes="detail-label")
192
- yield Static(self.task_data.get("description", ""), classes="detail-description")
193
-
194
- with Horizontal(classes="action-buttons"):
195
- status = self.task_data.get("status", "")
196
- if status == "open":
197
- yield Button("Start", variant="primary", id="btn-start")
198
- yield Button("Expand", id="btn-expand")
199
- elif status == "in_progress":
200
- yield Button("Complete", variant="success", id="btn-complete")
201
- elif status == "review":
202
- yield Button("Approve", variant="success", id="btn-approve")
203
- yield Button("Reopen", variant="error", id="btn-reopen")
204
-
205
- def watch_task_data(self, task_data: dict[str, Any] | None) -> None:
206
- """Recompose when task_data changes."""
207
- self.call_after_refresh(self.recompose)
208
-
209
- def update_task(self, task: dict[str, Any] | None) -> None:
210
- """Update the displayed task."""
211
- self.task_data = task
212
-
213
-
214
- class TasksScreen(Widget):
215
- """Tasks screen with tree view and detail panel."""
216
-
217
- DEFAULT_CSS = """
218
- TasksScreen {
219
- width: 1fr;
220
- height: 1fr;
221
- }
222
-
223
- TasksScreen #tasks-container {
224
- layout: horizontal;
225
- height: 1fr;
226
- }
227
-
228
- TasksScreen #tree-panel {
229
- width: 50%;
230
- }
231
-
232
- TasksScreen #detail-panel {
233
- width: 50%;
234
- }
235
-
236
- TasksScreen .loading-container {
237
- width: 1fr;
238
- height: 1fr;
239
- content-align: center middle;
240
- }
241
- """
242
-
243
- @dataclass
244
- class FilterChanged(Message):
245
- """Message sent when filters change."""
246
-
247
- status: str
248
- task_type: str
249
-
250
- @dataclass
251
- class TaskSelected(Message):
252
- """Message sent when a task is selected."""
253
-
254
- task_id: str
255
-
256
- loading = reactive(True)
257
- tasks: reactive[list[dict[str, Any]]] = reactive(list)
258
- selected_task_id: reactive[str | None] = reactive(None)
259
- current_filter_status = "all"
260
- current_filter_type = "all"
261
-
262
- def __init__(
263
- self,
264
- api_client: GobbyAPIClient,
265
- ws_client: GobbyWebSocketClient,
266
- **kwargs: Any,
267
- ) -> None:
268
- super().__init__(**kwargs)
269
- self.api_client = api_client
270
- self.ws_client = ws_client
271
- self._task_map: dict[str, dict[str, Any]] = {}
272
-
273
- def compose(self) -> ComposeResult:
274
- if self.loading:
275
- with Container(classes="loading-container"):
276
- yield LoadingIndicator()
277
- else:
278
- with Horizontal(id="tasks-container"):
279
- yield TaskTreePanel(id="tree-panel")
280
- yield TaskDetailPanel(id="detail-panel")
281
-
282
- async def on_mount(self) -> None:
283
- """Load data when mounted."""
284
- await self.refresh_data()
285
-
286
- async def refresh_data(self) -> None:
287
- """Refresh task list."""
288
- self.loading = True
289
- await self.recompose()
290
-
291
- try:
292
- async with GobbyAPIClient(self.api_client.base_url) as client:
293
- # Build filter args
294
- args: dict[str, Any] = {}
295
- if self.current_filter_status != "all":
296
- args["status"] = self.current_filter_status
297
- if self.current_filter_type != "all":
298
- args["task_type"] = self.current_filter_type
299
-
300
- tasks = await client.list_tasks(**args)
301
- self.task_datas = tasks
302
- self._task_map = {t.get("id", ""): t for t in tasks}
303
-
304
- except Exception as e:
305
- self.notify(f"Failed to load tasks: {e}", severity="error")
306
- finally:
307
- self.loading = False
308
- await self.recompose()
309
- self._populate_tree()
310
-
311
- def _populate_tree(self) -> None:
312
- """Populate the task tree with loaded tasks."""
313
- try:
314
- tree = self.query_one("#task-tree", Tree)
315
- tree.clear()
316
-
317
- # Build parent -> children mapping
318
- children_map: dict[str | None, list[dict[str, Any]]] = {}
319
- for task in self.task_datas:
320
- parent_id = task.get("parent_id")
321
- if parent_id not in children_map:
322
- children_map[parent_id] = []
323
- children_map[parent_id].append(task)
324
-
325
- # Add root level tasks
326
- root_tasks = children_map.get(None, [])
327
- for task in sorted(root_tasks, key=lambda t: t.get("priority", 3)):
328
- self._add_task_to_tree(tree.root, task, children_map)
329
-
330
- tree.root.expand()
331
-
332
- except Exception:
333
- pass # nosec B110 - TUI update failure is non-critical
334
-
335
- def _add_task_to_tree(
336
- self,
337
- parent: TreeNode[str],
338
- task: dict[str, Any],
339
- children_map: dict[str | None, list[dict[str, Any]]],
340
- ) -> None:
341
- """Recursively add a task and its children to the tree."""
342
- status = task.get("status", "open")
343
- status_icon = {
344
- "open": "○",
345
- "in_progress": "◐",
346
- "review": "◑",
347
- "closed": "●",
348
- }.get(status, "○")
349
-
350
- ref = task.get("ref", "")
351
- title = task.get("title", "Untitled")
352
- label = f"{status_icon} {ref} {title}"
353
-
354
- node = parent.add(label, data=task.get("id"))
355
-
356
- # Add children
357
- task_id = task.get("id")
358
- children = children_map.get(task_id, [])
359
- for child in sorted(children, key=lambda t: t.get("priority", 3)):
360
- self._add_task_to_tree(node, child, children_map)
361
-
362
- def on_tree_node_selected(self, event: Tree.NodeSelected[str]) -> None:
363
- """Handle task selection in tree."""
364
- task_id = event.node.data
365
- if task_id and task_id in self._task_map:
366
- self.selected_task_id = task_id
367
- task = self._task_map[task_id]
368
- try:
369
- detail_panel = self.query_one("#detail-panel", TaskDetailPanel)
370
- detail_panel.update_task(task)
371
- except Exception:
372
- pass # nosec B110 - widget may not be mounted yet
373
-
374
- async def on_filter_changed(self, event: FilterChanged) -> None:
375
- """Handle filter changes."""
376
- self.current_filter_status = event.status if event.status != "all" else "all"
377
- self.current_filter_type = event.task_type if event.task_type != "all" else "all"
378
- await self.refresh_data()
379
-
380
- async def on_button_pressed(self, event: Button.Pressed) -> None:
381
- """Handle action button presses."""
382
- if not self.selected_task_id:
383
- return
384
-
385
- button_id = event.button.id
386
- task_id = self.selected_task_id
387
-
388
- try:
389
- async with GobbyAPIClient(self.api_client.base_url) as client:
390
- if button_id == "btn-start":
391
- await client.update_task(task_id, status="in_progress")
392
- self.notify(f"Task started: {task_id}")
393
-
394
- elif button_id == "btn-expand":
395
- await client.call_tool(
396
- "gobby-tasks",
397
- "expand_task",
398
- {"task_id": task_id},
399
- )
400
- self.notify(f"Task expanded: {task_id}")
401
-
402
- elif button_id == "btn-complete":
403
- # Note: In real usage, this would need a commit SHA
404
- await client.close_task(
405
- task_id,
406
- reason="obsolete",
407
- )
408
- self.notify(f"Task completed: {task_id}")
409
-
410
- elif button_id == "btn-approve":
411
- await client.close_task(
412
- task_id,
413
- reason="obsolete",
414
- )
415
- self.notify(f"Task approved: {task_id}")
416
-
417
- elif button_id == "btn-reopen":
418
- await client.call_tool(
419
- "gobby-tasks",
420
- "reopen_task",
421
- {"task_id": task_id},
422
- )
423
- self.notify(f"Task reopened: {task_id}")
424
-
425
- # Refresh after action
426
- await self.refresh_data()
427
-
428
- except Exception as e:
429
- self.notify(f"Action failed: {e}", severity="error")
430
-
431
- def on_ws_event(self, event_type: str, data: dict[str, Any]) -> None:
432
- """Handle WebSocket events."""
433
- if event_type == "hook_event":
434
- hook_type = data.get("event_type", "")
435
- if "task" in hook_type.lower():
436
- self.run_worker(self.refresh_data(), name="refresh_data", exclusive=True)
437
-
438
- def activate_search(self) -> None:
439
- """Activate search mode."""
440
- self.notify("Search not yet implemented", severity="information")