phalanx-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. phalanx_cli-0.1.0/PKG-INFO +16 -0
  2. phalanx_cli-0.1.0/README.md +142 -0
  3. phalanx_cli-0.1.0/phalanx/__init__.py +3 -0
  4. phalanx_cli-0.1.0/phalanx/artifacts/__init__.py +16 -0
  5. phalanx_cli-0.1.0/phalanx/artifacts/reader.py +49 -0
  6. phalanx_cli-0.1.0/phalanx/artifacts/schema.py +31 -0
  7. phalanx_cli-0.1.0/phalanx/artifacts/writer.py +64 -0
  8. phalanx_cli-0.1.0/phalanx/backends/__init__.py +14 -0
  9. phalanx_cli-0.1.0/phalanx/backends/base.py +56 -0
  10. phalanx_cli-0.1.0/phalanx/backends/claude.py +74 -0
  11. phalanx_cli-0.1.0/phalanx/backends/codex.py +64 -0
  12. phalanx_cli-0.1.0/phalanx/backends/cursor.py +72 -0
  13. phalanx_cli-0.1.0/phalanx/backends/gemini.py +70 -0
  14. phalanx_cli-0.1.0/phalanx/backends/model_router.py +15 -0
  15. phalanx_cli-0.1.0/phalanx/backends/registry.py +48 -0
  16. phalanx_cli-0.1.0/phalanx/cli.py +597 -0
  17. phalanx_cli-0.1.0/phalanx/comms/__init__.py +1 -0
  18. phalanx_cli-0.1.0/phalanx/comms/file_lock.py +37 -0
  19. phalanx_cli-0.1.0/phalanx/comms/messaging.py +50 -0
  20. phalanx_cli-0.1.0/phalanx/config.py +107 -0
  21. phalanx_cli-0.1.0/phalanx/db.py +325 -0
  22. phalanx_cli-0.1.0/phalanx/defaults/config.toml +48 -0
  23. phalanx_cli-0.1.0/phalanx/init_cmd.py +215 -0
  24. phalanx_cli-0.1.0/phalanx/monitor/__init__.py +1 -0
  25. phalanx_cli-0.1.0/phalanx/monitor/gc.py +36 -0
  26. phalanx_cli-0.1.0/phalanx/monitor/heartbeat.py +41 -0
  27. phalanx_cli-0.1.0/phalanx/monitor/lifecycle.py +57 -0
  28. phalanx_cli-0.1.0/phalanx/monitor/stall.py +48 -0
  29. phalanx_cli-0.1.0/phalanx/process/__init__.py +1 -0
  30. phalanx_cli-0.1.0/phalanx/process/manager.py +124 -0
  31. phalanx_cli-0.1.0/phalanx/process/pool.py +77 -0
  32. phalanx_cli-0.1.0/phalanx/process/worktree.py +94 -0
  33. phalanx_cli-0.1.0/phalanx/soul/__init__.py +17 -0
  34. phalanx_cli-0.1.0/phalanx/soul/loader.py +103 -0
  35. phalanx_cli-0.1.0/phalanx/soul/skill_body.md +50 -0
  36. phalanx_cli-0.1.0/phalanx/soul/team_lead.md +27 -0
  37. phalanx_cli-0.1.0/phalanx/soul/worker.md +22 -0
  38. phalanx_cli-0.1.0/phalanx/team/__init__.py +12 -0
  39. phalanx_cli-0.1.0/phalanx/team/create.py +105 -0
  40. phalanx_cli-0.1.0/phalanx/team/orchestrator.py +98 -0
  41. phalanx_cli-0.1.0/phalanx/team/spawn.py +146 -0
  42. phalanx_cli-0.1.0/phalanx_cli.egg-info/PKG-INFO +16 -0
  43. phalanx_cli-0.1.0/phalanx_cli.egg-info/SOURCES.txt +47 -0
  44. phalanx_cli-0.1.0/phalanx_cli.egg-info/dependency_links.txt +1 -0
  45. phalanx_cli-0.1.0/phalanx_cli.egg-info/entry_points.txt +2 -0
  46. phalanx_cli-0.1.0/phalanx_cli.egg-info/requires.txt +13 -0
  47. phalanx_cli-0.1.0/phalanx_cli.egg-info/top_level.txt +1 -0
  48. phalanx_cli-0.1.0/pyproject.toml +45 -0
  49. phalanx_cli-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: phalanx-cli
