This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
sim is a unified CLI + HTTP runtime that lets LLM agents (and engineers) launch, drive, and observe CAD/CAE simulations across multiple solvers through one consistent interface. It is the "container runtime for simulations" — agents talk to sim, sim talks to solvers.
The runtime supports two execution modes:
- One-shot (
sim run script --solver=X): subprocess execution, result stored as a numbered run,sim logsto browse. - Persistent session (
sim serve+sim connect/exec/inspect/disconnect): a long-lived HTTP server holds a live solver session; agents send code snippets and inspect state without restarting the solver.
The shared runtime skill is bundled at src/sim/_skills/sim-cli/ and synced into agent skill directories by sim plugin sync-skills. Per-solver agent skills ship inside their own sim-plugin-<solver> packages.
# Install
uv pip install -e ".[dev]" # core + pytest + ruff
# Tests
pytest -q # unit tests (no solver needed)
pytest tests/test_lint.py # single test file
pytest -q -m integration # integration tests (need solvers + sim serve)
# Lint
ruff check src/sim tests
ruff check --fix src/sim tests
# CLI
sim serve --host 0.0.0.0 # start HTTP server (default port 7600)
sim --host <ip> connect --solver <name> --mode solver --ui-mode gui
sim --host <ip> exec "solver.settings.mesh.check()"
sim --host <ip> inspect session.summary
sim --host <ip> screenshot -o shot.png
sim --host <ip> disconnect
sim run script.py --solver pybamm # one-shot mode
sim logs # list runs
sim logs last --field voltage_V # extract a parsed field
sim check <name> # solver availability
sim lint script.py # validate before runningEnvironment variables: SIM_HOST, SIM_PORT (CLI client, also [server] in config), SIM_HOME (global config + history dir, default ~/.sim/), SIM_DIR (project dir, default ./.sim/).
Config files (issue #5): ~/.sim/config.toml (global) + .sim/config.toml (project). Resolution order env > project > global > default. With no config files present, behavior is unchanged from pre-config sim. Run sim config path | show | init to manage. See docs/architecture/multi-session-and-config.md for the full schema.
Click app with subcommands: serve, check, lint, run, connect, exec, inspect, ps, disconnect, screenshot, logs. The session-related commands (connect/exec/inspect/ps/disconnect/screenshot) all delegate to sim.session.SessionClient, an HTTP client that talks to a running sim serve. The non-session commands (run, lint, check, logs) work locally without a server.
FastAPI app exposing:
POST /connect— launch a solver, register a new session in_sessions: dict[str, SessionState]keyed by session_idPOST /exec—exec()a Python snippet against the livesession/meshing/solvernamespace for the session selected byX-Sim-Sessionheader (or the single live session if unambiguous); capture stdout/stderr/return value, append to that session's runsGET /inspect/<name>— querysession.summary,session.mode,last.result,workflow.summary(session-scoped)POST /run— one-shot script execution (no session required)GET /ps— list of all live sessions + default_session (set only when exactly one live)GET /screenshot— base64 PNG of the server's desktopPOST /disconnect— tear down the session selected byX-Sim-Session(or the sole live session)POST /shutdown— tear down all sessions, exit the server process
The server supports multiple concurrent sessions keyed by session_id. Each SessionState carries its own threading.Lock so exec/inspect against different sessions can run in parallel. A single solver name can only be live once (driver instances are module-level singletons).
sim serve --reload drops all sessions on any source change under the watched tree. uvicorn's reload watchdog observes file mtimes in src/sim/**; any edit (git pull, scp of a modified driver, even touching an unrelated module) restarts the worker, wiping _sessions. Child solver processes (out-of-process GUIs, separately spawned solver binaries) survive the reload because they're spawned separately, but the session handles to them are gone — you have to connect again. Driver temp files written into the solver's workspace live outside src/ so they don't retrigger. Practical rules:
- Don't edit driver code mid-experiment; finish the run, then edit.
- For long autonomous experiments where you're editing driver code iteratively, launch without
--reloadand restart manually when you want the new code picked up. - Reconnecting after a reload can take tens of seconds for GUI-mode drivers that re-adopt the existing window rather than relaunching it.
DriverProtocol (a runtime_checkable Protocol):
name: str— registered driver namedetect(script) -> bool— does this script target this solver?lint(script) -> LintResult— pre-execution validation, returnsDiagnosticsconnect() -> ConnectionInfo— package availability + version checkparse_output(stdout) -> dict— extract structured results (convention: last JSON line on stdout)run_file(script) -> RunResult— one-shot execution
LintResult, Diagnostic, RunResult, ConnectionInfo are dataclasses with to_dict() for JSON serialization.
sim-cli-core is solver-agnostic and ships with no in-tree solver drivers. Drivers are provided by installed plugin packages that expose the sim.drivers entry-point group. Use sim plugin list and sim plugin info <name> to inspect the plugins visible in the active environment.
Plugin implementation work belongs in the owning sim-plugin-<solver> repository, where the package metadata, DriverProtocol implementation, compatibility.yaml, bundled skills, and plugin tests live. Do not add solver drivers under src/sim/drivers/<name> in this repo.
A driver may set supports_session = True to implement the persistent-session lifecycle (launch/run/query/disconnect); the rest are one-shot only. get_driver(name) looks up by .name attribute and lazily imports the implementation module on first use, so a broken plugin does not crash the CLI.
cli.run→runner.execute_script(script, solver, driver)→ subprocess, captures stdout/stderr/durationdriver.parse_output(stdout)→ extract structured fieldshistory.append({cwd, solver, session_id, run_id, ...})→ single jsonl line in~/.sim/history.jsonlsim logs <id>reads back viahistory.get_by_id;sim logs --solver X --allfilters
cli.connect→ HTTPPOST /connectto server →driver.launch(...)→ newSessionStateadded to_sessions; response carries the session_id which the client storescli.exec→ HTTPPOST /execwith code +X-Sim-Session: <id>→ server routes to that session, then_execute_snippet()runsexec(code, namespace)wherenamespacehassession,meshing/solver,_resultcli.inspect <name>→ HTTPGET /inspect/<name>(session-scoped) → driver- or session-specific querycli.disconnect→ HTTPPOST /disconnect(session-scoped) → driver-specific teardown, remove from_sessions
Session routing rules: an explicit X-Sim-Session header wins (404 if unknown); otherwise the server falls back to the sole live session; otherwise /exec returns 400. Clients can also set SIM_SESSION env var or pass sim --session <id> ... to scope a whole CLI invocation.
Create or update solver drivers in the owning sim-plugin-<solver> repository. A plugin should implement DriverProtocol, register the driver through [project.entry-points."sim.drivers"] in its pyproject.toml, and carry its own compatibility metadata, bundled skills, and tests.
The core CLI should only change when the shared runtime contract or plugin discovery machinery changes. Before editing driver-facing docs or behavior, inspect the installed environment with sim plugin list and sim plugin info <solver>, then work in the plugin repo that owns that solver.
Prefer generic primitives over task-specific ones. Before adding a new
driver method purpose-built for one workflow (a resumable-sweep runner, an
auto-coupling helper, etc.), check whether bounded run(timeout_s=...) +
health()/progress inspection + the solver's own retained state already let
the agent script that workflow step-by-step. Add a new dedicated primitive
only for a genuine generic gap proven by a real session (missing timeout
guard, no orphan-process cleanup, no liveness check) — not for workflow
convenience alone. See sim-studio-desktop issues #62/#80 for the HFSS case
this pattern was drawn from.
tests/
__init__.py
conftest.py shared fixtures / execution paths
base/ core framework tests (no solver needed)
test_cli.py smoke tests for click commands
test_compat.py skills layering / profile resolution
test_config.py two-tier config resolution
test_connect.py driver.connect() availability checks
test_driver_discovery.py entry-point plugin discovery
test_history.py global run history persistence
test_lint.py lint protocol coverage
test_logs.py sim logs CLI
test_multi_session.py session routing + concurrency
test_run.py one-shot subprocess execution
fixtures/ mock scripts and shared test assets
Solver plugin tests live in their own plugin repos. Tests in this repo cover the shared CLI/runtime contract and plugin discovery behavior.
- Global run history lives in
~/.sim/history.jsonl(append-only; override dir viaSIM_HOME); git-ignored - The server supports multiple concurrent sessions keyed by
X-Sim-Sessionheader; a solver name can only be live once per server process (driver instances are module-level singletons) - Project uses
uvfor dependency locking (uv.lock) - The shared runtime skill lives at
src/sim/_skills/sim-cli/; per-solver skills ship in theirsim-plugin-<solver>packages (synced viasim plugin sync-skills)
- PyPI distribution name:
sim-cli-core(renamed fromsim-runtimein Phase 4 — seesrc/sim/plugins.pydual-lookup;sim-runtime 0.2.1–0.2.3remain on PyPI but are no longer released against). Both names were rejected forsim-cliitself, which is too similar to the existingsimcliplaceholder. - Console script + import name:
sim. The PyPI dist name and the import name intentionally differ;src/sim/__init__.pylooks upversion("sim-cli-core")first, falling back toversion("sim-runtime")for editable installs predating the rename, andtry/except PackageNotFoundErrorfor source checkouts. - Trusted publisher: GitHub OIDC, repo
svd-ai-lab/sim-cli, workflow.github/workflows/publish.yml, environmentpypi. Configured at https://pypi.org/manage/project/sim-cli-core/settings/publishing/. - Tag format:
v<MAJOR.MINOR.PATCH>matchingpyproject.tomlversionexactly. Always tag frommainafter PR-merging a release branch. - Don't skip the clean-venv smoke test before tagging. 0.2.1 shipped broken (
__init__.pyreferencedversion("sim-cli")after the rename) because no one ransim --versionin a fresh venv before pushing the tag. Twine check verifies packaging, not import.
When writing anything that lands in a public place — GitHub issues, PR titles/bodies/comments, public commit messages, public docs — keep engineering-relevant facts and drop diary-style disclosure that ties a specific commercial-software install to a specific machine or account. The two are easy to confuse.
Keep:
- The bug, the error message, the exit code, the reproducing input.
- Version info that is a genuine reproduction prereq — when a behavior is gated to a specific release of an open-source dependency, the version is part of the engineering claim and stays. Use neutral framing for closed-source dependencies ("a CFD solver release that changed the boundary-condition API") rather than naming the vendor and release.
- Platform when behavior is platform-gated (Linux vs Windows filesystem casing, COM availability, etc.).
Drop / replace:
- Personal usernames and hostnames → "a Windows test host", "a development machine", or elide.
- Personal IPs (including Tailscale
100.90.x.x) → elide. - Personal filesystem paths (
C:\Users\<you>\...,~/Documents/GitHub/...,C:\Python<NN>\...,C:\Program Files\<Vendor>\<version>\...) → "a local clone", "the editable install", or elide. - Specific commercial-software versions tied to a personal machine — vendor compliance teams treat these as license-audit signals even when the version alone would be fine. Replace with neutral phrasing.
- Tailscale tailnet names, OS account SIDs, MAC/serial numbers.
Sanitize existing artifacts by editing PR/issue/comment bodies
(gh pr edit, gh issue comment --edit-last). Avoid force-pushing
to rewrite commit-message history unless the disclosure is severe
and the branch is unmerged and not being collaborated on.