* Announce harness releases on Twitter
Mirror pydantic-ai's post-release tweet so a PyPI publish also posts to
the shared @pydantic account, keeping release visibility consistent
across the workspace. Version is read from the tag ref since the
release job emits no version output.
Requires the TWITTER_* secrets to be provisioned on this repo.
* Scope send-tweet secrets to the release environment
zizmor's secrets-outside-env audit fails the lint job because the tweet
job reads the TWITTER_* secrets without a dedicated environment. Bind it
to the release environment, matching the release job's convention for
secret-using jobs (the harness has no zizmor ignore config like
pydantic-ai does).
* ci: add non-blocking pydantic-ai v2 beta test job
The harness targets the pydantic-ai v1 line, but the `>=1.105.0` floor also
admits v2 prereleases once prerelease resolution is enabled. Nothing in CI
exercised that path, so v2-breaking changes were invisible until release.
This adds a `test-v2-beta` job that resolves the latest v2 beta and runs the
suite against it. It surfaces real breakage today: v2 dropped the `calls`
argument from `ToolManager.get_parallel_execution_mode`, which CodeMode still
calls with the v1 signature, so `code_mode` raises `TypeError` under v2.
The job is intentionally kept out of the `check` gate so an expected red
result on the unsupported v2 line never blocks a merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: cap the v2-beta pytest step so a hang fails fast
The v2 beta job is expected to surface breakage, but some v2 breaks hang
instead of failing cleanly. When code mode raises an unhandled exception
inside a DBOS workflow, DBOS's background recovery thread stays alive and the
Python process never exits, so the step rode to the 20-minute job timeout and
burned a full runner slot on every push.
Wrap the pytest invocation in `timeout -k 30 300` so any such hang fails fast
(exit 124). A per-test timeout plugin would not help: the hang happens during
interpreter shutdown, after the test body completes, so the cap has to be on
the process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(code_mode): call get_parallel_execution_mode compatibly across v1/v2
pydantic-ai v2 (#5339, shipped in 2.0.0b1) dropped the `calls` argument from
`ToolManager.get_parallel_execution_mode`: it now reads the run-scoped mode
from a context var and applies per-tool `sequential` barriers separately. The
harness passed `[]` specifically to isolate the context var from per-tool
flags, which is exactly what the no-arg v2 call returns, so the two are
equivalent.
Inspect the method arity and call the matching shape. Inspecting rather than
catching TypeError avoids swallowing a genuine TypeError raised inside the
method. The `Callable[...]` annotation erases the bound signature so both call
shapes typecheck whichever major's stubs pyright resolves.
Without this, every code_mode run under v2 raises TypeError; inside a DBOS
workflow that unhandled error also wedged the process (a non-daemon recovery
thread blocked interpreter shutdown), which is what hung the v2-beta CI job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(code_mode): cover both arities of the v1/v2 mode dispatch
The arity-based dispatch added an `else` branch that the v1-pinned coverage
gate never executes (the v2 no-arg call only runs under pydantic-ai v2), so
total branch coverage fell to 99% and failed the `fail_under=100` gate.
Extract the dispatch into `_global_mode_is_sequential` and unit-test both call
shapes directly, so both branches are exercised whichever major is installed.
This is honest coverage rather than a `# pragma: no cover` that would hide a
branch that does run (in the v2-beta job).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: deselect v1-only OTel assertions from the v2-beta job
Two instrumentation tests in test_managed_prompt.py assert pydantic-ai v1's
OpenTelemetry attribute and span names. v2 deliberately renamed these
(aggregated-usage attributes, GenAI-semconv span names), so the tests are
expected-red on v2 and carry no signal in this job. Deselecting them keeps the
v2-beta job a meaningful early-warning for capability breakage (code mode,
durable execution) instead of going red on documented instrumentation drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: keep managed prompt v2 signal
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: David SF <david.sanchez@pydantic.dev>
* bumping pydantic ai
* Fix breakage from pydantic-ai 1.107: capability toolsets no longer cross runs
All three failures came from pydantic-ai #5230 (on-demand capabilities):
- SubAgents inherit_tools transplanted capability-contributed toolsets into
sub-agent runs where the owning capability is not registered, which now
fails CapabilityOwnedToolset's ownership resolution. Inherit only the
parent's own toolsets; capability sharing is shared_capabilities' job.
This also drops the delegate tool, replacing the name-based filter.
- Tool search discovery moved from message-scanning to
RunContext.discovered_tool_names, so the code_mode test now derives it
via parse_discovered_tools like the agent graph does.
- ToolDefinition gained capability_id, refreshing the logfire snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test latest pydantic-ai in CI and set the floor to the verified minimum
The lock-resolved test matrix never exercises pydantic-ai releases newer
than the lock, and pydantic-ai's harness-compat job only covers harness
code that exists when a pydantic-ai PR merges. Harness code merged after
a core change (SubAgents, ManagedPrompt vs core #5230) was therefore
never tested against it until a manual bump. The new test-latest job
re-locks pydantic-ai-slim/pydantic-graph to the newest published
versions on every run, so release breakage surfaces immediately.
With latest covered by CI, the pydantic-ai-slim floor no longer needs to
chase releases: set it to 1.105.0, the first release with capability
ownership semantics (core #5230) that the harness now relies on, and
verified against the full suite via the test-floor resolution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Mirror pydantic-ai's lowest-versions matrix and unfreeze its resolution
The test-floor job was not testing the floor: under the workflow-level
UV_FROZEN=1, `uv sync --resolution lowest-direct` installs the locked
(latest) versions, so the job duplicated the regular matrix while
claiming lowest-version coverage. pydantic-ai's test-lowest-versions job
sets UV_FROZEN=0 on the sync step for exactly this reason; do the same,
and adopt the rest of its shape (full Python matrix, fail-fast off, job
timeouts) so floor regressions that only reproduce on some Python
version get caught.
A genuinely-lowest resolution surfaced one over-coupled snapshot: the
`logfire.metrics` span attribute only exists on logfire releases newer
than the extra's 4.31.0 floor, so treat it as volatile in the
ManagedPrompt span snapshots instead of raising the floor.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Replace UV_FROZEN with UV_LOCKED in CI
Frozen mode installs the lockfile blindly: lock drift goes unnoticed and
resolution flags become silent no-ops, which is how the lowest-versions
job ended up testing the locked versions. Locked mode validates the
lockfile against pyproject.toml and fails loudly on any mismatch, so a
job that cannot do what it claims turns red instead of green. The two
jobs that deviate from the lock on purpose (lowest-versions resolution,
latest pydantic-ai upgrade) opt out per step, each with a comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep filtering the delegate tool when SubAgentToolset is used directly
The capability-ownership filter only drops the delegate tool when the
toolset arrives via the SubAgents capability, since that is the path that
wraps it in CapabilityOwnedToolset. SubAgentToolset is publicly exported,
and registered directly in Agent(toolsets=[...]) nothing wraps it, so
inherit_tools=True forwarded delegate_task to sub-agents and re-enabled
recursive delegation. Restore the name filter on top of the capability
filter so both registration paths stay non-recursive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Adapt to per-delegate run controls from main
PR #283 made limits and call_counts required on SubAgentToolset, which
broke the direct-construction test from the previous commit once CI
merged the branch with main. The merge also stranded the
CapabilityOwnedToolset import below the new SubAgentLimits dataclass;
moved it back into the import block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(code_mode): honor Tool Search's deferred-loading contract
CodeMode flattened `defer_loading=True` tools into `run_code`'s description
regardless of discovery state — defeating progressive disclosure, and on
providers with native tool search also double-listing the same tool on the
wire (top-level `tools[]` with `defer_loading: true` *and* its signature in
`run_code.description`), plus busting the prompt cache on every discovery.
Keep `defer_loading=True` tools as native pass-through so ToolSearchToolset's
`defer_loading` / `with_native` flags reach `Model.prepare_request` unaltered;
they fold into `run_code` only once discovered (`defer_loading=False`). Migrate
the sibling `prefer_builtin` filter to its renamed `unless_native` form.
Sources pydantic-ai from `main` for pydantic/pydantic-ai#5143 (native Tool
Search); drop `[tool.uv.sources]` and bump the `pydantic-ai-slim` floor once
that ships in a release. The same pydantic-ai changes rename the `builtin=`
capability kwarg to `native=`, so the README and quick-start test are updated
to match.
Closes#232
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* deps: bump pydantic-ai-slim floor to >=1.95.0, drop temporary git pin
pydantic-ai 1.95.0 ships pydantic/pydantic-ai#5143 (native Tool Search), so
remove the `[tool.uv.sources]` git override and bump the floor to it. 1.95.0
also deprecates `Agent(instrument=...)` in favor of an `Instrumentation`
capability, so the OTel-spans test switches to `capabilities=[..., Instrumentation(...)]`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: say "local MCP toolset" not "FastMCP" in the CodeMode + MCP example
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: add ToolSearch to the ecosystem-agent README example
Show progressive tool discovery alongside CodeMode in the full ecosystem
showcase — deferred tools fold into `run_code` once discovered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: group ToolSearch with CodeMode as a meta-capability in the README example
Both transform the toolset before it reaches the model, so they sit together
under "Tool execution & discovery" rather than alongside the tool providers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: trim the ToolSearch comment to one line, matching the others
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: clarify CodeMode + Tool Search behavior and the cache escape hatch
Rewrite the code-mode README's Tool Search section to cover both the native
and local-fallback paths accurately, spell out that a discovered tool folds
into `run_code` (busting the prompt cache once at discovery), and document the
`tools=` selector workaround (`td.with_native is None`) for keeping a Tool
Search corpus fully native. Mirror the escape-hatch note in the toolset docstring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(compat): set UV_FROZEN=1 so the editable overlay isn't clobbered by uv run
`compat-test.yml` overlays `pydantic-ai-slim` / `pydantic-graph` from the local
checkout via `uv pip install --no-deps -e ...`, but doesn't set `UV_FROZEN`.
The next `uv run ruff/pyright/pytest` calls then auto-re-resolve against PyPI
("Ignoring existing lockfile due to change in resolution mode: `lowest-direct`
vs. `highest`"), silently uninstall the overlay, and install whatever
`pydantic-ai-slim` is on PyPI — so the compat check has been a no-op against
the actual pydantic-ai checkout we wanted to validate.
Caught when pydantic-ai 1.95.0 landed `current_otel_traceparent` with lazy
imports that trip the Temporal sandbox; harness-compat against the merge
commit went green (because it was testing 1.94.0 from PyPI, not the merge
commit), and only failed once 1.95.0 was published to PyPI.
`main.yml` already pins `UV_FROZEN: '1'` at workflow scope; do the same here.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* deps: re-pin pydantic-ai-slim to git main for the Temporal sandbox fix
Released `1.95.0` (the lock-resolved version under `pydantic-ai-slim>=1.95.0`)
still has the `current_otel_traceparent` lazy-import bug that hangs Temporal
workflows on the `test_code_mode_runs_in_temporal_workflow` path — the
`all-extras` jobs sat at 1h+ before this. Repin to pai `main` (now at
`cf0b9077e`, which has pydantic/pydantic-ai#5422 merged) until `1.95.1` is on
PyPI. Drop this section + bump the floor in a follow-up once the release lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shim package and its release workflow served the 0.1.x transition
from pydantic-harness to pydantic-ai-harness. With this merged, the
next pydantic-ai-harness release is free to be 0.2.0 without needing
to maintain the shim.
MERGE ONLY AFTER:
- pydantic-harness==0.1.1 has been published to PyPI
- All users are expected to have migrated, or are willing to pin the
old 0.1.x line
MERGE BEFORE:
- Tagging pydantic-ai-harness v0.2.0 (since the shim pins
pydantic-ai-harness<0.2 and there's no point keeping it around once
the upper bound kicks in)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Companion to pydantic-ai's removal of the `safe-for-ci` label gate. The
called workflow runs with `contents: read` only and no secrets, so fork-PR
exposure is equivalent to pydantic-ai's regular `ci.yml` test jobs and
doesn't need a separate deny-by-default gate.
Header comment now flags the fork-PR consideration for future expanders: if
this workflow ever grows steps that need elevated permissions or secrets, the
caller needs to gate fork PRs at the job level (e.g. require a `safe-for-ci`
label) before merging that change. Documents the pattern for future
re-introduction without enforcing the friction now.
* ci: scope `pyright` in compat-test to harness sources
The reusable workflow checks out pydantic-ai into `./pydantic-ai/` inside
the harness's working tree, then runs `uv run pyright`. Pyright's
`exclude` in pyproject.toml doesn't list `pydantic-ai` (no reason to —
the directory doesn't exist in normal harness CI), so it walks into
pydantic-ai's own `tests/typed_agent.py` etc. and surfaces 19899
unrelated findings, failing the job.
Scope pyright explicitly to `pydantic_ai_harness tests` so it only
type-checks harness code (with pydantic-ai-slim/-graph imported via the
editable installs). The actual pydantic-ai source is irrelevant to
"is the harness still healthy."
* Switch to moving pydantic-ai outside the harness tree (vs scoping pyright)
DouweM's call: addresses the root cause rather than patching individual
tools. Future analyzers/walkers (mypy, ruff in different mode, pytest
collection edge cases) won't trip either.
`actions/checkout` requires `path:` to live under `$GITHUB_WORKSPACE`,
so we check out to a workspace subdir then `mv` to `${RUNNER_TEMP}/pydantic-ai`
which is outside workspace and thus invisible to anything walking the
harness tree.
* test(code_mode): cover approved-tool re-raising `ApprovalRequired`
pydantic/pydantic-ai#5275 documents on `_resolve_single_deferred` that a
re-raised `CallDeferred` / `ApprovalRequired` from the post-approval tool
body bubbles up without re-invoking the handler. The harness's existing
`except (CallDeferred, ApprovalRequired)` clause then converts it to a
`UserError` → `MontyRuntimeError` → `ModelRetry`. Lock that contract in
with a regression test so we notice if the upstream behavior shifts.
Refresh `uv.lock` to pick up #5275 from pydantic-ai main, which restored
`wrap_validation_errors` on `ToolManager.handle_call`. No harness code
change is needed — the existing call site already uses the kwarg.
* ci: add `compat-test.yml` reusable workflow for pydantic-ai → harness compat checks
Called from pydantic-ai's CI (via `workflow_call`) to verify that an arbitrary
pydantic-ai ref doesn't break the harness's lint / typecheck / test suite. The
workflow:
- checks out harness@main and pydantic-ai @ the input ref (with optional
`pydantic-ai-repo` for fork PRs)
- rewrites `[tool.uv.sources]` to point `pydantic-ai-slim` at the local
checkout, then `uv lock --upgrade-package` to refresh the lock
- runs `ruff format --check`, `ruff check`, `pyright`, `pytest`
Runs without secrets and with `contents: read` only — safe to invoke from
fork-PR contexts (the calling pydantic-ai workflow gates fork PRs behind an
approval label before invoking, as defense-in-depth).
* chore: bump `pydantic-ai-slim` floor to `>=1.89.1`
1.89.1 ships pydantic/pydantic-ai#5275 (restored `wrap_validation_errors`
on `ToolManager.handle_call`). Pinning the floor there so harness users
on the broken 1.86–1.89.0 window get a clean resolver error instead of
a runtime `TypeError: ToolManager.handle_call() got an unexpected
keyword argument 'wrap_validation_errors'`.
`uv.lock` is unchanged: `tool.uv.sources` already pulls pydantic-ai-slim
from `main`, which is past 1.89.1.
* ci: bump `test-floor` pin to match new `>=1.89.1` floor
Companion to ae9428e: that commit bumped the floor in `pyproject.toml` but
left this job pinning 1.80.0, which (a) no longer matches the documented
floor and (b) would have started failing because 1.80.0 predates #5275 and
the harness's existing `handle_call(wrap_validation_errors=...)` call site
would TypeError against it.
* ci+chore: drop `[tool.uv.sources]` for pydantic-ai-slim; auto-floor `test-floor`; simplify compat-test
- Drop `[tool.uv.sources]` git override for pydantic-ai-slim so the lock
resolves from PyPI like users would. The harness's `test` matrix and
install path now mirror what users actually get; bleeding-edge compat
with pydantic-ai's main is verified by pydantic-ai's `harness compat`
job (called from its CI on PRs, main pushes, and tags). `uv.lock`
refreshed.
- Refactor `test-floor` to read the floor version from `pyproject.toml`
at runtime instead of hardcoding it. Bumping the floor in pyproject is
now the single source of truth — no separate CI pin to keep in sync.
- Simplify `compat-test.yml`: with no `[tool.uv.sources]` to fight, the
workflow just `uv sync`s the lock then `uv pip install --no-deps -e`'s
the local pydantic-ai checkout. Same pattern test-floor uses, just
pointing at a path instead of a PyPI version.
* ci: switch `test-floor` to `uv sync --resolution lowest-direct`; install `pydantic-graph` from checkout in compat-test
- Replace the bespoke "read floor from pyproject + pin slim only" Python
script in `test-floor` with `uv sync --resolution lowest-direct`,
matching pydantic-ai's `test-lowest-versions` job. Resolves *all* direct
deps to their floors so the job exercises the full claimed compatibility
envelope, not just slim's floor.
- Add explicit floors for the previously-unfloored dev deps so
`lowest-direct` doesn't drag pre-Python-3 versions: `pytest>=9.0.0`,
`anyio[trio]>=4.11.0` (where the pytest plugin started registering the
`anyio_mode` ini option), `coverage>=7.10.7`. Match pydantic-ai's
discipline. `pytest-anyio` has only 0.0.0 on PyPI so no floor.
- `compat-test.yml` now also `uv pip install -e`'s `pydantic-graph` from
the local pydantic-ai checkout alongside slim. The two are sibling
packages that version-track together; testing slim from-source against a
PyPI `pydantic-graph` would mask cross-package issues (e.g. a slim PR
depending on an unreleased graph change).
* feat(code_mode): resolve deferred tool calls via HandleDeferredToolCalls
Tools with `kind='external'` or `'unapproved'` (and tools that raise
ApprovalRequired/CallDeferred at runtime) are no longer excluded from the
sandbox and promoted back to native tools. They now take the normal sandboxed
path, and a HandleDeferredToolCalls capability on the agent can resolve them
inline — so the model sees the resolved return value instead of having the
deferral bounce out as a separate native tool call.
- Remove the td.defer filter in _partition_callable_tools (no more native
fallback for deferred tools).
- Drop the native_fallbacks return value and the corresponding deferred-tool
warning.
- Update the sandbox UserError message when no handler is configured to point
users at HandleDeferredToolCalls.
- Update the deferred_execution test to assert sandbox inclusion and the
approval-retry test to match the new error message.
Depends on pydantic/pydantic-ai#5142 landing and being released; once it does,
bump the pydantic-ai-slim lower bound.
* code_mode: record outcome='denied' on nested ToolReturnPart for handler denials
When the `HandleDeferredToolCalls` handler denies a tool call, `handle_call` now raises
`ToolDeniedError` (on pydantic-ai-slim once released) instead of returning the denial
message as a plain string. CodeMode catches it, records a
`ToolReturnPart(outcome='denied')` in `nested_returns` so message history reflects the
denial correctly, and re-raises so the sandbox surfaces the denial as an exception
rather than as what would look like a successful tool return.
The `ToolDeniedError` import is gated behind a compat shim so this module still loads
against the currently released pydantic-ai-slim (which lacks the exception); the shim
resolves to a placeholder class that never matches a real exception, leaving the except
clause inert until a release ships `ToolDeniedError`.
Depends on pydantic/pydantic-ai#5142.
* code_mode: handle denials via `ToolDenied` return value, not exception
`ToolManager.handle_call` no longer raises a (now-removed) `ToolDeniedError`
on handler denial — it returns the `ToolDenied` value the handler produced.
Drop the compat shim, import `ToolDenied` directly, and switch the dispatch
to inspect the return value: record the denial as `outcome='denied'` on the
nested `ToolReturnPart` and raise a `RuntimeError` inside the sandbox so the
script can't mistake the denial message for a regular string return.
* Bump pydantic-ai-slim lock + cover denial path
Now that the slim PR has merged to main, refresh the lockfile to pick up
the `HandleDeferredToolCalls` capability and `handle_call`'s `ToolDenied`
return value. Add a denial test that asserts the denied-call flow
surfaces as `ModelRetry` with the original denial message preserved in
the trace.
Notes on the test:
- The handler returns `ToolDenied('nope')`; the harness records
`outcome='denied'` on the nested `ToolReturnPart` and raises
`RuntimeError` inside the sandbox.
- The script doesn't catch the RuntimeError, so Monty surfaces it as
`MontyRuntimeError`, which the harness converts back to `ModelRetry`.
The retry message preserves the denial message so the model knows
what went wrong.
* ci: test against floor pydantic-ai-slim (1.80.0) in addition to main
The default `test` matrix uses the `[tool.uv.sources]` override pinning
slim to its main branch, so it never exercises the published-PyPI install
path. Add a `test-floor` job that overrides slim to the lowest version
declared in `pyproject.toml` (>=1.80.0) and runs the test suite, so we
catch any accidental dependency on unreleased slim features in code paths
that should be backward-compatible.
Gate the new HandleDeferredToolCalls denial test with `pytest.skip` when
the capability isn't importable — currently the only test that requires a
post-1.80.0 slim, but the pattern can be reused if more land later.
* fix coverage: pragma the floor-only skip in the denial test
The `except ImportError → pytest.skip` branch only fires when running
against the slim floor (1.80.0) where `HandleDeferredToolCalls` doesn't
exist yet. The default test matrix runs against slim main, so coverage
counted those two lines as uncovered.
Mark the branch `# pragma: no cover` since it's an explicit skip path
that the floor-slim CI job exercises but isn't included in the coverage
report (the floor job doesn't gate on coverage by design).
Creates legacy/pydantic-harness/ with a compatibility shim package that
depends on pydantic-ai-harness>=0.1.1,<0.2 and re-exports its public API.
Importing pydantic_harness emits a DeprecationWarning telling users to
migrate to pydantic-ai-harness.
The shim is built and published independently via release-shim.yml on
tags matching pydantic-harness-v*. The main.yml release trigger is
narrowed from '**' to 'v*' so shim tags don't trigger the main release.
When pydantic-ai-harness==0.2.0 ships, this shim will stop resolving
(upper bound <0.2) and existing users will be forced to migrate.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wildcard rule was requesting reviews from all team members on
every PR, which is noisy and unnecessary.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update pydantic-ai-slim dependency to >=1.76.0 (published capabilities API)
- Replace pytest-xdist with anyio/pytest-anyio for async test support
- Add pyright executionEnvironments for tests (reportPrivateUsage=false)
- Add anyio_mode=auto to pytest config
- Create __init__.py with docstring and empty __all__ ready for capability imports
- Create tests/conftest.py with shared fixtures (TestModel, test agent, tmp_dir, allow_model_requests)
- Update test_placeholder.py to exercise conftest fixtures for 100% coverage
- Create docs/ skeleton with index.md and capabilities/index.md
- Update CI workflow with Python version matrix (3.10, 3.12, 3.13), uv caching, and permissions
- Rewrite README with installation, quick start, and capability table
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set up pydantic-harness as a publishable Python package using hatchling,
with CI (lint, test across Python 3.10-3.13) and automated PyPI releases
on tag pushes via OIDC trusted publishing — mirroring pydantic-handlebars.