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.
Files changed (116) hide show
  1. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/AGENTS.md +28 -6
  2. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/PKG-INFO +56 -8
  3. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/README.md +55 -7
  4. agent_shell_py-0.2.0/agent-skills/delegating-code-review/SKILL.md +195 -0
  5. agent_shell_py-0.2.0/agent-skills/invoking-cli-agents/SKILL.md +334 -0
  6. agent_shell_py-0.2.0/agent-skills/invoking-cli-agents/api-reference.md +258 -0
  7. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/agent_parameter_comparison.md +92 -33
  8. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/disabled_tools.md +10 -1
  9. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/total_token_count.md +5 -1
  10. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/_version.py +2 -2
  11. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/agent_adapter_protocol.py +7 -0
  12. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/claude_code_adapter.py +30 -31
  13. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/codex_adapter.py +30 -31
  14. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/copilot_cli_adapter.py +39 -37
  15. agent_shell_py-0.2.0/src/agent_shell/adapters/cursor_adapter.py +305 -0
  16. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/health.py +15 -22
  17. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/opencode_adapter.py +30 -31
  18. agent_shell_py-0.2.0/src/agent_shell/adapters/outcome.py +68 -0
  19. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/pi_adapter.py +77 -40
  20. agent_shell_py-0.2.0/src/agent_shell/adapters/response.py +70 -0
  21. agent_shell_py-0.2.0/src/agent_shell/adapters/stderr_format.py +18 -0
  22. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/models/agent.py +35 -1
  23. agent_shell_py-0.2.0/src/agent_shell/process_cleanup.py +213 -0
  24. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/shell.py +15 -9
  25. agent_shell_py-0.2.0/tests/conftest.py +50 -0
  26. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_claude_code_e2e.py +22 -11
  27. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_codex_e2e.py +20 -9
  28. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_copilot_cli_e2e.py +67 -12
  29. agent_shell_py-0.2.0/tests/e2e/test_cursor_e2e.py +115 -0
  30. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_health_check_e2e.py +2 -1
  31. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_opencode_e2e.py +22 -10
  32. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/test_pi_e2e.py +28 -9
  33. agent_shell_py-0.2.0/tests/integration/test_cursor_integration.py +381 -0
  34. agent_shell_py-0.2.0/tests/integration/test_cursor_mcp_integration.py +36 -0
  35. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_health_check_integration.py +36 -3
  36. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_pi_integration.py +3 -1
  37. agent_shell_py-0.2.0/tests/integration/test_process_lifecycle.py +283 -0
  38. agent_shell_py-0.2.0/tests/unit/adapter_matrix.py +67 -0
  39. agent_shell_py-0.2.0/tests/unit/cursor_fixtures.py +171 -0
  40. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/opencode_fixtures.py +10 -0
  41. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/pi_fixtures.py +103 -3
  42. agent_shell_py-0.2.0/tests/unit/test_adapter_transport.py +486 -0
  43. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_cancel.py +4 -5
  44. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_cancel.py +3 -7
  45. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_cancel.py +3 -7
  46. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_execute.py +28 -5
  47. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_parse_event.py +17 -12
  48. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_copilot_cli_stream.py +7 -5
  49. agent_shell_py-0.2.0/tests/unit/test_cursor_cancel.py +21 -0
  50. agent_shell_py-0.2.0/tests/unit/test_cursor_execute.py +170 -0
  51. agent_shell_py-0.2.0/tests/unit/test_cursor_parse_event.py +220 -0
  52. agent_shell_py-0.2.0/tests/unit/test_cursor_warnings.py +211 -0
  53. agent_shell_py-0.2.0/tests/unit/test_execute_outcome.py +446 -0
  54. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_health_probe.py +17 -2
  55. agent_shell_py-0.2.0/tests/unit/test_models.py +97 -0
  56. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_cancel.py +4 -5
  57. agent_shell_py-0.2.0/tests/unit/test_pi_cancel.py +21 -0
  58. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_execute.py +15 -7
  59. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_parse_event.py +132 -7
  60. agent_shell_py-0.2.0/tests/unit/test_process_cleanup.py +596 -0
  61. agent_shell_py-0.2.0/tests/unit/test_response_aggregation.py +131 -0
  62. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell.py +24 -3
  63. agent_shell_py-0.2.0/tests/unit/test_stderr_format.py +67 -0
  64. agent_shell_py-0.1.16/agent-skills/delegating-code-review/SKILL.md +0 -146
  65. agent_shell_py-0.1.16/agent-skills/invoking-cli-agents/SKILL.md +0 -239
  66. agent_shell_py-0.1.16/agent-skills/invoking-cli-agents/api-reference.md +0 -140
  67. agent_shell_py-0.1.16/src/agent_shell/process_cleanup.py +0 -37
  68. agent_shell_py-0.1.16/tests/unit/test_adapter_transport.py +0 -239
  69. agent_shell_py-0.1.16/tests/unit/test_models.py +0 -33
  70. agent_shell_py-0.1.16/tests/unit/test_pi_cancel.py +0 -40
  71. agent_shell_py-0.1.16/tests/unit/test_process_cleanup.py +0 -89
  72. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/build.yml +0 -0
  73. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/ci.yml +0 -0
  74. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.github/workflows/publish.yml +0 -0
  75. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.gitignore +0 -0
  76. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/.python-version +0 -0
  77. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/LICENSE +0 -0
  78. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/docs/development/info.md +0 -0
  79. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/pyproject.toml +0 -0
  80. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/__init__.py +0 -0
  81. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/__init__.py +0 -0
  82. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/adapters/tool_denial.py +0 -0
  83. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/src/agent_shell/models/__init__.py +0 -0
  84. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/__init__.py +0 -0
  85. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/e2e/__init__.py +0 -0
  86. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/__init__.py +0 -0
  87. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_integration.py +0 -0
  88. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_claude_code_mcp_integration.py +0 -0
  89. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_codex_integration.py +0 -0
  90. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_codex_mcp_integration.py +0 -0
  91. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_integration.py +0 -0
  92. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_copilot_cli_mcp_integration.py +0 -0
  93. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_opencode_integration.py +0 -0
  94. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_opencode_mcp_integration.py +0 -0
  95. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/integration/test_pi_mcp_integration.py +0 -0
  96. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/__init__.py +0 -0
  97. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/codex_fixtures.py +0 -0
  98. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/copilot_fixtures.py +0 -0
  99. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/fixtures.py +0 -0
  100. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_execute.py +0 -0
  101. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_parse_event.py +0 -0
  102. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_codex_warnings.py +0 -0
  103. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_execute.py +0 -0
  104. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_mcp_server_spec.py +0 -0
  105. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_execute.py +0 -0
  106. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_parse_event.py +0 -0
  107. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_spawn.py +0 -0
  108. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_opencode_stream.py +0 -0
  109. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_parse_event.py +0 -0
  110. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_pi_warnings.py +0 -0
  111. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_process_group_registration.py +0 -0
  112. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell_cancellation.py +0 -0
  113. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_shell_mcp.py +0 -0
  114. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_stream.py +0 -0
  115. {agent_shell_py-0.1.16 → agent_shell_py-0.2.0}/tests/unit/test_tool_denial.py +0 -0
  116. {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 and returns `HealthCheckResult(healthy, exception)`. The verdict is derived from the normalised event stream (healthy = a `result` event with no `error`), not exit codes — which are unreliable, since some CLIs exit 0 on failure. The shared logic lives once in `adapters/health.py`; each adapter delegates to it.
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
- - [ ] Gemini CLI
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 yet implemented for Pi — all three methods raise `NotImplementedError` (Pi manages capability via `pi install` extensions, which needs investigation before wiring up).
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.1.16
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
- - **Five CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, and Pi behind a common
19
- adapter protocol.
20
- - **Execute or stream** — get one `AgentResponse`, or async-iterate normalized `StreamEvent`s
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's `run` mode auto-approves all tools. The `allowed_tools` and `effort` parameters are configured via `opencode.json`, not CLI flags.
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
- - **Five CLI agents** — Claude Code, OpenCode, Copilot CLI, Codex, and Pi behind a common
10
- adapter protocol.
11
- - **Execute or stream** — get one `AgentResponse`, or async-iterate normalized `StreamEvent`s
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's `run` mode auto-approves all tools. The `allowed_tools` and `effort` parameters are configured via `opencode.json`, not CLI flags.
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 |