agentshim 0.11.0__tar.gz → 0.13.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 (65) hide show
  1. {agentshim-0.11.0 → agentshim-0.13.0}/CHANGELOG.md +42 -0
  2. {agentshim-0.11.0 → agentshim-0.13.0}/PKG-INFO +6 -3
  3. {agentshim-0.11.0 → agentshim-0.13.0}/README.md +5 -2
  4. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/__init__.py +5 -1
  5. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/agent.py +24 -5
  6. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/__init__.py +4 -0
  7. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/errors.py +4 -0
  8. agentshim-0.13.0/agentshim/core/home.py +90 -0
  9. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/profile.py +27 -0
  10. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/provider.py +4 -1
  11. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/failures.py +17 -3
  12. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/parser.py +30 -11
  13. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/provider.py +14 -1
  14. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/scripted.py +35 -10
  15. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/provider.py +36 -0
  16. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/scripted.py +13 -3
  17. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/testing/__init__.py +5 -2
  18. {agentshim-0.11.0 → agentshim-0.13.0}/pyproject.toml +1 -1
  19. {agentshim-0.11.0 → agentshim-0.13.0}/.gitignore +0 -0
  20. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/_files.py +0 -0
  21. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/env.py +0 -0
  22. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/events.py +0 -0
  23. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/mcp.py +0 -0
  24. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/pricing.py +0 -0
  25. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/schema.py +0 -0
  26. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/skills.py +0 -0
  27. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/stream.py +0 -0
  28. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/turn.py +0 -0
  29. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/core/usage.py +0 -0
  30. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/execution/__init__.py +0 -0
  31. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/execution/executor.py +0 -0
  32. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/execution/host.py +0 -0
  33. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/execution/transform.py +0 -0
  34. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/__init__.py +0 -0
  35. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/__init__.py +0 -0
  36. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/events.py +0 -0
  37. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/hooks/__init__.py +0 -0
  38. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/hooks/confine_reads.py +0 -0
  39. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/sandbox.py +0 -0
  40. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/claude/user_hooks.py +0 -0
  41. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/__init__.py +0 -0
  42. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/_toml.py +0 -0
  43. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/events.py +0 -0
  44. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/failures.py +0 -0
  45. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/mcp_scope.py +0 -0
  46. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/parser.py +0 -0
  47. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/rules.py +0 -0
  48. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/sandbox.py +0 -0
  49. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/codex/skills.py +0 -0
  50. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/copilot/__init__.py +0 -0
  51. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/copilot/events.py +0 -0
  52. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/copilot/parser.py +0 -0
  53. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/copilot/provider.py +0 -0
  54. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/copilot/scripted.py +0 -0
  55. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/gemini/__init__.py +0 -0
  56. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/gemini/events.py +0 -0
  57. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/gemini/parser.py +0 -0
  58. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/gemini/provider.py +0 -0
  59. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/gemini/scripted.py +0 -0
  60. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/opencode/__init__.py +0 -0
  61. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/opencode/events.py +0 -0
  62. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/opencode/parser.py +0 -0
  63. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/opencode/provider.py +0 -0
  64. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/providers/opencode/scripted.py +0 -0
  65. {agentshim-0.11.0 → agentshim-0.13.0}/agentshim/py.typed +0 -0
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 (2026-10-03)
4
+
5
+ A session can leave out the user's own CLI configuration. Additive.
6
+
7
+ ### Added
8
+
9
+ - `ConfigScope` (`ALL`, `PROJECT`) as a session option on `start_session` and
10
+ `run`, `ProviderProfile.config_scopes`, and `ArgvContext.config_scope`.
11
+ `PROJECT` keeps the user's settings, hooks, global instructions, notify
12
+ commands, profiles and memory out of every turn. Claude Code:
13
+ `--setting-sources project,local` and `autoMemoryEnabled=false`. Codex: a
14
+ dedicated `CODEX_HOME` plus `--ignore-user-config --disable memories`;
15
+ `build_argv` refuses the scope in the user's own home. Copilot, Gemini and
16
+ opencode refuse it.
17
+ - `ProviderProfile.config_home_files` and `prepare_config_home`, which build
18
+ that dedicated home holding only the login and return the environment that
19
+ points the CLI at it. The login is a symlink to the user's own `auth.json`,
20
+ so a refresh token Codex rotates in either home stays valid in both.
21
+
22
+ ## 0.12.0 (2026-10-03)
23
+
24
+ A turn whose output never matched its schema says so. Additive except that
25
+ Claude's `error_max_structured_output_retries` is no longer `OTHER`.
26
+
27
+ ### Added
28
+
29
+ - `FailureKind.SCHEMA`: the provider gave up producing output that matches the
30
+ requested schema. Claude Code reports it for the
31
+ `error_max_structured_output_retries` result, with the validation errors of
32
+ the last rejected `StructuredOutput` call on `CliExitError.detail`. A resumed
33
+ turn that fails this way keeps its conversation. Codex never reports it:
34
+ `--output-schema` constrains decoding, so its output always parses against
35
+ the schema.
36
+ - `scripted_failure("claude", FailureKind.SCHEMA)`.
37
+
38
+ ### Changed
39
+
40
+ - `scripted_failure(provider, FailureKind.OTHER)` scripts a failure that is
41
+ not about the schema (Claude `error_during_execution`, Codex running out of
42
+ context). `scripted_failure("codex", FailureKind.SCHEMA)` raises
43
+ `ValueError`.
44
+
3
45
  ## 0.11.0 (2026-10-03)
4
46
 
5
47
  A failed turn says why it failed. Additive except that a classified failure of
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentshim
3
- Version: 0.11.0
3
+ Version: 0.13.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: tomli>=2; python_full_version < '3.11'
@@ -200,8 +200,11 @@ AgentShimError
200
200
  `CliExitError.kind` says why a turn failed, so a caller can pick a policy
