gobby 0.2.5__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 +3 -0
- gobby/adapters/__init__.py +30 -0
- gobby/adapters/base.py +93 -0
- gobby/adapters/claude_code.py +276 -0
- gobby/adapters/codex.py +1292 -0
- gobby/adapters/gemini.py +343 -0
- gobby/agents/__init__.py +37 -0
- gobby/agents/codex_session.py +120 -0
- gobby/agents/constants.py +112 -0
- gobby/agents/context.py +362 -0
- gobby/agents/definitions.py +133 -0
- gobby/agents/gemini_session.py +111 -0
- gobby/agents/registry.py +618 -0
- gobby/agents/runner.py +968 -0
- gobby/agents/session.py +259 -0
- gobby/agents/spawn.py +916 -0
- gobby/agents/spawners/__init__.py +77 -0
- gobby/agents/spawners/base.py +142 -0
- gobby/agents/spawners/cross_platform.py +266 -0
- gobby/agents/spawners/embedded.py +225 -0
- gobby/agents/spawners/headless.py +226 -0
- gobby/agents/spawners/linux.py +125 -0
- gobby/agents/spawners/macos.py +277 -0
- gobby/agents/spawners/windows.py +308 -0
- gobby/agents/tty_config.py +319 -0
- gobby/autonomous/__init__.py +32 -0
- gobby/autonomous/progress_tracker.py +447 -0
- gobby/autonomous/stop_registry.py +269 -0
- gobby/autonomous/stuck_detector.py +383 -0
- gobby/cli/__init__.py +67 -0
- gobby/cli/__main__.py +8 -0
- gobby/cli/agents.py +529 -0
- gobby/cli/artifacts.py +266 -0
- gobby/cli/daemon.py +329 -0
- gobby/cli/extensions.py +526 -0
- gobby/cli/github.py +263 -0
- gobby/cli/init.py +53 -0
- gobby/cli/install.py +614 -0
- gobby/cli/installers/__init__.py +37 -0
- gobby/cli/installers/antigravity.py +65 -0
- gobby/cli/installers/claude.py +363 -0
- gobby/cli/installers/codex.py +192 -0
- gobby/cli/installers/gemini.py +294 -0
- gobby/cli/installers/git_hooks.py +377 -0
- gobby/cli/installers/shared.py +737 -0
- gobby/cli/linear.py +250 -0
- gobby/cli/mcp.py +30 -0
- gobby/cli/mcp_proxy.py +698 -0
- gobby/cli/memory.py +304 -0
- gobby/cli/merge.py +384 -0
- gobby/cli/projects.py +79 -0
- gobby/cli/sessions.py +622 -0
- gobby/cli/tasks/__init__.py +30 -0
- gobby/cli/tasks/_utils.py +658 -0
- gobby/cli/tasks/ai.py +1025 -0
- gobby/cli/tasks/commits.py +169 -0
- gobby/cli/tasks/crud.py +685 -0
- gobby/cli/tasks/deps.py +135 -0
- gobby/cli/tasks/labels.py +63 -0
- gobby/cli/tasks/main.py +273 -0
- gobby/cli/tasks/search.py +178 -0
- gobby/cli/tui.py +34 -0
- gobby/cli/utils.py +513 -0
- gobby/cli/workflows.py +927 -0
- gobby/cli/worktrees.py +481 -0
- gobby/config/__init__.py +129 -0
- gobby/config/app.py +551 -0
- gobby/config/extensions.py +167 -0
- gobby/config/features.py +472 -0
- gobby/config/llm_providers.py +98 -0
- gobby/config/logging.py +66 -0
- gobby/config/mcp.py +346 -0
- gobby/config/persistence.py +247 -0
- gobby/config/servers.py +141 -0
- gobby/config/sessions.py +250 -0
- gobby/config/tasks.py +784 -0
- gobby/hooks/__init__.py +104 -0
- gobby/hooks/artifact_capture.py +213 -0
- gobby/hooks/broadcaster.py +243 -0
- gobby/hooks/event_handlers.py +723 -0
- gobby/hooks/events.py +218 -0
- gobby/hooks/git.py +169 -0
- gobby/hooks/health_monitor.py +171 -0
- gobby/hooks/hook_manager.py +856 -0
- gobby/hooks/hook_types.py +575 -0
- gobby/hooks/plugins.py +813 -0
- gobby/hooks/session_coordinator.py +396 -0
- gobby/hooks/verification_runner.py +268 -0
- gobby/hooks/webhooks.py +339 -0
- gobby/install/claude/commands/gobby/bug.md +51 -0
- gobby/install/claude/commands/gobby/chore.md +51 -0
- gobby/install/claude/commands/gobby/epic.md +52 -0
- gobby/install/claude/commands/gobby/eval.md +235 -0
- gobby/install/claude/commands/gobby/feat.md +49 -0
- gobby/install/claude/commands/gobby/nit.md +52 -0
- gobby/install/claude/commands/gobby/ref.md +52 -0
- gobby/install/claude/hooks/HOOK_SCHEMAS.md +632 -0
- gobby/install/claude/hooks/hook_dispatcher.py +364 -0
- gobby/install/claude/hooks/validate_settings.py +102 -0
- gobby/install/claude/hooks-template.json +118 -0
- gobby/install/codex/hooks/hook_dispatcher.py +153 -0
- gobby/install/codex/prompts/forget.md +7 -0
- gobby/install/codex/prompts/memories.md +7 -0
- gobby/install/codex/prompts/recall.md +7 -0
- gobby/install/codex/prompts/remember.md +13 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +268 -0
- gobby/install/gemini/hooks-template.json +138 -0
- gobby/install/shared/plugins/code_guardian.py +456 -0
- gobby/install/shared/plugins/example_notify.py +331 -0
- gobby/integrations/__init__.py +10 -0
- gobby/integrations/github.py +145 -0
- gobby/integrations/linear.py +145 -0
- gobby/llm/__init__.py +40 -0
- gobby/llm/base.py +120 -0
- gobby/llm/claude.py +578 -0
- gobby/llm/claude_executor.py +503 -0
- gobby/llm/codex.py +322 -0
- gobby/llm/codex_executor.py +513 -0
- gobby/llm/executor.py +316 -0
- gobby/llm/factory.py +34 -0
- gobby/llm/gemini.py +258 -0
- gobby/llm/gemini_executor.py +339 -0
- gobby/llm/litellm.py +287 -0
- gobby/llm/litellm_executor.py +303 -0
- gobby/llm/resolver.py +499 -0
- gobby/llm/service.py +236 -0
- gobby/mcp_proxy/__init__.py +29 -0
- gobby/mcp_proxy/actions.py +175 -0
- gobby/mcp_proxy/daemon_control.py +198 -0
- gobby/mcp_proxy/importer.py +436 -0
- gobby/mcp_proxy/lazy.py +325 -0
- gobby/mcp_proxy/manager.py +798 -0
- gobby/mcp_proxy/metrics.py +609 -0
- gobby/mcp_proxy/models.py +139 -0
- gobby/mcp_proxy/registries.py +215 -0
- gobby/mcp_proxy/schema_hash.py +381 -0
- gobby/mcp_proxy/semantic_search.py +706 -0
- gobby/mcp_proxy/server.py +549 -0
- gobby/mcp_proxy/services/__init__.py +0 -0
- gobby/mcp_proxy/services/fallback.py +306 -0
- gobby/mcp_proxy/services/recommendation.py +224 -0
- gobby/mcp_proxy/services/server_mgmt.py +214 -0
- gobby/mcp_proxy/services/system.py +72 -0
- gobby/mcp_proxy/services/tool_filter.py +231 -0
- gobby/mcp_proxy/services/tool_proxy.py +309 -0
- gobby/mcp_proxy/stdio.py +565 -0
- gobby/mcp_proxy/tools/__init__.py +27 -0
- gobby/mcp_proxy/tools/agents.py +1103 -0
- gobby/mcp_proxy/tools/artifacts.py +207 -0
- gobby/mcp_proxy/tools/hub.py +335 -0
- gobby/mcp_proxy/tools/internal.py +337 -0
- gobby/mcp_proxy/tools/memory.py +543 -0
- gobby/mcp_proxy/tools/merge.py +422 -0
- gobby/mcp_proxy/tools/metrics.py +283 -0
- gobby/mcp_proxy/tools/orchestration/__init__.py +23 -0
- gobby/mcp_proxy/tools/orchestration/cleanup.py +619 -0
- gobby/mcp_proxy/tools/orchestration/monitor.py +380 -0
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +746 -0
- gobby/mcp_proxy/tools/orchestration/review.py +736 -0
- gobby/mcp_proxy/tools/orchestration/utils.py +16 -0
- gobby/mcp_proxy/tools/session_messages.py +1056 -0
- gobby/mcp_proxy/tools/task_dependencies.py +219 -0
- gobby/mcp_proxy/tools/task_expansion.py +591 -0
- gobby/mcp_proxy/tools/task_github.py +393 -0
- gobby/mcp_proxy/tools/task_linear.py +379 -0
- gobby/mcp_proxy/tools/task_orchestration.py +77 -0
- gobby/mcp_proxy/tools/task_readiness.py +522 -0
- gobby/mcp_proxy/tools/task_sync.py +351 -0
- gobby/mcp_proxy/tools/task_validation.py +843 -0
- gobby/mcp_proxy/tools/tasks/__init__.py +25 -0
- gobby/mcp_proxy/tools/tasks/_context.py +112 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +516 -0
- gobby/mcp_proxy/tools/tasks/_factory.py +176 -0
- gobby/mcp_proxy/tools/tasks/_helpers.py +129 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +517 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +301 -0
- gobby/mcp_proxy/tools/tasks/_resolution.py +55 -0
- gobby/mcp_proxy/tools/tasks/_search.py +215 -0
- gobby/mcp_proxy/tools/tasks/_session.py +125 -0
- gobby/mcp_proxy/tools/workflows.py +973 -0
- gobby/mcp_proxy/tools/worktrees.py +1264 -0
- gobby/mcp_proxy/transports/__init__.py +0 -0
- gobby/mcp_proxy/transports/base.py +95 -0
- gobby/mcp_proxy/transports/factory.py +44 -0
- gobby/mcp_proxy/transports/http.py +139 -0
- gobby/mcp_proxy/transports/stdio.py +213 -0
- gobby/mcp_proxy/transports/websocket.py +136 -0
- gobby/memory/backends/__init__.py +116 -0
- gobby/memory/backends/mem0.py +408 -0
- gobby/memory/backends/memu.py +485 -0
- gobby/memory/backends/null.py +111 -0
- gobby/memory/backends/openmemory.py +537 -0
- gobby/memory/backends/sqlite.py +304 -0
- gobby/memory/context.py +87 -0
- gobby/memory/manager.py +1001 -0
- gobby/memory/protocol.py +451 -0
- gobby/memory/search/__init__.py +66 -0
- gobby/memory/search/text.py +127 -0
- gobby/memory/viz.py +258 -0
- gobby/prompts/__init__.py +13 -0
- gobby/prompts/defaults/expansion/system.md +119 -0
- gobby/prompts/defaults/expansion/user.md +48 -0
- gobby/prompts/defaults/external_validation/agent.md +72 -0
- gobby/prompts/defaults/external_validation/external.md +63 -0
- gobby/prompts/defaults/external_validation/spawn.md +83 -0
- gobby/prompts/defaults/external_validation/system.md +6 -0
- gobby/prompts/defaults/features/import_mcp.md +22 -0
- gobby/prompts/defaults/features/import_mcp_github.md +17 -0
- gobby/prompts/defaults/features/import_mcp_search.md +16 -0
- gobby/prompts/defaults/features/recommend_tools.md +32 -0
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +35 -0
- gobby/prompts/defaults/features/recommend_tools_llm.md +30 -0
- gobby/prompts/defaults/features/server_description.md +20 -0
- gobby/prompts/defaults/features/server_description_system.md +6 -0
- gobby/prompts/defaults/features/task_description.md +31 -0
- gobby/prompts/defaults/features/task_description_system.md +6 -0
- gobby/prompts/defaults/features/tool_summary.md +17 -0
- gobby/prompts/defaults/features/tool_summary_system.md +6 -0
- gobby/prompts/defaults/research/step.md +58 -0
- gobby/prompts/defaults/validation/criteria.md +47 -0
- gobby/prompts/defaults/validation/validate.md +38 -0
- gobby/prompts/loader.py +346 -0
- gobby/prompts/models.py +113 -0
- gobby/py.typed +0 -0
- gobby/runner.py +488 -0
- gobby/search/__init__.py +23 -0
- gobby/search/protocol.py +104 -0
- gobby/search/tfidf.py +232 -0
- gobby/servers/__init__.py +7 -0
- gobby/servers/http.py +636 -0
- gobby/servers/models.py +31 -0
- gobby/servers/routes/__init__.py +23 -0
- gobby/servers/routes/admin.py +416 -0
- gobby/servers/routes/dependencies.py +118 -0
- gobby/servers/routes/mcp/__init__.py +24 -0
- gobby/servers/routes/mcp/hooks.py +135 -0
- gobby/servers/routes/mcp/plugins.py +121 -0
- gobby/servers/routes/mcp/tools.py +1337 -0
- gobby/servers/routes/mcp/webhooks.py +159 -0
- gobby/servers/routes/sessions.py +582 -0
- gobby/servers/websocket.py +766 -0
- gobby/sessions/__init__.py +13 -0
- gobby/sessions/analyzer.py +322 -0
- gobby/sessions/lifecycle.py +240 -0
- gobby/sessions/manager.py +563 -0
- gobby/sessions/processor.py +225 -0
- gobby/sessions/summary.py +532 -0
- gobby/sessions/transcripts/__init__.py +41 -0
- gobby/sessions/transcripts/base.py +125 -0
- gobby/sessions/transcripts/claude.py +386 -0
- gobby/sessions/transcripts/codex.py +143 -0
- gobby/sessions/transcripts/gemini.py +195 -0
- gobby/storage/__init__.py +21 -0
- gobby/storage/agents.py +409 -0
- gobby/storage/artifact_classifier.py +341 -0
- gobby/storage/artifacts.py +285 -0
- gobby/storage/compaction.py +67 -0
- gobby/storage/database.py +357 -0
- gobby/storage/inter_session_messages.py +194 -0
- gobby/storage/mcp.py +680 -0
- gobby/storage/memories.py +562 -0
- gobby/storage/merge_resolutions.py +550 -0
- gobby/storage/migrations.py +860 -0
- gobby/storage/migrations_legacy.py +1359 -0
- gobby/storage/projects.py +166 -0
- gobby/storage/session_messages.py +251 -0
- gobby/storage/session_tasks.py +97 -0
- gobby/storage/sessions.py +817 -0
- gobby/storage/task_dependencies.py +223 -0
- gobby/storage/tasks/__init__.py +42 -0
- gobby/storage/tasks/_aggregates.py +180 -0
- gobby/storage/tasks/_crud.py +449 -0
- gobby/storage/tasks/_id.py +104 -0
- gobby/storage/tasks/_lifecycle.py +311 -0
- gobby/storage/tasks/_manager.py +889 -0
- gobby/storage/tasks/_models.py +300 -0
- gobby/storage/tasks/_ordering.py +119 -0
- gobby/storage/tasks/_path_cache.py +110 -0
- gobby/storage/tasks/_queries.py +343 -0
- gobby/storage/tasks/_search.py +143 -0
- gobby/storage/workflow_audit.py +393 -0
- gobby/storage/worktrees.py +547 -0
- gobby/sync/__init__.py +29 -0
- gobby/sync/github.py +333 -0
- gobby/sync/linear.py +304 -0
- gobby/sync/memories.py +284 -0
- gobby/sync/tasks.py +641 -0
- gobby/tasks/__init__.py +8 -0
- gobby/tasks/build_verification.py +193 -0
- gobby/tasks/commits.py +633 -0
- gobby/tasks/context.py +747 -0
- gobby/tasks/criteria.py +342 -0
- gobby/tasks/enhanced_validator.py +226 -0
- gobby/tasks/escalation.py +263 -0
- gobby/tasks/expansion.py +626 -0
- gobby/tasks/external_validator.py +764 -0
- gobby/tasks/issue_extraction.py +171 -0
- gobby/tasks/prompts/expand.py +327 -0
- gobby/tasks/research.py +421 -0
- gobby/tasks/tdd.py +352 -0
- gobby/tasks/tree_builder.py +263 -0
- gobby/tasks/validation.py +712 -0
- gobby/tasks/validation_history.py +357 -0
- gobby/tasks/validation_models.py +89 -0
- gobby/tools/__init__.py +0 -0
- gobby/tools/summarizer.py +170 -0
- gobby/tui/__init__.py +5 -0
- gobby/tui/api_client.py +281 -0
- gobby/tui/app.py +327 -0
- gobby/tui/screens/__init__.py +25 -0
- gobby/tui/screens/agents.py +333 -0
- gobby/tui/screens/chat.py +450 -0
- gobby/tui/screens/dashboard.py +377 -0
- gobby/tui/screens/memory.py +305 -0
- gobby/tui/screens/metrics.py +231 -0
- gobby/tui/screens/orchestrator.py +904 -0
- gobby/tui/screens/sessions.py +412 -0
- gobby/tui/screens/tasks.py +442 -0
- gobby/tui/screens/workflows.py +289 -0
- gobby/tui/screens/worktrees.py +174 -0
- gobby/tui/widgets/__init__.py +21 -0
- gobby/tui/widgets/chat.py +210 -0
- gobby/tui/widgets/conductor.py +104 -0
- gobby/tui/widgets/menu.py +132 -0
- gobby/tui/widgets/message_panel.py +160 -0
- gobby/tui/widgets/review_gate.py +224 -0
- gobby/tui/widgets/task_tree.py +99 -0
- gobby/tui/widgets/token_budget.py +166 -0
- gobby/tui/ws_client.py +258 -0
- gobby/utils/__init__.py +3 -0
- gobby/utils/daemon_client.py +235 -0
- gobby/utils/git.py +222 -0
- gobby/utils/id.py +38 -0
- gobby/utils/json_helpers.py +161 -0
- gobby/utils/logging.py +376 -0
- gobby/utils/machine_id.py +135 -0
- gobby/utils/metrics.py +589 -0
- gobby/utils/project_context.py +182 -0
- gobby/utils/project_init.py +263 -0
- gobby/utils/status.py +256 -0
- gobby/utils/validation.py +80 -0
- gobby/utils/version.py +23 -0
- gobby/workflows/__init__.py +4 -0
- gobby/workflows/actions.py +1310 -0
- gobby/workflows/approval_flow.py +138 -0
- gobby/workflows/artifact_actions.py +103 -0
- gobby/workflows/audit_helpers.py +110 -0
- gobby/workflows/autonomous_actions.py +286 -0
- gobby/workflows/context_actions.py +394 -0
- gobby/workflows/definitions.py +130 -0
- gobby/workflows/detection_helpers.py +208 -0
- gobby/workflows/engine.py +485 -0
- gobby/workflows/evaluator.py +669 -0
- gobby/workflows/git_utils.py +96 -0
- gobby/workflows/hooks.py +169 -0
- gobby/workflows/lifecycle_evaluator.py +613 -0
- gobby/workflows/llm_actions.py +70 -0
- gobby/workflows/loader.py +333 -0
- gobby/workflows/mcp_actions.py +60 -0
- gobby/workflows/memory_actions.py +272 -0
- gobby/workflows/premature_stop.py +164 -0
- gobby/workflows/session_actions.py +139 -0
- gobby/workflows/state_actions.py +123 -0
- gobby/workflows/state_manager.py +104 -0
- gobby/workflows/stop_signal_actions.py +163 -0
- gobby/workflows/summary_actions.py +344 -0
- gobby/workflows/task_actions.py +249 -0
- gobby/workflows/task_enforcement_actions.py +901 -0
- gobby/workflows/templates.py +52 -0
- gobby/workflows/todo_actions.py +84 -0
- gobby/workflows/webhook.py +223 -0
- gobby/workflows/webhook_executor.py +399 -0
- gobby/worktrees/__init__.py +5 -0
- gobby/worktrees/git.py +690 -0
- gobby/worktrees/merge/__init__.py +20 -0
- gobby/worktrees/merge/conflict_parser.py +177 -0
- gobby/worktrees/merge/resolver.py +485 -0
- gobby-0.2.5.dist-info/METADATA +351 -0
- gobby-0.2.5.dist-info/RECORD +383 -0
- gobby-0.2.5.dist-info/WHEEL +5 -0
- gobby-0.2.5.dist-info/entry_points.txt +2 -0
- gobby-0.2.5.dist-info/licenses/LICENSE.md +193 -0
- gobby-0.2.5.dist-info/top_level.txt +1 -0
gobby/cli/agents.py
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent management CLI commands.
|
|
3
|
+
|
|
4
|
+
Commands for managing subagent runs:
|
|
5
|
+
- start: Start a new agent
|
|
6
|
+
- list: List agent runs for a session
|
|
7
|
+
- show: Show details for an agent run
|
|
8
|
+
- status: Check status of a running agent
|
|
9
|
+
- stop: Stop a running agent (marks cancelled in DB, does not kill process)
|
|
10
|
+
- kill: Kill a running agent process (SIGTERM/SIGKILL)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from gobby.cli.utils import resolve_session_id
|
|
19
|
+
from gobby.storage.agents import LocalAgentRunManager
|
|
20
|
+
from gobby.storage.database import LocalDatabase
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_agent_run_manager() -> LocalAgentRunManager:
|
|
24
|
+
"""Get initialized agent run manager."""
|
|
25
|
+
db = LocalDatabase()
|
|
26
|
+
return LocalAgentRunManager(db)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_agent_run_id(run_ref: str) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Resolve agent run reference (exact or prefix) to full ID.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
run_ref: Agent run ID or prefix
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Full UUID string
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
click.ClickException: If run not found or ambiguous
|
|
41
|
+
"""
|
|
42
|
+
manager = get_agent_run_manager()
|
|
43
|
+
|
|
44
|
+
# Try exact match first
|
|
45
|
+
# Optimization: check 36 chars?
|
|
46
|
+
if len(run_ref) == 36 and manager.get(run_ref):
|
|
47
|
+
return run_ref
|
|
48
|
+
|
|
49
|
+
# Try prefix match
|
|
50
|
+
db = LocalDatabase()
|
|
51
|
+
rows = db.fetchall(
|
|
52
|
+
"SELECT id FROM agent_runs WHERE id LIKE ? LIMIT 5",
|
|
53
|
+
(f"{run_ref}%",),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not rows:
|
|
57
|
+
raise click.ClickException(f"Agent run not found: {run_ref}")
|
|
58
|
+
|
|
59
|
+
if len(rows) > 1:
|
|
60
|
+
click.echo(f"Ambiguous agent run reference '{run_ref}' matches:", err=True)
|
|
61
|
+
for row in rows:
|
|
62
|
+
click.echo(f" {row['id']}", err=True)
|
|
63
|
+
raise click.ClickException(f"Ambiguous agent run reference: {run_ref}")
|
|
64
|
+
|
|
65
|
+
return str(rows[0]["id"])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_daemon_url() -> str:
|
|
69
|
+
"""Get daemon URL from config."""
|
|
70
|
+
from gobby.config.app import load_config
|
|
71
|
+
|
|
72
|
+
config = load_config()
|
|
73
|
+
return f"http://localhost:{config.daemon_port}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@click.group()
|
|
77
|
+
def agents() -> None:
|
|
78
|
+
"""Manage subagent runs."""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@agents.command("start")
|
|
83
|
+
@click.argument("prompt")
|
|
84
|
+
@click.option("--session", "-s", "parent_session_id", required=True, help="Parent session ID")
|
|
85
|
+
@click.option("--workflow", "-w", help="Workflow name to execute")
|
|
86
|
+
@click.option("--task", "-t", help="Task ID or 'next' for auto-select")
|
|
87
|
+
@click.option(
|
|
88
|
+
"--mode",
|
|
89
|
+
"-m",
|
|
90
|
+
type=click.Choice(["in_process", "terminal", "embedded", "headless"]),
|
|
91
|
+
default="terminal",
|
|
92
|
+
help="Execution mode (default: terminal)",
|
|
93
|
+
)
|
|
94
|
+
@click.option(
|
|
95
|
+
"--terminal",
|
|
96
|
+
type=click.Choice(["auto", "ghostty", "iterm", "kitty", "wezterm", "terminal"]),
|
|
97
|
+
default="auto",
|
|
98
|
+
help="Terminal for terminal/embedded modes",
|
|
99
|
+
)
|
|
100
|
+
@click.option("--provider", "-p", default="claude", help="LLM provider (claude, gemini, etc.)")
|
|
101
|
+
@click.option("--model", help="Model override")
|
|
102
|
+
@click.option("--timeout", default=120.0, help="Execution timeout in seconds")
|
|
103
|
+
@click.option("--max-turns", default=10, help="Maximum turns")
|
|
104
|
+
@click.option(
|
|
105
|
+
"--context",
|
|
106
|
+
"-c",
|
|
107
|
+
"session_context",
|
|
108
|
+
default="summary_markdown",
|
|
109
|
+
help="Context source (summary_markdown, compact_markdown, transcript:<n>, file:<path>)",
|
|
110
|
+
)
|
|
111
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
112
|
+
def start_agent_cmd(
|
|
113
|
+
prompt: str,
|
|
114
|
+
parent_session_id: str,
|
|
115
|
+
workflow: str | None,
|
|
116
|
+
task: str | None,
|
|
117
|
+
mode: str,
|
|
118
|
+
terminal: str,
|
|
119
|
+
provider: str,
|
|
120
|
+
model: str | None,
|
|
121
|
+
timeout: float,
|
|
122
|
+
max_turns: int,
|
|
123
|
+
session_context: str,
|
|
124
|
+
json_format: bool,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Start a new agent with the given prompt.
|
|
127
|
+
|
|
128
|
+
Examples:
|
|
129
|
+
|
|
130
|
+
gobby agents start "Implement feature X" --session sess-abc123
|
|
131
|
+
|
|
132
|
+
gobby agents start "Fix the bug" -s sess-abc123 --mode terminal
|
|
133
|
+
|
|
134
|
+
gobby agents start "Run tests" -s sess-abc123 --mode headless
|
|
135
|
+
"""
|
|
136
|
+
daemon_url = get_daemon_url()
|
|
137
|
+
|
|
138
|
+
# Resolve session ID
|
|
139
|
+
try:
|
|
140
|
+
parent_session_id = resolve_session_id(parent_session_id)
|
|
141
|
+
except click.ClickException as e:
|
|
142
|
+
raise SystemExit(1) from e
|
|
143
|
+
|
|
144
|
+
# Build arguments for the MCP tool call
|
|
145
|
+
arguments = {
|
|
146
|
+
"prompt": prompt,
|
|
147
|
+
"parent_session_id": parent_session_id,
|
|
148
|
+
"mode": mode,
|
|
149
|
+
"terminal": terminal,
|
|
150
|
+
"provider": provider,
|
|
151
|
+
"timeout": timeout,
|
|
152
|
+
"max_turns": max_turns,
|
|
153
|
+
"session_context": session_context,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if workflow:
|
|
157
|
+
arguments["workflow"] = workflow
|
|
158
|
+
if task:
|
|
159
|
+
arguments["task"] = task
|
|
160
|
+
if model:
|
|
161
|
+
arguments["model"] = model
|
|
162
|
+
|
|
163
|
+
# Call the daemon's MCP tool endpoint
|
|
164
|
+
try:
|
|
165
|
+
response = httpx.post(
|
|
166
|
+
f"{daemon_url}/mcp/gobby-agents/tools/start_agent",
|
|
167
|
+
json=arguments,
|
|
168
|
+
timeout=30.0,
|
|
169
|
+
)
|
|
170
|
+
response.raise_for_status()
|
|
171
|
+
result = response.json()
|
|
172
|
+
except httpx.ConnectError:
|
|
173
|
+
click.echo("Error: Cannot connect to Gobby daemon. Is it running?", err=True)
|
|
174
|
+
click.echo("Start with: gobby start", err=True)
|
|
175
|
+
return
|
|
176
|
+
except httpx.HTTPStatusError as e:
|
|
177
|
+
click.echo(f"Error: Daemon returned {e.response.status_code}", err=True)
|
|
178
|
+
click.echo(e.response.text, err=True)
|
|
179
|
+
return
|
|
180
|
+
except Exception as e:
|
|
181
|
+
click.echo(f"Error: {e}", err=True)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if json_format:
|
|
185
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Check result
|
|
189
|
+
if result.get("success"):
|
|
190
|
+
run_id = result.get("run_id", "unknown")
|
|
191
|
+
child_session_id = result.get("child_session_id", "unknown")
|
|
192
|
+
status = result.get("status", "unknown")
|
|
193
|
+
|
|
194
|
+
click.echo(f"Started agent run: {run_id}")
|
|
195
|
+
click.echo(f" Child session: {child_session_id}")
|
|
196
|
+
click.echo(f" Status: {status}")
|
|
197
|
+
|
|
198
|
+
if result.get("message"):
|
|
199
|
+
click.echo(f" {result['message']}")
|
|
200
|
+
|
|
201
|
+
if mode == "in_process" and result.get("output"):
|
|
202
|
+
click.echo(f"\nOutput:\n{result['output']}")
|
|
203
|
+
else:
|
|
204
|
+
error = result.get("error", "Unknown error")
|
|
205
|
+
click.echo(f"Failed to start agent: {error}", err=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@agents.command("list")
|
|
209
|
+
@click.option("--session", "-s", "session_id", help="Filter by parent session ID")
|
|
210
|
+
@click.option(
|
|
211
|
+
"--status",
|
|
212
|
+
type=click.Choice(["pending", "running", "success", "error", "timeout", "cancelled"]),
|
|
213
|
+
help="Filter by status",
|
|
214
|
+
)
|
|
215
|
+
@click.option("--limit", "-n", default=20, help="Max runs to show")
|
|
216
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
217
|
+
def list_agents(
|
|
218
|
+
session_id: str | None,
|
|
219
|
+
status: str | None,
|
|
220
|
+
limit: int,
|
|
221
|
+
json_format: bool,
|
|
222
|
+
) -> None:
|
|
223
|
+
"""List agent runs."""
|
|
224
|
+
manager = get_agent_run_manager()
|
|
225
|
+
|
|
226
|
+
if session_id:
|
|
227
|
+
try:
|
|
228
|
+
session_id = resolve_session_id(session_id)
|
|
229
|
+
except click.ClickException as e:
|
|
230
|
+
raise SystemExit(1) from e
|
|
231
|
+
runs = manager.list_by_session(session_id, status=status, limit=limit) # type: ignore
|
|
232
|
+
elif status == "running":
|
|
233
|
+
runs = manager.list_running(limit=limit)
|
|
234
|
+
else:
|
|
235
|
+
# List recent runs across all sessions
|
|
236
|
+
# Note: This requires querying without session filter
|
|
237
|
+
db = LocalDatabase()
|
|
238
|
+
query = "SELECT * FROM agent_runs"
|
|
239
|
+
params: list[str | int] = []
|
|
240
|
+
|
|
241
|
+
if status:
|
|
242
|
+
query += " WHERE status = ?"
|
|
243
|
+
params.append(status)
|
|
244
|
+
|
|
245
|
+
query += " ORDER BY created_at DESC LIMIT ?"
|
|
246
|
+
params.append(limit)
|
|
247
|
+
|
|
248
|
+
rows = db.fetchall(query, tuple(params))
|
|
249
|
+
from gobby.storage.agents import AgentRun
|
|
250
|
+
|
|
251
|
+
runs = [AgentRun.from_row(row) for row in rows]
|
|
252
|
+
|
|
253
|
+
if json_format:
|
|
254
|
+
click.echo(json.dumps([r.to_dict() for r in runs], indent=2, default=str))
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
if not runs:
|
|
258
|
+
click.echo("No agent runs found.")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
click.echo(f"Found {len(runs)} agent run(s):\n")
|
|
262
|
+
for run in runs:
|
|
263
|
+
status_icon = {
|
|
264
|
+
"pending": "○",
|
|
265
|
+
"running": "◐",
|
|
266
|
+
"success": "✓",
|
|
267
|
+
"error": "✗",
|
|
268
|
+
"timeout": "⏱",
|
|
269
|
+
"cancelled": "⊘",
|
|
270
|
+
}.get(run.status, "?")
|
|
271
|
+
|
|
272
|
+
# Truncate prompt
|
|
273
|
+
prompt = run.prompt[:40] + "..." if len(run.prompt) > 40 else run.prompt
|
|
274
|
+
prompt = prompt.replace("\n", " ")
|
|
275
|
+
|
|
276
|
+
click.echo(f"{status_icon} {run.id[:12]} {run.status:<10} {run.provider:<8} {prompt}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@agents.command("show")
|
|
280
|
+
@click.argument("run_ref")
|
|
281
|
+
@click.option("--json", "json_format", is_flag=True, help="Output as JSON")
|
|
282
|
+
def show_agent(run_ref: str, json_format: bool) -> None:
|
|
283
|
+
"""Show details for an agent run (UUID or prefix)."""
|
|
284
|
+
run_id = resolve_agent_run_id(run_ref)
|
|
285
|
+
manager = get_agent_run_manager()
|
|
286
|
+
run = manager.get(run_id)
|
|
287
|
+
|
|
288
|
+
if not run:
|
|
289
|
+
# Should not happen if resolve succeeded, but safe check
|
|
290
|
+
click.echo(f"Agent run not found: {run_id}", err=True)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
if json_format:
|
|
294
|
+
click.echo(json.dumps(run.to_dict(), indent=2, default=str))
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
click.echo(f"Agent Run: {run.id}")
|
|
298
|
+
click.echo(f"Status: {run.status}")
|
|
299
|
+
click.echo(f"Provider: {run.provider}")
|
|
300
|
+
if run.model:
|
|
301
|
+
click.echo(f"Model: {run.model}")
|
|
302
|
+
click.echo(f"Parent Session: {run.parent_session_id}")
|
|
303
|
+
if run.child_session_id:
|
|
304
|
+
click.echo(f"Child Session: {run.child_session_id}")
|
|
305
|
+
if run.workflow_name:
|
|
306
|
+
click.echo(f"Workflow: {run.workflow_name}")
|
|
307
|
+
|
|
308
|
+
click.echo(f"\nPrompt:\n{run.prompt[:500]}")
|
|
309
|
+
if len(run.prompt) > 500:
|
|
310
|
+
click.echo("...")
|
|
311
|
+
|
|
312
|
+
if run.result:
|
|
313
|
+
click.echo(f"\nResult:\n{run.result[:500]}")
|
|
314
|
+
if len(run.result) > 500:
|
|
315
|
+
click.echo("...")
|
|
316
|
+
|
|
317
|
+
if run.error:
|
|
318
|
+
click.echo(f"\nError: {run.error}")
|
|
319
|
+
|
|
320
|
+
click.echo(f"\nTurns Used: {run.turns_used}")
|
|
321
|
+
click.echo(f"Tool Calls: {run.tool_calls_count}")
|
|
322
|
+
click.echo(f"Created: {run.created_at}")
|
|
323
|
+
if run.started_at:
|
|
324
|
+
click.echo(f"Started: {run.started_at}")
|
|
325
|
+
if run.completed_at:
|
|
326
|
+
click.echo(f"Completed: {run.completed_at}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@agents.command("status")
|
|
330
|
+
@click.argument("run_ref")
|
|
331
|
+
def agent_status(run_ref: str) -> None:
|
|
332
|
+
"""Check status of an agent run (UUID or prefix)."""
|
|
333
|
+
run_id = resolve_agent_run_id(run_ref)
|
|
334
|
+
manager = get_agent_run_manager()
|
|
335
|
+
run = manager.get(run_id)
|
|
336
|
+
|
|
337
|
+
if not run:
|
|
338
|
+
click.echo(f"Agent run not found: {run_id}", err=True)
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
status_icon = {
|
|
342
|
+
"pending": "○",
|
|
343
|
+
"running": "◐",
|
|
344
|
+
"success": "✓",
|
|
345
|
+
"error": "✗",
|
|
346
|
+
"timeout": "⏱",
|
|
347
|
+
"cancelled": "⊘",
|
|
348
|
+
}.get(run.status, "?")
|
|
349
|
+
|
|
350
|
+
click.echo(f"{status_icon} {run.id}: {run.status}")
|
|
351
|
+
|
|
352
|
+
if run.status == "running" and run.started_at:
|
|
353
|
+
click.echo(f" Running since: {run.started_at}")
|
|
354
|
+
click.echo(f" Turns used: {run.turns_used}")
|
|
355
|
+
elif run.status in ("success", "error", "timeout", "cancelled"):
|
|
356
|
+
if run.completed_at:
|
|
357
|
+
click.echo(f" Completed: {run.completed_at}")
|
|
358
|
+
if run.error:
|
|
359
|
+
click.echo(f" Error: {run.error}")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@agents.command("stop")
|
|
363
|
+
@click.argument("run_ref")
|
|
364
|
+
@click.confirmation_option(prompt="Are you sure you want to stop this agent run?")
|
|
365
|
+
def stop_agent(run_ref: str) -> None:
|
|
366
|
+
"""Stop a running agent (marks as cancelled, does not kill process)."""
|
|
367
|
+
run_id = resolve_agent_run_id(run_ref)
|
|
368
|
+
manager = get_agent_run_manager()
|
|
369
|
+
run = manager.get(run_id)
|
|
370
|
+
|
|
371
|
+
if not run:
|
|
372
|
+
click.echo(f"Agent run not found: {run_id}", err=True)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
if run.status not in ("pending", "running"):
|
|
376
|
+
click.echo(f"Cannot stop agent in status: {run.status}", err=True)
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
manager.cancel(run.id)
|
|
380
|
+
click.echo(f"Stopped agent run: {run.id}")
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@agents.command("kill")
|
|
384
|
+
@click.argument("run_ref")
|
|
385
|
+
@click.option("--force", "-f", is_flag=True, help="Use SIGKILL immediately")
|
|
386
|
+
@click.option("--stop", "-s", is_flag=True, help="Also end workflow (prevents restart)")
|
|
387
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
|
|
388
|
+
def kill_agent(run_ref: str, force: bool, stop: bool, yes: bool) -> None:
|
|
389
|
+
"""Kill a running agent process.
|
|
390
|
+
|
|
391
|
+
Sends SIGTERM (or SIGKILL with -f) to terminate the agent process.
|
|
392
|
+
Without --stop: workflow may restart the agent in a new terminal.
|
|
393
|
+
With --stop: also ends the workflow (prevents restart).
|
|
394
|
+
|
|
395
|
+
\b
|
|
396
|
+
Examples:
|
|
397
|
+
gobby agents kill abc123 -y # Kill with SIGTERM
|
|
398
|
+
gobby agents kill abc123 -f -y # Force kill with SIGKILL
|
|
399
|
+
gobby agents kill abc123 -s -y # Kill and end workflow
|
|
400
|
+
gobby agents kill abc123 -fs -y # Force kill and end workflow
|
|
401
|
+
"""
|
|
402
|
+
from gobby.utils.daemon_client import DaemonClient
|
|
403
|
+
|
|
404
|
+
run_id = resolve_agent_run_id(run_ref)
|
|
405
|
+
|
|
406
|
+
if not yes:
|
|
407
|
+
msg = "Force kill agent" if force else "Kill agent"
|
|
408
|
+
if stop:
|
|
409
|
+
msg += " and end workflow for"
|
|
410
|
+
if not click.confirm(f"{msg} {run_id[:12]}?"):
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
# Call daemon MCP tool
|
|
414
|
+
client = DaemonClient()
|
|
415
|
+
try:
|
|
416
|
+
result = client.call_mcp_tool(
|
|
417
|
+
server_name="gobby-agents",
|
|
418
|
+
tool_name="kill_agent",
|
|
419
|
+
arguments={
|
|
420
|
+
"run_id": run_id,
|
|
421
|
+
"force": force,
|
|
422
|
+
"stop": stop,
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
click.echo(f"Error: {e}", err=True)
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
if result.get("success"):
|
|
430
|
+
msg = result.get("message", f"Killed agent {run_id}")
|
|
431
|
+
click.echo(msg)
|
|
432
|
+
if result.get("found_via") == "pgrep":
|
|
433
|
+
click.echo(f" (found via pgrep, PID {result.get('pid')})")
|
|
434
|
+
if result.get("already_dead"):
|
|
435
|
+
click.echo(" (process was already terminated)")
|
|
436
|
+
if result.get("workflow_stopped"):
|
|
437
|
+
click.echo(" (workflow ended)")
|
|
438
|
+
else:
|
|
439
|
+
click.echo(f"Failed: {result.get('error')}", err=True)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@agents.command("stats")
|
|
443
|
+
@click.option("--session", "-s", "session_id", help="Filter by parent session ID")
|
|
444
|
+
def agent_stats(session_id: str | None) -> None:
|
|
445
|
+
"""Show agent run statistics."""
|
|
446
|
+
db = LocalDatabase()
|
|
447
|
+
|
|
448
|
+
if session_id:
|
|
449
|
+
try:
|
|
450
|
+
session_id = resolve_session_id(session_id)
|
|
451
|
+
except click.ClickException as e:
|
|
452
|
+
raise SystemExit(1) from e
|
|
453
|
+
manager = get_agent_run_manager()
|
|
454
|
+
counts = manager.count_by_session(session_id)
|
|
455
|
+
total = sum(counts.values())
|
|
456
|
+
|
|
457
|
+
click.echo(f"Agent Statistics for session {session_id[:12]}:")
|
|
458
|
+
click.echo(f" Total Runs: {total}")
|
|
459
|
+
else:
|
|
460
|
+
# Global stats
|
|
461
|
+
row = db.fetchone(
|
|
462
|
+
"""
|
|
463
|
+
SELECT
|
|
464
|
+
COUNT(*) as total,
|
|
465
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success,
|
|
466
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
467
|
+
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running,
|
|
468
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
469
|
+
SUM(CASE WHEN status = 'timeout' THEN 1 ELSE 0 END) as timeout,
|
|
470
|
+
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled
|
|
471
|
+
FROM agent_runs
|
|
472
|
+
"""
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if row:
|
|
476
|
+
click.echo("Agent Run Statistics:")
|
|
477
|
+
click.echo(f" Total Runs: {row['total']}")
|
|
478
|
+
click.echo(f" Running: {row['running']}")
|
|
479
|
+
click.echo(f" Pending: {row['pending']}")
|
|
480
|
+
click.echo(f" Success: {row['success']}")
|
|
481
|
+
click.echo(f" Error: {row['error']}")
|
|
482
|
+
click.echo(f" Timeout: {row['timeout']}")
|
|
483
|
+
click.echo(f" Cancelled: {row['cancelled']}")
|
|
484
|
+
|
|
485
|
+
if row["total"] > 0:
|
|
486
|
+
success_rate = (row["success"] / row["total"]) * 100
|
|
487
|
+
click.echo(f"\n Success Rate: {success_rate:.1f}%")
|
|
488
|
+
else:
|
|
489
|
+
click.echo("No agent runs found.")
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@agents.command("cleanup")
|
|
493
|
+
@click.option("--timeout", "-t", default=30, help="Timeout in minutes for stale runs")
|
|
494
|
+
@click.option("--dry-run", "-d", is_flag=True, help="Show what would be cleaned up")
|
|
495
|
+
def cleanup_agents(timeout: int, dry_run: bool) -> None:
|
|
496
|
+
"""Clean up stale agent runs."""
|
|
497
|
+
manager = get_agent_run_manager()
|
|
498
|
+
|
|
499
|
+
if dry_run:
|
|
500
|
+
# Show what would be cleaned up
|
|
501
|
+
db = LocalDatabase()
|
|
502
|
+
stale_running = db.fetchall(
|
|
503
|
+
"""
|
|
504
|
+
SELECT * FROM agent_runs
|
|
505
|
+
WHERE status = 'running'
|
|
506
|
+
AND datetime(started_at) < datetime('now', 'utc', ? || ' minutes')
|
|
507
|
+
""",
|
|
508
|
+
(f"-{timeout}",),
|
|
509
|
+
)
|
|
510
|
+
stale_pending = db.fetchall(
|
|
511
|
+
"""
|
|
512
|
+
SELECT * FROM agent_runs
|
|
513
|
+
WHERE status = 'pending'
|
|
514
|
+
AND datetime(created_at) < datetime('now', 'utc', '-60 minutes')
|
|
515
|
+
"""
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
click.echo(f"Stale running runs (>{timeout}m): {len(stale_running)}")
|
|
519
|
+
for row in stale_running[:5]:
|
|
520
|
+
click.echo(f" {row['id']}: started {row['started_at']}")
|
|
521
|
+
|
|
522
|
+
click.echo(f"Stale pending runs (>60m): {len(stale_pending)}")
|
|
523
|
+
for row in stale_pending[:5]:
|
|
524
|
+
click.echo(f" {row['id']}: created {row['created_at']}")
|
|
525
|
+
else:
|
|
526
|
+
timed_out = manager.cleanup_stale_runs(timeout_minutes=timeout)
|
|
527
|
+
failed = manager.cleanup_stale_pending_runs(timeout_minutes=60)
|
|
528
|
+
|
|
529
|
+
click.echo(f"Cleaned up {timed_out} timed-out runs and {failed} stale pending runs.")
|