3
+ Version: 0.1.0
4
+ Summary: Open-source, vendor-agnostic multi-agent orchestration CLI
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: click>=8.1
8
+ Requires-Dist: libtmux>=0.37
9
+ Requires-Dist: tomli>=2.0; python_version < "3.12"
10
+ Requires-Dist: tomli-w>=1.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: rich>=13.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == "dev"
15
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.8; extra == "dev"
@@ -0,0 +1,142 @@
1
+ # Phalanx
2
+
3
+ Open-source, vendor-agnostic multi-agent orchestration CLI.
4
+
5
+ Phalanx lets you spin up teams of AI coding agents from any supported backend (Cursor, Claude Code, Gemini CLI, Codex CLI) and orchestrate them through a single unified interface.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install -e ".[dev]"
11
+ ```
12
+
13
+ Requires: Python 3.11+, tmux.
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Single agent (proxies to your default backend)
19
+ phalanx run "fix the failing tests"
20
+
21
+ # Create a team of agents
22
+ phalanx create-team --task "refactor auth module" --agents researcher,coder:2,reviewer --json
23
+
24
+ # Check team status
25
+ phalanx team-status <team-id> --json
26
+
27
+ # Read results
28
+ phalanx team-result <team-id> --json
29
+
30
+ # Stop when done
31
+ phalanx stop <team-id>
32
+ ```
33
+
34
+ ## IDE Integration
35
+
36
+ ```bash
37
+ phalanx init
38
+ ```
39
+
40
+ Auto-detects installed IDEs and deploys skill files:
41
+ - **Cursor**: `.cursor/rules/phalanx.mdc`
42
+ - **Claude Code**: `.claude/commands/phalanx.md`
43
+ - **Gemini CLI**: `.gemini/phalanx-policy.md`
44
+ - **Codex CLI**: `AGENTS.md`
45
+
46
+ ## Supported Backends
47
+
48
+ | Backend | Binary | Worktree | Model Routing |
49
+ |---------|--------|----------|---------------|
50
+ | Cursor | `agent` | Native | All vendors |
51
+ | Claude Code | `claude` | Native | Anthropic |
52
+ | Gemini CLI | `gemini` | Phalanx-managed | Google |
53
+ | Codex CLI | `codex` | Phalanx-managed | OpenAI |
54
+
55
+ ## Model Routing
56
+
57
+ Phalanx automatically selects the best model per agent role and backend. Configurable in `~/.phalanx/config.toml`.
58
+
59
+ ```bash
60
+ phalanx models show # view routing table
61
+ phalanx models set cursor.coder opus-4.6 # override
62
+ phalanx models reset # restore defaults
63
+ ```
64
+
65
+ ## Agent Roles
66
+
67
+ | Role | Purpose |
68
+ |------|---------|
69
+ | `researcher` | Investigation, large-context analysis |
70
+ | `coder` | Implementation, bug fixes, tests |
71
+ | `reviewer` | Code review, large diffs |
72
+ | `architect` | Design decisions, high-stakes reasoning |
73
+ | `orchestrator` | Team lead (auto-assigned) |
74
+
75
+ ## Architecture
76
+
77
+ - **State**: SQLite (WAL mode) at `~/.phalanx/state.db`
78
+ - **Process isolation**: tmux sessions per agent
79
+ - **Artifacts**: Ephemeral JSON (deleted after 24h inactivity)
80
+ - **File locking**: Advisory locks via SQLite
81
+ - **Stall detection**: stream.log monitoring with exponential backoff retry
82
+ - **GC**: Opportunistic, runs on every command
83
+
84
+ ## Commands
85
+
86
+ ### User-Facing
87
+ | Command | Description |
88
+ |---------|-------------|
89
+ | `phalanx run "prompt"` | Single agent session |
90
+ | `phalanx init` | Deploy IDE skill files |
91
+ | `phalanx create-team` | Create agent team |
92
+ | `phalanx team-status <id>` | Team status |
93
+ | `phalanx team-result <id>` | Read team results |
94
+ | `phalanx message <id> "msg"` | Message team lead |
95
+ | `phalanx stop <id>` | Stop team |
96
+ | `phalanx resume <id>` | Resume team |
97
+ | `phalanx status` | List all teams |
98
+ | `phalanx config show/set` | Configuration |
99
+ | `phalanx models show/set/reset/update` | Model routing |
100
+
101
+ ### Agent Tools (used by spawned agents)
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | `phalanx write-artifact` | Write structured result |
105
+ | `phalanx agent-status` | Check peer status |
106
+ | `phalanx agent-result <id>` | Read peer artifact |
107
+ | `phalanx message-agent <id> "msg"` | Message a worker |
108
+ | `phalanx lock/unlock <path>` | File locking |
109
+
110
+ ## Testing
111
+
112
+ ```bash
113
+ pytest tests/unit/ # 125 unit tests
114
+ pytest tests/integration/ # 26 integration tests (requires tmux)
115
+ pytest tests/e2e/ # 11 end-to-end tests
116
+ pytest tests/ # all 162 tests
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ Global: `~/.phalanx/config.toml`
122
+ Workspace override: `.phalanx/config.toml`
123
+
124
+ ```toml
125
+ [defaults]
126
+ backend = "cursor"
127
+
128
+ [timeouts]
129
+ agent_inactivity_minutes = 30
130
+ team_gc_hours = 24
131
+ stall_seconds = 180
132
+
133
+ [models.cursor]
134
+ orchestrator = "sonnet-4.6"
135
+ coder = "sonnet-4.6"
136
+ researcher = "gemini-3.1-pro"
137
+ default = "gemini-3.1-pro"
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,3 @@
1
+ """Phalanx — open-source, vendor-agnostic multi-agent orchestration CLI."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,16 @@
1
+ """Artifact system: schema, writer, reader."""
2
+
3
+ from .schema import Artifact, ArtifactStatus
4
+ from .writer import write_artifact, get_artifact_path, get_stream_log_path
5
+ from .reader import read_artifact, read_team_result, list_artifacts
6
+
7
+ __all__ = [
8
+ "Artifact",
9
+ "ArtifactStatus",
10
+ "write_artifact",
11
+ "get_artifact_path",
12
+ "get_stream_log_path",
13
+ "read_artifact",
14
+ "read_team_result",
15
+ "list_artifacts",
16
+ ]
@@ -0,0 +1,49 @@
1
+ """Read artifacts written by agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .schema import Artifact
10
+ from .writer import get_artifact_path, TEAMS_DIR
11
+
12
+
13
+ def read_artifact(team_id: str, agent_id: str) -> Artifact | None:
14
+ """Read and validate an agent's artifact. Returns None if not found."""
15
+ path = get_artifact_path(team_id, agent_id)
16
+ if not path.exists():
17
+ return None
18
+ data = json.loads(path.read_text())
19
+ return Artifact(**data)
20
+
21
+
22
+ def read_team_result(team_id: str) -> Artifact | None:
23
+ """Read the team lead's artifact (the consolidated team result)."""
24
+ team_dir = TEAMS_DIR / team_id / "agents"
25
+ if not team_dir.exists():
26
+ return None
27
+
28
+ for agent_dir in team_dir.iterdir():
29
+ artifact_path = agent_dir / "artifact.json"
30
+ if artifact_path.exists():
31
+ data = json.loads(artifact_path.read_text())
32
+ if data.get("agent_id", "").startswith("lead"):
33
+ return Artifact(**data)
34
+ return None
35
+
36
+
37
+ def list_artifacts(team_id: str) -> list[Artifact]:
38
+ """List all artifacts for a team."""
39
+ team_dir = TEAMS_DIR / team_id / "agents"
40
+ if not team_dir.exists():
41
+ return []
42
+
43
+ artifacts = []
44
+ for agent_dir in sorted(team_dir.iterdir()):
45
+ artifact_path = agent_dir / "artifact.json"
46
+ if artifact_path.exists():
47
+ data = json.loads(artifact_path.read_text())
48
+ artifacts.append(Artifact(**data))
49
+ return artifacts
@@ -0,0 +1,31 @@
1
+ """Artifact Pydantic models and validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class ArtifactStatus(str, Enum):
13
+ SUCCESS = "success"
14
+ FAILURE = "failure"
15
+ ESCALATION = "escalation_required"
16
+
17
+
18
+ class TokenUsage(BaseModel):
19
+ input_tokens: int = 0
20
+ output_tokens: int = 0
21
+ total_cost_usd: float = 0.0
22
+
23
+
24
+ class Artifact(BaseModel):
25
+ status: ArtifactStatus
26
+ agent_id: str
27
+ team_id: str
28
+ output: dict[str, Any] = Field(default_factory=dict)
29
+ warnings: list[str] = Field(default_factory=list)
30
+ token_usage: TokenUsage = Field(default_factory=TokenUsage)
31
+ created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
@@ -0,0 +1,64 @@
1
+ """Atomic artifact writer — used by agents via `phalanx write-artifact`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .schema import Artifact, ArtifactStatus
12
+
13
+
14
+ TEAMS_DIR = Path.home() / ".phalanx" / "teams"
15
+
16
+
17
+ def get_artifact_path(team_id: str, agent_id: str) -> Path:
18
+ return TEAMS_DIR / team_id / "agents" / agent_id / "artifact.json"
19
+
20
+
21
+ def get_stream_log_path(team_id: str, agent_id: str) -> Path:
22
+ return TEAMS_DIR / team_id / "agents" / agent_id / "stream.log"
23
+
24
+
25
+ def write_artifact(
26
+ status: str,
27
+ output: dict[str, Any],
28
+ team_id: str | None = None,
29
+ agent_id: str | None = None,
30
+ warnings: list[str] | None = None,
31
+ ) -> Artifact:
32
+ """Validate and atomically write an artifact to disk.
33
+
34
+ Reads PHALANX_TEAM_ID and PHALANX_AGENT_ID from env if not provided.
35
+ """
36
+ team_id = team_id or os.environ.get("PHALANX_TEAM_ID", "")
37
+ agent_id = agent_id or os.environ.get("PHALANX_AGENT_ID", "")
38
+
39
+ if not team_id or not agent_id:
40
+ raise ValueError("team_id and agent_id are required (set env or pass explicitly)")
41
+
42
+ artifact = Artifact(
43
+ status=ArtifactStatus(status),
44
+ agent_id=agent_id,
45
+ team_id=team_id,
46
+ output=output,
47
+ warnings=warnings or [],
48
+ )
49
+
50
+ path = get_artifact_path(team_id, agent_id)
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+
53
+ # Atomic write: write to temp file then rename
54
+ fd, tmp_path = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")
55
+ try:
56
+ with os.fdopen(fd, "w") as f:
57
+ f.write(artifact.model_dump_json(indent=2))
58
+ os.replace(tmp_path, str(path))
59
+ except Exception:
60
+ if os.path.exists(tmp_path):
61
+ os.unlink(tmp_path)
62
+ raise
63
+
64
+ return artifact
@@ -0,0 +1,14 @@
1
+ """Backend adapters for agent CLIs."""
2
+
3
+ from .base import AgentBackend
4
+ from .registry import detect_available, detect_default, get_backend, list_backends
5
+ from .model_router import resolve_model
6
+
7
+ __all__ = [
8
+ "AgentBackend",
9
+ "detect_available",
10
+ "detect_default",
11
+ "get_backend",
12
+ "list_backends",
13
+ "resolve_model",
14
+ ]
@@ -0,0 +1,56 @@
1
+ """Abstract base class for agent CLI backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from pathlib import Path
7
+
8
+
9
+ class AgentBackend(ABC):
10
+ """Interface every CLI adapter must implement."""
11
+
12
+ name: str
13
+
14
+ @abstractmethod
15
+ def build_interactive_command(
16
+ self,
17
+ prompt: str,
18
+ workspace: Path,
19
+ model: str | None = None,
20
+ worktree: str | None = None,
21
+ soul_file: Path | None = None,
22
+ ) -> list[str]:
23
+ """Command for TUI mode (user-facing agent)."""
24
+
25
+ @abstractmethod
26
+ def build_headless_command(
27
+ self,
28
+ prompt: str,
29
+ workspace: Path,
30
+ model: str | None = None,
31
+ worktree: str | None = None,
32
+ soul_file: Path | None = None,
33
+ json_output: bool = True,
34
+ auto_approve: bool = True,
35
+ ) -> list[str]:
36
+ """Command for --print / headless mode (team agents in tmux)."""
37
+
38
+ @abstractmethod
39
+ def build_resume_command(
40
+ self,
41
+ chat_id: str,
42
+ message: str | None = None,
43
+ ) -> list[str]:
44
+ """Resume an existing session."""
45
+
46
+ @abstractmethod
47
+ def detect(self) -> bool:
48
+ """Return True if this CLI is installed and available."""
49
+
50
+ @abstractmethod
51
+ def supports_worktree(self) -> bool:
52
+ """Whether the CLI has native --worktree support."""
53
+
54
+ @abstractmethod
55
+ def binary_name(self) -> str:
56
+ """The CLI binary name (e.g. 'agent', 'claude')."""
@@ -0,0 +1,74 @@
1
+ """Claude Code CLI adapter (binary: claude)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from .base import AgentBackend
9
+
10
+
11
+ class ClaudeBackend(AgentBackend):
12
+ name = "claude"
13
+
14
+ def binary_name(self) -> str:
15
+ return "claude"
16
+
17
+ def detect(self) -> bool:
18
+ return shutil.which("claude") is not None
19
+
20
+ def supports_worktree(self) -> bool:
21
+ return True
22
+
23
+ def build_interactive_command(
24
+ self,
25
+ prompt: str,
26
+ workspace: Path,
27
+ model: str | None = None,
28
+ worktree: str | None = None,
29
+ soul_file: Path | None = None,
30
+ ) -> list[str]:
31
+ cmd = ["claude"]
32
+ if model:
33
+ cmd += ["--model", model]
34
+ if worktree:
35
+ cmd += ["--worktree", worktree]
36
+ if soul_file:
37
+ cmd += ["--append-system-prompt", soul_file.read_text()]
38
+ if prompt:
39
+ cmd.append(prompt)
40
+ return cmd
41
+
42
+ def build_headless_command(
43
+ self,
44
+ prompt: str,
45
+ workspace: Path,
46
+ model: str | None = None,
47
+ worktree: str | None = None,
48
+ soul_file: Path | None = None,
49
+ json_output: bool = True,
50
+ auto_approve: bool = True,
51
+ ) -> list[str]:
52
+ cmd = ["claude", "--print"]
53
+ if auto_approve:
54
+ cmd += ["--dangerously-skip-permissions"]
55
+ if model:
56
+ cmd += ["--model", model]
57
+ if worktree:
58
+ cmd += ["--worktree", worktree]
59
+ if json_output:
60
+ cmd += ["--output-format", "stream-json"]
61
+ if soul_file:
62
+ cmd += ["--append-system-prompt", soul_file.read_text()]
63
+ cmd.append(prompt)
64
+ return cmd
65
+
66
+ def build_resume_command(
67
+ self,
68
+ chat_id: str,
69
+ message: str | None = None,
70
+ ) -> list[str]:
71
+ cmd = ["claude", "--resume", chat_id, "--print", "--dangerously-skip-permissions"]
72
+ if message:
73
+ cmd.append(message)
74
+ return cmd
@@ -0,0 +1,64 @@
1
+ """Codex CLI adapter (binary: codex)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from .base import AgentBackend
9
+
10
+
11
+ class CodexBackend(AgentBackend):
12
+ name = "codex"
13
+
14
+ def binary_name(self) -> str:
15
+ return "codex"
16
+
17
+ def detect(self) -> bool:
18
+ return shutil.which("codex") is not None
19
+
20
+ def supports_worktree(self) -> bool:
21
+ return False
22
+
23
+ def build_interactive_command(
24
+ self,
25
+ prompt: str,
26
+ workspace: Path,
27
+ model: str | None = None,
28
+ worktree: str | None = None,
29
+ soul_file: Path | None = None,
30
+ ) -> list[str]:
31
+ cmd = ["codex"]
32
+ if model:
33
+ cmd += ["--model", model]
34
+ cmd += ["--cd", str(workspace)]
35
+ if prompt:
36
+ cmd.append(prompt)
37
+ return cmd
38
+
39
+ def build_headless_command(
40
+ self,
41
+ prompt: str,
42
+ workspace: Path,
43
+ model: str | None = None,
44
+ worktree: str | None = None,
45
+ soul_file: Path | None = None,
46
+ json_output: bool = True,
47
+ auto_approve: bool = True,
48
+ ) -> list[str]:
49
+ cmd = ["codex", "exec"]
50
+ if model:
51
+ cmd += ["--model", model]
52
+ cmd += ["--cd", str(workspace)]
53
+ if auto_approve:
54
+ cmd += ["--sandbox", "workspace-write", "-a", "never"]
55
+ cmd.append(prompt)
56
+ return cmd
57
+
58
+ def build_resume_command(
59
+ self,
60
+ chat_id: str,
61
+ message: str | None = None,
62
+ ) -> list[str]:
63
+ cmd = ["codex", "resume", "--last"]
64
+ return cmd
@@ -0,0 +1,72 @@
1
+ """Cursor CLI adapter (binary: agent)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from .base import AgentBackend
9
+
10
+
11
+ class CursorBackend(AgentBackend):
12
+ name = "cursor"
13
+
14
+ def binary_name(self) -> str:
15
+ return "agent"
16
+
17
+ def detect(self) -> bool:
18
+ return shutil.which("agent") is not None
19
+
20
+ def supports_worktree(self) -> bool:
21
+ return True
22
+
23
+ def build_interactive_command(
24
+ self,
25
+ prompt: str,
26
+ workspace: Path,
27
+ model: str | None = None,
28
+ worktree: str | None = None,
29
+ soul_file: Path | None = None,
30
+ ) -> list[str]:
31
+ cmd = ["agent"]
32
+ if model:
33
+ cmd += ["--model", model]
34
+ if worktree:
35
+ cmd += ["--worktree", worktree]
36
+ cmd += ["--workspace", str(workspace)]
37
+ if prompt:
38
+ cmd.append(prompt)
39
+ return cmd
40
+
41
+ def build_headless_command(
42
+ self,
43
+ prompt: str,
44
+ workspace: Path,
45
+ model: str | None = None,
46
+ worktree: str | None = None,
47
+ soul_file: Path | None = None,
48
+ json_output: bool = True,
49
+ auto_approve: bool = True,
50
+ ) -> list[str]:
51
+ cmd = ["agent", "--print"]
52
+ if model:
53
+ cmd += ["--model", model]
54
+ if worktree:
55
+ cmd += ["--worktree", worktree]
56
+ if json_output:
57
+ cmd += ["--output-format", "stream-json"]
58
+ cmd += ["--workspace", str(workspace)]
59
+ if auto_approve:
60
+ cmd += ["--trust", "--force", "--approve-mcps"]
61
+ cmd.append(prompt)
62
+ return cmd
63
+
64
+ def build_resume_command(
65
+ self,
66
+ chat_id: str,
67
+ message: str | None = None,
68
+ ) -> list[str]:
69
+ cmd = ["agent", "--resume", chat_id, "--print", "--force", "--trust"]
70
+ if message:
71
+ cmd.append(message)
72
+ return cmd
@@ -0,0 +1,70 @@
1
+ """Gemini CLI adapter (binary: gemini)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from .base import AgentBackend
9
+
10
+
11
+ class GeminiBackend(AgentBackend):
12
+ name = "gemini"
13
+
14
+ def binary_name(self) -> str:
15
+ return "gemini"
16
+
17
+ def detect(self) -> bool:
18
+ return shutil.which("gemini") is not None
19
+
20
+ def supports_worktree(self) -> bool:
21
+ return False
22
+
23
+ def build_interactive_command(
24
+ self,
25
+ prompt: str,
26
+ workspace: Path,
27
+ model: str | None = None,
28
+ worktree: str | None = None,
29
+ soul_file: Path | None = None,
30
+ ) -> list[str]:
31
+ cmd = ["gemini"]
32
+ if model:
33
+ cmd += ["--model", model]
34
+ if soul_file:
35
+ cmd += ["--policy", str(soul_file)]
36
+ if prompt:
37
+ cmd.append(prompt)
38
+ return cmd
39
+
40
+ def build_headless_command(
41
+ self,
42
+ prompt: str,
43
+ workspace: Path,
44
+ model: str | None = None,
45
+ worktree: str | None = None,
46
+ soul_file: Path | None = None,
47
+ json_output: bool = True,
48
+ auto_approve: bool = True,
49
+ ) -> list[str]:
50
+ cmd = ["gemini"]
51
+ if model:
52
+ cmd += ["--model", model]
53
+ if json_output:
54
+ cmd += ["-o", "stream-json"]
55
+ if soul_file:
56
+ cmd += ["--policy", str(soul_file)]
57
+ if auto_approve:
58
+ cmd += ["--yolo"]
59
+ cmd += ["-p", prompt]
60
+ return cmd
61
+
62
+ def build_resume_command(
63
+ self,
64
+ chat_id: str,
65
+ message: str | None = None,
66
+ ) -> list[str]:
67
+ cmd = ["gemini", "--resume", chat_id, "--yolo"]
68
+ if message:
69
+ cmd += ["-p", message]
70
+ return cmd
@@ -0,0 +1,15 @@
1
+ """Config-driven model routing: role + backend → model name."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def resolve_model(backend: str, role: str, config: dict[str, Any]) -> str:
9
+ """Look up the model for a given backend and role from config.
10
+
11
+ Fallback: config[backend][role] → config[backend]["default"].
12
+ Raises KeyError if the backend section is missing from config.
13
+ """
14
+ models = config["models"][backend]
15
+ return models.get(role, models["default"])