gobby 0.2.8__py3-none-any.whl → 0.2.11__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 +6 -0
- gobby/adapters/base.py +11 -2
- gobby/adapters/claude_code.py +5 -28
- gobby/adapters/codex_impl/adapter.py +38 -43
- gobby/adapters/copilot.py +324 -0
- gobby/adapters/cursor.py +373 -0
- gobby/adapters/gemini.py +2 -26
- gobby/adapters/windsurf.py +359 -0
- gobby/agents/definitions.py +162 -2
- gobby/agents/isolation.py +33 -1
- gobby/agents/pty_reader.py +192 -0
- gobby/agents/registry.py +10 -1
- gobby/agents/runner.py +24 -8
- gobby/agents/sandbox.py +8 -3
- gobby/agents/session.py +4 -0
- gobby/agents/spawn.py +9 -2
- gobby/agents/spawn_executor.py +49 -61
- gobby/agents/spawners/command_builder.py +4 -4
- gobby/app_context.py +64 -0
- gobby/cli/__init__.py +4 -0
- gobby/cli/install.py +259 -4
- gobby/cli/installers/__init__.py +12 -0
- gobby/cli/installers/copilot.py +242 -0
- gobby/cli/installers/cursor.py +244 -0
- gobby/cli/installers/shared.py +3 -0
- gobby/cli/installers/windsurf.py +242 -0
- gobby/cli/pipelines.py +639 -0
- gobby/cli/sessions.py +3 -1
- gobby/cli/skills.py +209 -0
- gobby/cli/tasks/crud.py +6 -5
- gobby/cli/tasks/search.py +1 -1
- gobby/cli/ui.py +116 -0
- gobby/cli/utils.py +5 -17
- gobby/cli/workflows.py +38 -17
- gobby/config/app.py +5 -0
- gobby/config/features.py +0 -20
- gobby/config/skills.py +23 -2
- gobby/config/tasks.py +4 -0
- gobby/hooks/broadcaster.py +9 -0
- gobby/hooks/event_handlers/__init__.py +155 -0
- gobby/hooks/event_handlers/_agent.py +175 -0
- gobby/hooks/event_handlers/_base.py +92 -0
- gobby/hooks/event_handlers/_misc.py +66 -0
- gobby/hooks/event_handlers/_session.py +487 -0
- gobby/hooks/event_handlers/_tool.py +196 -0
- gobby/hooks/events.py +48 -0
- gobby/hooks/hook_manager.py +27 -3
- gobby/install/copilot/hooks/hook_dispatcher.py +203 -0
- gobby/install/cursor/hooks/hook_dispatcher.py +203 -0
- gobby/install/gemini/hooks/hook_dispatcher.py +8 -0
- gobby/install/windsurf/hooks/hook_dispatcher.py +205 -0
- gobby/llm/__init__.py +14 -1
- gobby/llm/claude.py +594 -43
- gobby/llm/service.py +149 -0
- gobby/mcp_proxy/importer.py +4 -41
- gobby/mcp_proxy/instructions.py +9 -27
- gobby/mcp_proxy/manager.py +13 -3
- gobby/mcp_proxy/models.py +1 -0
- gobby/mcp_proxy/registries.py +66 -5
- gobby/mcp_proxy/server.py +6 -2
- gobby/mcp_proxy/services/recommendation.py +2 -28
- gobby/mcp_proxy/services/tool_filter.py +7 -0
- gobby/mcp_proxy/services/tool_proxy.py +19 -1
- gobby/mcp_proxy/stdio.py +37 -21
- gobby/mcp_proxy/tools/agents.py +7 -0
- gobby/mcp_proxy/tools/artifacts.py +3 -3
- gobby/mcp_proxy/tools/hub.py +30 -1
- gobby/mcp_proxy/tools/orchestration/cleanup.py +5 -5
- gobby/mcp_proxy/tools/orchestration/monitor.py +1 -1
- gobby/mcp_proxy/tools/orchestration/orchestrate.py +8 -3
- gobby/mcp_proxy/tools/orchestration/review.py +17 -4
- gobby/mcp_proxy/tools/orchestration/wait.py +7 -7
- gobby/mcp_proxy/tools/pipelines/__init__.py +254 -0
- gobby/mcp_proxy/tools/pipelines/_discovery.py +67 -0
- gobby/mcp_proxy/tools/pipelines/_execution.py +281 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +4 -4
- gobby/mcp_proxy/tools/sessions/_handoff.py +1 -1
- gobby/mcp_proxy/tools/skills/__init__.py +184 -30
- gobby/mcp_proxy/tools/spawn_agent.py +229 -14
- gobby/mcp_proxy/tools/task_readiness.py +27 -4
- gobby/mcp_proxy/tools/tasks/_context.py +8 -0
- gobby/mcp_proxy/tools/tasks/_crud.py +27 -1
- gobby/mcp_proxy/tools/tasks/_helpers.py +1 -1
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +125 -8
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +2 -1
- gobby/mcp_proxy/tools/tasks/_search.py +1 -1
- gobby/mcp_proxy/tools/workflows/__init__.py +273 -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 +332 -0
- gobby/mcp_proxy/tools/workflows/_query.py +226 -0
- gobby/mcp_proxy/tools/workflows/_resolution.py +78 -0
- gobby/mcp_proxy/tools/workflows/_terminal.py +175 -0
- gobby/mcp_proxy/tools/worktrees.py +54 -15
- gobby/memory/components/__init__.py +0 -0
- gobby/memory/components/ingestion.py +98 -0
- gobby/memory/components/search.py +108 -0
- gobby/memory/context.py +5 -5
- gobby/memory/manager.py +16 -25
- gobby/paths.py +51 -0
- gobby/prompts/loader.py +1 -35
- gobby/runner.py +131 -16
- gobby/servers/http.py +193 -150
- gobby/servers/routes/__init__.py +2 -0
- gobby/servers/routes/admin.py +56 -0
- gobby/servers/routes/mcp/endpoints/execution.py +33 -32
- gobby/servers/routes/mcp/endpoints/registry.py +8 -8
- gobby/servers/routes/mcp/hooks.py +10 -1
- gobby/servers/routes/pipelines.py +227 -0
- gobby/servers/websocket.py +314 -1
- gobby/sessions/analyzer.py +89 -3
- gobby/sessions/manager.py +5 -5
- gobby/sessions/transcripts/__init__.py +3 -0
- gobby/sessions/transcripts/claude.py +5 -0
- gobby/sessions/transcripts/codex.py +5 -0
- gobby/sessions/transcripts/gemini.py +5 -0
- gobby/skills/hubs/__init__.py +25 -0
- gobby/skills/hubs/base.py +234 -0
- gobby/skills/hubs/claude_plugins.py +328 -0
- gobby/skills/hubs/clawdhub.py +289 -0
- gobby/skills/hubs/github_collection.py +465 -0
- gobby/skills/hubs/manager.py +263 -0
- gobby/skills/hubs/skillhub.py +342 -0
- gobby/skills/parser.py +23 -0
- gobby/skills/sync.py +5 -4
- gobby/storage/artifacts.py +19 -0
- gobby/storage/memories.py +4 -4
- gobby/storage/migrations.py +118 -3
- gobby/storage/pipelines.py +367 -0
- gobby/storage/sessions.py +23 -4
- gobby/storage/skills.py +48 -8
- gobby/storage/tasks/_aggregates.py +2 -2
- gobby/storage/tasks/_lifecycle.py +4 -4
- gobby/storage/tasks/_models.py +7 -1
- gobby/storage/tasks/_queries.py +3 -3
- gobby/sync/memories.py +4 -3
- gobby/tasks/commits.py +48 -17
- 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 +80 -0
- gobby/workflows/context_actions.py +265 -27
- gobby/workflows/definitions.py +119 -1
- gobby/workflows/detection_helpers.py +23 -11
- gobby/workflows/enforcement/__init__.py +11 -1
- gobby/workflows/enforcement/blocking.py +96 -0
- gobby/workflows/enforcement/handlers.py +35 -1
- gobby/workflows/enforcement/task_policy.py +18 -0
- gobby/workflows/engine.py +26 -4
- gobby/workflows/evaluator.py +8 -5
- gobby/workflows/lifecycle_evaluator.py +59 -27
- gobby/workflows/loader.py +567 -30
- gobby/workflows/lobster_compat.py +147 -0
- gobby/workflows/pipeline_executor.py +801 -0
- gobby/workflows/pipeline_state.py +172 -0
- gobby/workflows/pipeline_webhooks.py +206 -0
- gobby/workflows/premature_stop.py +5 -0
- gobby/worktrees/git.py +135 -20
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/METADATA +56 -22
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/RECORD +166 -122
- gobby/hooks/event_handlers.py +0 -1008
- gobby/mcp_proxy/tools/workflows.py +0 -1023
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/WHEEL +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.8.dist-info → gobby-0.2.11.dist-info}/top_level.txt +0 -0
gobby/cli/skills.py
CHANGED
|
@@ -239,11 +239,15 @@ def install(ctx: click.Context, source: str, project: bool) -> None:
|
|
|
239
239
|
"""Install a skill from a source.
|
|
240
240
|
|
|
241
241
|
SOURCE can be:
|
|
242
|
+
- A hub reference (e.g., clawdhub:commit-message, skillhub:code-review)
|
|
242
243
|
- A local directory path (e.g., ./my-skill or /path/to/skill)
|
|
243
244
|
- A path to a SKILL.md file (e.g., ./SKILL.md)
|
|
244
245
|
- A GitHub URL (owner/repo, github:owner/repo, https://github.com/owner/repo)
|
|
245
246
|
- A ZIP archive path (e.g., ./skills.zip)
|
|
246
247
|
|
|
248
|
+
Use 'gobby skills hub list' to see available hubs.
|
|
249
|
+
Use 'gobby skills search <query>' to find skills.
|
|
250
|
+
|
|
247
251
|
Use --project to scope the skill to the current project.
|
|
248
252
|
|
|
249
253
|
Requires daemon to be running.
|
|
@@ -856,3 +860,208 @@ def disable(ctx: click.Context, name: str) -> None:
|
|
|
856
860
|
click.echo(f"Error disabling skill: {e}", err=True)
|
|
857
861
|
sys.exit(1)
|
|
858
862
|
click.echo(f"Disabled skill: {name}")
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
@skills.command()
|
|
866
|
+
@click.argument("query")
|
|
867
|
+
@click.option("--hub", "-h", "hub_name", default=None, help="Search only in specific hub")
|
|
868
|
+
@click.option("--limit", "-n", default=20, help="Maximum results to show")
|
|
869
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
870
|
+
@click.pass_context
|
|
871
|
+
def search(
|
|
872
|
+
ctx: click.Context,
|
|
873
|
+
query: str,
|
|
874
|
+
hub_name: str | None,
|
|
875
|
+
limit: int,
|
|
876
|
+
json_output: bool,
|
|
877
|
+
) -> None:
|
|
878
|
+
"""Search for skills across configured hubs.
|
|
879
|
+
|
|
880
|
+
QUERY is the search term (e.g., 'commit message', 'code review').
|
|
881
|
+
|
|
882
|
+
Use --hub to search only in a specific hub.
|
|
883
|
+
|
|
884
|
+
Requires daemon to be running.
|
|
885
|
+
"""
|
|
886
|
+
client = get_daemon_client(ctx)
|
|
887
|
+
if not check_daemon(client):
|
|
888
|
+
sys.exit(1)
|
|
889
|
+
|
|
890
|
+
arguments: dict[str, Any] = {"query": query, "limit": limit}
|
|
891
|
+
if hub_name:
|
|
892
|
+
arguments["hub_name"] = hub_name
|
|
893
|
+
|
|
894
|
+
result = call_skills_tool(client, "search_hub", arguments)
|
|
895
|
+
|
|
896
|
+
if result is None:
|
|
897
|
+
click.echo("Error: Failed to communicate with daemon", err=True)
|
|
898
|
+
sys.exit(1)
|
|
899
|
+
elif not result.get("success"):
|
|
900
|
+
click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
|
|
901
|
+
sys.exit(1)
|
|
902
|
+
|
|
903
|
+
results_list = result.get("results", [])
|
|
904
|
+
|
|
905
|
+
if json_output:
|
|
906
|
+
click.echo(json.dumps(results_list, indent=2))
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
if not results_list:
|
|
910
|
+
click.echo("No skills found matching your query.")
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
click.echo(f"Found {len(results_list)} skill(s):\n")
|
|
914
|
+
for skill in results_list:
|
|
915
|
+
hub = skill.get("hub_name", "unknown")
|
|
916
|
+
slug = skill.get("slug", "unknown")
|
|
917
|
+
name = skill.get("display_name", slug)
|
|
918
|
+
desc = skill.get("description", "")[:60]
|
|
919
|
+
click.echo(f" [{hub}] {name}")
|
|
920
|
+
if desc:
|
|
921
|
+
click.echo(f" {desc}")
|
|
922
|
+
click.echo(f" Install: gobby skills install {hub}:{slug}")
|
|
923
|
+
click.echo("")
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
# Hub subcommand group
|
|
927
|
+
@skills.group()
|
|
928
|
+
def hub() -> None:
|
|
929
|
+
"""Manage skill hubs (registries)."""
|
|
930
|
+
pass
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
@hub.command("list")
|
|
934
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
935
|
+
@click.pass_context
|
|
936
|
+
def hub_list(ctx: click.Context, json_output: bool) -> None:
|
|
937
|
+
"""List configured skill hubs.
|
|
938
|
+
|
|
939
|
+
Shows all configured skill hubs with their type and status.
|
|
940
|
+
|
|
941
|
+
Requires daemon to be running.
|
|
942
|
+
"""
|
|
943
|
+
client = get_daemon_client(ctx)
|
|
944
|
+
if not check_daemon(client):
|
|
945
|
+
sys.exit(1)
|
|
946
|
+
|
|
947
|
+
result = call_skills_tool(client, "list_hubs", {})
|
|
948
|
+
|
|
949
|
+
if result is None:
|
|
950
|
+
click.echo("Error: Failed to communicate with daemon", err=True)
|
|
951
|
+
sys.exit(1)
|
|
952
|
+
elif not result.get("success"):
|
|
953
|
+
click.echo(f"Error: {result.get('error', 'Unknown error')}", err=True)
|
|
954
|
+
sys.exit(1)
|
|
955
|
+
|
|
956
|
+
hubs_list = result.get("hubs", [])
|
|
957
|
+
|
|
958
|
+
if json_output:
|
|
959
|
+
click.echo(json.dumps(hubs_list, indent=2))
|
|
960
|
+
return
|
|
961
|
+
|
|
962
|
+
if not hubs_list:
|
|
963
|
+
click.echo("No hubs configured.")
|
|
964
|
+
click.echo("\nTo add hubs, update your config.yaml with a 'hubs' section.")
|
|
965
|
+
return
|
|
966
|
+
|
|
967
|
+
click.echo("Configured hubs:\n")
|
|
968
|
+
for h in hubs_list:
|
|
969
|
+
name = h.get("name", "unknown")
|
|
970
|
+
hub_type = h.get("type", "unknown")
|
|
971
|
+
base_url = h.get("base_url", "")
|
|
972
|
+
url_str = f" ({base_url})" if base_url else ""
|
|
973
|
+
click.echo(f" {name} [{hub_type}]{url_str}")
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
@hub.command("add")
|
|
977
|
+
@click.argument("name")
|
|
978
|
+
@click.option("--type", "hub_type", required=True, help="Hub type (clawdhub, skillhub, github)")
|
|
979
|
+
@click.option("--url", "base_url", default=None, help="Base URL for skillhub type")
|
|
980
|
+
@click.option("--repo", default=None, help="GitHub repo (owner/repo) for github type")
|
|
981
|
+
@click.option("--branch", default=None, help="Branch for github type (default: main)")
|
|
982
|
+
@click.option(
|
|
983
|
+
"--auth-key", "auth_key_name", default=None, help="Environment variable name for auth key"
|
|
984
|
+
)
|
|
985
|
+
@click.pass_context
|
|
986
|
+
def hub_add(
|
|
987
|
+
ctx: click.Context,
|
|
988
|
+
name: str,
|
|
989
|
+
hub_type: str,
|
|
990
|
+
base_url: str | None,
|
|
991
|
+
repo: str | None,
|
|
992
|
+
branch: str | None,
|
|
993
|
+
auth_key_name: str | None,
|
|
994
|
+
) -> None:
|
|
995
|
+
"""Add a new skill hub.
|
|
996
|
+
|
|
997
|
+
NAME is the hub name (e.g., 'my-skills', 'company-hub').
|
|
998
|
+
|
|
999
|
+
Hub types:
|
|
1000
|
+
- clawdhub: ClawdHub CLI-based hub
|
|
1001
|
+
- skillhub: REST API-based skill hub (requires --url)
|
|
1002
|
+
- github: GitHub repository collection (requires --repo)
|
|
1003
|
+
|
|
1004
|
+
Examples:
|
|
1005
|
+
gobby skills hub add my-skillhub --type skillhub --url https://skillhub.example.com
|
|
1006
|
+
gobby skills hub add company-skills --type github --repo myorg/skills
|
|
1007
|
+
"""
|
|
1008
|
+
import yaml
|
|
1009
|
+
|
|
1010
|
+
# Validate hub type
|
|
1011
|
+
valid_types = ["clawdhub", "skillhub", "github"]
|
|
1012
|
+
if hub_type not in valid_types:
|
|
1013
|
+
click.echo(
|
|
1014
|
+
f"Error: Invalid hub type '{hub_type}'. Must be one of: {', '.join(valid_types)}",
|
|
1015
|
+
err=True,
|
|
1016
|
+
)
|
|
1017
|
+
sys.exit(1)
|
|
1018
|
+
|
|
1019
|
+
# Validate required options for each type
|
|
1020
|
+
if hub_type == "skillhub" and not base_url:
|
|
1021
|
+
click.echo("Error: --url is required for skillhub type", err=True)
|
|
1022
|
+
sys.exit(1)
|
|
1023
|
+
|
|
1024
|
+
if hub_type == "github" and not repo:
|
|
1025
|
+
click.echo("Error: --repo is required for github type", err=True)
|
|
1026
|
+
sys.exit(1)
|
|
1027
|
+
|
|
1028
|
+
# Build hub config
|
|
1029
|
+
hub_config: dict[str, Any] = {"type": hub_type}
|
|
1030
|
+
if base_url:
|
|
1031
|
+
hub_config["base_url"] = base_url
|
|
1032
|
+
if repo:
|
|
1033
|
+
hub_config["repo"] = repo
|
|
1034
|
+
if branch:
|
|
1035
|
+
hub_config["branch"] = branch
|
|
1036
|
+
if auth_key_name:
|
|
1037
|
+
hub_config["auth_key_name"] = auth_key_name
|
|
1038
|
+
|
|
1039
|
+
# Load existing config
|
|
1040
|
+
config_path = Path.home() / ".gobby" / "config.yaml"
|
|
1041
|
+
if config_path.exists():
|
|
1042
|
+
with open(config_path, encoding="utf-8") as f:
|
|
1043
|
+
config = yaml.safe_load(f) or {}
|
|
1044
|
+
else:
|
|
1045
|
+
config = {}
|
|
1046
|
+
|
|
1047
|
+
# Ensure hubs section exists
|
|
1048
|
+
if "hubs" not in config:
|
|
1049
|
+
config["hubs"] = {}
|
|
1050
|
+
|
|
1051
|
+
# Check if hub already exists
|
|
1052
|
+
if name in config["hubs"]:
|
|
1053
|
+
click.echo(
|
|
1054
|
+
f"Error: Hub '{name}' already exists. Use 'hub remove' first to replace it.", err=True
|
|
1055
|
+
)
|
|
1056
|
+
sys.exit(1)
|
|
1057
|
+
|
|
1058
|
+
# Add the hub
|
|
1059
|
+
config["hubs"][name] = hub_config
|
|
1060
|
+
|
|
1061
|
+
# Write config
|
|
1062
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1063
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
1064
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
1065
|
+
|
|
1066
|
+
click.echo(f"Added hub: {name} [{hub_type}]")
|
|
1067
|
+
click.echo("\nRestart the daemon for changes to take effect: gobby restart")
|
gobby/cli/tasks/crud.py
CHANGED
|
@@ -26,7 +26,7 @@ from gobby.utils.project_context import get_project_context
|
|
|
26
26
|
@click.option(
|
|
27
27
|
"--status",
|
|
28
28
|
"-s",
|
|
29
|
-
help="Filter by status (open, in_progress,
|
|
29
|
+
help="Filter by status (open, in_progress, needs_review, closed, blocked). Comma-separated for multiple.",
|
|
30
30
|
)
|
|
31
31
|
@click.option(
|
|
32
32
|
"--active",
|
|
@@ -266,7 +266,7 @@ def task_stats(project_ref: str | None, json_format: bool) -> None:
|
|
|
266
266
|
# Get counts by status
|
|
267
267
|
all_tasks = manager.list_tasks(project_id=project_id, limit=10000)
|
|
268
268
|
total = len(all_tasks)
|
|
269
|
-
by_status = {"open": 0, "in_progress": 0, "
|
|
269
|
+
by_status = {"open": 0, "in_progress": 0, "needs_review": 0, "closed": 0}
|
|
270
270
|
by_priority = {1: 0, 2: 0, 3: 0}
|
|
271
271
|
by_type: dict[str, int] = {}
|
|
272
272
|
|
|
@@ -302,7 +302,7 @@ def task_stats(project_ref: str | None, json_format: bool) -> None:
|
|
|
302
302
|
click.echo(f" Total: {total}")
|
|
303
303
|
click.echo(f" Open: {by_status.get('open', 0)}")
|
|
304
304
|
click.echo(f" In Progress: {by_status.get('in_progress', 0)}")
|
|
305
|
-
click.echo(f" Review: {by_status.get('
|
|
305
|
+
click.echo(f" Needs Review: {by_status.get('needs_review', 0)}")
|
|
306
306
|
click.echo(f" Closed: {by_status.get('closed', 0)}")
|
|
307
307
|
click.echo(f"\n Ready (no blockers): {ready_count}")
|
|
308
308
|
click.echo(f" Blocked: {blocked_count}")
|
|
@@ -547,9 +547,10 @@ def reopen_task_cmd(task_id: str, reason: str | None) -> None:
|
|
|
547
547
|
# Use standardized ref for errors
|
|
548
548
|
resolved_ref = f"#{resolved.seq_num}" if resolved.seq_num else resolved.id[:8]
|
|
549
549
|
|
|
550
|
-
if resolved.status not in ("closed", "
|
|
550
|
+
if resolved.status not in ("closed", "needs_review"):
|
|
551
551
|
click.echo(
|
|
552
|
-
f"Task {resolved_ref} is not closed or in
|
|
552
|
+
f"Task {resolved_ref} is not closed or in needs_review (status: {resolved.status})",
|
|
553
|
+
err=True,
|
|
553
554
|
)
|
|
554
555
|
return
|
|
555
556
|
|
gobby/cli/tasks/search.py
CHANGED
|
@@ -18,7 +18,7 @@ from gobby.cli.utils import resolve_project_ref
|
|
|
18
18
|
@click.option(
|
|
19
19
|
"--status",
|
|
20
20
|
"-s",
|
|
21
|
-
help="Filter by status (open, in_progress,
|
|
21
|
+
help="Filter by status (open, in_progress, needs_review, closed). Comma-separated for multiple.",
|
|
22
22
|
)
|
|
23
23
|
@click.option(
|
|
24
24
|
"--type",
|
gobby/cli/ui.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for Gobby web UI development.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import subprocess # nosec B404 - subprocess needed for npm commands
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
# Path to web UI directory
|
|
12
|
+
WEB_UI_DIR = Path(__file__).parent.parent / "ui" / "web"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group()
|
|
16
|
+
def ui() -> None:
|
|
17
|
+
"""Web UI development commands."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@ui.command()
|
|
22
|
+
@click.option("--port", "-p", default=5173, help="Dev server port")
|
|
23
|
+
@click.option("--host", "-h", default="localhost", help="Dev server host")
|
|
24
|
+
def dev(port: int, host: str) -> None:
|
|
25
|
+
"""Start the web UI development server with hot-reload."""
|
|
26
|
+
if not WEB_UI_DIR.exists():
|
|
27
|
+
click.echo(f"Error: Web UI directory not found at {WEB_UI_DIR}", err=True)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
package_json = WEB_UI_DIR / "package.json"
|
|
31
|
+
if not package_json.exists():
|
|
32
|
+
click.echo(f"Error: package.json not found at {package_json}", err=True)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
node_modules = WEB_UI_DIR / "node_modules"
|
|
36
|
+
if not node_modules.exists():
|
|
37
|
+
click.echo("Installing dependencies...")
|
|
38
|
+
result = subprocess.run( # nosec B603 B607
|
|
39
|
+
["npm", "install"],
|
|
40
|
+
cwd=WEB_UI_DIR,
|
|
41
|
+
capture_output=False,
|
|
42
|
+
)
|
|
43
|
+
if result.returncode != 0:
|
|
44
|
+
click.echo("Failed to install dependencies", err=True)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
|
|
47
|
+
click.echo(f"Starting dev server at http://{host}:{port}")
|
|
48
|
+
click.echo("Press Ctrl+C to stop")
|
|
49
|
+
click.echo()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
subprocess.run( # nosec B603 B607
|
|
53
|
+
["npm", "run", "dev", "--", "--host", host, "--port", str(port)],
|
|
54
|
+
cwd=WEB_UI_DIR,
|
|
55
|
+
check=True,
|
|
56
|
+
)
|
|
57
|
+
except KeyboardInterrupt:
|
|
58
|
+
click.echo("\nDev server stopped")
|
|
59
|
+
except subprocess.CalledProcessError as e:
|
|
60
|
+
click.echo(f"Dev server failed with code {e.returncode}", err=True)
|
|
61
|
+
sys.exit(e.returncode)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@ui.command()
|
|
65
|
+
def build() -> None:
|
|
66
|
+
"""Build the web UI for production."""
|
|
67
|
+
if not WEB_UI_DIR.exists():
|
|
68
|
+
click.echo(f"Error: Web UI directory not found at {WEB_UI_DIR}", err=True)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
node_modules = WEB_UI_DIR / "node_modules"
|
|
72
|
+
if not node_modules.exists():
|
|
73
|
+
click.echo("Installing dependencies...")
|
|
74
|
+
result = subprocess.run( # nosec B603 B607
|
|
75
|
+
["npm", "install"],
|
|
76
|
+
cwd=WEB_UI_DIR,
|
|
77
|
+
capture_output=False,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
click.echo("Failed to install dependencies", err=True)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
click.echo("Building web UI...")
|
|
84
|
+
result = subprocess.run( # nosec B603 B607
|
|
85
|
+
["npm", "run", "build"],
|
|
86
|
+
cwd=WEB_UI_DIR,
|
|
87
|
+
capture_output=False,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if result.returncode == 0:
|
|
91
|
+
dist_dir = WEB_UI_DIR / "dist"
|
|
92
|
+
click.echo(f"Build complete: {dist_dir}")
|
|
93
|
+
else:
|
|
94
|
+
click.echo("Build failed", err=True)
|
|
95
|
+
sys.exit(result.returncode)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@ui.command()
|
|
99
|
+
def install_deps() -> None:
|
|
100
|
+
"""Install web UI dependencies."""
|
|
101
|
+
if not WEB_UI_DIR.exists():
|
|
102
|
+
click.echo(f"Error: Web UI directory not found at {WEB_UI_DIR}", err=True)
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
|
|
105
|
+
click.echo("Installing dependencies...")
|
|
106
|
+
result = subprocess.run( # nosec B603 B607
|
|
107
|
+
["npm", "install"],
|
|
108
|
+
cwd=WEB_UI_DIR,
|
|
109
|
+
capture_output=False,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if result.returncode == 0:
|
|
113
|
+
click.echo("Dependencies installed")
|
|
114
|
+
else:
|
|
115
|
+
click.echo("Failed to install dependencies", err=True)
|
|
116
|
+
sys.exit(result.returncode)
|
gobby/cli/utils.py
CHANGED
|
@@ -394,23 +394,10 @@ def get_install_dir() -> Path:
|
|
|
394
394
|
Returns:
|
|
395
395
|
Path to the install directory
|
|
396
396
|
"""
|
|
397
|
-
|
|
397
|
+
# Import from centralized paths module to avoid duplication
|
|
398
|
+
from gobby.paths import get_install_dir as _get_install_dir
|
|
398
399
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
# Try to find source directory (project root)
|
|
402
|
-
current = Path(gobby.__file__).resolve()
|
|
403
|
-
source_install_dir = None
|
|
404
|
-
|
|
405
|
-
for parent in current.parents:
|
|
406
|
-
potential_source = parent / "src" / "gobby" / "install"
|
|
407
|
-
if potential_source.exists():
|
|
408
|
-
source_install_dir = potential_source
|
|
409
|
-
break
|
|
410
|
-
|
|
411
|
-
if source_install_dir and source_install_dir.exists():
|
|
412
|
-
return source_install_dir
|
|
413
|
-
return package_install_dir
|
|
400
|
+
return _get_install_dir()
|
|
414
401
|
|
|
415
402
|
|
|
416
403
|
def _is_process_alive(pid: int) -> bool:
|
|
@@ -472,7 +459,8 @@ def stop_daemon(quiet: bool = False) -> bool:
|
|
|
472
459
|
click.echo(f"Sent shutdown signal to Gobby daemon (PID {pid})")
|
|
473
460
|
|
|
474
461
|
# Wait for graceful shutdown
|
|
475
|
-
|
|
462
|
+
# Match daemon's uvicorn timeout_graceful_shutdown (15s) + buffer
|
|
463
|
+
max_wait = 20
|
|
476
464
|
for _ in range(max_wait * 10):
|
|
477
465
|
time.sleep(0.1)
|
|
478
466
|
if not _is_process_alive(pid):
|
gobby/cli/workflows.py
CHANGED
|
@@ -11,6 +11,7 @@ import yaml
|
|
|
11
11
|
|
|
12
12
|
from gobby.cli.utils import resolve_session_id
|
|
13
13
|
from gobby.storage.database import LocalDatabase
|
|
14
|
+
from gobby.workflows.definitions import WorkflowDefinition
|
|
14
15
|
from gobby.workflows.loader import WorkflowLoader
|
|
15
16
|
from gobby.workflows.state_manager import WorkflowStateManager
|
|
16
17
|
|
|
@@ -151,23 +152,34 @@ def show_workflow(ctx: click.Context, name: str, json_format: bool) -> None:
|
|
|
151
152
|
|
|
152
153
|
if definition.steps:
|
|
153
154
|
click.echo(f"\nSteps ({len(definition.steps)}):")
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if step.allowed_tools
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
155
|
+
if isinstance(definition, WorkflowDefinition):
|
|
156
|
+
for step in definition.steps:
|
|
157
|
+
click.echo(f" - {step.name}")
|
|
158
|
+
if step.description:
|
|
159
|
+
click.echo(f" {step.description}")
|
|
160
|
+
if step.allowed_tools:
|
|
161
|
+
if step.allowed_tools == "all":
|
|
162
|
+
click.echo(" Allowed tools: all")
|
|
163
|
+
else:
|
|
164
|
+
tools = step.allowed_tools[:5]
|
|
165
|
+
more = (
|
|
166
|
+
f" (+{len(step.allowed_tools) - 5})"
|
|
167
|
+
if len(step.allowed_tools) > 5
|
|
168
|
+
else ""
|
|
169
|
+
)
|
|
170
|
+
click.echo(f" Allowed tools: {', '.join(tools)}{more}")
|
|
171
|
+
if step.blocked_tools:
|
|
172
|
+
click.echo(f" Blocked tools: {', '.join(step.blocked_tools[:5])}")
|
|
173
|
+
else:
|
|
174
|
+
# PipelineDefinition
|
|
175
|
+
for pstep in definition.steps:
|
|
176
|
+
click.echo(f" - {pstep.id}")
|
|
177
|
+
if pstep.exec:
|
|
178
|
+
click.echo(f" exec: {pstep.exec[:60]}...")
|
|
179
|
+
elif pstep.prompt:
|
|
180
|
+
click.echo(f" prompt: {pstep.prompt[:60]}...")
|
|
181
|
+
|
|
182
|
+
if isinstance(definition, WorkflowDefinition) and definition.triggers:
|
|
171
183
|
click.echo("\nTriggers:")
|
|
172
184
|
for trigger_name, actions in definition.triggers.items():
|
|
173
185
|
click.echo(f" {trigger_name}: {len(actions)} action(s)")
|
|
@@ -271,6 +283,11 @@ def set_workflow(
|
|
|
271
283
|
click.echo("Use 'gobby workflows set' only for step-based workflows.", err=True)
|
|
272
284
|
raise SystemExit(1)
|
|
273
285
|
|
|
286
|
+
if not isinstance(definition, WorkflowDefinition):
|
|
287
|
+
click.echo(f"'{name}' is a pipeline, not a step-based workflow.", err=True)
|
|
288
|
+
click.echo("Use 'gobby pipelines run' for pipelines.", err=True)
|
|
289
|
+
raise SystemExit(1)
|
|
290
|
+
|
|
274
291
|
# Get session
|
|
275
292
|
try:
|
|
276
293
|
session_id = resolve_session_id(session_id)
|
|
@@ -374,6 +391,10 @@ def set_step(ctx: click.Context, step_name: str, session_id: str | None, force:
|
|
|
374
391
|
click.echo(f"Workflow '{state.workflow_name}' not found.", err=True)
|
|
375
392
|
raise SystemExit(1)
|
|
376
393
|
|
|
394
|
+
if not isinstance(definition, WorkflowDefinition):
|
|
395
|
+
click.echo(f"'{state.workflow_name}' is a pipeline, not a step-based workflow.", err=True)
|
|
396
|
+
raise SystemExit(1)
|
|
397
|
+
|
|
377
398
|
if not any(s.name == step_name for s in definition.steps):
|
|
378
399
|
click.echo(f"Step '{step_name}' not found in workflow.", err=True)
|
|
379
400
|
click.echo(f"Available steps: {', '.join(s.name for s in definition.steps)}")
|
gobby/config/app.py
CHANGED
|
@@ -141,6 +141,11 @@ class DaemonConfig(BaseModel):
|
|
|
141
141
|
default=60887,
|
|
142
142
|
description="Port for daemon to listen on",
|
|
143
143
|
)
|
|
144
|
+
bind_host: str = Field(
|
|
145
|
+
default="localhost",
|
|
146
|
+
description="Host/IP to bind servers to. Use 'localhost' for local-only access, "
|
|
147
|
+
"'0.0.0.0' for all interfaces, or a specific IP (e.g., Tailscale IP) for restricted access.",
|
|
148
|
+
)
|
|
144
149
|
daemon_health_check_interval: float = Field(
|
|
145
150
|
default=10.0,
|
|
146
151
|
description="Daemon health check interval in seconds",
|
gobby/config/features.py
CHANGED
|
@@ -23,7 +23,6 @@ __all__ = [
|
|
|
23
23
|
"HookStageConfig",
|
|
24
24
|
"HooksConfig",
|
|
25
25
|
"TaskDescriptionConfig",
|
|
26
|
-
"DEFAULT_IMPORT_MCP_SERVER_PROMPT",
|
|
27
26
|
]
|
|
28
27
|
|
|
29
28
|
|
|
@@ -139,25 +138,6 @@ class RecommendToolsConfig(BaseModel):
|
|
|
139
138
|
)
|
|
140
139
|
|
|
141
140
|
|
|
142
|
-
DEFAULT_IMPORT_MCP_SERVER_PROMPT = """You are an MCP server configuration extractor. Given documentation for an MCP server, extract the configuration needed to connect to it.
|
|
143
|
-
|
|
144
|
-
Return ONLY a valid JSON object (no markdown, no code blocks) with these fields:
|
|
145
|
-
- name: Server name (lowercase, no spaces, use hyphens)
|
|
146
|
-
- transport: "http", "stdio", or "websocket"
|
|
147
|
-
- url: Server URL (required for http/websocket transports)
|
|
148
|
-
- command: Command to run (required for stdio, e.g., "npx", "uv", "node")
|
|
149
|
-
- args: Array of command arguments (for stdio)
|
|
150
|
-
- env: Object of environment variables needed (use placeholder "<YOUR_KEY_NAME>" for secrets)
|
|
151
|
-
- headers: Object of HTTP headers needed (use placeholder "<YOUR_KEY_NAME>" for secrets)
|
|
152
|
-
- instructions: How to obtain any required API keys or setup steps
|
|
153
|
-
|
|
154
|
-
Example stdio server:
|
|
155
|
-
{"name": "filesystem", "transport": "stdio", "command": "npx", "args": ["-y", "@anthropic-ai/filesystem-mcp"], "env": {}, "instructions": "No setup required"}
|
|
156
|
-
|
|
157
|
-
Example http server with API key:
|
|
158
|
-
{"name": "exa", "transport": "http", "url": "https://mcp.exa.ai/mcp", "headers": {"EXA_API_KEY": "<YOUR_EXA_API_KEY>"}, "instructions": "Get your API key from https://exa.ai/dashboard"}"""
|
|
159
|
-
|
|
160
|
-
|
|
161
141
|
class ImportMCPServerConfig(BaseModel):
|
|
162
142
|
"""MCP server import configuration."""
|
|
163
143
|
|
gobby/config/skills.py
CHANGED
|
@@ -16,9 +16,9 @@ class HubConfig(BaseModel):
|
|
|
16
16
|
Configuration for a skill hub or collection.
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
type: Literal["clawdhub", "skillhub", "github-collection"] = Field(
|
|
19
|
+
type: Literal["clawdhub", "skillhub", "github-collection", "claude-plugins"] = Field(
|
|
20
20
|
...,
|
|
21
|
-
description="Type of the hub: 'clawdhub', 'skillhub',
|
|
21
|
+
description="Type of the hub: 'clawdhub', 'skillhub', 'github-collection', or 'claude-plugins'",
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
base_url: str | None = Field(
|
|
@@ -36,6 +36,11 @@ class HubConfig(BaseModel):
|
|
|
36
36
|
description="Git branch to use",
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
+
path: str | None = Field(
|
|
40
|
+
default=None,
|
|
41
|
+
description="Subdirectory path within the repository where skills are located",
|
|
42
|
+
)
|
|
43
|
+
|
|
39
44
|
auth_key_name: str | None = Field(
|
|
40
45
|
default=None,
|
|
41
46
|
description="Environment variable name for auth key",
|
|
@@ -64,6 +69,22 @@ class SkillsConfig(BaseModel):
|
|
|
64
69
|
description="Format for skill injection: 'summary' (names only), 'full' (with content), 'none' (disabled)",
|
|
65
70
|
)
|
|
66
71
|
|
|
72
|
+
hubs: dict[str, HubConfig] = Field(
|
|
73
|
+
default_factory=lambda: {
|
|
74
|
+
"anthropic-skills": HubConfig(
|
|
75
|
+
type="github-collection",
|
|
76
|
+
repo="anthropics/skills",
|
|
77
|
+
branch="main",
|
|
78
|
+
path="skills",
|
|
79
|
+
),
|
|
80
|
+
"claude-plugins": HubConfig(
|
|
81
|
+
type="claude-plugins",
|
|
82
|
+
base_url="https://claude-plugins.dev",
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
description="Configured skill hubs keyed by hub name",
|
|
86
|
+
)
|
|
87
|
+
|
|
67
88
|
@field_validator("injection_format")
|
|
68
89
|
@classmethod
|
|
69
90
|
def validate_injection_format(cls, v: str) -> str:
|
gobby/config/tasks.py
CHANGED
|
@@ -669,6 +669,10 @@ class WorkflowConfig(BaseModel):
|
|
|
669
669
|
default_factory=lambda: ["Edit", "Write", "Update", "NotebookEdit"],
|
|
670
670
|
description="Tools that require an active task when require_task_before_edit is enabled",
|
|
671
671
|
)
|
|
672
|
+
debug_echo_context: bool = Field(
|
|
673
|
+
default=False,
|
|
674
|
+
description="Debug: echo additionalContext to system_message for terminal visibility",
|
|
675
|
+
)
|
|
672
676
|
|
|
673
677
|
@field_validator("timeout")
|
|
674
678
|
@classmethod
|
gobby/hooks/broadcaster.py
CHANGED
|
@@ -117,6 +117,15 @@ class HookEventBroadcaster:
|
|
|
117
117
|
if "permission_type" not in raw_input:
|
|
118
118
|
raw_input["permission_type"] = "unknown"
|
|
119
119
|
|
|
120
|
+
# Ensure 'tool_name' has a default for before_tool_selection events
|
|
121
|
+
# These events fire before a specific tool is selected, so tool_name is not available
|
|
122
|
+
if (
|
|
123
|
+
enum_hook_type == HookType.PRE_TOOL_USE
|
|
124
|
+
and event.event_type.value == "before_tool_selection"
|
|
125
|
+
):
|
|
126
|
+
if "tool_name" not in raw_input:
|
|
127
|
+
raw_input["tool_name"] = "(tool_selection)"
|
|
128
|
+
|
|
120
129
|
# Validate input data structure matches Pydantic model
|
|
121
130
|
# Use construct/model_validate to avoid strict validation errors if possible,
|
|
122
131
|
# or just try/except. Let's rely on standard validation.
|