Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions plugins/runner/skills/sub-agents/scripts/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ def build_command(cli: str, prompt: str) -> tuple[str, list]:
return "claude", ["--output-format", "stream-json", "--verbose", "-p", prompt]

if cli == "gemini":
# --skip-trust is required for headless runs in untrusted folders;
# passing --cwd is itself a trust statement, and Gemini otherwise
# downgrades the approval mode to "default" (interactive prompts)
# which deadlocks here.
return "gemini", ["--skip-trust", "--output-format", "stream-json", "-p", prompt]
# Headless `-p` mode shows no interactive trust dialog (no TTY), and the
# permission mapping passes --approval-mode, which is the trust/approval
# signal. Gemini CLI >=0.35 removed the --skip-trust flag and now errors
# ("Unknown arguments: skip-trust, skipTrust") if it is passed.
return "gemini", ["--output-format", "stream-json", "-p", prompt]

if cli == "cursor-agent":
# API key is forwarded via CURSOR_API_KEY env (in build_invocation_args),
Expand Down
16 changes: 15 additions & 1 deletion plugins/runner/skills/sub-agents/scripts/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,21 @@ def process_line(self, line: str) -> bool:
try:
data = json.loads(line)
except json.JSONDecodeError:
return False
# Some CLIs prepend a non-JSON banner to the first event line. Gemini
# CLI, for example, emits "MCP issues detected. Run /mcp list for
# status." glued onto the `init` JSON when an MCP server is
# unreachable. Without recovery the `init` line never parses,
# is_gemini stays False, and all assistant text is dropped (empty
# result). Recover by parsing from the first brace; a line with no
# JSON object (or one whose JSON starts at column 0 yet still failed)
# is ignored, as before.
brace = line.find("{")
if brace <= 0:
return False
try:
data = json.loads(line[brace:])
except json.JSONDecodeError:
return False

if data.get("type") == "init":
self.is_gemini = True
Expand Down
10 changes: 5 additions & 5 deletions skills/sub-agents/scripts/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ def build_command(cli: str, prompt: str) -> tuple[str, list]:
return "claude", ["--output-format", "stream-json", "--verbose", "-p", prompt]

if cli == "gemini":
# --skip-trust is required for headless runs in untrusted folders;
# passing --cwd is itself a trust statement, and Gemini otherwise
# downgrades the approval mode to "default" (interactive prompts)
# which deadlocks here.
return "gemini", ["--skip-trust", "--output-format", "stream-json", "-p", prompt]
# Headless `-p` mode shows no interactive trust dialog (no TTY), and the
# permission mapping passes --approval-mode, which is the trust/approval
# signal. Gemini CLI >=0.35 removed the --skip-trust flag and now errors
# ("Unknown arguments: skip-trust, skipTrust") if it is passed.
return "gemini", ["--output-format", "stream-json", "-p", prompt]

if cli == "cursor-agent":
# API key is forwarded via CURSOR_API_KEY env (in build_invocation_args),
Expand Down
16 changes: 15 additions & 1 deletion skills/sub-agents/scripts/_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,21 @@ def process_line(self, line: str) -> bool:
try:
data = json.loads(line)
except json.JSONDecodeError:
return False
# Some CLIs prepend a non-JSON banner to the first event line. Gemini
# CLI, for example, emits "MCP issues detected. Run /mcp list for
# status." glued onto the `init` JSON when an MCP server is
# unreachable. Without recovery the `init` line never parses,
# is_gemini stays False, and all assistant text is dropped (empty
# result). Recover by parsing from the first brace; a line with no
# JSON object (or one whose JSON starts at column 0 yet still failed)
# is ignored, as before.
brace = line.find("{")
if brace <= 0:
return False
try:
data = json.loads(line[brace:])
except json.JSONDecodeError:
return False

if data.get("type") == "init":
self.is_gemini = True
Expand Down
6 changes: 3 additions & 3 deletions tests/test_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ def test_gemini_returns_streaming_json_command(self):
cmd, args = build_command("gemini", "test prompt")
assert cmd == "gemini"
assert args == [
"--skip-trust",
"--output-format",
"stream-json",
"-p",
"test prompt",
]
# Gemini CLI >=0.35 removed --skip-trust; it must not be passed.
assert "--skip-trust" not in args

def test_unknown_cli_raises_error(self):
with pytest.raises(ValueError, match="Unknown CLI"):
Expand Down Expand Up @@ -165,8 +166,7 @@ def test_claude_flags(self):
assert permission_flags("claude", "yolo") == ["--dangerously-skip-permissions"]

def test_gemini_flags(self):
# --skip-trust lives in build_command (headless prerequisite), not in
# the permission mapping.
# --approval-mode is the trust/approval signal in headless mode.
assert permission_flags("gemini", "read-only") == ["--approval-mode", "plan"]
assert permission_flags("gemini", "safe-edit") == ["--approval-mode", "auto_edit"]
assert permission_flags("gemini", "yolo") == ["-y"]
Expand Down
15 changes: 15 additions & 0 deletions tests/test_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@ def test_gemini_stream(self):
result = processor.get_result()
assert result["result"] == "part1part2"

def test_gemini_stream_with_banner_prefix(self):
# Gemini CLI can prepend a non-JSON banner to the first event line, e.g.
# "MCP issues detected. Run /mcp list for status." glued onto `init`.
# The processor must recover the JSON so is_gemini is set and assistant
# text is captured (otherwise the result is silently empty).
processor = StreamProcessor()
assert not processor.process_line(
'MCP issues detected. Run /mcp list for status.{"type": "init"}'
)
assert not processor.process_line(
'{"type": "message", "role": "assistant", "content": "hi"}'
)
assert processor.process_line('{"type": "result", "status": "success"}')
assert processor.get_result()["result"] == "hi"

def test_codex_stream(self):
processor = StreamProcessor()
assert not processor.process_line('{"type": "thread.started"}')
Expand Down