201
201
  without knowing how any CLI words its errors: `TRANSIENT` (an overload, rate
202
202
  limit or server error that waiting may outlast), `USAGE_LIMIT` (a quota or
203
- billing limit), `AUTH` (credentials), or `OTHER`. Claude Code and Codex
204
- classify their failures; the other providers report `OTHER`.
203
+ billing limit), `AUTH` (credentials), `SCHEMA` (the provider gave up producing
204
+ output that matches the requested schema; `.detail` holds the last validation
205
+ errors and the conversation survives, so a correction can follow), or `OTHER`.
206
+ Claude Code and Codex classify their failures; the other providers report
207
+ `OTHER`. Codex never reports `SCHEMA`: it constrains decoding to the schema.
205
208
  `agentshim.testing.scripted_failure(provider, kind)` scripts each one.
206
209
 
207
210
  ## Adding a provider
@@ -190,8 +190,11 @@ AgentShimError
190
190
  `CliExitError.kind` says why a turn failed, so a caller can pick a policy
191
191
  without knowing how any CLI words its errors: `TRANSIENT` (an overload, rate
192
192
  limit or server error that waiting may outlast), `USAGE_LIMIT` (a quota or
193
- billing limit), `AUTH` (credentials), or `OTHER`. Claude Code and Codex
194
- classify their failures; the other providers report `OTHER`.
193
+ billing limit), `AUTH` (credentials), `SCHEMA` (the provider gave up producing
194
+ output that matches the requested schema; `.detail` holds the last validation
195
+ errors and the conversation survives, so a correction can follow), or `OTHER`.
196
+ Claude Code and Codex classify their failures; the other providers report
197
+ `OTHER`. Codex never reports `SCHEMA`: it constrains decoding to the schema.
195
198
  `agentshim.testing.scripted_failure(provider, kind)` scripts each one.
196
199
 
197
200
  ## Adding a provider
