agent-shell-py 0.1.16__tar.gz → 0.2.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.
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/AGENTS.md +28 -6
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/PKG-INFO +56 -8
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/README.md +55 -7
- agent_shell_py-0.2.0/agent-skills/delegating-code-review/SKILL.md +195 -0
- agent_shell_py-0.2.0/agent-skills/invoking-cli-agents/SKILL.md +334 -0
- agent_shell_py-0.2.0/agent-skills/invoking-cli-agents/api-reference.md +258 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/agent_parameter_comparison.md +92 -33
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/disabled_tools.md +10 -1
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/total_token_count.md +5 -1
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/_version.py +2 -2
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/agent_adapter_protocol.py +7 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/claude_code_adapter.py +30 -31
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/codex_adapter.py +30 -31
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/copilot_cli_adapter.py +39 -37
- agent_shell_py-0.2.0/src/agent_shell/adapters/cursor_adapter.py +305 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/health.py +15 -22
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/opencode_adapter.py +30 -31
- agent_shell_py-0.2.0/src/agent_shell/adapters/outcome.py +68 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/pi_adapter.py +77 -40
- agent_shell_py-0.2.0/src/agent_shell/adapters/response.py +70 -0
- agent_shell_py-0.2.0/src/agent_shell/adapters/stderr_format.py +18 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/models/agent.py +35 -1
- agent_shell_py-0.2.0/src/agent_shell/process_cleanup.py +213 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/shell.py +15 -9
- agent_shell_py-0.2.0/tests/conftest.py +50 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_claude_code_e2e.py +22 -11
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_codex_e2e.py +20 -9
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_copilot_cli_e2e.py +67 -12
- agent_shell_py-0.2.0/tests/e2e/test_cursor_e2e.py +115 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_health_check_e2e.py +2 -1
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_opencode_e2e.py +22 -10
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_pi_e2e.py +28 -9
- agent_shell_py-0.2.0/tests/integration/test_cursor_integration.py +381 -0
- agent_shell_py-0.2.0/tests/integration/test_cursor_mcp_integration.py +36 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_health_check_integration.py +36 -3
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_pi_integration.py +3 -1
- agent_shell_py-0.2.0/tests/integration/test_process_lifecycle.py +283 -0
- agent_shell_py-0.2.0/tests/unit/adapter_matrix.py +67 -0
- agent_shell_py-0.2.0/tests/unit/cursor_fixtures.py +171 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/opencode_fixtures.py +10 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/pi_fixtures.py +103 -3
- agent_shell_py-0.2.0/tests/unit/test_adapter_transport.py +486 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_cancel.py +4 -5
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_cancel.py +3 -7
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_cancel.py +3 -7
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_execute.py +28 -5
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_parse_event.py +17 -12
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_stream.py +7 -5
- agent_shell_py-0.2.0/tests/unit/test_cursor_cancel.py +21 -0
- agent_shell_py-0.2.0/tests/unit/test_cursor_execute.py +170 -0
- agent_shell_py-0.2.0/tests/unit/test_cursor_parse_event.py +220 -0
- agent_shell_py-0.2.0/tests/unit/test_cursor_warnings.py +211 -0
- agent_shell_py-0.2.0/tests/unit/test_execute_outcome.py +446 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_health_probe.py +17 -2
- agent_shell_py-0.2.0/tests/unit/test_models.py +97 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_cancel.py +4 -5
- agent_shell_py-0.2.0/tests/unit/test_pi_cancel.py +21 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_execute.py +15 -7
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_parse_event.py +132 -7
- agent_shell_py-0.2.0/tests/unit/test_process_cleanup.py +596 -0
- agent_shell_py-0.2.0/tests/unit/test_response_aggregation.py +131 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell.py +24 -3
- agent_shell_py-0.2.0/tests/unit/test_stderr_format.py +67 -0
- agent_shell_py-0.1.16/agent-skills/delegating-code-review/SKILL.md +0 -146
- agent_shell_py-0.1.16/agent-skills/invoking-cli-agents/SKILL.md +0 -239
- agent_shell_py-0.1.16/agent-skills/invoking-cli-agents/api-reference.md +0 -140
- agent_shell_py-0.1.16/src/agent_shell/process_cleanup.py +0 -37
- agent_shell_py-0.1.16/tests/unit/test_adapter_transport.py +0 -239
- agent_shell_py-0.1.16/tests/unit/test_models.py +0 -33
- agent_shell_py-0.1.16/tests/unit/test_pi_cancel.py +0 -40
- agent_shell_py-0.1.16/tests/unit/test_process_cleanup.py +0 -89
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/build.yml +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/ci.yml +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/publish.yml +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.gitignore +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.python-version +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/LICENSE +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/info.md +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/pyproject.toml +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/tool_denial.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/models/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_mcp_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_codex_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_codex_mcp_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_opencode_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_opencode_mcp_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_pi_mcp_integration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/__init__.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/codex_fixtures.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/copilot_fixtures.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/fixtures.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_execute.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_parse_event.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_warnings.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_execute.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_mcp_server_spec.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_execute.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_parse_event.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_spawn.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_stream.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_parse_event.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_warnings.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_process_group_registration.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell_cancellation.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell_mcp.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_stream.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_tool_denial.py +0 -0
- {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/uv.lock +0 -0
|
@@ -38,6 +38,16 @@ classDiagram
|
|
|
38
38
|
-_parse_event(event, include_thinking) list~StreamEvent~
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
class AgentExecutionError {
|
|
42
|
+
<<Exception>>
|
|
43
|
+
+str reason
|
|
44
|
+
+str response
|
|
45
|
+
+float cost
|
|
46
|
+
+str session_id
|
|
47
|
+
+float duration
|
|
48
|
+
+int output_tokens
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
class MCPServerSpec {
|
|
42
52
|
+str name
|
|
43
53
|
+MCPServerType type
|
|
@@ -74,22 +84,24 @@ classDiagram
|
|
|
74
84
|
+float duration
|
|
75
85
|
+str session_id
|
|
76
86
|
+int output_tokens
|
|
87
|
+
+str error
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
class AgentType {
|
|
80
91
|
<<StrEnum>>
|
|
81
92
|
CLAUDE_CODE
|
|
82
93
|
OPENCODE
|
|
83
|
-
GEMINI_CLI
|
|
84
94
|
COPILOT_CLI
|
|
85
95
|
CODEX
|
|
86
96
|
PI
|
|
97
|
+
CURSOR
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
AgentShell --> AgentAdapter : delegates to
|
|
90
101
|
AgentShell --> AgentType : resolves via
|
|
91
102
|
ClaudeCodeAdapter ..|> AgentAdapter : satisfies
|
|
92
|
-
AgentShell ..> AgentResponse : returns
|
|
103
|
+
AgentShell ..> AgentResponse : returns on success
|
|
104
|
+
AgentShell ..> AgentExecutionError : raises on failure
|
|
93
105
|
AgentShell ..> HealthCheckResult : returns
|
|
94
106
|
AgentShell ..> StreamEvent : yields
|
|
95
107
|
AgentShell ..> MCPServerSpec : accepts/returns
|
|
@@ -101,7 +113,17 @@ The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC
|
|
|
101
113
|
|
|
102
114
|
`output_tokens` is a cost measure — the billed output-token count, which **includes reasoning tokens** (billed at the output rate). Each adapter normalises this so the value is consistent across agents (e.g. OpenCode reports reasoning in a sibling field, so its adapter adds it back).
|
|
103
115
|
|
|
104
|
-
`health_check(cwd, model, timeout)` probes an agent + model combination with a trivial prompt
|
|
116
|
+
`health_check(cwd, model, timeout)` probes an agent + model combination with a trivial prompt
|
|
117
|
+
and returns `HealthCheckResult(healthy, exception)`. The verdict is derived from the normalised
|
|
118
|
+
event stream (healthy = the LAST `result` event says `content == "ok"` and no `error` event
|
|
119
|
+
arrived — pi emits one `result` per agent loop, and its auto-retry runs more than one), not exit
|
|
120
|
+
codes — which are unreliable, since some CLIs exit 0 on failure. `execute()` judges a run by the
|
|
121
|
+
same rule and raises `AgentExecutionError` when it fails, carrying whatever partial
|
|
122
|
+
response/cost/session/token data the run produced. The success/failure verdict lives once in
|
|
123
|
+
`adapters/outcome.py`, shared by both surfaces so they report identical reasons for the same
|
|
124
|
+
stream; `adapters/health.py` wraps it for the health probe, and `adapters/response.py` wraps it
|
|
125
|
+
for `execute()`'s stream-to-`AgentResponse` collection (used by every adapter — each `execute()`
|
|
126
|
+
is a single delegating call into it).
|
|
105
127
|
|
|
106
128
|
## Supported Agents
|
|
107
129
|
|
|
@@ -110,14 +132,14 @@ The adapter pattern uses Python's `Protocol` (structural typing) rather than ABC
|
|
|
110
132
|
- [x] Copilot CLI
|
|
111
133
|
- [x] Codex
|
|
112
134
|
- [x] Pi
|
|
113
|
-
- [
|
|
135
|
+
- [x] Cursor
|
|
114
136
|
|
|
115
137
|
## MCP Server Configuration
|
|
116
138
|
|
|
117
139
|
`AgentShell` exposes a unified API for registering MCP servers across all supported agents:
|
|
118
140
|
|
|
119
141
|
```python
|
|
120
|
-
from agent_shell import AgentShell
|
|
142
|
+
from agent_shell.shell import AgentShell
|
|
121
143
|
from agent_shell.models.agent import AgentType, MCPServerSpec, MCPServerType
|
|
122
144
|
|
|
123
145
|
shell = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
@@ -140,7 +162,7 @@ All adapters write to user-scope configuration:
|
|
|
140
162
|
| Copilot CLI | direct JSON file write | `~/.copilot/mcp-config.json` |
|
|
141
163
|
| Codex | `codex mcp add` subprocess | Codex config |
|
|
142
164
|
|
|
143
|
-
Adds are idempotent (overwrite existing entries with the same name). Removes warn rather than raise when the named server is not found. Claude Code listing reads the user-scope `mcpServers` entries from `~/.claude.json` directly, avoiding the health checks and human-readable output of `claude mcp list`. MCP is not
|
|
165
|
+
Adds are idempotent (overwrite existing entries with the same name). Removes warn rather than raise when the named server is not found. Claude Code listing reads the user-scope `mcpServers` entries from `~/.claude.json` directly, avoiding the health checks and human-readable output of `claude mcp list`. MCP is not implemented for Pi or Cursor — all three methods raise `NotImplementedError`. Pi manages capability via `pi install` extensions (which needs investigation before wiring up); Cursor's `mcp` subcommands are login/list/list-tools/enable/disable only (no add/remove — servers are declared in `.cursor/mcp.json`), and `mcp list` reports only `name: status`, not the transport an `MCPServerSpec` needs.
|
|
144
166
|
|
|
145
167
|
## Test Philosophy
|
|
146
168
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-shell-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A lightweight abstraction for executing CLI coding agents headlessly
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -15,10 +15,10 @@ and returning the output that can be used programatically as a unified contract
|
|
|
15
15
|
|
|
16
16
|
- **One unified contract** — the same `execute` / `stream` / `health_check` API across every
|
|
17
17
|
agent; swap the backend without changing a line of consuming code.
|
|
18
|
-
- **
|
|
19
|
-
adapter protocol.
|
|
20
|
-
- **Execute or stream** — get one `AgentResponse
|
|
21
|
-
with optional thinking/reasoning.
|
|
18
|
+
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
19
|
+
common adapter protocol.
|
|
20
|
+
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
21
|
+
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
22
22
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
23
23
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
24
24
|
regardless of how each CLI reports them.
|
|
@@ -77,6 +77,22 @@ follow_up = await shell.execute(
|
|
|
77
77
|
> `output_tokens` is a cost measure: the billed output-token count, which **includes reasoning
|
|
78
78
|
> tokens** (they are billed at the output rate). It is reported consistently across all adapters.
|
|
79
79
|
|
|
80
|
+
### Failure handling
|
|
81
|
+
|
|
82
|
+
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
83
|
+
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
84
|
+
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
85
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
92
|
+
except AgentExecutionError as e:
|
|
93
|
+
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
94
|
+
```
|
|
95
|
+
|
|
80
96
|
### Stream
|
|
81
97
|
|
|
82
98
|
```python
|
|
@@ -137,7 +153,8 @@ response = await shell.execute(
|
|
|
137
153
|
- Where a backend cannot enforce a deny, the adapter emits a `UserWarning` listing the
|
|
138
154
|
ignored tools rather than failing silently. Coverage varies: Claude and OpenCode enforce
|
|
139
155
|
all five canonical names; Copilot enforces only `bash`/`edit` canonically (use a verbatim
|
|
140
|
-
name for its other tools); Codex can only deny `web_search
|
|
156
|
+
name for its other tools); Codex can only deny `web_search`; Cursor cannot enforce any
|
|
157
|
+
per-call deny (its tool policy lives in `.cursor/cli.json`).
|
|
141
158
|
- Denying `edit` or `read` is **best-effort**: a model can still modify or read files through
|
|
142
159
|
the shell, so also deny `bash` when you need a hard file boundary.
|
|
143
160
|
|
|
@@ -167,7 +184,38 @@ follow_up = await shell.execute(
|
|
|
167
184
|
)
|
|
168
185
|
```
|
|
169
186
|
|
|
170
|
-
> **Note:** OpenCode
|
|
187
|
+
> **Note:** For OpenCode, `allowed_tools` and `effort` are **ignored** — the adapter maps
|
|
188
|
+
> neither to a CLI flag nor to `opencode.json`. To restrict an OpenCode agent, use
|
|
189
|
+
> `disallowed_tools` (see [Restricting tools](#restricting-tools-disallowed_tools)): it is
|
|
190
|
+
> enforced via a per-run `OPENCODE_PERMISSION` environment variable and holds even under
|
|
191
|
+
> auto-approve. Keep `auto_approve=True` (the default) — with `auto_approve=False`,
|
|
192
|
+
> `opencode run` auto-*rejects* permission prompts non-interactively and can silently abort the run.
|
|
193
|
+
|
|
194
|
+
### Cursor
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from agent_shell.shell import AgentShell
|
|
198
|
+
from agent_shell.models.agent import AgentType
|
|
199
|
+
|
|
200
|
+
shell = AgentShell(agent_type=AgentType.CURSOR)
|
|
201
|
+
|
|
202
|
+
response = await shell.execute(
|
|
203
|
+
cwd="/path/to/project",
|
|
204
|
+
prompt="Can you tell me about this project?",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
print(response.response)
|
|
208
|
+
print(f"Session: {response.session_id}")
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
> **Note:** Cursor runs headlessly via `cursor-agent --print --output-format stream-json` and
|
|
212
|
+
> **requires** workspace trust, which the adapter always passes (`--trust`). With
|
|
213
|
+
> `auto_approve=True` (the default) it also passes `--force` so tools auto-run; otherwise
|
|
214
|
+
> tools are auto-*rejected* but the run still completes. `allowed_tools`, `effort`, and
|
|
215
|
+
> `disallowed_tools` are **ignored** — Cursor exposes no per-call tool policy or effort flag
|
|
216
|
+
> (tool policy lives in `.cursor/cli.json`), so each emits a `UserWarning`. On a Free plan
|
|
217
|
+
> only `model=None`/`"auto"` works. MCP servers are declared in `.cursor/mcp.json`; the
|
|
218
|
+
> `add`/`remove`/`list` MCP methods raise `NotImplementedError`.
|
|
171
219
|
|
|
172
220
|
## MCP Servers
|
|
173
221
|
|
|
@@ -208,7 +256,7 @@ await shell.add_mcp_server(MCPServerSpec(
|
|
|
208
256
|
))
|
|
209
257
|
```
|
|
210
258
|
|
|
211
|
-
`add_mcp_server` overwrites an existing server with the same name. `remove_mcp_server` warns rather than raises when the named server is not found. `list_mcp_servers()` works for Claude Code, OpenCode, Copilot CLI, and Codex. Claude Code reads user-scope entries directly from `~/.claude.json`, so listing does not launch configured servers for health checks.
|
|
259
|
+
`add_mcp_server` overwrites an existing server with the same name. `remove_mcp_server` warns rather than raises when the named server is not found. `list_mcp_servers()` works for Claude Code, OpenCode, Copilot CLI, and Codex. Claude Code reads user-scope entries directly from `~/.claude.json`, so listing does not launch configured servers for health checks. MCP is not supported for Pi or Cursor — neither CLI exposes an add/remove subcommand, so all three MCP methods raise `NotImplementedError`.
|
|
212
260
|
|
|
213
261
|
## Logging
|
|
214
262
|
|
|
@@ -6,10 +6,10 @@ and returning the output that can be used programatically as a unified contract
|
|
|
6
6
|
|
|
7
7
|
- **One unified contract** — the same `execute` / `stream` / `health_check` API across every
|
|
8
8
|
agent; swap the backend without changing a line of consuming code.
|
|
9
|
-
- **
|
|
10
|
-
adapter protocol.
|
|
11
|
-
- **Execute or stream** — get one `AgentResponse
|
|
12
|
-
with optional thinking/reasoning.
|
|
9
|
+
- **Six CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, Pi, and Cursor behind a
|
|
10
|
+
common adapter protocol.
|
|
11
|
+
- **Execute or stream** — get one `AgentResponse` (raises `AgentExecutionError` on a failed run),
|
|
12
|
+
or async-iterate normalized `StreamEvent`s with optional thinking/reasoning.
|
|
13
13
|
- **Session resumption** — continue any conversation by passing back its `session_id`.
|
|
14
14
|
- **Normalized cost & tokens** — consistent `cost` and `output_tokens` (reasoning included)
|
|
15
15
|
regardless of how each CLI reports them.
|
|
@@ -68,6 +68,22 @@ follow_up = await shell.execute(
|
|
|
68
68
|
> `output_tokens` is a cost measure: the billed output-token count, which **includes reasoning
|
|
69
69
|
> tokens** (they are billed at the output rate). It is reported consistently across all adapters.
|
|
70
70
|
|
|
71
|
+
### Failure handling
|
|
72
|
+
|
|
73
|
+
`execute()` raises `AgentExecutionError` instead of returning when a run failed — an `error`
|
|
74
|
+
event was emitted, the terminal `result` had `content == "error"`, or no terminal `result`
|
|
75
|
+
arrived at all. `str(e)` is the bare reason; the exception also carries whatever partial
|
|
76
|
+
`response`/`cost`/`session_id`/`duration`/`output_tokens` the run produced before failing.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
response = await shell.execute(cwd="/path/to/project", prompt="Fix the failing test")
|
|
83
|
+
except AgentExecutionError as e:
|
|
84
|
+
print(f"run failed: {e}") # e.g. "500 model name=qwen3.6-27b-8Q failed to load"
|
|
85
|
+
```
|
|
86
|
+
|
|
71
87
|
### Stream
|
|
72
88
|
|
|
73
89
|
```python
|
|
@@ -128,7 +144,8 @@ response = await shell.execute(
|
|
|
128
144
|
- Where a backend cannot enforce a deny, the adapter emits a `UserWarning` listing the
|
|
129
145
|
ignored tools rather than failing silently. Coverage varies: Claude and OpenCode enforce
|
|
130
146
|
all five canonical names; Copilot enforces only `bash`/`edit` canonically (use a verbatim
|
|
131
|
-
name for its other tools); Codex can only deny `web_search
|
|
147
|
+
name for its other tools); Codex can only deny `web_search`; Cursor cannot enforce any
|
|
148
|
+
per-call deny (its tool policy lives in `.cursor/cli.json`).
|
|
132
149
|
- Denying `edit` or `read` is **best-effort**: a model can still modify or read files through
|
|
133
150
|
the shell, so also deny `bash` when you need a hard file boundary.
|
|
134
151
|
|
|
@@ -158,7 +175,38 @@ follow_up = await shell.execute(
|
|
|
158
175
|
)
|
|
159
176
|
```
|
|
160
177
|
|
|
161
|
-
> **Note:** OpenCode
|
|
178
|
+
> **Note:** For OpenCode, `allowed_tools` and `effort` are **ignored** — the adapter maps
|
|
179
|
+
> neither to a CLI flag nor to `opencode.json`. To restrict an OpenCode agent, use
|
|
180
|
+
> `disallowed_tools` (see [Restricting tools](#restricting-tools-disallowed_tools)): it is
|
|
181
|
+
> enforced via a per-run `OPENCODE_PERMISSION` environment variable and holds even under
|
|
182
|
+
> auto-approve. Keep `auto_approve=True` (the default) — with `auto_approve=False`,
|
|
183
|
+
> `opencode run` auto-*rejects* permission prompts non-interactively and can silently abort the run.
|
|
184
|
+
|
|
185
|
+
### Cursor
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from agent_shell.shell import AgentShell
|
|
189
|
+
from agent_shell.models.agent import AgentType
|
|
190
|
+
|
|
191
|
+
shell = AgentShell(agent_type=AgentType.CURSOR)
|
|
192
|
+
|
|
193
|
+
response = await shell.execute(
|
|
194
|
+
cwd="/path/to/project",
|
|
195
|
+
prompt="Can you tell me about this project?",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
print(response.response)
|
|
199
|
+
print(f"Session: {response.session_id}")
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> **Note:** Cursor runs headlessly via `cursor-agent --print --output-format stream-json` and
|
|
203
|
+
> **requires** workspace trust, which the adapter always passes (`--trust`). With
|
|
204
|
+
> `auto_approve=True` (the default) it also passes `--force` so tools auto-run; otherwise
|
|
205
|
+
> tools are auto-*rejected* but the run still completes. `allowed_tools`, `effort`, and
|
|
206
|
+
> `disallowed_tools` are **ignored** — Cursor exposes no per-call tool policy or effort flag
|
|
207
|
+
> (tool policy lives in `.cursor/cli.json`), so each emits a `UserWarning`. On a Free plan
|
|
208
|
+
> only `model=None`/`"auto"` works. MCP servers are declared in `.cursor/mcp.json`; the
|
|
209
|
+
> `add`/`remove`/`list` MCP methods raise `NotImplementedError`.
|
|
162
210
|
|
|
163
211
|
## MCP Servers
|
|
164
212
|
|
|
@@ -199,7 +247,7 @@ await shell.add_mcp_server(MCPServerSpec(
|
|
|
199
247
|
))
|
|
200
248
|
```
|
|
201
249
|
|
|
202
|
-
`add_mcp_server` overwrites an existing server with the same name. `remove_mcp_server` warns rather than raises when the named server is not found. `list_mcp_servers()` works for Claude Code, OpenCode, Copilot CLI, and Codex. Claude Code reads user-scope entries directly from `~/.claude.json`, so listing does not launch configured servers for health checks.
|
|
250
|
+
`add_mcp_server` overwrites an existing server with the same name. `remove_mcp_server` warns rather than raises when the named server is not found. `list_mcp_servers()` works for Claude Code, OpenCode, Copilot CLI, and Codex. Claude Code reads user-scope entries directly from `~/.claude.json`, so listing does not launch configured servers for health checks. MCP is not supported for Pi or Cursor — neither CLI exposes an add/remove subcommand, so all three MCP methods raise `NotImplementedError`.
|
|
203
251
|
|
|
204
252
|
## Logging
|
|
205
253
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegating-code-review
|
|
3
|
+
description: Use when you have made code changes and want another CLI agent to review them before committing or continuing — a second-opinion, security, correctness, requirements, or test-coverage review, or a cross-agent independent review. Keywords: reviewer agent, read-only review, disallowed_tools, git diff review, review uncommitted changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delegating Code Review to Another Agent
|
|
7
|
+
|
|
8
|
+
Use AgentShell to invoke another CLI agent to review changes in a repository. The reviewing
|
|
9
|
+
agent has its own tools — point it at the repo, tell it what to look at, and let it run
|
|
10
|
+
`git diff` itself.
|
|
11
|
+
|
|
12
|
+
Assumes familiarity with AgentShell basics. See [invoking-cli-agents](../invoking-cli-agents/SKILL.md)
|
|
13
|
+
for setup, the full parameter list, and the per-agent capability matrix.
|
|
14
|
+
|
|
15
|
+
## When to Use
|
|
16
|
+
|
|
17
|
+
- You have made code changes and want a second opinion before committing
|
|
18
|
+
- You want to validate that changes meet requirements before marking work complete
|
|
19
|
+
- You need a security, performance, or correctness review
|
|
20
|
+
- You want to check for regressions or unintended side effects
|
|
21
|
+
|
|
22
|
+
## Keep the Reviewer Read-Only
|
|
23
|
+
|
|
24
|
+
A reviewer should not modify the code it reviews. Don't rely on a "don't edit files"
|
|
25
|
+
instruction in the prompt — restrict the tools. But mind how the two controls actually enforce
|
|
26
|
+
(the [core skill](../invoking-cli-agents/SKILL.md#tool-restriction-safety) has the detail):
|
|
27
|
+
|
|
28
|
+
- **Claude Code / Copilot / Pi** — whitelist read + git tools **and set `auto_approve=False`**.
|
|
29
|
+
The whitelist is *inert under the default `auto_approve=True`* (`--dangerously-skip-permissions`
|
|
30
|
+
auto-approves everything), so the `auto_approve=False` is what makes it bite:
|
|
31
|
+
`allowed_tools=["Read", "Glob", "Grep", "Bash"], auto_approve=False`.
|
|
32
|
+
- **OpenCode** — it ignores `allowed_tools`, but its denylist **is** genuinely enforced (a
|
|
33
|
+
per-subprocess `OPENCODE_PERMISSION` env var), and deny beats `auto_approve`, so keep the
|
|
34
|
+
default `auto_approve=True`: `disallowed_tools=["edit"]`.
|
|
35
|
+
- **Codex / Cursor** — **in-library tool scoping gives you nothing here.** They ignore
|
|
36
|
+
`allowed_tools`, *and* they cannot enforce `disallowed_tools=["edit"]`. Codex can only deny
|
|
37
|
+
`web_search`; anything else prints
|
|
38
|
+
`UserWarning: Codex can only deny web_search; ignoring ['edit']` and the reviewer then runs
|
|
39
|
+
with unrestricted write access. Cursor has no per-call deny at all (its tool policy lives in
|
|
40
|
+
`.cursor/cli.json`). If a Codex/Cursor reviewer must not write, enforce it outside the
|
|
41
|
+
library: capture `git diff` yourself and pass it in the prompt, run the reviewer under an
|
|
42
|
+
OS-level read-only sandbox, and/or use a different agent type when read-only really matters.
|
|
43
|
+
|
|
44
|
+
**This is defence-in-depth, not a sandbox.** Any reviewer that keeps `bash` (for `git diff`) can
|
|
45
|
+
still write via the shell, and in-library scoping doesn't cover MCP-provided tools. For a *hard*
|
|
46
|
+
guarantee — an untrusted model, or reviewing hostile input that could prompt-inject the reviewer
|
|
47
|
+
— capture `git diff` yourself, pass it in the prompt, remove the shell too
|
|
48
|
+
(`disallowed_tools=["edit", "bash"]`, or `allowed_tools=["Read","Glob","Grep"], auto_approve=False`),
|
|
49
|
+
and/or run under an OS-level read-only mount.
|
|
50
|
+
|
|
51
|
+
## Review Uncommitted Changes
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from agent_shell.shell import AgentShell
|
|
55
|
+
from agent_shell.models.agent import AgentType
|
|
56
|
+
|
|
57
|
+
reviewer = AgentShell(agent_type=AgentType.CLAUDE_CODE)
|
|
58
|
+
|
|
59
|
+
review = await reviewer.execute(
|
|
60
|
+
cwd="/path/to/project",
|
|
61
|
+
prompt="""Review the uncommitted changes in this repository.
|
|
62
|
+
Focus on correctness, security, and design.
|
|
63
|
+
Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.""",
|
|
64
|
+
allowed_tools=["Read", "Glob", "Grep", "Bash"], # read + git, no edit tool
|
|
65
|
+
auto_approve=False, # required for the whitelist to enforce
|
|
66
|
+
model="sonnet",
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The reviewer will run `git diff` itself, read surrounding code for context, and report findings.
|
|
71
|
+
|
|
72
|
+
## Handling a Failed Review
|
|
73
|
+
|
|
74
|
+
A failed reviewer run raises `AgentExecutionError` instead of returning a response — catch it
|
|
75
|
+
so a crashed or rate-limited reviewer doesn't look like a silent "no issues found":
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from agent_shell.models.agent import AgentExecutionError
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
review = await reviewer.execute(
|
|
82
|
+
cwd="/path/to/project",
|
|
83
|
+
prompt="Review the uncommitted changes in this repository.",
|
|
84
|
+
allowed_tools=["Read", "Glob", "Grep", "Bash"],
|
|
85
|
+
auto_approve=False,
|
|
86
|
+
model="sonnet",
|
|
87
|
+
)
|
|
88
|
+
except AgentExecutionError as e:
|
|
89
|
+
print(f"review failed: {e}") # e.g. "500 model name=... failed to load"
|
|
90
|
+
raise
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Review a Specific Commit or Range
|
|
94
|
+
|
|
95
|
+
Point the reviewer at a particular changeset — only the prompt changes.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
review = await reviewer.execute(
|
|
99
|
+
cwd="/path/to/project",
|
|
100
|
+
prompt="""Review the changes in commit abc1234.
|
|
101
|
+
Focus on correctness, security, and design.
|
|
102
|
+
Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.""",
|
|
103
|
+
allowed_tools=["Read", "Glob", "Grep", "Bash"],
|
|
104
|
+
auto_approve=False,
|
|
105
|
+
model="sonnet",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Or a range: "Review all changes between main and HEAD. ..."
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Follow Up
|
|
112
|
+
|
|
113
|
+
Use session resumption to ask the reviewer to clarify or elaborate.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
clarification = await reviewer.execute(
|
|
117
|
+
cwd="/path/to/project",
|
|
118
|
+
prompt="Explain the security concern in more detail and suggest a specific fix.",
|
|
119
|
+
allowed_tools=["Read", "Glob", "Grep", "Bash"],
|
|
120
|
+
auto_approve=False,
|
|
121
|
+
model="sonnet",
|
|
122
|
+
session_id=review.session_id,
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Cross-Agent Review
|
|
127
|
+
|
|
128
|
+
Use a different agent or model than the one that wrote the code for genuine independence.
|
|
129
|
+
OpenCode ignores `allowed_tools`, so restrict it with `disallowed_tools` instead — that deny
|
|
130
|
+
*is* enforced. Don't swap `AgentType.CODEX` or `AgentType.CURSOR` into this example expecting
|
|
131
|
+
the same protection: they drop the deny with only a `UserWarning`.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
reviewer = AgentShell(agent_type=AgentType.OPENCODE)
|
|
135
|
+
|
|
136
|
+
review = await reviewer.execute(
|
|
137
|
+
cwd="/path/to/project",
|
|
138
|
+
prompt="""Review the uncommitted changes in this repository.
|
|
139
|
+
Focus on correctness, security, and design.
|
|
140
|
+
Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.""",
|
|
141
|
+
disallowed_tools=["edit"], # enforced: cannot Edit/Write/patch
|
|
142
|
+
model="github-copilot/gpt-5.4",
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> OpenCode reports `cost` as `0.0` for many models — use `output_tokens` if you need a usage
|
|
147
|
+
> figure. And a failed reviewer run raises `AgentExecutionError` rather than returning — wrap
|
|
148
|
+
> the call (see [Handling a Failed Review](#handling-a-failed-review) below), since OpenCode can
|
|
149
|
+
> truncate a review with no terminal event and no error (see the core skill's Error Handling).
|
|
150
|
+
|
|
151
|
+
## Prompt Patterns
|
|
152
|
+
|
|
153
|
+
### General Review
|
|
154
|
+
```
|
|
155
|
+
Review the uncommitted changes in this repository.
|
|
156
|
+
Focus on correctness, security, and design.
|
|
157
|
+
Flag issues by severity: CRITICAL, WARNING, or SUGGESTION.
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Focused Security Review
|
|
161
|
+
```
|
|
162
|
+
Review the uncommitted changes for security vulnerabilities only. Check for:
|
|
163
|
+
- SQL injection, XSS, command injection
|
|
164
|
+
- Authentication/authorisation gaps
|
|
165
|
+
- Secrets or credentials in code
|
|
166
|
+
- Unsafe deserialization
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Requirements Validation
|
|
170
|
+
```
|
|
171
|
+
The requirement was: "{original_requirement}"
|
|
172
|
+
|
|
173
|
+
Review the uncommitted changes and assess whether they fully satisfy the requirement.
|
|
174
|
+
Identify any gaps, missing edge cases, or partial implementations.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Test Coverage Review
|
|
178
|
+
```
|
|
179
|
+
Review the uncommitted changes and identify test scenarios that are missing.
|
|
180
|
+
For each gap, describe the test case that should exist and why it matters.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Common Mistakes
|
|
184
|
+
|
|
185
|
+
| Mistake | Fix |
|
|
186
|
+
|---------|-----|
|
|
187
|
+
| Relying on a "do not modify files" prompt instruction for safety | Enforce it (whitelist + `auto_approve=False`, or `disallowed_tools`) |
|
|
188
|
+
| Whitelisting tools but leaving the default `auto_approve=True` | `--dangerously-skip-permissions` makes the whitelist inert — set `auto_approve=False` |
|
|
189
|
+
| Assuming `allowed_tools` restricts OpenCode, Codex, or Cursor | All three ignore it |
|
|
190
|
+
| `disallowed_tools` to keep a Codex/Cursor reviewer read-only | It only warns; sandbox instead |
|
|
191
|
+
| Believing `disallowed_tools=["edit"]` makes the reviewer read-only | It doesn't — the model writes via `bash`; also deny `bash` (and OS-sandbox for a hard guarantee) |
|
|
192
|
+
| Not giving the reviewer `Bash`/git access | Without it the reviewer can't run `git diff` — keep `bash`, or pass the diff in the prompt |
|
|
193
|
+
| Not catching `AgentExecutionError` | A failed reviewer run raises, it does not return |
|
|
194
|
+
| Reviewing with the same model that wrote the code | Use a different model or agent type for independence |
|
|
195
|
+
| Ignoring the review and committing anyway | At minimum, address all CRITICAL items first |
|