Skip to content

Commit bdecea3

Browse files
authored
feat: --cli-arg and --cli-args (#19)
1 parent 45ecc66 commit bdecea3

File tree

8 files changed

+102
-0
lines changed

8 files changed

+102
-0
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,33 @@ Override Docker image
9696
pitaya "task" --plugin codex --docker-image ghcr.io/me/codex-cli:mytag
9797
```
9898

99+
Agent CLI passthrough
100+
101+
Forward raw arguments directly to the underlying agent CLI (Codex or Claude). This is useful for advanced flags or experimental options exposed by the tools.
102+
103+
```bash
104+
# Single tokens (repeatable)
105+
pitaya "fix bug" --plugin codex \
106+
--cli-arg -c \
107+
--cli-arg 'feature_flag=true' \
108+
--cli-arg --no-color
109+
110+
# Quoted list (recommended for ordered sequences)
111+
pitaya "refactor module" --plugin codex \
112+
--cli-args '-c model="gpt-4o-mini" --dry-run -v'
113+
114+
# Mix-and-match
115+
pitaya "task" --plugin claude-code \
116+
--cli-arg --verbose \
117+
--cli-args '--max-turns 8'
118+
```
119+
120+
Notes
121+
122+
- Passthrough args are inserted before the final prompt position.
123+
- `--cli-args` uses POSIX shlex splitting; quote values that contain spaces.
124+
- If both forms are provided, Pitaya combines all `--cli-arg` tokens followed by tokens from `--cli-args`. For strict ordering, prefer a single `--cli-args` string.
125+
99126
Resume a run
100127

101128
```bash

src/cli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,20 @@ def create_parser(cls) -> argparse.ArgumentParser:
157157
type=str,
158158
help="Override Docker image (e.g., myrepo/custom:tag)",
159159
)
160+
# Agent CLI passthrough
161+
g_model.add_argument(
162+
"--cli-arg",
163+
action="append",
164+
dest="agent_cli_arg",
165+
metavar="ARG",
166+
help="Pass-through single argument to the agent CLI (repeatable)",
167+
)
168+
g_model.add_argument(
169+
"--cli-args",
170+
dest="agent_cli_args_str",
171+
metavar="'ARGS'",
172+
help="Quoted list of arguments to pass to the agent CLI (e.g., --cli-args '-c key=\"v\" --flag')",
173+
)
160174

161175
# Repository group
162176
g_repo = parser.add_argument_group("Repository")
@@ -1876,6 +1890,20 @@ def _red(k, v):
18761890

18771891
# Proxy automatic egress defaults removed
18781892

1893+
# Combine agent CLI passthrough args and create orchestrator
1894+
try:
1895+
import shlex as _shlex
1896+
1897+
_agent_cli_args: list[str] = []
1898+
if getattr(args, "agent_cli_arg", None):
1899+
_agent_cli_args.extend(
1900+
[str(a) for a in args.agent_cli_arg if a is not None]
1901+
)
1902+
if getattr(args, "agent_cli_args_str", None):
1903+
_agent_cli_args.extend(_shlex.split(str(args.agent_cli_args_str)))
1904+
except Exception:
1905+
_agent_cli_args = []
1906+
18791907
# Create orchestrator
18801908
self.orchestrator = Orchestrator(
18811909
max_parallel_instances=max_parallel_val,
@@ -1936,6 +1964,7 @@ def _red(k, v):
19361964
full_config.get("model", getattr(args, "model", "sonnet"))
19371965
),
19381966
default_docker_image=full_config.get("runner", {}).get("docker_image"),
1967+
default_agent_cli_args=_agent_cli_args or None,
19391968
)
19401969

19411970
# Initialize orchestrator

src/instance_runner/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def run_instance(
5454
model_mapping_checksum: Optional[str] = None,
5555
allow_overwrite_protected_refs: bool = False,
5656
allow_global_session_volume: bool = False,
57+
agent_cli_args: Optional[list[str]] = None,
5758
) -> InstanceResult:
5859
"""
5960
Execute a single AI coding instance in an isolated environment.
@@ -131,6 +132,7 @@ async def run_instance(
131132
model_mapping_checksum=model_mapping_checksum,
132133
allow_overwrite_protected_refs=allow_overwrite_protected_refs,
133134
allow_global_session_volume=allow_global_session_volume,
135+
agent_cli_args=agent_cli_args,
134136
)
135137

136138

src/instance_runner/plugins/claude_code.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ async def build_command(
145145
if "append_system_prompt" in kwargs and kwargs["append_system_prompt"]:
146146
command.extend(["--append-system-prompt", kwargs["append_system_prompt"]])
147147

148+
# Add passthrough CLI args, if any
149+
try:
150+
extra_args = kwargs.get("agent_cli_args") or []
151+
if isinstance(extra_args, (list, tuple)):
152+
command.extend([str(x) for x in extra_args if x is not None])
153+
except Exception:
154+
pass
155+
148156
# Always pass the prompt when provided. On resume, Orchestrator supplies
149157
# a minimal continuation prompt (e.g., "Continue") so the agent keeps going.
150158
if prompt:

src/instance_runner/plugins/codex.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ async def build_command(
155155
if system_prompt:
156156
cmd += ["--system-prompt", str(system_prompt)]
157157

158+
# Agent CLI passthrough args: insert before prompt
159+
try:
160+
extra_args = kwargs.get("agent_cli_args") or []
161+
if isinstance(extra_args, (list, tuple)):
162+
cmd += [str(x) for x in extra_args if x is not None]
163+
except Exception:
164+
pass
165+
158166
# Final positional: the prompt
159167
if prompt:
160168
cmd.append(prompt)

src/instance_runner/runner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ async def run_instance(
103103
model_mapping_checksum: Optional[str] = None,
104104
allow_overwrite_protected_refs: bool = False,
105105
allow_global_session_volume: bool = False,
106+
agent_cli_args: Optional[list[str]] = None,
106107
) -> InstanceResult:
107108
"""
108109
Execute a single AI coding instance in Docker.
@@ -257,6 +258,7 @@ async def run_instance(
257258
max_turns=max_turns,
258259
allow_overwrite_protected_refs=allow_overwrite_protected_refs,
259260
allow_global_session_volume=allow_global_session_volume,
261+
agent_cli_args=agent_cli_args,
260262
)
261263

