Skip to content

Latest commit

 

History

History
196 lines (151 loc) · 13.7 KB

File metadata and controls

196 lines (151 loc) · 13.7 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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 logs to 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.

Commands

# 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 running

Environment 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.

Architecture

CLI (src/sim/cli.py)

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.

HTTP server (src/sim/server.py)

FastAPI app exposing:

  • POST /connect — launch a solver, register a new session in _sessions: dict[str, SessionState] keyed by session_id
  • POST /execexec() a Python snippet against the live session/meshing/solver namespace for the session selected by X-Sim-Session header (or the single live session if unambiguous); capture stdout/stderr/return value, append to that session's runs
  • GET /inspect/<name> — query session.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 desktop
  • POST /disconnect — tear down the session selected by X-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 --reload and 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.

Driver protocol (src/sim/driver.py)

DriverProtocol (a runtime_checkable Protocol):

  • name: str — registered driver name
  • detect(script) -> bool — does this script target this solver?
  • lint(script) -> LintResult — pre-execution validation, returns Diagnostics
  • connect() -> ConnectionInfo — package availability + version check
  • parse_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.

Driver registry (plugin entry points)

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.

Execution pipeline — one-shot (run)

  1. cli.runrunner.execute_script(script, solver, driver) → subprocess, captures stdout/stderr/duration
  2. driver.parse_output(stdout) → extract structured fields
  3. history.append({cwd, solver, session_id, run_id, ...}) → single jsonl line in ~/.sim/history.jsonl
  4. sim logs <id> reads back via history.get_by_id; sim logs --solver X --all filters

Execution pipeline — persistent session (exec)

  1. cli.connect → HTTP POST /connect to server → driver.launch(...) → new SessionState added to _sessions; response carries the session_id which the client stores
  2. cli.exec → HTTP POST /exec with code + X-Sim-Session: <id> → server routes to that session, then _execute_snippet() runs exec(code, namespace) where namespace has session, meshing/solver, _result
  3. cli.inspect <name> → HTTP GET /inspect/<name> (session-scoped) → driver- or session-specific query
  4. cli.disconnect → HTTP POST /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.

Adding or changing a solver driver

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.

Test Layout

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.

Notes

  • Global run history lives in ~/.sim/history.jsonl (append-only; override dir via SIM_HOME); git-ignored
  • The server supports multiple concurrent sessions keyed by X-Sim-Session header; a solver name can only be live once per server process (driver instances are module-level singletons)
  • Project uses uv for dependency locking (uv.lock)
  • The shared runtime skill lives at src/sim/_skills/sim-cli/; per-solver skills ship in their sim-plugin-<solver> packages (synced via sim plugin sync-skills)

Releases

  • PyPI distribution name: sim-cli-core (renamed from sim-runtime in Phase 4 — see src/sim/plugins.py dual-lookup; sim-runtime 0.2.10.2.3 remain on PyPI but are no longer released against). Both names were rejected for sim-cli itself, which is too similar to the existing simcli placeholder.
  • Console script + import name: sim. The PyPI dist name and the import name intentionally differ; src/sim/__init__.py looks up version("sim-cli-core") first, falling back to version("sim-runtime") for editable installs predating the rename, and try/except PackageNotFoundError for source checkouts.
  • Trusted publisher: GitHub OIDC, repo svd-ai-lab/sim-cli, workflow .github/workflows/publish.yml, environment pypi. Configured at https://pypi.org/manage/project/sim-cli-core/settings/publishing/.
  • Tag format: v<MAJOR.MINOR.PATCH> matching pyproject.toml version exactly. Always tag from main after PR-merging a release branch.
  • Don't skip the clean-venv smoke test before tagging. 0.2.1 shipped broken (__init__.py referenced version("sim-cli") after the rename) because no one ran sim --version in a fresh venv before pushing the tag. Twine check verifies packaging, not import.

Public-artifact privacy / license safety

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.