@@ -19,6 +19,7 @@ from .core import (
19
19
  CliTimeoutError,
20
20
  CompositeEventHandler,
21
21
  ConfigFileInstallation,
22
+ ConfigScope,
22
23
  ConsoleEventHandler,
23
24
  EventHandlerBase,
24
25
  FailureKind,
@@ -79,6 +80,7 @@ from .core import (
79
80
  normalize,
80
81
  normalized_usage,
81
82
  parse_json_object,
83
+ prepare_config_home,
82
84
  price_for,
83
85
  )
84
86
  from .execution import (
@@ -99,7 +101,7 @@ from .providers.copilot import CopilotProvider
99
101
  from .providers.gemini import GeminiProvider
100
102
  from .providers.opencode import OpencodeProvider
101
103
 
102
- __version__ = "0.11.0"
104
+ __version__ = "0.13.0"
103
105
 
104
106
  __all__ = [
105
107
  "AgentEvent",
@@ -125,6 +127,7 @@ __all__ = [
125
127
  "CommandStreamSink",
126
128
  "CompositeEventHandler",
127
129
  "ConfigFileInstallation",
130
+ "ConfigScope",
128
131
  "ConsoleEventHandler",
129
132
  "CopilotProvider",
130
133
  "EventHandlerBase",
@@ -194,6 +197,7 @@ __all__ = [
194
197
  "normalize",
195
198
  "normalized_usage",
196
199
  "parse_json_object",
200
+ "prepare_config_home",
197
201
  "price_for",
198
202
  "provider_names",
199
203
  ]
@@ -24,6 +24,7 @@ from agentshim.core.errors import (
24
24
  )
25
25
  from agentshim.core.events import RunFinished, RunStarted, compose_event_handlers
26
26
  from agentshim.core.profile import (
27
+ ConfigScope,
27
28
  McpMechanism,
28
29
  McpScope,
29
30
  OutputSchemaStyle,
@@ -114,7 +115,7 @@ class CliAgent:
114
115
  self.executor.check_binary(self.binary_path, self.env, timeout=check_timeout)
115
116
  self.log(f"{self.profile.display_name} ready at {self.binary_path}")
116
117
 
117
- def start_session(
118
+ def start_session( # noqa: PLR0913 - one keyword per independent session option
118
119
  self,
119
120
  *,
120
121
  cwd: str | None = None,
@@ -122,11 +123,12 @@ class CliAgent:
122
123
  session_id: str | None = None,
123
124
  skill_scope: SkillScope = SkillScope.ALL,
124
125
  mcp_scope: McpScope = McpScope.ALL,
126
+ config_scope: ConfigScope = ConfigScope.ALL,
125
127
  ) -> AgentSession:
126
128
  """Open a conversation whose turns resume one another.
127
129
 
128
- ``skill_scope`` and ``mcp_scope`` are fixed for the conversation (see
129
- ``AgentSession``).
130
+ ``skill_scope``, ``mcp_scope`` and ``config_scope`` are fixed for the
131
+ conversation (see ``AgentSession``).
130
132
  """
131
133
  return AgentSession(
132
134
  self,
@@ -135,9 +137,10 @@ class CliAgent:
135
137
  session_id=session_id,
136
138
  skill_scope=skill_scope,
137
139
  mcp_scope=mcp_scope,
140
+ config_scope=config_scope,
138
141
  )
139
142
 
140
- def run(
143
+ def run( # noqa: PLR0913 - one keyword per independent session option
141
144
  self,
142
145
  request: TurnRequest | str,
143
146
  *,
@@ -145,10 +148,15 @@ class CliAgent:
145
148
  timeout: float | None = None,
146
149
  skill_scope: SkillScope = SkillScope.ALL,
147
150
  mcp_scope: McpScope = McpScope.ALL,
151
+ config_scope: ConfigScope = ConfigScope.ALL,
148
152
  ) -> TurnResult:
149
153
  """Run one turn in a throwaway session."""
150
154
  session = self.start_session(
151
- cwd=cwd, timeout=timeout, skill_scope=skill_scope, mcp_scope=mcp_scope
155
+ cwd=cwd,
156
+ timeout=timeout,
157
+ skill_scope=skill_scope,
158
+ mcp_scope=mcp_scope,
159
+ config_scope=config_scope,
152
160
  )
153
161
  return session.turn(request)
154
162
 
@@ -169,6 +177,7 @@ class AgentSession:
169
177
  session_id: str | None = None,
170
178
  skill_scope: SkillScope = SkillScope.ALL,
171
179
  mcp_scope: McpScope = McpScope.ALL,
180
+ config_scope: ConfigScope = ConfigScope.ALL,
172
181
  ) -> None:
173
182
  """Bind a conversation to one agent, with per-turn defaults.
174
183
 
@@ -183,6 +192,11 @@ class AgentSession:
183
192
  limits which MCP servers every turn's CLI may connect to
184
193
  (``McpScope.SESSION``: only those in ``TurnRequest.mcp_servers``) and
185
194
  is checked against ``profile.mcp_scopes`` the same way.
195
+ ``config_scope`` limits which of the user's own CLI configuration
196
+ (settings, hooks, global instructions, memory) every turn loads, and
197
+ is checked against ``profile.config_scopes``; a provider with
198
+ ``profile.config_home_files`` additionally needs its state root
199
+ pointed at a home from ``prepare_config_home``.
186
200
  """
187
201
  if skill_scope not in agent.profile.skill_scopes:
188
202
  msg = f"{agent.profile.name} cannot limit skills to scope {skill_scope.value!r}"
@@ -190,7 +204,11 @@ class AgentSession:
190
204
  if mcp_scope not in agent.profile.mcp_scopes:
191
205
  msg = f"{agent.profile.name} cannot limit MCP servers to scope {mcp_scope.value!r}"
192
206
  raise ProviderCapabilityError(msg)
207
+ if config_scope not in agent.profile.config_scopes:
208
+ msg = f"{agent.profile.name} cannot limit user configuration to scope {config_scope.value!r}"
209
+ raise ProviderCapabilityError(msg)
193
210
  self.skill_scope = skill_scope
211
+ self.config_scope = config_scope
194
212
  self.mcp_scope = mcp_scope
195
213
  self._agent = agent
196
214
  self._cwd = cwd
@@ -302,6 +320,7 @@ class AgentSession:
302
320
  cwd=cwd,
303
321
  skill_scope=self.skill_scope,
304
322
  mcp_scope=self.mcp_scope,
323
+ config_scope=self.config_scope,
305
324
  mcp_servers=tuple(req.mcp_servers),
306
325
  )
307
326
  )
@@ -43,6 +43,7 @@ from .events import (
43
43
  UsageReport,
44
44
  compose_event_handlers,
45
45
  )
46
+ from .home import prepare_config_home
46
47
  from .mcp import (
47
48
  ConfigFileInstallation,
48
49
  FlagsInstallation,
@@ -55,6 +56,7 @@ from .mcp import (
55
56
  )
56
57
  from .pricing import ModelPricing, PricingTable, cost_usd, default_pricing, price_for
57
58
  from .profile import (
59
+ ConfigScope,
58
60
  McpMechanism,
59
61
  McpScope,
60
62
  OutputSchemaStyle,
@@ -82,6 +84,7 @@ __all__ = [
82
84
  "CliTimeoutError",
83
85
  "CompositeEventHandler",
84
86
  "ConfigFileInstallation",
87
+ "ConfigScope",
85
88
  "ConsoleEventHandler",
86
89
  "EventHandlerBase",
87
90
  "FailureKind",
@@ -142,5 +145,6 @@ __all__ = [
142
145
  "normalize",
143
146
  "normalized_usage",
144
147
  "parse_json_object",
148
+ "prepare_config_home",
145
149
  "price_for",
146
150
  ]
@@ -29,6 +29,10 @@ class FailureKind(str, Enum):
29
29
  USAGE_LIMIT = "usage_limit"
30
30
  #: Missing, expired, or rejected credentials.
31
31
  AUTH = "auth"
32
+ #: The provider gave up producing output that matches the requested output
33
+ #: schema. ``detail`` carries the last validation errors it reported, and
34
+ #: the conversation survives: a correction can be sent as the next turn.
35
+ SCHEMA = "schema"
32
36
  #: Anything else, including every failure a provider cannot classify.
33
37
  OTHER = "other"
34
38
 
@@ -0,0 +1,90 @@
1
+ """Dedicated CLI homes for ``ConfigScope.PROJECT``.
2
+
3
+ Some CLIs read the user's global instructions, hooks and memory from their
4
+ state root (Codex: ``$CODEX_HOME/AGENTS.md``, ``hooks.json``, ``memories/``)
5
+ with no flag to skip them. The only way to keep them out is to run the CLI
6
+ against another state root that holds nothing but the credentials. That root
7
+ is the caller's to own, because the CLI keeps its conversations there too: a
8
+ session resumes only from the home it started in.
9
+
10
+ The credentials are linked, not copied. Codex rotates its OAuth refresh token
11
+ on refresh (the server rejects a reused one with ``refresh_token_reused``) and
12
+ saves ``auth.json`` by opening it with ``O_TRUNC`` and writing in place
13
+ (codex-rs ``login/src/auth/storage.rs``, ``FileAuthStorage::save``), which
14
+ follows a symlink. A copy would strand one of the two logins after the first
15
+ refresh; a link keeps a single file that every process rereads before it
16
+ refreshes (``AuthManager::refresh_token``'s guarded reload).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING
24
+
25
+ from ._files import real_path
26
+
27
+ if TYPE_CHECKING:
28
+ from collections.abc import Mapping
29
+
30
+ from .profile import ProviderProfile
31
+
32
+
33
+ def state_root(profile: ProviderProfile, env: Mapping[str, str]) -> Path | None:
34
+ """The state root the CLI run with *env* reads, or ``None`` if it has none.
35
+
36
+ ``$<state_root_env>`` when set, else ``$HOME/<state_dirs[0]>``.
37
+ """
38
+ if profile.state_root_env and env.get(profile.state_root_env):
39
+ return Path(env[profile.state_root_env]).expanduser()
40
+ if not profile.state_dirs:
41
+ return None
42
+ home = env.get("HOME") or os.path.expanduser("~") # noqa: PTH111 - a str contract, not a Path
43
+ return Path(home) / profile.state_dirs[0]
44
+
45
+
46
+ def prepare_config_home(
47
+ profile: ProviderProfile, home: Path, env: Mapping[str, str]
48
+ ) -> dict[str, str]:
49
+ """Make *home* a state root holding only the credentials, and point at it.
50
+
51
+ Links each of ``profile.config_home_files`` that exists in the state root
52
+ *env* selects into *home* as a symlink to that file's real path (replacing
53
+ whatever was there, such as a copy from an older agentshim), so a token
54
+ the CLI refreshes in either home is the one both read next. Returns the
55
+ environment overrides that make the CLI use *home*. A sandbox that runs
56
+ the CLI must expose the link's target read-write. Everything else in *home* (conversations, caches) is the
57
+ CLI's own and is left alone. A provider with no ``config_home_files``
58
+ isolates by flags alone: nothing is written and the result is empty.
59
+
60
+ Raises:
61
+ ValueError: *home* is not absolute, or is the state root *env*
62
+ already selects, which would isolate nothing.
63
+ """
64
+ if not profile.config_home_files:
65
+ return {}
66
+ if not home.is_absolute():
67
+ msg = f"{profile.name} config home must be an absolute path, got {home}"
68
+ raise ValueError(msg)
69
+ assert profile.state_root_env is not None, "config_home_files needs a state_root_env" # noqa: S101 - profile invariant
70
+ source = state_root(profile, env)
71
+ if source is not None and real_path(source) == real_path(home):
72
+ msg = f"{profile.name} config home {home} is the user's own state root"
73
+ raise ValueError(msg)
74
+ home.mkdir(parents=True, exist_ok=True)
75
+ for name in profile.config_home_files:
76
+ origin = None if source is None else source / name
77
+ if origin is not None and origin.is_file():
78
+ _link(home / name, real_path(origin))
79
+ return {profile.state_root_env: str(home)}
80
+
81
+
82
+ def _link(link: Path, target: Path) -> None:
83
+ """Point *link* at *target*, atomically replacing any file already there."""
84
+ if link.is_symlink() and link.readlink() == target:
85
+ return
86
+ link.parent.mkdir(parents=True, exist_ok=True)
87
+ staged = link.with_name(f".{link.name}.{os.getpid()}.link")
88
+ staged.unlink(missing_ok=True)
89
+ staged.symlink_to(target)
90
+ staged.replace(link)
@@ -93,6 +93,26 @@ class McpScope(Enum):
93
93
  SESSION = "session"
94
94
 
95
95
 
96
+ class ConfigScope(Enum):
97
+ """Which of the user's own CLI configuration a session loads.
98
+
99
+ ``ALL``: whatever the CLI loads by default: the user's settings, hooks,
100
+ global instructions (``~/.claude/CLAUDE.md``, ``$CODEX_HOME/AGENTS.md``),
101
+ notify commands, profiles and memory. ``PROJECT``: none of that; the
102
+ session sees the workspace's own instructions and settings plus what
103
+ agentshim passes for the turn, so the user running agentshim does not
104
+ change what the agent does. Credentials keep working. A provider that
105
+ lists a ``ProviderProfile.config_home_files`` can only isolate through a
106
+ dedicated home: ``prepare_config_home`` builds it and returns the
107
+ environment that points the CLI at it. A provider declares the scopes it
108
+ can enforce in ``ProviderProfile.config_scopes``; asking for another one
109
+ is an error, never a silent ``ALL``.
110
+ """
111
+
112
+ ALL = "all"
113
+ PROJECT = "project"
114
+
115
+
96
116
  def _no_container_env() -> Mapping[str, str]:
97
117
  """Empty read-only default: a frozen spec must not carry a mutable one.
98
118
 
@@ -145,3 +165,10 @@ class ProviderProfile:
145
165
  skill_scopes: frozenset[SkillScope] = frozenset({SkillScope.ALL})
146
166
  #: The ``McpScope`` values a session on this provider may request.
147
167
  mcp_scopes: frozenset[McpScope] = frozenset({McpScope.ALL})
168
+ #: The ``ConfigScope`` values a session on this provider may request.
169
+ config_scopes: frozenset[ConfigScope] = frozenset({ConfigScope.ALL})
170
+ #: Files, relative to the state root (``state_dirs[0]``, or
171
+ #: ``$<state_root_env>``), that ``prepare_config_home`` copies into a
172
+ #: dedicated home for ``ConfigScope.PROJECT``. Empty when the provider
173
+ #: isolates by flags alone and needs no home.
174
+ config_home_files: tuple[str, ...] = ()
@@ -6,7 +6,7 @@ from dataclasses import dataclass, field
6
6
  from typing import TYPE_CHECKING, Any, Protocol
7
7
 
8
8
  from .errors import FailureKind
9
- from .profile import McpScope, SkillScope
9
+ from .profile import ConfigScope, McpScope, SkillScope
10
10
  from .usage import ProviderUsage
11
11
 
12
12
  if TYPE_CHECKING:
@@ -49,6 +49,9 @@ class ArgvContext:
49
49
  #: A provider enforcing ``McpScope.SESSION`` renders them itself when its
50
50
  #: installed config file is not enough to exclude the user's own.
51
51
  mcp_servers: Sequence[McpServer] = ()
52
+ #: Which of the user's own CLI configuration the CLI may load; the
53
+ #: session has already checked that the provider's profile supports it.
54
+ config_scope: ConfigScope = ConfigScope.ALL
52
55
 
53
56
 
54
57
  @dataclass(frozen=True)
@@ -4,7 +4,9 @@ Claude reports a failed API request three ways, newest first: an ``error``
4
4
  kind on the synthetic assistant message it writes for the failure, an
5
5
  ``api_error_status`` on the final ``result`` frame, and the text
6
6
  ``API Error: <status> <body>`` in that frame's ``result``. Older builds only
7
- have the text, so all three are read.
7
+ have the text, so all three are read. A turn whose ``StructuredOutput`` calls
8
+ never validated ends with the ``result`` subtype
9
+ ``error_max_structured_output_retries`` instead.
8
10
  """
9
11
 
10
12
  from __future__ import annotations
@@ -39,17 +41,29 @@ _AUTH_STATUSES = frozenset({401, 403})
39
41
  #: A request timeout or a rate limit; every 5xx (529 is an overload) is transient too.
40
42
  _TRANSIENT_STATUSES = frozenset({408, 429})
41
43
  _SERVER_ERRORS = range(500, 600)
44
+ #: The ``result`` subtype of a turn that used up its output-schema retries.
45
+ SCHEMA_RETRIES_SUBTYPE = "error_max_structured_output_retries"
42
46
 
43
47
 
44
- def classify_failure(*, api_error: str | None, status: int | None, text: str) -> FailureKind:
48
+ def classify_failure(
49
+ *, subtype: str | None, api_error: str | None, status: int | None, text: str
50
+ ) -> FailureKind:
45
51
  """Classify one failed turn from what Claude reported about it.
46
52
 
47
- ``api_error`` is the assistant frame's ``error`` kind, ``status`` the
53
+ ``subtype`` is the error ``result`` frame's subtype, ``api_error`` the
54
+ assistant frame's ``error`` kind, ``status`` the
48
55
  result frame's ``api_error_status``, and ``text`` the error text the
49
56
  stream (or, when the stream had none, stderr) carried. Tool output is
50
57
  never part of ``text``, so a tool that printed an API error cannot make
51
58
  the turn look like one.
52
59
  """
60
+ if subtype == SCHEMA_RETRIES_SUBTYPE:
61
+ return FailureKind.SCHEMA
62
+ return _classify_api_failure(api_error=api_error, status=status, text=text)
63
+
64
+
65
+ def _classify_api_failure(*, api_error: str | None, status: int | None, text: str) -> FailureKind:
66
+ """Classify a failure from the API error Claude reported, if it reported one."""
53
67
  if api_error in _AUTH_KINDS:
54
68
  return FailureKind.AUTH
55
69
  if api_error in _USAGE_LIMIT_KINDS:
@@ -32,7 +32,7 @@ from .events import (
32
32
  ToolResultBlock,
33
33
  parse_frame,
34
34
  )
35
- from .failures import classify_failure
35
+ from .failures import SCHEMA_RETRIES_SUBTYPE, classify_failure
36
36
 
37
37
  if TYPE_CHECKING:
38
38
  from collections.abc import Callable, Mapping
@@ -45,6 +45,10 @@ PROVIDER_NAME = "claude"
45
45
  #: The built-in tool through which Claude Code loads a skill.
46
46
  SKILL_TOOL = "Skill"
47
47
 
48
+ #: The tool through which Claude Code submits a schema-constrained answer;
49
+ #: its error result is the validation failure of one submission.
50
+ STRUCTURED_OUTPUT_TOOL = "StructuredOutput"
51
+
48
52
  #: The built-in tool through which Claude Code reads a file.
49
53
  READ_TOOL = "Read"
50
54
 
@@ -142,6 +146,9 @@ class ClaudeStreamParser:
142
146
  self._api_error: str | None = None
143
147
  self._api_error_text: str | None = None
144
148
  self._api_error_status: int | None = None
149
+ self._subtype: str | None = None
150
+ # The validation errors of the last rejected StructuredOutput call.
151
+ self._schema_errors: str | None = None
145
152
  self._stderr: list[str] = []
146
153
 
147
154
  def feed_stdout(self, line: str) -> None:
@@ -189,7 +196,12 @@ class ClaudeStreamParser:
189
196
  """
190
197
  reported = [text for text in (self._error, self._api_error_text) if text]
191
198
  text = "\n".join(reported) if reported else "\n".join(self._stderr)
192
- return classify_failure(api_error=self._api_error, status=self._api_error_status, text=text)
199
+ return classify_failure(
200
+ subtype=self._subtype,
201
+ api_error=self._api_error,
202
+ status=self._api_error_status,
203
+ text=text,
204
+ )
193
205
 
194
206
  def _handle(self, frame: object) -> None:
195
207
  if isinstance(frame, SystemInit):
@@ -238,6 +250,8 @@ class ClaudeStreamParser:
238
250
 
239
251
  def _tool_result(self, frame: ToolResultBlock) -> None:
240
252
  name = self._tools.name(frame.tool_id)
253
+ if name == STRUCTURED_OUTPUT_TOOL and frame.is_error:
254
+ self._schema_errors = frame.output
241
255
  self._emit(
242
256
  ToolResult(
243
257
  tool_id=frame.tool_id,
@@ -262,7 +276,8 @@ class ClaudeStreamParser:
262
276
  self._emit(UsageReport(self._usage, frame.total_cost_usd))
263
277
  if frame.is_error:
264
278
  self._api_error_status = frame.api_error_status
265
- self._error = _error_text(frame)
279
+ self._subtype = frame.subtype
280
+ self._error = self._error_text(frame)
266
281
  self._emit(ProviderError(self._error))
267
282
 
268
283
  def _structured_payload(self, frame: ResultFrame) -> object | None:
@@ -281,12 +296,16 @@ class ClaudeStreamParser:
281
296
  except (json.JSONDecodeError, ValueError):
282
297
  return None
283
298
 
299
+ def _error_text(self, frame: ResultFrame) -> str:
300
+ """Say what an error ``result`` frame reported.
284
301
 
285
- def _error_text(frame: ResultFrame) -> str:
286
- """Say what an error ``result`` frame reported.
287
-
288
- An ``error_*`` subtype carries its messages in ``errors`` and often no
289
- ``result`` text at all, so both are read before falling back to the
290
- subtype, which is the only part that names some failures.
291
- """
292
- return frame.text or "; ".join(frame.errors) or frame.subtype or "claude reported an error"
302
+ A turn that used up its schema retries is described by the validation
303
+ errors of its last rejected submission, which only the
304
+ ``StructuredOutput`` tool result carries. Otherwise an ``error_*``
305
+ subtype carries its messages in ``errors`` and often no ``result``
306
+ text at all, so both are read before falling back to the subtype,
307
+ which is the only part that names some failures.
308
+ """
309
+ if frame.subtype == SCHEMA_RETRIES_SUBTYPE and self._schema_errors:
310
+ return self._schema_errors
311
+ return frame.text or "; ".join(frame.errors) or frame.subtype or "claude reported an error"
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
8
8
  from agentshim.core.errors import FailureKind, ProviderCapabilityError, SessionResumeError
9
9
  from agentshim.core.mcp import HttpMcpServer, NoopInstallation, StdioMcpServer, install_config_file
10
10
  from agentshim.core.profile import (
11
+ ConfigScope,
11
12
  McpMechanism,
12
13
  McpScope,
13
14
  OutputSchemaStyle,
@@ -44,6 +45,14 @@ MCP_SERVER_KEY = "mcpServers"
44
45
  #: account connectors are not user settings and stay.
45
46
  PROJECT_SETTING_SOURCES = ("--setting-sources", "project,local")
46
47
 
48
+ #: ``ConfigScope.PROJECT``: the same ``--setting-sources`` drops the user's
49
+ #: settings (hooks, ``env``, permissions, default model), ``~/.claude/CLAUDE.md``
50
+ #: and plugins; auto-memory is not a setting source and is switched off in the
51
+ #: turn's ``--settings`` instead, so ``~/.claude/projects/*/memory`` is neither
52
+ #: read nor written. Built-in skills, the workspace's ``CLAUDE.md``,
53
+ #: ``.claude/`` settings and skills, and credentials keep working.
54
+ NO_AUTO_MEMORY_SETTINGS = {"autoMemoryEnabled": False}
55
+
47
56
  #: ``McpScope.SESSION``: ``--strict-mcp-config`` makes ``--mcp-config`` the
48
57
  #: only source of MCP servers. It drops the user's and the project's
49
58
  #: (``.mcp.json``) servers, plugin servers and claude.ai account connectors,
@@ -75,6 +84,7 @@ PROFILE = ProviderProfile(
75
84
  skill_invocation=SkillSignal.STRUCTURED,
76
85
  skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
77
86
  mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
87
+ config_scopes=frozenset({ConfigScope.ALL, ConfigScope.PROJECT}),
78
88
  container_install=(
79
89
  "apt-get update && apt-get install -y --no-install-recommends curl ca-certificates",
80
90
  "curl -fsSL https://claude.ai/install.sh | bash",
@@ -138,7 +148,8 @@ class ClaudeProvider:
138
148
  "stream-json",
139
149
  "--verbose",
140
150
  ]
141
- if ctx.skill_scope is SkillScope.PROJECT:
151
+ isolate_config = ctx.config_scope is ConfigScope.PROJECT
152
+ if ctx.skill_scope is SkillScope.PROJECT or isolate_config:
142
153
  argv += PROJECT_SETTING_SOURCES
143
154
  if ctx.model:
144
155
  argv += ["--model", ctx.model]
@@ -147,6 +158,8 @@ class ClaudeProvider:
147
158
  if ctx.schema_inline:
148
159
  argv += ["--json-schema", ctx.schema_inline]
149
160
  settings = build_settings(self.sandbox, hooks=self.hooks)
161
+ if isolate_config:
162
+ settings.update(NO_AUTO_MEMORY_SETTINGS)
150
163
  if settings:
151
164
  argv += ["--settings", json.dumps(settings)]
152
165
  if ctx.mcp_scope is McpScope.SESSION:
@@ -120,9 +120,7 @@ def resume_failure_lines(*, session_id: str | None = None) -> tuple[list[str], l
120
120
 
121
121
 
122
122
  #: What Claude writes for a failed request of each kind: the assistant
123
- #: ``error`` kind, the HTTP status, and the message. ``OTHER`` is a turn that
124
- #: ran but could not satisfy its output schema, the subtype Claude reports
125
- #: when no StructuredOutput call validated.
123
+ #: ``error`` kind, the HTTP status, and the message.
126
124
  _FAILURES: dict[FailureKind, tuple[str, int, str]] = {
127
125
  FailureKind.TRANSIENT: (
128
126
  "overloaded",
@@ -133,6 +131,10 @@ _FAILURES: dict[FailureKind, tuple[str, int, str]] = {
133
131
  FailureKind.AUTH: ("authentication_failed", 401, "Invalid API key \u00b7 Please run /login"),
134
132
  }
135
133
 
134
+ #: The validation errors Claude's ``StructuredOutput`` tool returns for a
135
+ #: submission that does not match the schema.
136
+ SCHEMA_ERRORS = "Output does not match required schema: root: must have required property 'answer'"
137
+
136
138
 
137
139
  def failure_lines(
138
140
  kind: FailureKind, *, session_id: str | None = None
@@ -140,8 +142,10 @@ def failure_lines(
140
142
  """Build the stdout, stderr and exit code of a turn that fails with *kind*.
141
143
 
142
144
  An API failure is the synthetic assistant message Claude writes for it,
143
- then an error ``result`` frame with the HTTP status. ``OTHER`` is an
144
- ``error_max_structured_output_retries`` result, which names no API error.
145
+ then an error ``result`` frame with the HTTP status. ``SCHEMA`` is a
146
+ ``StructuredOutput`` call the tool rejects, then an
147
+ ``error_max_structured_output_retries`` result. ``OTHER`` is an
148
+ ``error_during_execution`` result, which names no API error.
145
149
 
146
150
  Args:
147
151
  kind: The classification the scripted failure must produce.
@@ -156,12 +160,13 @@ def failure_lines(
156
160
  result: dict[str, Any] = {"type": "result", "is_error": True, "num_turns": 1}
157
161
  if session_id is not None:
158
162
  result["session_id"] = session_id
159
- failure = _FAILURES.get(kind)
160
- if failure is None:
161
- result["subtype"] = "error_max_structured_output_retries"
162
- result["errors"] = ["no StructuredOutput call produced a valid output"]
163
+ if kind is FailureKind.SCHEMA:
164
+ lines.extend(_rejected_structured_output())
165
+ result.update(subtype="error_max_structured_output_retries", errors=[])
166
+ elif kind is FailureKind.OTHER:
167
+ result.update(subtype="error_during_execution", errors=["the turn stopped unexpectedly"])
163
168
  else:
164
- error, status, text = failure
169
+ error, status, text = _FAILURES[kind]
165
170
  message = {"role": "assistant", "content": [{"type": "text", "text": text}]}
166
171
  lines.append(_line({"type": "assistant", "message": message, "error": error}))
167
172
  result.update(subtype="success", result=text, api_error_status=status)
@@ -169,6 +174,26 @@ def failure_lines(
169
174
  return lines, [], 1
170
175
 
171
176
 
177
+ def _rejected_structured_output() -> list[str]:
178
+ """One ``StructuredOutput`` call and the validation error it got back."""
179
+ call: dict[str, Any] = {
180
+ "type": "tool_use",
181
+ "id": "toolu_schema",
182
+ "name": "StructuredOutput",
183
+ "input": {},
184
+ }
185
+ rejection = {
186
+ "type": "tool_result",
187
+ "tool_use_id": "toolu_schema",
188
+ "content": SCHEMA_ERRORS,
189
+ "is_error": True,
190
+ }
191
+ return [
192
+ _line({"type": "assistant", "message": {"role": "assistant", "content": [call]}}),
193
+ _line({"type": "user", "message": {"role": "user", "content": [rejection]}}),
194
+ ]
195
+
196
+
172
197
  def _usage_payload(usage: TokenUsage | None) -> dict[str, Any]:
173
198
  if usage is None:
174
199
  return {
@@ -7,8 +7,10 @@ import re
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from agentshim.core.errors import ProviderCapabilityError, SessionResumeError
10
+ from agentshim.core.home import state_root
10
11
  from agentshim.core.mcp import FlagsInstallation, HttpMcpServer, NoopInstallation
11
12
  from agentshim.core.profile import (
13
+ ConfigScope,
12
14
  McpMechanism,
13
15
  McpScope,
14
16
  OutputSchemaStyle,
@@ -85,6 +87,10 @@ PROFILE = ProviderProfile(
85
87
  skill_invocation=SkillSignal.INFERRED,
86
88
  skill_scopes=frozenset({SkillScope.ALL, SkillScope.PROJECT}),
87
89
  mcp_scopes=frozenset({McpScope.ALL, McpScope.SESSION}),
90
+ config_scopes=frozenset({ConfigScope.ALL, ConfigScope.PROJECT}),
91
+ # Codex reads AGENTS.md, hooks.json and memories/ from $CODEX_HOME with no
92
+ # flag to skip them, so ConfigScope.PROJECT needs a home of its own.
93
+ config_home_files=("auth.json",),
88
94
  container_install=(_NODE_INSTALL, _CODEX_INSTALL),
89
95
  # Documented Codex CLI variable that relocates ~/.codex (``codex --help``:
90
96
  # "Layer $CODEX_HOME/<name>.config.toml on top of the base user config";
@@ -105,6 +111,13 @@ BYPASS_FLAG = "--dangerously-bypass-approvals-and-sandbox"
105
111
  #: Keeps user and project ``.rules`` files out of a sandboxed turn.
106
112
  IGNORE_RULES_FLAG = "--ignore-rules"
107
113
 
114
+ #: ``ConfigScope.PROJECT``: ``$CODEX_HOME/config.toml`` (profiles, notify,
115
+ #: ``developer_instructions``, project trust) is not loaded, and the memories
116
+ #: Codex would otherwise write and reread across sessions in the home stay
117
+ #: off. The global ``AGENTS.md``, ``hooks.json`` and existing memories are
118
+ #: kept out by the dedicated home itself (``prepare_config_home``).
119
+ PROJECT_CONFIG_FLAGS = ("--ignore-user-config", "--disable", "memories")
120
+
108
121
 
109
122
  class CodexProvider:
110
123
  """Codex (``codex exec --json``). ``sandbox`` is a provider option."""
@@ -136,6 +149,9 @@ class CodexProvider:
136
149
  if ctx.model:
137
150
  argv += ["--model", ctx.model]
138
151
  argv += _shell_path_config(ctx.env)
152
+ if ctx.config_scope is ConfigScope.PROJECT:
153
+ _check_config_home(ctx)
154
+ argv += PROJECT_CONFIG_FLAGS
139
155
  argv += _scope_overrides(ctx)
140
156
  if ctx.reasoning_effort:
141
157
  argv += ["--config", f"model_reasoning_effort={toml_str(ctx.reasoning_effort)}"]
@@ -250,6 +266,26 @@ def _writable_dirs(config: CodexSandboxConfig, ctx: ArgvContext) -> list[str]:
250
266
  return dirs
251
267
 
252
268
 
269
+ def _check_config_home(ctx: ArgvContext) -> None:
270
+ """Refuse ``ConfigScope.PROJECT`` unless the turn runs in a home of its own.
271
+
272
+ The user's global ``AGENTS.md``, hooks and memories live in the state root
273
+ and no flag skips them, so a turn still pointed at the user's root would
274
+ load them while claiming to be isolated.
275
+ """
276
+ home = ctx.env.get("CODEX_HOME")
277
+ if not home or not os.path.isabs(home): # noqa: PTH117 - a str contract, not a Path
278
+ msg = (
279
+ "ConfigScope.PROJECT needs CODEX_HOME set to the absolute path of a "
280
+ f"dedicated home prepared with prepare_config_home (got {home!r})"
281
+ )
282
+ raise ProviderCapabilityError(msg)
283
+ user_root = state_root(PROFILE, {key: v for key, v in ctx.env.items() if key != "CODEX_HOME"})
284
+ if user_root is not None and os.path.realpath(home) == os.path.realpath(user_root):
285
+ msg = f"ConfigScope.PROJECT needs a dedicated CODEX_HOME, not the user's own {home}"
286
+ raise ProviderCapabilityError(msg)
287
+
288
+
253
289
  def _is_within(path: str, directory: str) -> bool:
254
290
  resolved, parent = os.path.realpath(path), os.path.realpath(directory)
255
291
  return resolved == parent or resolved.startswith(parent.rstrip(os.sep) + os.sep)
@@ -130,17 +130,25 @@ _FAILURES: dict[FailureKind, str] = {
130
130
  "(https://chatgpt.com/explore/plus), or try again in 2 hours."
131
131
  ),
132
132
  FailureKind.AUTH: "unexpected status 401 Unauthorized: Missing bearer authentication",
133
- FailureKind.OTHER: "model response did not match the output schema",
133
+ FailureKind.OTHER: "Codex ran out of room in the model's context window.",
134
134
  }
135
135
 
136
136
 
137
+ #: Codex sends ``--output-schema`` to the model as a strict response format,
138
+ #: so decoding is constrained to the schema and a turn never fails over it.
139
+ _NO_SCHEMA_FAILURE = (
140
+ "codex never fails a turn over its output schema: decoding is constrained to the schema"
141
+ )
142
+
143
+
137
144
  def failure_lines(
138
145
  kind: FailureKind, *, session_id: str | None = None
139
146
  ) -> tuple[list[str], list[str], int]:
140
147
  """Build the stdout, stderr and exit code of a turn that fails with *kind*.
141
148
 
142
149
  Codex reports the failure as an ``error`` event followed by
143
- ``turn.failed``, both carrying the same message.
150
+ ``turn.failed``, both carrying the same message. ``SCHEMA`` raises
151
+ ``ValueError``: Codex has no such failure.
144
152
 
145
153
  Args:
146
154
  kind: The classification the scripted failure must produce.
@@ -149,7 +157,9 @@ def failure_lines(
149
157
  Returns:
150
158
  ``(stdout, stderr, returncode)`` for a ``FakeRun``.
151
159
  """
152
- message = _FAILURES[kind]
160
+ message = _FAILURES.get(kind)
161
+ if message is None:
162
+ raise ValueError(_NO_SCHEMA_FAILURE)
153
163
  lines: list[str] = []
154
164
  if session_id is not None:
155
165
  lines.append(_line({"type": "thread.started", "thread_id": session_id}))
@@ -222,14 +222,17 @@ def scripted_failure(provider: str, kind: FailureKind, *, session_id: str | None
222
222
  """Build a ``FakeRun`` for a turn that fails and classifies as *kind*.
223
223
 
224
224
  The run is *provider*'s real report of such a failure (an overload, a
225
- usage limit, a rejected login, or an unclassifiable failure), so the
225
+ usage limit, a rejected login, output that never matched the requested
226
+ schema, or an unclassifiable failure), so the
226
227
  raised ``CliExitError`` carries ``kind`` through the real parser. A
227
228
  consumer can therefore test its retry policy without knowing how any
228
229
  CLI phrases a failure.
229
230
 
230
231
  Args:
231
232
  provider: A provider whose stream reports failure kinds (``claude``
232
- or ``codex``); any other raises ``ValueError``.
233
+ or ``codex``); any other raises ``ValueError``, as does
234
+ ``FailureKind.SCHEMA`` on ``codex``, whose decoding is
235
+ constrained to the schema.
233
236
  kind: The ``FailureKind`` the raised error must carry.
234
237
  session_id: The conversation the failed run names, if any.
235
238
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.11.0"
3
+ version = "0.13.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
File without changes