gobby 0.2.6__py3-none-any.whl → 0.2.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gobby/__init__.py +1 -1
- gobby/adapters/__init__.py +2 -1
- gobby/adapters/codex_impl/__init__.py +28 -0
- gobby/adapters/codex_impl/adapter.py +722 -0
- gobby/adapters/codex_impl/client.py +679 -0
- gobby/adapters/codex_impl/protocol.py +20 -0
- gobby/adapters/codex_impl/types.py +68 -0
- gobby/agents/definitions.py +11 -1
- gobby/agents/isolation.py +395 -0
- gobby/agents/sandbox.py +261 -0
- gobby/agents/spawn.py +42 -287
- gobby/agents/spawn_executor.py +385 -0
- gobby/agents/spawners/__init__.py +24 -0
- gobby/agents/spawners/command_builder.py +189 -0
- gobby/agents/spawners/embedded.py +21 -2
- gobby/agents/spawners/headless.py +21 -2
- gobby/agents/spawners/prompt_manager.py +125 -0
- gobby/cli/install.py +4 -4
- gobby/cli/installers/claude.py +6 -0
- gobby/cli/installers/gemini.py +6 -0
- gobby/cli/installers/shared.py +103 -4
- gobby/cli/sessions.py +1 -1
- gobby/cli/utils.py +9 -2
- gobby/config/__init__.py +12 -97
- gobby/config/app.py +10 -94
- gobby/config/extensions.py +2 -2
- gobby/config/features.py +7 -130
- gobby/config/tasks.py +4 -28
- gobby/hooks/__init__.py +0 -13
- gobby/hooks/event_handlers.py +45 -2
- gobby/hooks/hook_manager.py +2 -2
- gobby/hooks/plugins.py +1 -1
- gobby/hooks/webhooks.py +1 -1
- gobby/llm/resolver.py +3 -2
- gobby/mcp_proxy/importer.py +62 -4
- gobby/mcp_proxy/instructions.py +2 -0
- gobby/mcp_proxy/registries.py +1 -4
- gobby/mcp_proxy/services/recommendation.py +43 -11
- gobby/mcp_proxy/tools/agents.py +31 -731
- gobby/mcp_proxy/tools/clones.py +0 -385
- gobby/mcp_proxy/tools/memory.py +2 -2
- gobby/mcp_proxy/tools/sessions/__init__.py +14 -0
- gobby/mcp_proxy/tools/sessions/_commits.py +232 -0
- gobby/mcp_proxy/tools/sessions/_crud.py +253 -0
- gobby/mcp_proxy/tools/sessions/_factory.py +63 -0
- gobby/mcp_proxy/tools/sessions/_handoff.py +499 -0
- gobby/mcp_proxy/tools/sessions/_messages.py +138 -0
- gobby/mcp_proxy/tools/skills/__init__.py +14 -29
- gobby/mcp_proxy/tools/spawn_agent.py +417 -0
- gobby/mcp_proxy/tools/tasks/_lifecycle.py +52 -18
- gobby/mcp_proxy/tools/tasks/_lifecycle_validation.py +1 -1
- gobby/mcp_proxy/tools/worktrees.py +0 -343
- gobby/memory/ingestion/__init__.py +5 -0
- gobby/memory/ingestion/multimodal.py +221 -0
- gobby/memory/manager.py +62 -283
- gobby/memory/search/__init__.py +10 -0
- gobby/memory/search/coordinator.py +248 -0
- gobby/memory/services/__init__.py +5 -0
- gobby/memory/services/crossref.py +142 -0
- gobby/prompts/loader.py +5 -2
- gobby/servers/http.py +1 -4
- gobby/servers/routes/admin.py +14 -0
- gobby/servers/routes/mcp/endpoints/__init__.py +61 -0
- gobby/servers/routes/mcp/endpoints/discovery.py +405 -0
- gobby/servers/routes/mcp/endpoints/execution.py +568 -0
- gobby/servers/routes/mcp/endpoints/registry.py +378 -0
- gobby/servers/routes/mcp/endpoints/server.py +304 -0
- gobby/servers/routes/mcp/hooks.py +1 -1
- gobby/servers/routes/mcp/tools.py +48 -1506
- gobby/sessions/lifecycle.py +1 -1
- gobby/sessions/processor.py +10 -0
- gobby/sessions/transcripts/base.py +1 -0
- gobby/sessions/transcripts/claude.py +15 -5
- gobby/skills/parser.py +30 -2
- gobby/storage/migrations.py +159 -372
- gobby/storage/sessions.py +43 -7
- gobby/storage/skills.py +37 -4
- gobby/storage/tasks/_lifecycle.py +18 -3
- gobby/sync/memories.py +1 -1
- gobby/tasks/external_validator.py +1 -1
- gobby/tasks/validation.py +22 -20
- gobby/tools/summarizer.py +91 -10
- gobby/utils/project_context.py +2 -3
- gobby/utils/status.py +13 -0
- gobby/workflows/actions.py +221 -1217
- gobby/workflows/artifact_actions.py +31 -0
- gobby/workflows/autonomous_actions.py +11 -0
- gobby/workflows/context_actions.py +50 -1
- gobby/workflows/enforcement/__init__.py +47 -0
- gobby/workflows/enforcement/blocking.py +269 -0
- gobby/workflows/enforcement/commit_policy.py +283 -0
- gobby/workflows/enforcement/handlers.py +269 -0
- gobby/workflows/enforcement/task_policy.py +542 -0
- gobby/workflows/git_utils.py +106 -0
- gobby/workflows/llm_actions.py +30 -0
- gobby/workflows/mcp_actions.py +20 -1
- gobby/workflows/memory_actions.py +80 -0
- gobby/workflows/safe_evaluator.py +183 -0
- gobby/workflows/session_actions.py +44 -0
- gobby/workflows/state_actions.py +60 -1
- gobby/workflows/stop_signal_actions.py +55 -0
- gobby/workflows/summary_actions.py +94 -1
- gobby/workflows/task_sync_actions.py +347 -0
- gobby/workflows/todo_actions.py +34 -1
- gobby/workflows/webhook_actions.py +185 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/METADATA +6 -1
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/RECORD +111 -111
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/WHEEL +1 -1
- gobby/adapters/codex.py +0 -1332
- gobby/install/claude/commands/gobby/bug.md +0 -51
- gobby/install/claude/commands/gobby/chore.md +0 -51
- gobby/install/claude/commands/gobby/epic.md +0 -52
- gobby/install/claude/commands/gobby/eval.md +0 -235
- gobby/install/claude/commands/gobby/feat.md +0 -49
- gobby/install/claude/commands/gobby/nit.md +0 -52
- gobby/install/claude/commands/gobby/ref.md +0 -52
- gobby/mcp_proxy/tools/session_messages.py +0 -1055
- gobby/prompts/defaults/expansion/system.md +0 -119
- gobby/prompts/defaults/expansion/user.md +0 -48
- gobby/prompts/defaults/external_validation/agent.md +0 -72
- gobby/prompts/defaults/external_validation/external.md +0 -63
- gobby/prompts/defaults/external_validation/spawn.md +0 -83
- gobby/prompts/defaults/external_validation/system.md +0 -6
- gobby/prompts/defaults/features/import_mcp.md +0 -22
- gobby/prompts/defaults/features/import_mcp_github.md +0 -17
- gobby/prompts/defaults/features/import_mcp_search.md +0 -16
- gobby/prompts/defaults/features/recommend_tools.md +0 -32
- gobby/prompts/defaults/features/recommend_tools_hybrid.md +0 -35
- gobby/prompts/defaults/features/recommend_tools_llm.md +0 -30
- gobby/prompts/defaults/features/server_description.md +0 -20
- gobby/prompts/defaults/features/server_description_system.md +0 -6
- gobby/prompts/defaults/features/task_description.md +0 -31
- gobby/prompts/defaults/features/task_description_system.md +0 -6
- gobby/prompts/defaults/features/tool_summary.md +0 -17
- gobby/prompts/defaults/features/tool_summary_system.md +0 -6
- gobby/prompts/defaults/handoff/compact.md +0 -63
- gobby/prompts/defaults/handoff/session_end.md +0 -57
- gobby/prompts/defaults/memory/extract.md +0 -61
- gobby/prompts/defaults/research/step.md +0 -58
- gobby/prompts/defaults/validation/criteria.md +0 -47
- gobby/prompts/defaults/validation/validate.md +0 -38
- gobby/storage/migrations_legacy.py +0 -1359
- gobby/workflows/task_enforcement_actions.py +0 -1343
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/entry_points.txt +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/licenses/LICENSE.md +0 -0
- {gobby-0.2.6.dist-info → gobby-0.2.7.dist-info}/top_level.txt +0 -0
gobby/mcp_proxy/tools/clones.py
CHANGED
|
@@ -13,13 +13,11 @@ via the downstream proxy pattern (call_tool, list_tools, get_tool_schema).
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
|
-
from pathlib import Path
|
|
17
16
|
from typing import TYPE_CHECKING, Any, Literal
|
|
18
17
|
|
|
19
18
|
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
20
19
|
|
|
21
20
|
if TYPE_CHECKING:
|
|
22
|
-
from gobby.agents.runner import AgentRunner
|
|
23
21
|
from gobby.clones.git import CloneGitManager
|
|
24
22
|
from gobby.storage.clones import LocalCloneManager
|
|
25
23
|
|
|
@@ -30,7 +28,6 @@ def create_clones_registry(
|
|
|
30
28
|
clone_storage: LocalCloneManager,
|
|
31
29
|
git_manager: CloneGitManager,
|
|
32
30
|
project_id: str,
|
|
33
|
-
agent_runner: AgentRunner | None = None,
|
|
34
31
|
) -> InternalToolRegistry:
|
|
35
32
|
"""
|
|
36
33
|
Create the gobby-clones MCP server registry.
|
|
@@ -39,7 +36,6 @@ def create_clones_registry(
|
|
|
39
36
|
clone_storage: Clone storage manager for CRUD operations
|
|
40
37
|
git_manager: Git manager for clone operations
|
|
41
38
|
project_id: Default project ID for new clones
|
|
42
|
-
agent_runner: Optional agent runner for spawning agents in clones
|
|
43
39
|
|
|
44
40
|
Returns:
|
|
45
41
|
InternalToolRegistry with clone management tools
|
|
@@ -519,385 +515,4 @@ def create_clones_registry(
|
|
|
519
515
|
func=merge_clone_to_target,
|
|
520
516
|
)
|
|
521
517
|
|
|
522
|
-
# ===== spawn_agent_in_clone =====
|
|
523
|
-
async def spawn_agent_in_clone(
|
|
524
|
-
prompt: str,
|
|
525
|
-
branch_name: str,
|
|
526
|
-
parent_session_id: str | None = None,
|
|
527
|
-
task_id: str | None = None,
|
|
528
|
-
base_branch: str = "main",
|
|
529
|
-
clone_path: str | None = None,
|
|
530
|
-
mode: str = "terminal",
|
|
531
|
-
terminal: str = "auto",
|
|
532
|
-
provider: Literal["claude", "gemini", "codex", "antigravity"] = "claude",
|
|
533
|
-
model: str | None = None,
|
|
534
|
-
workflow: str | None = None,
|
|
535
|
-
timeout: float = 120.0,
|
|
536
|
-
max_turns: int = 10,
|
|
537
|
-
) -> dict[str, Any]:
|
|
538
|
-
"""
|
|
539
|
-
Create a clone (if needed) and spawn an agent in it.
|
|
540
|
-
|
|
541
|
-
This combines clone creation with agent spawning for isolated development.
|
|
542
|
-
Unlike worktrees, clones are full repository copies that can be worked on
|
|
543
|
-
independently without affecting the main repository.
|
|
544
|
-
|
|
545
|
-
Args:
|
|
546
|
-
prompt: The task/prompt for the agent.
|
|
547
|
-
branch_name: Name for the branch in the clone.
|
|
548
|
-
parent_session_id: Parent session ID for context (required).
|
|
549
|
-
task_id: Optional task ID to link to this clone.
|
|
550
|
-
base_branch: Branch to clone from (default: main).
|
|
551
|
-
clone_path: Optional custom path for the clone.
|
|
552
|
-
mode: Execution mode (terminal, embedded, headless).
|
|
553
|
-
terminal: Terminal for terminal/embedded modes (auto, ghostty, etc.).
|
|
554
|
-
provider: LLM provider (claude, gemini, etc.).
|
|
555
|
-
model: Optional model override.
|
|
556
|
-
workflow: Workflow name to execute.
|
|
557
|
-
timeout: Execution timeout in seconds (default: 120).
|
|
558
|
-
max_turns: Maximum turns (default: 10).
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
Dict with clone_id, run_id, and status.
|
|
562
|
-
"""
|
|
563
|
-
if agent_runner is None:
|
|
564
|
-
return {
|
|
565
|
-
"success": False,
|
|
566
|
-
"error": "Agent runner not configured. Cannot spawn agent.",
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if parent_session_id is None:
|
|
570
|
-
return {
|
|
571
|
-
"success": False,
|
|
572
|
-
"error": "parent_session_id is required for agent spawning.",
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
# Handle mode aliases and validation
|
|
576
|
-
if mode == "interactive":
|
|
577
|
-
mode = "terminal"
|
|
578
|
-
|
|
579
|
-
valid_modes = ["terminal", "embedded", "headless"]
|
|
580
|
-
if mode not in valid_modes:
|
|
581
|
-
return {
|
|
582
|
-
"success": False,
|
|
583
|
-
"error": (
|
|
584
|
-
f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}. "
|
|
585
|
-
f"Note: 'in_process' mode is not supported for spawn_agent_in_clone."
|
|
586
|
-
),
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
# Normalize terminal parameter to lowercase
|
|
590
|
-
if isinstance(terminal, str):
|
|
591
|
-
terminal = terminal.lower()
|
|
592
|
-
|
|
593
|
-
# Check spawn depth limit
|
|
594
|
-
can_spawn, reason, _depth = agent_runner.can_spawn(parent_session_id)
|
|
595
|
-
if not can_spawn:
|
|
596
|
-
return {
|
|
597
|
-
"success": False,
|
|
598
|
-
"error": reason,
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
# Check if clone already exists for this branch
|
|
602
|
-
existing = clone_storage.get_by_branch(project_id, branch_name)
|
|
603
|
-
if existing:
|
|
604
|
-
clone = existing
|
|
605
|
-
logger.info(f"Using existing clone for branch '{branch_name}'")
|
|
606
|
-
else:
|
|
607
|
-
# Get remote URL
|
|
608
|
-
remote_url = git_manager.get_remote_url() if git_manager else None
|
|
609
|
-
if not remote_url:
|
|
610
|
-
return {
|
|
611
|
-
"success": False,
|
|
612
|
-
"error": "No remote URL available. Cannot create clone.",
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
# Generate clone path if not provided
|
|
616
|
-
if clone_path is None:
|
|
617
|
-
import platform
|
|
618
|
-
import tempfile
|
|
619
|
-
|
|
620
|
-
if platform.system() == "Windows":
|
|
621
|
-
base = Path(tempfile.gettempdir()) / "gobby-clones"
|
|
622
|
-
else:
|
|
623
|
-
# nosec B108: /tmp is intentional for clones - they're temporary
|
|
624
|
-
base = Path("/tmp").resolve() / "gobby-clones" # nosec B108
|
|
625
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
626
|
-
safe_branch = branch_name.replace("/", "-")
|
|
627
|
-
clone_path = str(base / f"{project_id}-{safe_branch}")
|
|
628
|
-
|
|
629
|
-
# Create the clone
|
|
630
|
-
result = git_manager.shallow_clone(
|
|
631
|
-
remote_url=remote_url,
|
|
632
|
-
clone_path=clone_path,
|
|
633
|
-
branch=base_branch,
|
|
634
|
-
depth=1,
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
if not result.success:
|
|
638
|
-
return {
|
|
639
|
-
"success": False,
|
|
640
|
-
"error": f"Clone failed: {result.error or result.message}",
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
# Store clone record
|
|
644
|
-
clone = clone_storage.create(
|
|
645
|
-
project_id=project_id,
|
|
646
|
-
branch_name=branch_name,
|
|
647
|
-
clone_path=clone_path,
|
|
648
|
-
base_branch=base_branch,
|
|
649
|
-
task_id=task_id,
|
|
650
|
-
remote_url=remote_url,
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
# Import AgentConfig and get machine_id
|
|
654
|
-
from gobby.agents.runner import AgentConfig
|
|
655
|
-
from gobby.utils.machine_id import get_machine_id
|
|
656
|
-
|
|
657
|
-
machine_id = get_machine_id()
|
|
658
|
-
|
|
659
|
-
# Create agent config
|
|
660
|
-
config = AgentConfig(
|
|
661
|
-
prompt=prompt,
|
|
662
|
-
parent_session_id=parent_session_id,
|
|
663
|
-
project_id=project_id,
|
|
664
|
-
machine_id=machine_id,
|
|
665
|
-
source=provider,
|
|
666
|
-
workflow=workflow,
|
|
667
|
-
task=task_id,
|
|
668
|
-
session_context="summary_markdown",
|
|
669
|
-
mode=mode,
|
|
670
|
-
terminal=terminal,
|
|
671
|
-
provider=provider,
|
|
672
|
-
model=model,
|
|
673
|
-
max_turns=max_turns,
|
|
674
|
-
timeout=timeout,
|
|
675
|
-
project_path=clone.clone_path,
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
# Prepare the run
|
|
679
|
-
from gobby.llm.executor import AgentResult
|
|
680
|
-
|
|
681
|
-
prepare_result = agent_runner.prepare_run(config)
|
|
682
|
-
if isinstance(prepare_result, AgentResult):
|
|
683
|
-
return {
|
|
684
|
-
"success": False,
|
|
685
|
-
"clone_id": clone.id,
|
|
686
|
-
"clone_path": clone.clone_path,
|
|
687
|
-
"branch_name": clone.branch_name,
|
|
688
|
-
"error": prepare_result.error,
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
context = prepare_result
|
|
692
|
-
if context.session is None or context.run is None:
|
|
693
|
-
return {
|
|
694
|
-
"success": False,
|
|
695
|
-
"clone_id": clone.id,
|
|
696
|
-
"error": "Internal error: context missing session or run",
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
child_session = context.session
|
|
700
|
-
agent_run = context.run
|
|
701
|
-
|
|
702
|
-
# Claim clone for the child session
|
|
703
|
-
clone_storage.claim(clone.id, child_session.id)
|
|
704
|
-
|
|
705
|
-
# Build enhanced prompt with clone context
|
|
706
|
-
context_lines = [
|
|
707
|
-
"## CRITICAL: Clone Context",
|
|
708
|
-
"You are working in an ISOLATED git clone, NOT the main repository.",
|
|
709
|
-
"",
|
|
710
|
-
f"**Your workspace:** {clone.clone_path}",
|
|
711
|
-
f"**Your branch:** {clone.branch_name}",
|
|
712
|
-
]
|
|
713
|
-
if task_id:
|
|
714
|
-
context_lines.append(f"**Your task:** {task_id}")
|
|
715
|
-
context_lines.extend(
|
|
716
|
-
[
|
|
717
|
-
"",
|
|
718
|
-
"**IMPORTANT RULES:**",
|
|
719
|
-
f"1. ALL file operations must be within {clone.clone_path}",
|
|
720
|
-
"2. Do NOT access the main repository",
|
|
721
|
-
"3. Run `pwd` to verify your location before any file operations",
|
|
722
|
-
f"4. Commit to YOUR branch ({clone.branch_name})",
|
|
723
|
-
"5. When your assigned task is complete, STOP - do not claim other tasks",
|
|
724
|
-
"",
|
|
725
|
-
"---",
|
|
726
|
-
"",
|
|
727
|
-
]
|
|
728
|
-
)
|
|
729
|
-
enhanced_prompt = "\n".join(context_lines) + prompt
|
|
730
|
-
|
|
731
|
-
# Spawn based on mode
|
|
732
|
-
if mode == "terminal":
|
|
733
|
-
from gobby.agents.spawn import TerminalSpawner
|
|
734
|
-
|
|
735
|
-
terminal_spawner = TerminalSpawner()
|
|
736
|
-
terminal_result = terminal_spawner.spawn_agent(
|
|
737
|
-
cli=provider,
|
|
738
|
-
cwd=clone.clone_path,
|
|
739
|
-
session_id=child_session.id,
|
|
740
|
-
parent_session_id=parent_session_id,
|
|
741
|
-
agent_run_id=agent_run.id,
|
|
742
|
-
project_id=project_id,
|
|
743
|
-
workflow_name=workflow,
|
|
744
|
-
agent_depth=child_session.agent_depth,
|
|
745
|
-
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
746
|
-
terminal=terminal,
|
|
747
|
-
prompt=enhanced_prompt,
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
if not terminal_result.success:
|
|
751
|
-
return {
|
|
752
|
-
"success": False,
|
|
753
|
-
"clone_id": clone.id,
|
|
754
|
-
"clone_path": clone.clone_path,
|
|
755
|
-
"branch_name": clone.branch_name,
|
|
756
|
-
"run_id": agent_run.id,
|
|
757
|
-
"child_session_id": child_session.id,
|
|
758
|
-
"error": terminal_result.error or terminal_result.message,
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
"success": True,
|
|
763
|
-
"clone_id": clone.id,
|
|
764
|
-
"clone_path": clone.clone_path,
|
|
765
|
-
"branch_name": clone.branch_name,
|
|
766
|
-
"run_id": agent_run.id,
|
|
767
|
-
"child_session_id": child_session.id,
|
|
768
|
-
"status": "pending",
|
|
769
|
-
"message": f"Agent spawned in {terminal_result.terminal_type} (PID: {terminal_result.pid})",
|
|
770
|
-
"terminal_type": terminal_result.terminal_type,
|
|
771
|
-
"pid": terminal_result.pid,
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
elif mode == "embedded":
|
|
775
|
-
from gobby.agents.spawn import EmbeddedSpawner
|
|
776
|
-
|
|
777
|
-
embedded_spawner = EmbeddedSpawner()
|
|
778
|
-
embedded_result = embedded_spawner.spawn_agent(
|
|
779
|
-
cli=provider,
|
|
780
|
-
cwd=clone.clone_path,
|
|
781
|
-
session_id=child_session.id,
|
|
782
|
-
parent_session_id=parent_session_id,
|
|
783
|
-
agent_run_id=agent_run.id,
|
|
784
|
-
project_id=project_id,
|
|
785
|
-
workflow_name=workflow,
|
|
786
|
-
agent_depth=child_session.agent_depth,
|
|
787
|
-
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
788
|
-
prompt=enhanced_prompt,
|
|
789
|
-
)
|
|
790
|
-
|
|
791
|
-
return {
|
|
792
|
-
"success": embedded_result.success,
|
|
793
|
-
"clone_id": clone.id,
|
|
794
|
-
"clone_path": clone.clone_path,
|
|
795
|
-
"branch_name": clone.branch_name,
|
|
796
|
-
"run_id": agent_run.id,
|
|
797
|
-
"child_session_id": child_session.id,
|
|
798
|
-
"status": "pending" if embedded_result.success else "error",
|
|
799
|
-
"error": embedded_result.error if not embedded_result.success else None,
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
else: # headless
|
|
803
|
-
from gobby.agents.spawn import HeadlessSpawner
|
|
804
|
-
|
|
805
|
-
headless_spawner = HeadlessSpawner()
|
|
806
|
-
headless_result = headless_spawner.spawn_agent(
|
|
807
|
-
cli=provider,
|
|
808
|
-
cwd=clone.clone_path,
|
|
809
|
-
session_id=child_session.id,
|
|
810
|
-
parent_session_id=parent_session_id,
|
|
811
|
-
agent_run_id=agent_run.id,
|
|
812
|
-
project_id=project_id,
|
|
813
|
-
workflow_name=workflow,
|
|
814
|
-
agent_depth=child_session.agent_depth,
|
|
815
|
-
max_agent_depth=agent_runner._child_session_manager.max_agent_depth,
|
|
816
|
-
prompt=enhanced_prompt,
|
|
817
|
-
)
|
|
818
|
-
|
|
819
|
-
return {
|
|
820
|
-
"success": headless_result.success,
|
|
821
|
-
"clone_id": clone.id,
|
|
822
|
-
"clone_path": clone.clone_path,
|
|
823
|
-
"branch_name": clone.branch_name,
|
|
824
|
-
"run_id": agent_run.id,
|
|
825
|
-
"child_session_id": child_session.id,
|
|
826
|
-
"status": "pending" if headless_result.success else "error",
|
|
827
|
-
"pid": headless_result.pid if headless_result.success else None,
|
|
828
|
-
"error": headless_result.error if not headless_result.success else None,
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
registry.register(
|
|
832
|
-
name="spawn_agent_in_clone",
|
|
833
|
-
description="Create a clone and spawn an agent to work in it",
|
|
834
|
-
input_schema={
|
|
835
|
-
"type": "object",
|
|
836
|
-
"properties": {
|
|
837
|
-
"prompt": {
|
|
838
|
-
"type": "string",
|
|
839
|
-
"description": "The task/prompt for the agent",
|
|
840
|
-
},
|
|
841
|
-
"branch_name": {
|
|
842
|
-
"type": "string",
|
|
843
|
-
"description": "Name for the branch in the clone",
|
|
844
|
-
},
|
|
845
|
-
"parent_session_id": {
|
|
846
|
-
"type": "string",
|
|
847
|
-
"description": "Parent session ID for context (required)",
|
|
848
|
-
},
|
|
849
|
-
"task_id": {
|
|
850
|
-
"type": "string",
|
|
851
|
-
"description": "Optional task ID to link to this clone",
|
|
852
|
-
},
|
|
853
|
-
"base_branch": {
|
|
854
|
-
"type": "string",
|
|
855
|
-
"description": "Branch to clone from",
|
|
856
|
-
"default": "main",
|
|
857
|
-
},
|
|
858
|
-
"clone_path": {
|
|
859
|
-
"type": "string",
|
|
860
|
-
"description": "Optional custom path for the clone",
|
|
861
|
-
},
|
|
862
|
-
"mode": {
|
|
863
|
-
"type": "string",
|
|
864
|
-
"description": "Execution mode",
|
|
865
|
-
"enum": ["terminal", "embedded", "headless"],
|
|
866
|
-
"default": "terminal",
|
|
867
|
-
},
|
|
868
|
-
"terminal": {
|
|
869
|
-
"type": "string",
|
|
870
|
-
"description": "Terminal type for terminal/embedded modes",
|
|
871
|
-
"default": "auto",
|
|
872
|
-
},
|
|
873
|
-
"provider": {
|
|
874
|
-
"type": "string",
|
|
875
|
-
"description": "LLM provider",
|
|
876
|
-
"enum": ["claude", "gemini", "codex", "antigravity"],
|
|
877
|
-
"default": "claude",
|
|
878
|
-
},
|
|
879
|
-
"model": {
|
|
880
|
-
"type": "string",
|
|
881
|
-
"description": "Optional model override",
|
|
882
|
-
},
|
|
883
|
-
"workflow": {
|
|
884
|
-
"type": "string",
|
|
885
|
-
"description": "Workflow name to execute",
|
|
886
|
-
},
|
|
887
|
-
"timeout": {
|
|
888
|
-
"type": "number",
|
|
889
|
-
"description": "Execution timeout in seconds",
|
|
890
|
-
"default": 120.0,
|
|
891
|
-
},
|
|
892
|
-
"max_turns": {
|
|
893
|
-
"type": "integer",
|
|
894
|
-
"description": "Maximum turns",
|
|
895
|
-
"default": 10,
|
|
896
|
-
},
|
|
897
|
-
},
|
|
898
|
-
"required": ["prompt", "branch_name", "parent_session_id"],
|
|
899
|
-
},
|
|
900
|
-
func=spawn_agent_in_clone,
|
|
901
|
-
)
|
|
902
|
-
|
|
903
518
|
return registry
|
gobby/mcp_proxy/tools/memory.py
CHANGED
|
@@ -255,7 +255,7 @@ def create_memory_registry(
|
|
|
255
255
|
name="get_related_memories",
|
|
256
256
|
description="Get memories related to a specific memory via cross-references.",
|
|
257
257
|
)
|
|
258
|
-
def get_related_memories(
|
|
258
|
+
async def get_related_memories(
|
|
259
259
|
memory_id: str,
|
|
260
260
|
limit: int = 5,
|
|
261
261
|
min_similarity: float = 0.0,
|
|
@@ -272,7 +272,7 @@ def create_memory_registry(
|
|
|
272
272
|
min_similarity: Minimum similarity threshold (0.0-1.0)
|
|
273
273
|
"""
|
|
274
274
|
try:
|
|
275
|
-
memories = memory_manager.get_related(
|
|
275
|
+
memories = await memory_manager.get_related(
|
|
276
276
|
memory_id=memory_id,
|
|
277
277
|
limit=limit,
|
|
278
278
|
min_similarity=min_similarity,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Session tools package.
|
|
2
|
+
|
|
3
|
+
This package provides MCP tools for session management. Re-exports maintain
|
|
4
|
+
backwards compatibility with the original session_messages.py module.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
- create_session_messages_registry: Factory function to create the session tool registry
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from gobby.mcp_proxy.tools.sessions._factory import create_session_messages_registry
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"create_session_messages_registry",
|
|
14
|
+
]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Commits and workflow tools for session management.
|
|
2
|
+
|
|
3
|
+
This module contains MCP tools for:
|
|
4
|
+
- Getting session commits (get_session_commits)
|
|
5
|
+
- Marking autonomous loop complete (mark_loop_complete)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import UTC
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from gobby.mcp_proxy.tools.internal import InternalToolRegistry
|
|
15
|
+
from gobby.storage.sessions import LocalSessionManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_commits_tools(
|
|
19
|
+
registry: InternalToolRegistry,
|
|
20
|
+
session_manager: LocalSessionManager,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Register commits and workflow tools with a registry.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
registry: The InternalToolRegistry to register tools with
|
|
27
|
+
session_manager: LocalSessionManager instance for session operations
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
@registry.tool(
|
|
31
|
+
name="get_session_commits",
|
|
32
|
+
description="Get git commits made during a session timeframe.",
|
|
33
|
+
)
|
|
34
|
+
def get_session_commits(
|
|
35
|
+
session_id: str,
|
|
36
|
+
max_commits: int = 20,
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Get git commits made during a session's active timeframe.
|
|
40
|
+
|
|
41
|
+
Uses session.created_at and session.updated_at to filter
|
|
42
|
+
git log within that timeframe.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
session_id: Session ID
|
|
46
|
+
max_commits: Maximum commits to return (default 20)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Session ID, list of commits, and count
|
|
50
|
+
"""
|
|
51
|
+
import subprocess # nosec B404 - subprocess needed for git commands
|
|
52
|
+
from datetime import datetime
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
if session_manager is None:
|
|
56
|
+
return {"error": "Session manager not available"}
|
|
57
|
+
|
|
58
|
+
# Get session
|
|
59
|
+
session = session_manager.get(session_id)
|
|
60
|
+
if not session:
|
|
61
|
+
# Try prefix match
|
|
62
|
+
sessions = session_manager.list(limit=100)
|
|
63
|
+
matches = [s for s in sessions if s.id.startswith(session_id)]
|
|
64
|
+
if len(matches) == 1:
|
|
65
|
+
session = matches[0]
|
|
66
|
+
elif len(matches) > 1:
|
|
67
|
+
return {
|
|
68
|
+
"error": f"Ambiguous session ID prefix '{session_id}'",
|
|
69
|
+
"matches": [s.id for s in matches[:5]],
|
|
70
|
+
}
|
|
71
|
+
else:
|
|
72
|
+
return {"error": f"Session {session_id} not found"}
|
|
73
|
+
|
|
74
|
+
# Get working directory from transcript path or project
|
|
75
|
+
cwd = None
|
|
76
|
+
if session.jsonl_path:
|
|
77
|
+
cwd = str(Path(session.jsonl_path).parent)
|
|
78
|
+
|
|
79
|
+
# Format timestamps for git --since/--until
|
|
80
|
+
# Git expects ISO format or relative dates
|
|
81
|
+
# Session timestamps may be ISO strings or datetime objects
|
|
82
|
+
if isinstance(session.created_at, str):
|
|
83
|
+
since_time = datetime.fromisoformat(session.created_at.replace("Z", "+00:00"))
|
|
84
|
+
else:
|
|
85
|
+
since_time = session.created_at
|
|
86
|
+
|
|
87
|
+
if session.updated_at:
|
|
88
|
+
if isinstance(session.updated_at, str):
|
|
89
|
+
until_time = datetime.fromisoformat(session.updated_at.replace("Z", "+00:00"))
|
|
90
|
+
else:
|
|
91
|
+
until_time = session.updated_at
|
|
92
|
+
else:
|
|
93
|
+
until_time = datetime.now(UTC)
|
|
94
|
+
|
|
95
|
+
# Format as ISO 8601 for git
|
|
96
|
+
since_str = since_time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
97
|
+
until_str = until_time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
# Get commits within timeframe
|
|
101
|
+
cmd = [
|
|
102
|
+
"git",
|
|
103
|
+
"log",
|
|
104
|
+
f"--since={since_str}",
|
|
105
|
+
f"--until={until_str}",
|
|
106
|
+
f"-{max_commits}",
|
|
107
|
+
"--format=%H|%s|%aI", # hash|subject|author-date-iso
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
result = subprocess.run( # nosec B603 - cmd built from hardcoded git arguments
|
|
111
|
+
cmd,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
timeout=10,
|
|
115
|
+
cwd=cwd,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if result.returncode != 0:
|
|
119
|
+
return {
|
|
120
|
+
"session_id": session.id,
|
|
121
|
+
"error": "Git command failed",
|
|
122
|
+
"stderr": result.stderr.strip(),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
commits = []
|
|
126
|
+
for line in result.stdout.strip().split("\n"):
|
|
127
|
+
if "|" in line:
|
|
128
|
+
parts = line.split("|", 2)
|
|
129
|
+
if len(parts) >= 2:
|
|
130
|
+
commit = {
|
|
131
|
+
"hash": parts[0],
|
|
132
|
+
"message": parts[1],
|
|
133
|
+
}
|
|
134
|
+
if len(parts) >= 3:
|
|
135
|
+
commit["timestamp"] = parts[2]
|
|
136
|
+
commits.append(commit)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"session_id": session.id,
|
|
140
|
+
"commits": commits,
|
|
141
|
+
"count": len(commits),
|
|
142
|
+
"timeframe": {
|
|
143
|
+
"since": since_str,
|
|
144
|
+
"until": until_str,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
return {
|
|
150
|
+
"session_id": session.id,
|
|
151
|
+
"error": "Git command timed out",
|
|
152
|
+
}
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
return {
|
|
155
|
+
"session_id": session.id,
|
|
156
|
+
"error": "Git not found or not a git repository",
|
|
157
|
+
}
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return {
|
|
160
|
+
"session_id": session.id,
|
|
161
|
+
"error": f"Failed to get commits: {e!s}",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@registry.tool(
|
|
165
|
+
name="mark_loop_complete",
|
|
166
|
+
description="""Mark the autonomous loop as complete, preventing session chaining.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
session_id: (REQUIRED) Your session ID. Get it from:
|
|
170
|
+
1. Your injected context (look for 'session_id: xxx')
|
|
171
|
+
2. Or call get_current(external_id, source) first""",
|
|
172
|
+
)
|
|
173
|
+
def mark_loop_complete(session_id: str) -> dict[str, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Mark the autonomous loop as complete for a session.
|
|
176
|
+
|
|
177
|
+
This sets stop_reason='completed' in the workflow state, which
|
|
178
|
+
signals the auto-loop workflow to NOT chain a new session
|
|
179
|
+
when this session ends.
|
|
180
|
+
|
|
181
|
+
Use this when:
|
|
182
|
+
- A task is fully complete and no more work is needed
|
|
183
|
+
- You want to exit the autonomous loop gracefully
|
|
184
|
+
- The user has explicitly asked to stop
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
session_id: Session ID (REQUIRED)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Success status and session details
|
|
191
|
+
"""
|
|
192
|
+
if not session_manager:
|
|
193
|
+
raise RuntimeError("Session manager not available")
|
|
194
|
+
|
|
195
|
+
# Find session - session_id is now required
|
|
196
|
+
session = session_manager.get(session_id)
|
|
197
|
+
|
|
198
|
+
if not session:
|
|
199
|
+
return {"error": f"Session {session_id} not found", "session_id": session_id}
|
|
200
|
+
|
|
201
|
+
# Load and update workflow state
|
|
202
|
+
from gobby.storage.database import LocalDatabase
|
|
203
|
+
from gobby.workflows.definitions import WorkflowState
|
|
204
|
+
from gobby.workflows.state_manager import WorkflowStateManager
|
|
205
|
+
|
|
206
|
+
db = LocalDatabase()
|
|
207
|
+
state_manager = WorkflowStateManager(db)
|
|
208
|
+
|
|
209
|
+
# Get or create state for session
|
|
210
|
+
state = state_manager.get_state(session.id)
|
|
211
|
+
if not state:
|
|
212
|
+
# Create minimal state just to hold the variable
|
|
213
|
+
state = WorkflowState(
|
|
214
|
+
session_id=session.id,
|
|
215
|
+
workflow_name="auto-loop",
|
|
216
|
+
step="active",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Mark loop complete using the action function
|
|
220
|
+
from gobby.workflows.state_actions import mark_loop_complete as action_mark_complete
|
|
221
|
+
|
|
222
|
+
action_mark_complete(state)
|
|
223
|
+
|
|
224
|
+
# Save updated state
|
|
225
|
+
state_manager.save_state(state)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"success": True,
|
|
229
|
+
"session_id": session.id,
|
|
230
|
+
"stop_reason": "completed",
|
|
231
|
+
"message": "Autonomous loop marked complete - session will not chain",
|
|
232
|
+
}
|