262264
# Success or non-retryable error
@@ -362,6 +364,7 @@ async def _run_instance_attempt(
362364
max_turns: Optional[int] = None,
363365
allow_overwrite_protected_refs: bool = False,
364366
allow_global_session_volume: bool = False,
367+
agent_cli_args: Optional[list[str]] = None,
365368
) -> InstanceResult:
366369
"""Single attempt at running an instance (internal helper for retry logic)."""
367370
start_time = time.time()
@@ -646,6 +649,7 @@ def emit_event(event_type: str, data: Dict[str, Any]) -> None:
646649
max_turns=max_turns,
647650
stream_log_path=log_path,
648651
**codex_provider_kwargs,
652+
agent_cli_args=(agent_cli_args or []),
649653
)
650654

651655
agent_session_id = result_data.get("session_id")

src/orchestration/orchestrator.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(
7272
default_plugin_name: str = "claude-code",
7373
default_model_alias: str = "sonnet",
7474
default_docker_image: Optional[str] = None,
75+
default_agent_cli_args: Optional[List[str]] = None,
7576
):
7677
"""
7778
Initialize orchestrator.
@@ -98,6 +99,11 @@ def __init__(
9899
self.default_network_egress = str(default_network_egress or "online").lower()
99100
# Optional global Docker image override
100101
self.default_docker_image = default_docker_image
102+
# Optional default agent CLI passthrough args
103+
try:
104+
self.default_agent_cli_args: List[str] = list(default_agent_cli_args or [])
105+
except Exception:
106+
self.default_agent_cli_args = []
101107
# Load model mapping checksum for handshake (single-process still validates equality)
102108
try:
103109
from ..utils.model_mapping import load_model_mapping
@@ -1741,6 +1747,7 @@ def _write_last_active():
17411747
model_mapping_checksum=self._models_checksum,
17421748
allow_overwrite_protected_refs=self.allow_overwrite_protected_refs,
17431749
allow_global_session_volume=self.allow_global_session_volume,
1750+
agent_cli_args=(info.metadata or {}).get("agent_cli_args"),
17441751
)
17451752
logger.info(
17461753
f"_execute_instance: run_instance finished iid={instance_id} success={result.success} status={getattr(result,'status',None)}"
@@ -2681,6 +2688,7 @@ async def _run_instance_from_saved_state(self, instance_info) -> InstanceResult:
26812688
),
26822689
allow_overwrite_protected_refs=self.allow_overwrite_protected_refs,
26832690
allow_global_session_volume=self.allow_global_session_volume,
2691+
agent_cli_args=(instance_info.metadata or {}).get("agent_cli_args"),
26842692
)
26852693

26862694
# Update state

src/orchestration/strategy_context.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ async def spawn_instance(
140140
metadata = {}
141141
metadata["model"] = model
142142

143+
# Include default agent CLI passthrough args when configured
144+
try:
145+
_args = getattr(self._orchestrator, "default_agent_cli_args", [])
146+
if _args and "agent_cli_args" not in metadata:
147+
metadata["agent_cli_args"] = list(_args)
148+
except Exception:
149+
pass
150+
143151
instance_id = await self._orchestrator.spawn_instance(
144152
prompt=prompt,
145153
repo_path=self._orchestrator.repo_path, # Context knows the repo
@@ -351,6 +359,14 @@ def _drop_nulls(obj):
351359
),
352360
}
353361

362+
# Include default agent CLI passthrough args when configured
363+
try:
364+
_args = getattr(self._orchestrator, "default_agent_cli_args", [])
365+
if _args and "agent_cli_args" not in _metadata:
366+
_metadata["agent_cli_args"] = list(_args)
367+
except Exception:
368+
pass
369+
354370
# Orchestration-level scheduling retry policy
355371
max_attempts = 1
356372
base_s = 0.0

0 commit comments

Comments
 (0)