sel2pw — Selenium Java → Python Playwright migration agent
Implements the agent described in
selenium_java_to_playwright_migration_agent_system_prompt.md (bundled as sel2pw/prompts/system_prompt.md)
three ways, all on one shared core:
| Surface | Command | Who does the reasoning |
|---|---|---|
| CLI | sel2pw inventory / migrate / validate |
Built-in agent + any LLM provider |
| MCP server | sel2pw mcp (or sel2pw-mcp) |
The MCP host's LLM (Claude Code/Desktop, Copilot, Cursor…), or the built-in agent via run_migration_agent |
| Library | MigrationAgent(get_provider(...), Workspace.create(...)).run() |
Any LLMProvider |
Architecture
src/sel2pw/
workspace.py sandbox: named roots (source=ro, target=rw, --reference=ro), path-escape checks, secret redaction
inventory.py deterministic Java scan: tests (IDs "Class#method"), lifecycle hooks, page objects,
dependencies, locators, data-provider rows, constructs to flag (waits, frames, alerts, JS, ...)
validate.py compile (in-process), pytest --collect-only, pytest run (opt-in only)
ledger.py structured mappings + issues recorded by the agent
gates.py deterministic acceptance gates
reports.py code-generated migration_map.md / migration_validation.md
manifest.py run fingerprint + manifest under <target>/.sel2pw/runs/
tools.py ONE tool registry + input validation, used by both the agent and the MCP server
agent.py provider-agnostic loop (append-only history, gate-driven repair rounds, finalize)
llm/ base.py (neutral messages) + anthropic/openai adapters + registry + cache.py (record/replay)
prompts/ system_prompt.md (extracted from the spec) + static TOOL LAYER appendix
cli.py, mcp_server.py
The spec says "prompt text alone is not a security boundary", so the tool layer enforces it:
the source root is never writable; no path can leave its root; secrets are redacted before reaching any LLM;
overwriting a target file that existed before the run is recorded and reported; browser execution
(run_check kind=run) is refused unless the operator passes --allow-browser-tests.
There is no generic shell tool.
Install
pip install -e ".[all]" # or .[anthropic] / .[openai] / .[mcp]; .[dev] adds pytest
CLI
# Deterministic inventory (no LLM, free)
sel2pw inventory path/to/selenium-project --out inventory.md
# Full migration with the built-in agent
sel2pw migrate --source path/to/selenium-project --target path/to/innoharmony-repo \
--task "Migrate LoginTest and its page objects" --scope "LoginTest#*"
# Static checks on migrated code (add --collect for pytest --collect-only)
sel2pw validate path/to/innoharmony-repo --collect --python path/to/repo/.venv/Scripts/python
Useful migrate flags: --reference innoharmony=path/to/framework (extra read-only root),
--allow-browser-tests (approved non-prod env only), --python (target repo's interpreter for pytest),
--scope, --max-repairs, --gate-collect, --gate-run, --min-migrated N|N%, --allow-exclude PATTERN, --report-dir, --cache, --max-turns, --max-total-tokens N (stop cleanly with stop=budget), --system-prompt FILE.
Exit code is 0 only when the acceptance gates pass.
Large suites: --batch-by class (or package) runs one agent session per unit, each scoped to exactly that
unit's in-scope tests, with one shared ledger. Progress is saved to <target>/.sel2pw/batch/state.json after
every unit, and --resume continues a batch, skipping units already accepted. When all units have run, the
gates run across the whole scope and the combined reports are written.
Determinism
LLM output can't be made bit-for-bit deterministic. Claude Opus 5.5 rejects temperature, and OpenAI's seed
is only best effort. sel2pw makes every outcome deterministic and every run reproducible:
| Layer | Mechanism |
|---|---|
| What counts as "done" | Deterministic acceptance gates: scope_coverage (every in-scope test mapped), mapping_integrity (target file + symbol exist; blocked/excluded have reasons; a partial test has a MEDIUM-or-higher issue and a blocked test a BLOCKER/HIGH one; a test is excluded only if it matches --allow-exclude), migration_yield (at least --min-migrated in-scope tests, default 1, are migrated or partial), assertion_parity, parametrize_parity (data-provider rows = parametrize cases), disabled_preserved (stays skipped), compile, lint (no unjustified sleeps/networkidle, no secrets or redacted placeholders, no assertions that always pass, no expect() without a matcher), optional collect, and optional run (--gate-run with --allow-browser-tests: the mapped tests are executed once and each test's passed / failed / skipped result goes into both reports). Failures are sent back to the model for up to --max-repairs rounds, and the run fails if they persist. |
| Reports | The agent records structured data (record_mapping, record_issue). It cannot write migration_map.md or migration_validation.md. Code renders both, with no timestamps, so identical inputs give byte-identical reports. Text the model wrote is shown in labelled "agent" columns or sections. |
| Reproducing a run | Record/replay cache (--cache auto by default) in <target>/.sel2pw/cache. It is keyed by a SHA-256 of the provider settings, system prompt, tool schemas and the whole conversation, including tool results. Re-running with the same inputs and the same starting target replays the run exactly without calling the model. --cache replay makes any deviation an error, which suits CI. --cache off forces a fresh attempt. Tool output is normalized (sorted listings, pytest timings stripped) so keys stay stable. |
| Traceability | Every run writes <target>/.sel2pw/runs/<run_id>/: manifest.json (model + settings, prompt/tools/task/source-tree/inventory hashes, target tree hash before/after, cache hits, usage, SDK versions), gates.json, ledger.json, events.jsonl. The stable part of the fingerprint is also in the validation report. |
Escape hatches the gates recognise (all appear in reports for reviewers): a justification on a mapping
(intentional assertion, case or skip difference), and line comments # sel2pw: allow-sleep: <reason>,
# sel2pw: allow-networkidle: <reason>, # sel2pw: allow-secret (test data, not credentials).
Add .sel2pw/ to the target's .gitignore, or commit .sel2pw/cache if you want others to be able to replay your runs.
LLM providers (sel2pw providers)
--provider |
Notes |
|---|---|
anthropic (default) |
Default model claude-opus-5-5. Auth: ANTHROPIC_API_KEY or ant auth login. |
openai |
OPENAI_API_KEY, --model required |
azure-openai |
AZURE_OPENAI_BASE_URL (v1 endpoint) + AZURE_OPENAI_API_KEY; --model = deployment |
gemini |
GEMINI_API_KEY, OpenAI-compatible endpoint, --model required |
ollama |
Local, OLLAMA_BASE_URL (default http://localhost:11434/v1); use a tool-capable model |
openai-compatible |
vLLM, LM Studio, OpenRouter, LiteLLM… via SEL2PW_BASE_URL / SEL2PW_API_KEY |
my_pkg.module:MyProvider |
Plugin: any sel2pw.llm.base.LLMProvider subclass |
Defaults can come from env: SEL2PW_PROVIDER, SEL2PW_MODEL, SEL2PW_EFFORT.
Anthropic adapter specifics: streaming with eager_input_streaming tools (whole-file writes stream as they are
generated; inputs are re-validated before running), adaptive thinking with --effort (default high),
automatic prompt caching, and server-side refusal fallbacks enabled by default (fallbacks="default").
Pass --no-fallbacks to turn them off; you must do that on Bedrock/Vertex/Foundry gateways, where the parameter is rejected.
Long runs use server-side compaction (compact-2026-01-12, at 150K input tokens, with a summary prompt that keeps
the migration state); the history stays append-only, so thinking blocks remain valid. --no-compaction turns it off.
OpenAI-compatible providers get client-side clearing instead: once tool output in the history passes 400K characters, older large results are sent as a short stub (the three newest tool-result messages always go in full).
Adding a provider
from sel2pw.llm.base import LLMProvider, AssistantTurn, ToolCall
class MyProvider(LLMProvider):
name = "mine"
def __init__(self, model=None, **kw):
super().__init__(model or "my-model")
def complete(self, system, messages, tools) -> AssistantTurn:
... # convert UserMessage / AssistantTurn(raw) / ToolResultsMessage to your API, call it,
# return AssistantTurn(text, [ToolCall(id, name, args)], stop_reason, raw=<native turn>)
sel2pw migrate --provider my_pkg.providers:MyProvider ...
MCP server
Tools: java_inventory, list_files, read_file, search, write_file, edit_file, run_check,
record_mapping, record_issue, check_acceptance, finalize_migration (gates + generated reports + run manifest),
reset_session (start a new migration on the same server), run_migration_agent (hide with --no-agent-tool). Prompt: migrate_selenium_to_playwright.
Resource: sel2pw://system-prompt. The roots, --scope and gate options are fixed at server start, so a host
can't widen the scope. Host-driven runs get the same gates and generated reports through finalize_migration.
Claude Code
claude mcp add sel2pw -- sel2pw mcp --source C:/work/selenium-tests --target C:/work/innoharmony-tests
VS Code / Cursor / Claude Desktop (mcp.json / claude_desktop_config.json)
{
"mcpServers": {
"sel2pw": {
"command": "C:/myprojects/selenium2playwright/.venv/Scripts/sel2pw.exe",
"args": ["mcp", "--source", "C:/work/selenium-tests", "--target", "C:/work/innoharmony-tests"]
}
}
}
HTTP transport: sel2pw mcp ... --transport streamable-http [--host 127.0.0.1] [--port 8000]. The server exposes
write tools, so it binds to 127.0.0.1 by default and refuses any other address unless SEL2PW_MCP_TOKEN is set;
with a token, every request needs Authorization: Bearer <token>. One server instance serves one migration at
a time: its recorded mappings and written files are shared by every client that connects, so run a separate
server per concurrent migration, and call reset_session before starting a new one. run_migration_agent
keeps its own record of written files.
Security audit
security-audit.bat # Windows
./security-audit.sh # Linux, macOS, Git Bash
Creates and activates .venv with the project and its dev extras, installs the pinned tools in
requirements-security.txt into a separate .venv-audit (Semgrep pins an older mcp than this project needs),
downloads checksum-verified gitleaks, Trivy and CodeQL into .tools/ on first use
(CodeQL is a one-time ~665 MB download), then runs gitleaks, Trivy, Bandit, Semgrep, pip-audit and CodeQL
(Python and GitHub Actions). Open security-reports/security-report.html for all results, with a page per tool.
Options: -SkipCodeQL / --skip-codeql, -SkipInstall / --skip-install, -Out DIR / --out DIR.
Tests
pytest -q
No test calls a real LLM. The provider adapters are tested against fake clients, and the MCP tests run the server in-process.
Metadata
Release files for sel2pw 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sel2pw-0.2.0.tar.gz | 89.5 kB | With provenance | Details for sel2pw-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sel2pw-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for sel2pw-0.2.0-py3-none-any.whl |
Total release size: 166.5 kB
Release files / sel2pw-0.2.0.tar.gz
| Download URL | sel2pw-0.2.0.tar.gz |
|---|---|
| Size | 89.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7add9a3db846df8d6f4b4bb2bd88a47eb220d47cc96afec608f2594f279ef16c
|
|
BLAKE2b-256 checksum How to use checksums |
c914dc88ce5da9622ac2325224ae827a93bf9c7c06b234db60404cc4967323a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / sel2pw-0.2.0-py3-none-any.whl
| Download URL | sel2pw-0.2.0-py3-none-any.whl |
|---|---|
| Size | 77.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
296e9dce3a3215b3f073f9231ffec50f157786c6bab296991e74fcfaa0607295
|
|
BLAKE2b-256 checksum How to use checksums |
082f6b3fbd46f63396990d3b787571f29b51f9b7e86cca107b141e81cfb0bbe8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for sel2pw 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sel2pw-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.