agentshim 0.5.0__tar.gz → 0.6.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.
- {agentshim-0.5.0 → agentshim-0.6.0}/.gitignore +1 -0
- agentshim-0.6.0/CHANGELOG.md +351 -0
- agentshim-0.6.0/PKG-INFO +237 -0
- agentshim-0.6.0/README.md +228 -0
- agentshim-0.6.0/agentshim/__init__.py +167 -0
- agentshim-0.6.0/agentshim/agent.py +424 -0
- agentshim-0.6.0/agentshim/core/__init__.py +118 -0
- agentshim-0.6.0/agentshim/core/_files.py +44 -0
- agentshim-0.6.0/agentshim/core/env.py +84 -0
- agentshim-0.6.0/agentshim/core/errors.py +137 -0
- agentshim-0.6.0/agentshim/core/events.py +357 -0
- agentshim-0.6.0/agentshim/core/mcp.py +348 -0
- agentshim-0.6.0/agentshim/core/profile.py +57 -0
- agentshim-0.6.0/agentshim/core/provider.py +142 -0
- agentshim-0.6.0/agentshim/core/schema.py +281 -0
- agentshim-0.6.0/agentshim/core/stream.py +67 -0
- agentshim-0.6.0/agentshim/core/turn.py +71 -0
- agentshim-0.6.0/agentshim/core/usage.py +78 -0
- agentshim-0.6.0/agentshim/execution/__init__.py +28 -0
- agentshim-0.6.0/agentshim/execution/executor.py +131 -0
- agentshim-0.6.0/agentshim/execution/host.py +295 -0
- agentshim-0.6.0/agentshim/execution/transform.py +75 -0
- agentshim-0.6.0/agentshim/providers/__init__.py +104 -0
- agentshim-0.6.0/agentshim/providers/claude/__init__.py +19 -0
- agentshim-0.6.0/agentshim/providers/claude/events.py +230 -0
- agentshim-0.6.0/agentshim/providers/claude/hooks/__init__.py +1 -0
- {agentshim-0.5.0/agentshim → agentshim-0.6.0/agentshim/providers}/claude/hooks/confine_reads.py +17 -5
- agentshim-0.6.0/agentshim/providers/claude/parser.py +208 -0
- agentshim-0.6.0/agentshim/providers/claude/provider.py +164 -0
- agentshim-0.6.0/agentshim/providers/claude/sandbox.py +151 -0
- agentshim-0.6.0/agentshim/providers/claude/scripted.py +105 -0
- agentshim-0.6.0/agentshim/providers/codex/__init__.py +14 -0
- agentshim-0.6.0/agentshim/providers/codex/events.py +230 -0
- agentshim-0.6.0/agentshim/providers/codex/parser.py +232 -0
- agentshim-0.6.0/agentshim/providers/codex/provider.py +201 -0
- agentshim-0.6.0/agentshim/providers/codex/scripted.py +101 -0
- agentshim-0.6.0/agentshim/providers/copilot/__init__.py +15 -0
- agentshim-0.6.0/agentshim/providers/copilot/events.py +309 -0
- agentshim-0.6.0/agentshim/providers/copilot/parser.py +264 -0
- agentshim-0.6.0/agentshim/providers/copilot/provider.py +134 -0
- agentshim-0.6.0/agentshim/providers/copilot/scripted.py +123 -0
- agentshim-0.6.0/agentshim/providers/gemini/__init__.py +15 -0
- agentshim-0.6.0/agentshim/providers/gemini/events.py +160 -0
- agentshim-0.6.0/agentshim/providers/gemini/parser.py +191 -0
- agentshim-0.6.0/agentshim/providers/gemini/provider.py +159 -0
- agentshim-0.6.0/agentshim/providers/gemini/scripted.py +121 -0
- agentshim-0.6.0/agentshim/providers/opencode/__init__.py +15 -0
- agentshim-0.6.0/agentshim/providers/opencode/events.py +185 -0
- agentshim-0.6.0/agentshim/providers/opencode/parser.py +206 -0
- agentshim-0.6.0/agentshim/providers/opencode/provider.py +165 -0
- agentshim-0.6.0/agentshim/providers/opencode/scripted.py +134 -0
- agentshim-0.6.0/agentshim/testing/__init__.py +174 -0
- agentshim-0.6.0/pyproject.toml +88 -0
- agentshim-0.5.0/.github/workflows/ci.yml +0 -41
- agentshim-0.5.0/.github/workflows/publish.yml +0 -41
- agentshim-0.5.0/PKG-INFO +0 -360
- agentshim-0.5.0/README.md +0 -349
- agentshim-0.5.0/agentshim/__init__.py +0 -46
- agentshim-0.5.0/agentshim/base.py +0 -315
- agentshim-0.5.0/agentshim/claude/__init__.py +0 -3
- agentshim-0.5.0/agentshim/claude/agent.py +0 -242
- agentshim-0.5.0/agentshim/claude/events.py +0 -125
- agentshim-0.5.0/agentshim/claude/hooks/__init__.py +0 -0
- agentshim-0.5.0/agentshim/claude_events.py +0 -21
- agentshim-0.5.0/agentshim/cli_agent.py +0 -324
- agentshim-0.5.0/agentshim/codex/__init__.py +0 -3
- agentshim-0.5.0/agentshim/codex/agent.py +0 -265
- agentshim-0.5.0/agentshim/codex/events.py +0 -206
- agentshim-0.5.0/agentshim/codex_events.py +0 -23
- agentshim-0.5.0/agentshim/copilot/__init__.py +0 -3
- agentshim-0.5.0/agentshim/copilot/agent.py +0 -263
- agentshim-0.5.0/agentshim/copilot/events.py +0 -243
- agentshim-0.5.0/agentshim/copilot_events.py +0 -29
- agentshim-0.5.0/agentshim/events.py +0 -263
- agentshim-0.5.0/agentshim/executor.py +0 -319
- agentshim-0.5.0/agentshim/gemini/__init__.py +0 -3
- agentshim-0.5.0/agentshim/gemini/agent.py +0 -161
- agentshim-0.5.0/agentshim/gemini/events.py +0 -53
- agentshim-0.5.0/agentshim/gemini_events.py +0 -11
- agentshim-0.5.0/agentshim/llm_client.py +0 -53
- agentshim-0.5.0/agentshim/mcp_config.py +0 -25
- agentshim-0.5.0/agentshim/opencode/__init__.py +0 -3
- agentshim-0.5.0/agentshim/opencode/agent.py +0 -180
- agentshim-0.5.0/agentshim/opencode/events.py +0 -57
- agentshim-0.5.0/agentshim/opencode_events.py +0 -11
- agentshim-0.5.0/agentshim/sandbox.py +0 -133
- agentshim-0.5.0/agentshim/subagent.py +0 -93
- agentshim-0.5.0/agentshim/usage.py +0 -63
- agentshim-0.5.0/agentshim/utils.py +0 -120
- agentshim-0.5.0/pyproject.toml +0 -44
- agentshim-0.5.0/tests/fixtures/copilot/session_turn_1.jsonl +0 -11
- agentshim-0.5.0/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -11
- agentshim-0.5.0/tests/fixtures/copilot/streaming_dedup.jsonl +0 -5
- agentshim-0.5.0/tests/fixtures/copilot/tool_and_usage.jsonl +0 -6
- agentshim-0.5.0/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -557
- agentshim-0.5.0/tests/unit/cli_agent/test_check_cli.py +0 -38
- agentshim-0.5.0/tests/unit/cli_agent/test_cli_prompt_passing.py +0 -360
- agentshim-0.5.0/tests/unit/cli_agent/test_command_executor.py +0 -245
- agentshim-0.5.0/tests/unit/llm/conftest.py +0 -33
- agentshim-0.5.0/tests/unit/llm/test_claude_stream.py +0 -601
- agentshim-0.5.0/tests/unit/llm/test_gemini_fixture.py +0 -84
- agentshim-0.5.0/tests/unit/llm/test_gemini_stream.py +0 -186
- agentshim-0.5.0/tests/unit/test_agent_cli_claude.py +0 -314
- agentshim-0.5.0/tests/unit/test_agent_cli_codex.py +0 -243
- agentshim-0.5.0/tests/unit/test_agent_cli_copilot.py +0 -198
- agentshim-0.5.0/tests/unit/test_agent_cli_copilot_fixtures.py +0 -99
- agentshim-0.5.0/tests/unit/test_agent_cli_event_parsing.py +0 -421
- agentshim-0.5.0/tests/unit/test_agent_cli_mcp_unsupported.py +0 -38
- agentshim-0.5.0/tests/unit/test_agent_cli_resume.py +0 -336
- agentshim-0.5.0/tests/unit/test_agent_cli_sandbox.py +0 -141
- agentshim-0.5.0/tests/unit/test_cli_agent_usage.py +0 -237
- agentshim-0.5.0/tests/unit/test_coding_agent_facade.py +0 -227
- agentshim-0.5.0/tests/unit/test_event_handlers.py +0 -83
- agentshim-0.5.0/tests/unit/test_mcp_config.py +0 -77
- agentshim-0.5.0/uv.lock +0 -2200
- {agentshim-0.5.0 → agentshim-0.6.0}/agentshim/py.typed +0 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
A restructure, not an upgrade. 0.6 replaces the whole public API of 0.5 and
|
|
6
|
+
ships **no compatibility layer**: a 0.5 consumer will not import, let alone
|
|
7
|
+
run, until it is migrated. The sections below list every removed and renamed
|
|
8
|
+
name so a migration can be done mechanically.
|
|
9
|
+
|
|
10
|
+
### Restructure
|
|
11
|
+
|
|
12
|
+
The package is now four layers that import strictly inward:
|
|
13
|
+
`agentshim.testing` -> `agentshim/agent.py` -> `agentshim/providers/` ->
|
|
14
|
+
(`agentshim/core/`, `agentshim/execution/`). `agentshim/agent.py` is the
|
|
15
|
+
composition layer, and the only thing that turns a provider name into a
|
|
16
|
+
provider. `import-linter` enforces the ordering as a build gate
|
|
17
|
+
(`scripts/check_imports.sh`).
|
|
18
|
+
|
|
19
|
+
- `agentshim/core/` holds the provider-agnostic types: `TurnRequest`,
|
|
20
|
+
`TurnResult`, `OutputSchema`, the event dataclasses, `TokenUsage`,
|
|
21
|
+
`ProviderUsage`, the error hierarchy, `ProviderProfile`, the `Provider`
|
|
22
|
+
and `StreamParser` protocols, `ParsedTurn`, MCP specs, schema dialect
|
|
23
|
+
checks, and `interactive_env()`.
|
|
24
|
+
- `agentshim/execution/` holds the process transport: `CommandExecutor`,
|
|
25
|
+
`HostCommandExecutor`, `TransformingExecutor`.
|
|
26
|
+
- `agentshim/providers/<name>/` holds one CLI each, all with the same
|
|
27
|
+
layout: `provider.py`, `parser.py`, `events.py`, `scripted.py`, and an
|
|
28
|
+
`__init__.py` exporting `<Name>Provider`, `PROFILE`, `scripted_lines`.
|
|
29
|
+
All five ship: claude, codex, copilot, gemini, opencode. `fold_usage` is
|
|
30
|
+
not part of that contract: every package has one and no two take the same
|
|
31
|
+
arguments, so it stays private to its own `parser.py`.
|
|
32
|
+
- `agentshim/testing/` is new and part of the public API.
|
|
33
|
+
|
|
34
|
+
`ProviderProfile` declares every optional behaviour, so no caller probes a
|
|
35
|
+
provider object: resume, reasoning effort, MCP mechanism, output-schema
|
|
36
|
+
style and dialect, state dirs, auth env vars, skill dirs, container install.
|
|
37
|
+
|
|
38
|
+
### Breaking changes from 0.5
|
|
39
|
+
|
|
40
|
+
**Removed from the public API.**
|
|
41
|
+
|
|
42
|
+
| 0.5 | Replacement |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `CodingAgent` | `CliAgent` |
|
|
45
|
+
| `BaseCodingAgent` | the `Provider` protocol, in `agentshim/core/provider.py` |
|
|
46
|
+
| `BaseAgentSession` | `AgentSession` |
|
|
47
|
+
| `ClaudeCodeCodingAgent` | `CliAgent("claude")`, or `agentshim.providers.claude.ClaudeProvider` |
|
|
48
|
+
| `CodexCodingAgent` | `CliAgent("codex")` |
|
|
49
|
+
| `CopilotCodingAgent` | `CliAgent("copilot")` |
|
|
50
|
+
| `GeminiCodingAgent` | `CliAgent("gemini")` |
|
|
51
|
+
| `OpencodeCodingAgent` | `CliAgent("opencode")` |
|
|
52
|
+
| `get_provider_class(name)` | `get_provider(name)`, which returns an instance |
|
|
53
|
+
| `list_providers()` | `provider_names()` |
|
|
54
|
+
| `register_provider(...)` | none: pass a `Provider` instance to `CliAgent` |
|
|
55
|
+
| `McpServerConfig` | `McpServer` |
|
|
56
|
+
| `SandboxConfig` (top level) | `agentshim.providers.claude.SandboxConfig` |
|
|
57
|
+
|
|
58
|
+
Whole modules are gone: `agentshim.base`, `agentshim.cli_agent`,
|
|
59
|
+
`agentshim.events`, `agentshim.executor`, `agentshim.mcp_config`,
|
|
60
|
+
`agentshim.sandbox`, `agentshim.usage`, `agentshim.utils`,
|
|
61
|
+
`agentshim.subagent`, `agentshim.llm_client`, and the five
|
|
62
|
+
`agentshim.<name>_events` compatibility shims. `subagent.py` and
|
|
63
|
+
`llm_client.py` made direct LLM API calls, which is not what a CLI shim is
|
|
64
|
+
for; the display helpers in `utils.py` are now private to
|
|
65
|
+
`ConsoleEventHandler`.
|
|
66
|
+
|
|
67
|
+
**Renamed.**
|
|
68
|
+
|
|
69
|
+
| 0.5 | 0.6 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `session.generate(prompt) -> str` | `session.turn(TurnRequest \| str) -> TurnResult` |
|
|
72
|
+
| `get_interactive_env()` | `interactive_env(*, refresh=False)`, now cached per process |
|
|
73
|
+
| `build_claude_sandbox_settings()` | `build_settings()` |
|
|
74
|
+
| `<Provider>CodingAgent` | `<Provider>Provider` |
|
|
75
|
+
|
|
76
|
+
**Event handling is typed.** 0.5 dispatched seven optional callbacks
|
|
77
|
+
(`on_run_start`, `on_run_end`, `on_thinking`, `on_tool_call`,
|
|
78
|
+
`on_tool_result`, `on_usage`, `on_stderr`), three of which were not even on
|
|
79
|
+
the protocol and were reached through `getattr`. 0.6 has one method,
|
|
80
|
+
`on_event(event)`, taking a frozen dataclass from the `AgentEvent` union:
|
|
81
|
+
`RunStarted`, `RunFinished`, `SessionStarted`, `AssistantText`, `Reasoning`,
|
|
82
|
+
`ToolCall`, `ToolResult`, `UsageReport`, `Lifecycle`, `Stderr`, `RawOutput`,
|
|
83
|
+
`ProviderError`. `on_thinking` covered assistant text and reasoning
|
|
84
|
+
together; those are now separate events. `SessionStarted`, `Lifecycle`,
|
|
85
|
+
`RawOutput` and `ProviderError` have no 0.5 counterpart.
|
|
86
|
+
|
|
87
|
+
`event_handler=` and `event_handlers=` are now combined rather than
|
|
88
|
+
mutually exclusive; passing both no longer raises.
|
|
89
|
+
|
|
90
|
+
**Errors are typed.** 0.5 raised bare `RuntimeError` for every CLI failure.
|
|
91
|
+
0.6 raises only `AgentShimError` subclasses, and nothing else escapes
|
|
92
|
+
`turn()`:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
AgentShimError
|
|
96
|
+
CliNotFoundError binary not on PATH
|
|
97
|
+
CliCheckError binary found but the health check failed
|
|
98
|
+
CliExitError nonzero exit: argv, returncode, stdout, stderr
|
|
99
|
+
SessionResumeError the conversation is gone: session_id
|
|
100
|
+
CliTimeoutError argv, timeout
|
|
101
|
+
ProviderCapabilityError the provider cannot do what the request asked
|
|
102
|
+
SchemaDialectError problems: list[str]
|
|
103
|
+
McpConfigError config file unreadable or not an object
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`SessionResumeError` is new; there was no 0.5 name for it.
|
|
107
|
+
|
|
108
|
+
**The prompt is no longer in argv.** 0.5 wrote the prompt to stdin *and*
|
|
109
|
+
appended it to argv on three providers: claude as a bare positional, copilot
|
|
110
|
+
as `-p <prompt>`, opencode as a positional wrapped in literal double quotes
|
|
111
|
+
(`f'"{prompt}"'`, which reached the CLI with the quotes in the string,
|
|
112
|
+
since argv is not shell-parsed). 0.6 delivers the prompt on stdin only, on
|
|
113
|
+
every provider, so an agent's own `pkill -f` cannot match the CLI by prompt
|
|
114
|
+
text and the prompt does not appear in the process table. `ArgvContext` has
|
|
115
|
+
no prompt field.
|
|
116
|
+
|
|
117
|
+
**The opencode default model is gone.** 0.5 substituted
|
|
118
|
+
`google-vertex/gemini-3-pro-preview` whenever no model was given. 0.6 omits
|
|
119
|
+
`--model` entirely, so opencode uses the model from the user's own config.
|
|
120
|
+
Callers who relied on the implicit default must pass `model=` explicitly.
|
|
121
|
+
|
|
122
|
+
**Dependencies removed.** 0.5 declared `litellm>=1.0.0` and `loguru>=0.7.2`,
|
|
123
|
+
and used pydantic transitively through litellm for the MCP server specs.
|
|
124
|
+
0.6 declares `dependencies = []`. litellm went with `subagent.py` and
|
|
125
|
+
`llm_client.py`; the MCP specs are frozen dataclasses; and logging is a
|
|
126
|
+
`Callable[[str], None]` the caller supplies, with `ConsoleEventHandler`
|
|
127
|
+
writing to a `TextIO` given at construction instead of to a loguru logger.
|
|
128
|
+
`bind_event_handler_context` and `default_event_handler` are gone with it.
|
|
129
|
+
|
|
130
|
+
**Usage gained two fields.** `TokenUsage` adds `cache_write_input_tokens`
|
|
131
|
+
and `reasoning_output_tokens`, so `to_dict()` now emits six keys rather than
|
|
132
|
+
four. `ProviderUsage` adds `raw`, which holds the CLI's own usage mapping
|
|
133
|
+
for diagnostics and is deliberately excluded from `to_dict()`. Every
|
|
134
|
+
provider now normalizes to the invariant `cached_input_tokens <=
|
|
135
|
+
input_tokens`.
|
|
136
|
+
|
|
137
|
+
**Structured output is a first-class request.** `TurnRequest.output_schema`
|
|
138
|
+
takes an `OutputSchema`, `ProviderProfile.output_schema` and
|
|
139
|
+
`schema_dialect` declare what a provider accepts, and a schema the provider
|
|
140
|
+
cannot express raises `SchemaDialectError` before the process starts.
|
|
141
|
+
|
|
142
|
+
**MCP servers are per-turn, and Claude installs them differently.** 0.5 took
|
|
143
|
+
`mcp_servers` on the agent constructor and, for Claude, rendered them into
|
|
144
|
+
`--mcp-config <json> --strict-mcp-config`. 0.6 takes them on
|
|
145
|
+
`TurnRequest.mcp_servers`, and `ProviderProfile.mcp` declares the mechanism:
|
|
146
|
+
Claude, Gemini and opencode merge into a workspace config file
|
|
147
|
+
(`.mcp.json`, `.gemini/settings.json`, `opencode.json`), keeping the
|
|
148
|
+
original bytes and restoring them when the turn ends, including when it
|
|
149
|
+
raised; Codex and Copilot render flags. A config-file provider therefore
|
|
150
|
+
needs a `cwd`, and raises `ProviderCapabilityError` without one.
|
|
151
|
+
|
|
152
|
+
### Fixed
|
|
153
|
+
|
|
154
|
+
- **Codex tool results.** A failed tool was reported on `ToolResult.stdout`
|
|
155
|
+
with an empty `stderr`, so a renderer showed a failure as success.
|
|
156
|
+
Failures now go on `stderr` with a nonzero `exit_code`, which is the rule
|
|
157
|
+
on all five providers.
|
|
158
|
+
- **Non-object JSON lines.** A stdout line that was valid JSON but not an
|
|
159
|
+
object (a bare `42`, a top-level array) is no longer treated as a frame.
|
|
160
|
+
`parse_json_object` returns `None` for blank lines, invalid JSON and
|
|
161
|
+
non-objects alike, and the parser emits `RawOutput` so the line stays
|
|
162
|
+
observable instead of crashing the turn or being dropped.
|
|
163
|
+
- **`ConsoleEventHandler` painted failed tool results green.** It chose the
|
|
164
|
+
colour from whether there was any output rather than from `exit_code`, so a
|
|
165
|
+
failure read as a success. Failures are red now, and a failed result with no
|
|
166
|
+
output says so instead of "ran successfully".
|
|
167
|
+
- **`AgentSession.forget()` could race a running turn.** It cleared
|
|
168
|
+
`session_id` without the session lock, so an id the in-flight turn was about
|
|
169
|
+
to write survived the forget, or an id nobody else had was dropped. It now
|
|
170
|
+
follows `adopt`'s rule, refusing under the lock while a turn is in flight,
|
|
171
|
+
and returns `bool` to say which happened.
|
|
172
|
+
- **`interactive_env()` truncated multi-line variables.** The probe parsed
|
|
173
|
+
`env` output by splitting on newlines, so a value containing one (a key, an
|
|
174
|
+
exported shell function) was cut short and its remaining lines became junk
|
|
175
|
+
keys. The probe asks for `env -0` and splits on NUL, falling back to plain
|
|
176
|
+
`env` where `-0` is not supported.
|
|
177
|
+
- **Claude's read-confinement hook quoted paths by hand.** The command was
|
|
178
|
+
assembled by wrapping each part in literal double quotes, which leaves `$`,
|
|
179
|
+
backticks and `"` inside a path live for the shell Claude runs it in. It uses
|
|
180
|
+
`shlex.join` now.
|
|
181
|
+
- **Claude tool results rendered as Python dict reprs.** A `tool_result`
|
|
182
|
+
whose content is a list of blocks, which is how Claude sends anything but
|
|
183
|
+
plain text, was flattened with `str()`, so `ToolResult.stdout` carried
|
|
184
|
+
`{'type': 'text', 'text': 'hi'}` instead of `hi`. Text blocks now
|
|
185
|
+
contribute their text and every other block is serialized as JSON.
|
|
186
|
+
- **`HttpMcpServer` meant a different transport on every provider.** The same
|
|
187
|
+
spec became SSE on claude and copilot but streamable HTTP on gemini, codex
|
|
188
|
+
and opencode, so a server reachable on one provider silently failed on
|
|
189
|
+
another. `HttpMcpServer` now takes
|
|
190
|
+
`transport: Literal["http", "sse"] = "http"`, and each provider renders it
|
|
191
|
+
the way its CLI names it (`{"type": ...}` on claude and copilot,
|
|
192
|
+
`httpUrl`/`url` on gemini). opencode's single `remote` type and Codex's
|
|
193
|
+
`--config` URL override cannot express the choice, so both work it out from
|
|
194
|
+
the endpoint; `docs/mcp.md` has the table. The default changes claude and
|
|
195
|
+
copilot from SSE to streamable HTTP.
|
|
196
|
+
- **Three non-`AgentShimError` exceptions escaped `turn()`**, against the
|
|
197
|
+
documented contract that catching `AgentShimError` is enough. Installing MCP
|
|
198
|
+
servers into a read-only workspace raised `PermissionError` from the config
|
|
199
|
+
write, and now raises `McpConfigError`. A schema carrying a `NaN` or an
|
|
200
|
+
infinity raised `ValueError` from `materialize`, and now raises
|
|
201
|
+
`ProviderCapabilityError`. `TransformingExecutor.check_binary` raised
|
|
202
|
+
`FileNotFoundError` when the transform's own prefix binary (`docker`, a
|
|
203
|
+
sandbox wrapper) was missing, and now raises `CliCheckError`.
|
|
204
|
+
`tests/unit/test_turn_contract.py` pins all three.
|
|
205
|
+
- **Atomic writes replaced symlinks.** `atomic_write` renamed its temporary
|
|
206
|
+
file onto the path it was given, so a `.mcp.json` (or `.gemini/settings.json`,
|
|
207
|
+
or `opencode.json`) that was a symlink into a dotfiles checkout became a
|
|
208
|
+
regular file, and the file it pointed at went stale. Symlinks are now
|
|
209
|
+
followed to the file they name. `install_config_file` resolves the same way,
|
|
210
|
+
so the backup, the merged write and the restore all address one inode, and
|
|
211
|
+
the installation reports the resolved path.
|
|
212
|
+
- **`$schema` and `$id` were rejected everywhere, and unfixable.** Both
|
|
213
|
+
dialects reported them as unsupported keywords, so a schema straight out of
|
|
214
|
+
a generator failed before the process started, and `normalize()` could not
|
|
215
|
+
repair it because it did not strip them. `dialect_problems` now reports them
|
|
216
|
+
under `STRICT` only, which is where they are genuinely refused (Codex's
|
|
217
|
+
`--output-schema` subset); `OPEN` ignores them the way the CLI does. And
|
|
218
|
+
`normalize()` drops the document metadata, `$schema`, `$id`, `title`,
|
|
219
|
+
`description` and `examples`, so a normalized schema is accepted in either
|
|
220
|
+
dialect. A property named `title` is untouched: `properties` is traversed as
|
|
221
|
+
a map of subschemas, not as keywords.
|
|
222
|
+
- **A cancel before the CLI spawned was dropped.** `cancel()` looked only at
|
|
223
|
+
the process handle, which the executor publishes after it has started the
|
|
224
|
+
process. A cancel arriving while the turn was installing MCP servers,
|
|
225
|
+
building argv or spawning therefore did nothing at all. The request is now
|
|
226
|
+
recorded under the session lock and paid out the moment the handle appears.
|
|
227
|
+
It is only ever recorded while a turn is in flight and is cleared when that
|
|
228
|
+
turn ends, so cancelling an idle session still leaves the next turn alone.
|
|
229
|
+
- **A timed-out turn emitted no `RunFinished` and never finished its
|
|
230
|
+
parser.** An `AgentShimError` from the executor skipped the run's closing
|
|
231
|
+
path entirely, so a handler saw a run that started and never ended, and
|
|
232
|
+
whatever the parser had read, the session id included, was thrown away. The
|
|
233
|
+
run now closes the same way it does on a clean exit: `RunFinished(None)`,
|
|
234
|
+
`parser.finish()`, adopt the session id, re-raise. `CliTimeoutError` gained
|
|
235
|
+
`partial: ParsedTurn | None` carrying that reading.
|
|
236
|
+
- **A turn that named its conversation and then failed was unresumable.** The
|
|
237
|
+
nonzero-exit check raised before `parsed.session_id` was adopted, so the id
|
|
238
|
+
the provider had already printed was lost. Adoption now happens first, and
|
|
239
|
+
is skipped only when `classify_exit` reported `SessionResumeError`, which is
|
|
240
|
+
the provider saying the conversation is gone.
|
|
241
|
+
- **MCP restore could destroy the turn.** `ConfigFileInstallation.restore()`
|
|
242
|
+
marked itself done before doing any work and raised `McpConfigError` when
|
|
243
|
+
the agent had left the config file non-JSON or non-object. Raised from the
|
|
244
|
+
session's `finally`, that discarded a successful `TurnResult` or masked the
|
|
245
|
+
real `CliExitError`, and left agentshim's injected entries in the file
|
|
246
|
+
forever. `restore()` now never raises: anything the precise unmerge cannot
|
|
247
|
+
handle falls back to writing the file's original bytes verbatim (removing
|
|
248
|
+
the file when there were none), and it reports what it did as a note the
|
|
249
|
+
session logs through the agent's `log`. `McpInstallation.restore()`
|
|
250
|
+
therefore returns `str | None` rather than `None`.
|
|
251
|
+
- **Undecodable CLI output.** The child was read in text mode with the
|
|
252
|
+
platform default codec and no error handler, and the reader thread
|
|
253
|
+
swallowed the resulting `UnicodeDecodeError`. One byte that is not valid
|
|
254
|
+
UTF-8 therefore discarded the rest of the turn's output and returned exit 0
|
|
255
|
+
with an empty transcript, or, with a large output and no timeout, hung the
|
|
256
|
+
turn forever because nobody was draining the child's pipe. The streams are
|
|
257
|
+
now decoded as UTF-8 with `errors="replace"`, the reader closes its end of
|
|
258
|
+
the pipe when it stops for any reason, and a reader that did fail raises
|
|
259
|
+
`CliExitError` instead of presenting a truncated stream as a clean EOF.
|
|
260
|
+
- **Stdin deadlock.** 0.5 wrote the whole prompt to the child's stdin on the
|
|
261
|
+
calling thread. A prompt larger than the pipe buffer, sent to a CLI that
|
|
262
|
+
was not reading stdin, blocked the turn forever. `HostCommandExecutor`
|
|
263
|
+
now writes stdin from a helper thread and closes it in `finally`.
|
|
264
|
+
- **Callback threading.** 0.5 called the sink directly from two reader
|
|
265
|
+
threads, so an event handler ran concurrently on both and had to do its
|
|
266
|
+
own locking. The executor now drains a queue on the thread that called
|
|
267
|
+
`turn()`, so every callback is serialized there. This is a documented
|
|
268
|
+
contract, not an implementation detail.
|
|
269
|
+
- **Claude read-confinement hook.** The hook was registered as
|
|
270
|
+
`"<path>/confine_reads.py" <roots>`, which needs the file to be
|
|
271
|
+
executable and its shebang to name a usable interpreter. It now runs
|
|
272
|
+
through `sys.executable`, so it uses the interpreter agentshim is running
|
|
273
|
+
under.
|
|
274
|
+
- **opencode tool status.** Tool parts were reported only when their status
|
|
275
|
+
was `"success"` or `"error"`. opencode's terminal status is `"completed"`,
|
|
276
|
+
so every successful tool call was silently dropped. All three terminal
|
|
277
|
+
states are now paired into `ToolCall` and `ToolResult`, with the part's own
|
|
278
|
+
`time` block supplying the duration.
|
|
279
|
+
- **Error frames.** Codex `turn.failed` and top-level `error` frames,
|
|
280
|
+
Gemini `error` frames, opencode `error` frames and Copilot
|
|
281
|
+
`session.error` frames now become `ProviderError` events and set
|
|
282
|
+
`ParsedTurn.error`, instead of being ignored or folded into the turn
|
|
283
|
+
text. Codex stderr lines are `Stderr` events rather than prose in the
|
|
284
|
+
result.
|
|
285
|
+
|
|
286
|
+
### Added
|
|
287
|
+
|
|
288
|
+
- `agentshim.testing`: `FakeExecutor`, `FakeRun`, `FakeCommandHandle`,
|
|
289
|
+
`RecordingEventHandler` and `scripted_turn(provider, ...)`. There was no
|
|
290
|
+
0.5 equivalent; a consumer had to write its own `CommandExecutor` fake and
|
|
291
|
+
hand-roll each provider's JSON. `scripted_turn` emits the provider's real
|
|
292
|
+
stream format, produced by `providers/<name>/scripted.py` and round-tripped
|
|
293
|
+
through that provider's real parser, so a consumer's tests never encode a
|
|
294
|
+
provider's wire format.
|
|
295
|
+
- `TransformingExecutor`, for running the CLI under an OS sandbox or inside
|
|
296
|
+
a container by rewriting argv. `HostCommandExecutor.check_binary` now goes
|
|
297
|
+
through `run`, so the health check is transformed too and a
|
|
298
|
+
container-executed CLI can actually be probed; the probe is a normal
|
|
299
|
+
command with a pipe for stdin, so it can never inherit a TTY.
|
|
300
|
+
- `AgentSession.adopt()`, `forget()`, and a thread-safe `cancel()` that
|
|
301
|
+
terminates the process group and kills it after a grace period.
|
|
302
|
+
- Gemini and opencode classify any nonzero exit of a resumed turn as
|
|
303
|
+
`SessionResumeError`, the rule Claude already followed: neither CLI can
|
|
304
|
+
tell a lost session apart from another failure, and a caller holding a
|
|
305
|
+
checkpoint would otherwise offer the same dead id forever. Codex keeps
|
|
306
|
+
matching its explicit "no rollout found" message.
|
|
307
|
+
- The Claude parser only reports `structured_output` for a turn that asked
|
|
308
|
+
for a schema, so an unrequested field can never replace the prose answer.
|
|
309
|
+
- `TurnRequest.mcp_workspace`: the host directory that receives config-file
|
|
310
|
+
MCP installs when the turn has no host `cwd`, which is the container case
|
|
311
|
+
(the CLI runs at the container path while the config file belongs on the
|
|
312
|
+
bind-mounted host workspace). Defaults to `cwd`.
|
|
313
|
+
- `ClaudeProvider`, `CodexProvider`, `CopilotProvider`, `GeminiProvider`,
|
|
314
|
+
`OpencodeProvider` and `SandboxConfig` are exported from `agentshim`
|
|
315
|
+
directly. The docs already told callers to construct them, so they were
|
|
316
|
+
reaching into `agentshim.providers.<name>` for something the public API
|
|
317
|
+
should have carried.
|
|
318
|
+
- `scripts/check_imports.sh` and the `[tool.importlinter]` contract.
|
|
319
|
+
- `__version__` on the package.
|
|
320
|
+
|
|
321
|
+
### Known limitations
|
|
322
|
+
|
|
323
|
+
- **Copilot CLI reports no token usage.** Verified on Copilot CLI 1.0.83: a
|
|
324
|
+
run prints no `assistant.usage` frame and no per-message `outputTokens`,
|
|
325
|
+
so `TurnResult.usage.tokens` is all zeros for this provider. The
|
|
326
|
+
`session.usage_checkpoint` frame it does print carries
|
|
327
|
+
`totalPremiumRequests` and `totalNanoAiu`, which are billing units rather
|
|
328
|
+
than tokens, plus prompt-cache diagnostics (`prompt_tokens`,
|
|
329
|
+
`frontier_tokens`, `tool_tokens`, per-segment `tokens`) that describe how
|
|
330
|
+
the prompt was assembled and not what the turn was charged. Folding those
|
|
331
|
+
into `input_tokens` would report a number that is not the turn's usage, so
|
|
332
|
+
the parser leaves the frame alone.
|
|
333
|
+
`tests/fixtures/copilot/usage_checkpoint_1_0_83.jsonl` is a recording of
|
|
334
|
+
such a run and pins the behaviour. Copilot also reports no cost.
|
|
335
|
+
- **Codex and Gemini report no cost.** `TurnResult.cost_usd` is `None` on
|
|
336
|
+
both; only Claude Code and opencode report one.
|
|
337
|
+
- **Only Claude Code and Codex accept a native output schema**, and only
|
|
338
|
+
they accept a reasoning effort; asking Gemini, opencode or Copilot for
|
|
339
|
+
either raises `ProviderCapabilityError`. Copilot has an `--effort` flag,
|
|
340
|
+
but its levels are model-specific and unvalidated, so the provider does
|
|
341
|
+
not expose it rather than mistranslating a portable one.
|
|
342
|
+
- **Codex cannot carry HTTP headers on an MCP server.** It configures
|
|
343
|
+
servers through `--config` overrides, which have nowhere to put them, so
|
|
344
|
+
an `HttpMcpServer` with headers raises `ProviderCapabilityError` rather
|
|
345
|
+
than having them dropped silently. The other four pass headers through.
|
|
346
|
+
- **Resume diagnosis is provider-dependent.** Only Codex reports a lost
|
|
347
|
+
conversation distinguishably, on stderr. Claude maps any nonzero exit on a
|
|
348
|
+
resumed turn to `SessionResumeError`, because `claude --resume` gives no
|
|
349
|
+
distinguishable exit code and a failed resumed turn is unusable either
|
|
350
|
+
way. Gemini, opencode and Copilot give no signal at all, so a failed
|
|
351
|
+
resumed turn raises a plain `CliExitError` there.
|
agentshim-0.6.0/PKG-INFO
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agentshim
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Provides-Extra: test
|
|
7
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# agentshim
|
|
11
|
+
|
|
12
|
+
`agentshim` runs coding-agent CLIs (Claude Code, Codex, Gemini CLI, opencode,
|
|
13
|
+
Copilot CLI) as subprocesses and turns their output into typed events and a
|
|
14
|
+
typed turn result.
|
|
15
|
+
|
|
16
|
+
It owns everything that answers "how do I run provider X and understand what
|
|
17
|
+
it printed". It does not own application policy: which provider to use, when
|
|
18
|
+
to retire a conversation, how to sandbox the host, or how to render events.
|
|
19
|
+
|
|
20
|
+
No required runtime dependencies. Python 3.10+.
|
|
21
|
+
|
|
22
|
+
## What it includes
|
|
23
|
+
|
|
24
|
+
- `CliAgent` / `AgentSession`: one turn at a time, resumable, cancellable
|
|
25
|
+
- typed events delivered on the calling thread
|
|
26
|
+
- normalized token accounting where `cached_input_tokens <= input_tokens` on
|
|
27
|
+
every provider
|
|
28
|
+
- declared capabilities on `ProviderProfile`, so no caller probes a provider
|
|
29
|
+
- injectable `CommandExecutor`s for running the CLI in a container or over a
|
|
30
|
+
remote shell
|
|
31
|
+
- MCP server installation and restoration per turn
|
|
32
|
+
- native structured output with per-provider schema dialect checks
|
|
33
|
+
- `agentshim.testing`: doubles that emit each provider's real stream format
|
|
34
|
+
|
|
35
|
+
0.6 ships the core, the execution layer, and all five providers: Claude
|
|
36
|
+
Code, Codex, Gemini CLI, opencode, and Copilot CLI. It is not
|
|
37
|
+
source-compatible with 0.5; see [`CHANGELOG.md`](CHANGELOG.md) for what
|
|
38
|
+
changed and how to migrate.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv add agentshim
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
agentshim does not bundle the agent CLIs. Install and authenticate the one
|
|
47
|
+
you want (`claude`, `codex`, `gemini`, `opencode`, or `copilot`) yourself.
|
|
48
|
+
|
|
49
|
+
## One turn
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from agentshim import CliAgent
|
|
53
|
+
|
|
54
|
+
agent = CliAgent("claude", model="sonnet")
|
|
55
|
+
result = agent.run("Write a short summary of this codebase.", cwd=".")
|
|
56
|
+
|
|
57
|
+
print(result.text)
|
|
58
|
+
print(result.usage.tokens.input_tokens, result.cost_usd, result.duration_ms)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`model` is an opaque provider-specific string, passed through to the CLI
|
|
62
|
+
unchanged; `None` leaves the CLI's own default.
|
|
63
|
+
|
|
64
|
+
Binary lookup and the CLI health check run once, in the constructor, so a
|
|
65
|
+
broken install fails immediately rather than halfway through a turn.
|
|
66
|
+
|
|
67
|
+
## A conversation
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
session = agent.start_session(cwd=".", timeout=600)
|
|
71
|
+
|
|
72
|
+
session.turn("What does this project do?")
|
|
73
|
+
second = session.turn("Which files should I read first?")
|
|
74
|
+
|
|
75
|
+
assert second.resumed
|
|
76
|
+
print(session.session_id)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`adopt(session_id)` continues a conversation you checkpointed earlier and
|
|
80
|
+
returns `False` if the provider cannot resume or a turn is in flight;
|
|
81
|
+
`forget()` starts fresh on the next turn, and likewise returns `False` while
|
|
82
|
+
a turn is in flight. `cancel()` is thread-safe: it terminates the process
|
|
83
|
+
group, then kills it after a grace period, and works even before the CLI has
|
|
84
|
+
been spawned.
|
|
85
|
+
|
|
86
|
+
## Per-turn options
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
from agentshim import OutputSchema, StdioMcpServer, TurnRequest
|
|
91
|
+
|
|
92
|
+
schema = {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {"summary": {"type": "string"}},
|
|
95
|
+
"required": ["summary"],
|
|
96
|
+
"additionalProperties": False,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
result = session.turn(
|
|
100
|
+
TurnRequest(
|
|
101
|
+
prompt="Summarize the failing test.",
|
|
102
|
+
cwd="/workspace",
|
|
103
|
+
timeout=300, # None means no limit
|
|
104
|
+
reasoning_effort="high",
|
|
105
|
+
output_schema=OutputSchema(schema=schema, host_dir=Path("/tmp/schemas")),
|
|
106
|
+
mcp_servers=[StdioMcpServer(name="issues", command="python", args=["-m", "board.mcp"])],
|
|
107
|
+
env={"CI": "1"},
|
|
108
|
+
extra_args=("--append-system-prompt", "Be terse."),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
print(result.structured_output)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
MCP servers are installed before the turn and restored after it, including
|
|
116
|
+
when the turn fails. Asking for something the provider cannot do raises
|
|
117
|
+
`ProviderCapabilityError` before the process starts.
|
|
118
|
+
|
|
119
|
+
## Events
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from agentshim import AssistantText, CliAgent, EventHandlerBase, ToolCall
|
|
123
|
+
|
|
124
|
+
class Watcher(EventHandlerBase):
|
|
125
|
+
def on_event(self, event):
|
|
126
|
+
if isinstance(event, ToolCall):
|
|
127
|
+
print("tool:", event.tool)
|
|
128
|
+
elif isinstance(event, AssistantText):
|
|
129
|
+
print(event.text, end="")
|
|
130
|
+
|
|
131
|
+
agent = CliAgent("claude", event_handler=Watcher())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`on_event` always runs on the thread that called `turn()`. The executor reads
|
|
135
|
+
the CLI's pipes on helper threads but drains them on the calling thread, so a
|
|
136
|
+
handler needs no locking of its own.
|
|
137
|
+
|
|
138
|
+
`ConsoleEventHandler` renders to any text stream; `CompositeEventHandler` fans
|
|
139
|
+
out; `NullEventHandler` drops everything.
|
|
140
|
+
|
|
141
|
+
## Executors
|
|
142
|
+
|
|
143
|
+
Implement `CommandExecutor` to run a provider CLI somewhere other than the
|
|
144
|
+
local host. `TransformingExecutor` covers the common case of rewriting argv,
|
|
145
|
+
and applies to the health check too.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from dataclasses import replace
|
|
149
|
+
from agentshim import CliAgent, CommandRequest, HostCommandExecutor, TransformingExecutor
|
|
150
|
+
|
|
151
|
+
def in_container(request: CommandRequest) -> CommandRequest:
|
|
152
|
+
return replace(request, argv=["docker", "exec", "-i", "workspace", *request.argv])
|
|
153
|
+
|
|
154
|
+
agent = CliAgent("claude", executor=TransformingExecutor(HostCommandExecutor(), in_container))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Testing against agentshim
|
|
158
|
+
|
|
159
|
+
`agentshim.testing` emits each provider's real stream format, so your tests
|
|
160
|
+
never encode a provider's JSON shape.
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from agentshim import AssistantText, CliAgent
|
|
164
|
+
from agentshim.testing import FakeExecutor, RecordingEventHandler, scripted_turn
|
|
165
|
+
|
|
166
|
+
events = RecordingEventHandler()
|
|
167
|
+
agent = CliAgent(
|
|
168
|
+
"claude",
|
|
169
|
+
executor=FakeExecutor(scripted_turn("claude", text="pong", session_id="s1")),
|
|
170
|
+
event_handler=events,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
result = agent.run("ping")
|
|
174
|
+
assert result.text == "pong"
|
|
175
|
+
assert result.session_id == "s1"
|
|
176
|
+
assert any(isinstance(event, AssistantText) for event in events.events)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Assert on `TurnResult` and the typed events, never on internal attributes.
|
|
180
|
+
|
|
181
|
+
## Errors
|
|
182
|
+
|
|
183
|
+
Everything that escapes `turn()` is an `AgentShimError`:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
AgentShimError
|
|
187
|
+
CliNotFoundError binary not on PATH
|
|
188
|
+
CliCheckError binary found but the health check failed
|
|
189
|
+
CliExitError nonzero exit: argv, returncode, stdout, stderr
|
|
190
|
+
SessionResumeError the conversation is gone: session_id
|
|
191
|
+
CliTimeoutError argv, timeout
|
|
192
|
+
ProviderCapabilityError the provider cannot do what the request asked
|
|
193
|
+
SchemaDialectError problems: list[str]
|
|
194
|
+
McpConfigError config file unreadable or not an object
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Adding a provider
|
|
198
|
+
|
|
199
|
+
See [`docs/extending.md`](docs/extending.md) and
|
|
200
|
+
[`docs/architecture.md`](docs/architecture.md). A provider package holds only
|
|
201
|
+
argv construction, stream parsing, and provider-specific options; everything
|
|
202
|
+
shared already lives in `agentshim/core/`.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
uv sync --dev
|
|
208
|
+
uv run pytest
|
|
209
|
+
./scripts/format_code.sh --check
|
|
210
|
+
./scripts/check_errors.sh
|
|
211
|
+
./scripts/type_check.sh
|
|
212
|
+
./scripts/check_imports.sh
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
End-to-end tests under `tests/e2e/` run the real CLIs. They are skipped
|
|
216
|
+
unless `AGENTSHIM_E2E=1` and the binary is on PATH, so CI never runs them.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
AGENTSHIM_E2E=1 uv run pytest tests/e2e -q
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Gemini needs a model the account is entitled to, and opencode takes one when
|
|
223
|
+
the model in your own opencode config is not the one to test:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
AGENTSHIM_E2E=1 AGENTSHIM_E2E_GEMINI_MODEL=gemini-2.5-flash \
|
|
227
|
+
uv run pytest tests/e2e/test_gemini_e2e.py -q
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
See [`docs/development.md`](docs/development.md) for the full gate list.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
uv build # package
|
|
234
|
+
uv publish # release
|
|
235
|
+
|
|
236
|
+
uv run --group docs mkdocs build --strict
|
|
237
|
+
```
|