CLAUDE.md · diff
git:20260906.dd04920 to git:20260911.a5764cd
1 added, 1 removed. Audit B to B.
# CLAUDE.md / AGENTS.md
This file provides shared guidance to both Claude Code (`CLAUDE.md`) and Codex (`AGENTS.md`, symlinked to this file) when working with code in this repository.
Keep future guidance platform-neutral unless a section explicitly calls out a Claude Code- or Codex-specific workflow.
## What this is
**OpenAI4S** is a pure-stdlib hybrid scientific research agent: provider-native JSON `Tool` calls form the orchestration/permission control plane, while persistent Python/R Code-as-Action kernels form the scientific execution plane. Python can call the in-kernel `host` singleton synchronously mid-cell; R is an independent analysis channel.
**Core is zero-dependency by design.** The engine, the LLM client (over `urllib`), and the web server (`http.server` + a hand-rolled WebSocket) use only the Python standard library. This is a hard constraint — see conventions below.
## Commands
```bash
./setup.sh # one-time: locked lightweight .venv + pre-commit hook
./setup.sh --with-kernel-envs # also create comprehensive Python + R envs
./setup.sh --update-kernel-envs # sync existing Python + R envs, without pruning
./start.sh # launch daemon + web UI at http://127.0.0.1:8760/
uv run pytest # full offline test suite (LLM is mocked — no network, no keys)
uv run pytest -n auto --maxprocesses=4 --dist loadfile # the same suite, the way CI runs it (~7min vs ~20min)
uv run pytest tests/test_agent.py::test_max_turns_stop # a single test
uv run pytest tests/test_kernel.py -k background # tests matching a pattern
uv run mypy # strict agent/Host + orchestration contracts
uv run pre-commit run --all-files # format + lint + mypy (black · isort --profile black · ruff)
uv run openai4s run "…" -v # run one Code-as-Action task in-process, no daemon
openai4s setup --profile standard # build Python + R from envs/*.yml
openai4s setup # build all 4 envs (--dry-run to preview)
```
CLI subcommands (`openai4s <cmd>`): `serve` · `status` · `stop` · `url` · `run` · `init` · `setup` · `doctor` · `diagnostics` · `verify-package` · `benchmark` · `env` (`plan`/`apply`/`list`/`rollback`/`recover`) · `jupyter` (`describe`/`export`/`install`) · `share` · `relay`. `start.sh` just runs `openai4s serve`.
**`uv run pytest` is not the whole gate.** CI runs each of the following as its own job, and each fails independently — a green pytest run says nothing about them:
```bash
uv run python scripts/check_directory_readmes.py # bilingual per-directory README coverage
uv run python3 -m harness.cli run --tier pr --offline # deterministic scenario contracts
uv run python scripts/capture_response_schemas.py --check # frozen response shapes still match reality
uv run python scripts/capture_response_contract.py --check # every routable route still has a contract
python scripts/source_secret_scan.py # no credential-shaped literal in release sources
bash scripts/container_smoke.sh # the image builds AND the daemon runs inside it
OPENAI4S_KERNEL_SANDBOX=enforce OPENAI4S_KERNEL_ALLOW_RAW_NETWORK=1 \
uv run python -m harness.smoke.linux_bwrap_interrupt # Linux+bwrap: Python/R persistent SIGINT
OPENAI4S_KERNEL_SANDBOX=enforce \
uv run python -m harness.smoke.linux_sandbox # Linux+bwrap: full filesystem/egress boundary
node tests/browser_smoke.mjs # workbench E2E, needs a daemon on :8760
node tests/browser_admission_fault.mjs # pinned-comment admission survives a lost response
node tests/browser_sandbox_preview.mjs # artifact preview executes on the sandbox origin (owns its daemon)
node tests/browser_matrix.mjs --browser=firefox # cross-engine breadth (chromium/firefox/webkit)
OPENAI4S_STAGE0_SELF_TEST=1 node tests/browser_stage0_acceptance.mjs # Stage 0 harness self-check, no browser/daemon
node tools/skills-installer/selftest.mjs # the npx Skill installer: extraction safety, install/uninstall
uv sync --locked --extra singlecell && \
uv run python -m pytest tests/test_single_cell_rna_analysis_skill.py -m "not network" # the locked single-cell stack contracts (py3.11+)
node tools/skills-installer/check_package.mjs # `npm pack` still carries the CLI *and* the Skill tree
```
The hosted-Linux interrupt smoke deliberately allows raw worker networking so
its private-PID evidence is independent of network-namespace setup. It proves
the team info-fd/procfs/pidfd signal path and post-interrupt kernel reuse, not
the full Linux filesystem-and-egress boundary. That full boundary is a separate
independent job (`harness.smoke.linux_sandbox`) that refuses the raw-network
override; it is attested at the frozen SHA and stays out of the release
workflow's `platform-checks` matrix until multiple scheduled greens land.
- The browser jobs need `npm ci --ignore-scripts && ./node_modules/.bin/playwright install <engine>` plus a daemon already serving on `127.0.0.1:8760`. They need **Node 20+**: `package.json`'s `engines` floor is `>=18` for the published Skill installer, but the pinned Playwright devDependency refuses anything older, and `npm ci` only warns.
+ The browser jobs need `npm ci --ignore-scripts && ./node_modules/.bin/playwright install <engine>` plus a daemon already serving on `127.0.0.1:8760`. That daemon's venv must carry the `science` extra (`uv sync --locked --extra science`, which `./setup.sh` already does): `browser_smoke.mjs`'s figure scene executes `import matplotlib` in the kernel and asserts a captured figure, so a lightweight venv fails C1. They need **Node 20+**: `package.json`'s `engines` floor is `>=18` for the published Skill installer, but the pinned Playwright devDependency refuses anything older, and `npm ci` only warns.
Tests are **offline**: `tests/conftest.py` redirects `~/.openai4s` to a tmp dir per test, sets a fake `deepseek` provider + key, and pins the deny-by-default posture (`OPENAI4S_UNATTENDED_APPROVAL=deny`, `OPENAI4S_SECRET_STORE=plaintext`, a loopback telemetry endpoint, share vars cleared). Don't add tests that require live LLM/network calls to the default suite.
- **Marker policy is a mechanism, not a convention.** `pyproject.toml`'s `addopts` carries `--strict-markers` and deselects `external`/`network`/`live_llm`/`gpu`/`ssh`/`lab`/`docker`/`browser`. Anything needing a live resource must carry one of those markers; opt in explicitly with `uv run pytest -m gpu`. An unregistered marker is a collection error, not a silent skip.
- **A test that stubs a service must be marked `stubbed_backend`.** `scripts/capture_response_schemas.py` re-runs the suite with a recorder installed and freezes what the routes returned into `docs/response-schemas.json`, whose entire claim is that it was captured from *real* responses. The marker pauses the recorder; without it a stub's fabricated shape gets published as the route's contract — provenance that is wrong rather than absent, which is worse because it gets believed.
## The dual loop (the central architecture)
Read `docs/architecture.md` first. The system is two nested loops:
- **Outer loop** — `openai4s/agent/engine.py`, composed for the CLI by `agent/loop.py` and for Web sessions by `server/agent_run.py`. The provider-neutral loop routes exactly one action: an ordered native JSON tool batch; a sole Engine-owned `FinalizeAction`; or one complete fenced Python/R Cell. Native calls take priority over code. A sole valid `finalize_response` is an explicit Engine completion even after earlier Cells; `host.submit_output(...)` is the only completion emitted from inside a Python Cell. Plain prose, ordinary tool results, R Cells, cancellation, and max-turn exhaustion are not completion.
- **Inner loop** — `openai4s/kernel/manager.py`. *Within a single cell*, agent code may call `host.llm(...)` / `host.delegate(...)` / `host.compute(...)` any number of times. The kernel worker emits a `host_call` frame mid-execution over a channel separate from stdout; the manager routes it to the `HostDispatcher`, writes back `host_response`, and the blocked cell resumes. This synchronous mid-cell RPC is what a `tool_use` architecture lacks.
**Kernels are lazy and persistent once started.** Tool/Finalize-only CLI and Web turns do not spawn a worker. Web Python/R slots have independent durable generation IDs; the Notebook shares them when the explicit REPL flag is enabled. A FIFO execution coordinator serializes Agent, user-REPL, lifecycle, and recovery writers and interrupts only an exact execution owner/lease.
**Kernel I/O is thread- and deadlock-sensitive — preserve its discipline.** `worker.py` holds `_HOST_CALL_LOCK` for the whole `host_call` request/response transaction (only one RPC in flight at a time) and serializes stdout writes; `manager.py` bumps `generation` on every respawn. When touching the kernel/manager protocol, keep the single-frame-reader loop, the id-routed `host_response`, and the transaction lock intact — and re-run `tests/test_kernel.py` after any change.
## Where things live
- **`openai4s/agent/`** — provider-neutral outer loop (`engine.py`), CLI/runtime composition (`loop.py`, `runtime.py`), action routing/ledger/finalization, context `compaction.py` (summarize old turns past a token threshold, archive raw slices), and `delegation.py` (concurrent sub-agents via `host.delegate`; fanout cap 48, session cap 1000, `MAX_DEPTH` 4 — depth-4 children are leaves that can't re-delegate).
- **`openai4s/kernel/`** — `manager.py` (host side: spawns a worker via `argv`, drives the language-neutral JSON-per-line protocol + inner RPC), `lazy.py` (thread-safe one-shot CLI worker ownership), `worker.py` (the python subprocess kernel; per-cell `compile(code, "<kernel:N>")` via `linecache` for accurate `error_lineno`, `getrusage` accounting, arms the in-kernel dlopen guard), **`r_kernel.py` + `r_worker.R`** (the R sibling: same manager, same frame/result contract; spawned as `sh -c 'exec Rscript --vanilla r_worker.R 3>&1 4<&0 </dev/null 1>&2'` so protocol frames ride fd3/fd4 and stray prints land on stderr — the shell-redirection equivalent of worker.py's dup2 swap; interpreter resolves selected env → the prebuilt `r` env → PATH, never silently python), `environments.py` (per-task conda-env selection), `background.py` (`host.exec_background`), `guards.py`, `provenance.py` (python-only). Python cells run under `sys.executable` by default (i.e. the active venv) or a selected conda env.
- **Artifact environment provenance comes from the kernel generation, not the daemon.** `ArtifactManager.capture_environment` looks up the generation for the frame and language that produced the files and records its runtime, interpreter, environment name and `generation_id`. This was a zero-argument freeze of the daemon process stamped `kind: "python"`, so an R cell's artifact carried a Python package list — provenance that was wrong rather than absent, which is worse because it was believed. A package list is only attributed to an interpreter it was actually read from (`preinstall.freeze_for`); otherwise the row records why it is missing rather than borrowing the daemon's.
- **`openai4s/kernel/provenance.py`** — object-level data lineage, running *inside* the worker. It tags objects read from an artifact with that artifact's source `version_id`, propagates the tag through indexing/slicing/`json.loads`/scalar ops, and on write reports `lineage_edges` (input version → output version) to the host. Escape hatch: `OPENAI4S_PROVENANCE_OFF=1`. This backs the UI's "produced by cell N / inputs" provenance view.
- **`openai4s/sdk/host.py` + `sdk/compute.py`** — the compatible `host` facade injected into Python and the remote-compute namespace. `host.bash` remains kernel-local, but subprocess launch now requires a short-lived, one-shot Host token bound to command hash, cwd, active worker generation, and challenge; the frame ID is audit context, while the Host authorizes/audits and never executes shell.
- **`openai4s/host_dispatch.py` + `openai4s/host/`** — `HostDispatcher` is the shared permission/approval/audit/replay/injection/step-event routing envelope. Capability behaviour lives in focused services for files, LLM, completion, data/lineage, delegation, remote science, progress, skills, MCP, endpoints, and credentials. **Soft-fail contract:** a handler may return a single-key `{"error": msg}` dict; the worker turns that into a `RuntimeError`.
- **`openai4s/llm/` + `openai4s/llm/__init__.py`** — normalized replies, provider-native tool calls, wire assembly, and stdlib transport behind the compatible `chat()` facade. Supported wires include OpenAI-compatible Chat/Responses, Anthropic Messages, and Gemini `generateContent`.
- **`openai4s/tools/`** — native JSON control tools are named `Tool` subclasses. Each capability module contains its schema, safety policy, and real `execute()` behaviour; only `registry.py:TOOL_TYPES` creates built-in instances. Never add a module-level tool singleton. Shell, scientific computation, and `submit_output` are not native tools.
- **`openai4s/store.py` + `openai4s/storage/`** — `Store` owns the one SQLite connection, schema/migrations, query guard, and compatible public facade. Frame, artifact, metadata, settings, permission, plan, annotation, agent, connector, and memory SQL lives in repositories sharing that connection and lock. `Store.close()` is idempotent and evicts only that exact cached singleton, so `get_store(path)` can safely create a new generation for the same path. The DB is exposed **read-only** to the agent via `host.query`.
- **`openai4s/server/`** — `gateway.py` is the stdlib HTTP/WebSocket composition adapter. Focused services own Cell execution, queueing, artifacts, Timeline projection, checkpoints/CAS/revert, recovery journal/control, Python/R Notebook export, renderer metadata, context/security projections, plans/review, skills, and titles. Session-domain REST adapters exist, and recovery execution, branch fork/activate/revert controls, and the `.ipynb` export control are wired end to end in the UI. One thing is narrower than it looks: fork-from-cell succeeds only from a cell carrying a cursor checkpoint and returns 409 otherwise (deliberate — a fork without a checkpoint cannot reconstruct state). Still do not infer end-to-end product completion from a service or route alone; check the UI call site.
- **User-visible completion is a projection, not the terminal signal.** Web tool/cell-only turns receive safe deterministic progress prose; a successful structured completion is rendered from `output` + `completion_bullets` + the actual Artifact-version delta before the terminal frame event. A direct protocol-only `host.submit_output(...)` cell still executes and remains in the raw audit log, but is hidden from the live/read-only Notebook. Native `writes_files=True` tools are captured at the Web control-tool boundary so they create Artifacts without double-registering in-kernel file writes.
- **`frontend/`** — Preact 10 + `@preact/signals` + TypeScript (strict) + Vite + Vitest workbench. `npm run dev` serves `http://127.0.0.1:5173/static/dist/` and proxies `/api`, `/ws`, and `/static` to the daemon on `:8760`. `npm run build` writes `openai4s/server/webui/dist/`; that committed tree is the default SPA shell. Dist and source land in the same PR. `npm test` is Vitest. `frontend/package-lock.json` pins rebuilds.
- **`openai4s/server/webui/`** — gateway static tree: committed Vite `dist/` (default shell at `/` and workbench deep links), shared `style.css` / `theme-bootstrap.js` / `scientific_renderers.js` / `favicon.js`, vendored 3Dmol/Ketcher/fonts, satellite pages (`login`/`share`/`replay`/`ketcher-page`), and the frozen `index.html` + `app.js` escape hatch (`OPENAI4S_WEBUI=legacy`). Do not add features to `app.js`. `/static/` is ordinary files under this directory either way.
- **`openai4s/security/` + `kernel/environment.py`** — strict child-env allowlisting keeps daemon secrets out of Python/R/subprocesses. The OS sandbox adapter uses Seatbelt/bubblewrap with `auto|enforce|off`, private temp/workspace writes, raw-network denial, and a real self-test; `auto` degrades visibly, `enforce` fails closed. Static/LLM code classification, shell checks, biosecurity, injection screening, dlopen audit hook, durable approvals (unattended defaults deny), and application egress remain independent layers.
- **`openai4s/compute/`** + **`openai4s_compute_provider/`** — BYOC remote GPU. `compute/` is the host-side manager/registry; `openai4s_compute_provider/` is the **stdlib-only sandboxed SDK that runs on the remote machine**. Secret scrubbing is two-staged: `__main__` runs the provider-agnostic `scrub_secret_env()` baseline **before** it imports `provider.py`, then the resident prologue re-scrubs with the loaded provider's own declared `secret_env_prefixes` before the credential is read (from stdin/fd-3) — so provider top-level code cannot read credential-shaped or known-prefix env vars (a name-based heuristic — a secret in an unrecognized variable name is NOT scrubbed), and the credential itself is never placed in the env. Provider shims that import third-party SDKs live only in `skills/remote-compute-<id>/provider.py`. `host.fold` (single-sequence Protenix/AF3-class) runs under a strict no-fabrication policy.
- **`skills/`** + **`openai4s/skills_loader/loader.py`** — 604 bundled Skills: 43 curated OpenAI4S directories plus the pinned 561-recipe GPTomics/bioSkills collection under `skills/bioskills/`. A **collection** is any `skills/<dir>/` holding a `COLLECTION.json` (id + the prompt line it wants) with its members one level lower; the loader discovers it from that marker, so no directory name or retrieval policy is hardcoded, and `list_skills` reports it as one entry (`collection=<id>` + `offset` enumerates it, following each returned `next_offset`). Each Skill is a **recipe of code, not a JSON schema**; ordinary Skills may include an optional `kernel.py` sidecar. User-authored content lives only under `<data_dir>/user-skills`; bundled directories win name collisions and remain read-only. Host authoring preserves `draft → personal`, while Web Customize documents use `user`. The default loader resolves capability state through the current Store generation on every operation, so it must not retain a repository from a closed Store. Progressive disclosure lists curated name + summary lines and one aggregate bioSkills line until `host.search_skills()`/load; explicitly scoped specialists get the same aggregate line counted over their allowlist, and sidecars are compile-checked before use.
- **`openai4s/execution/`** — the FIFO execution coordinator, its dependency model, and the watchdog, kept out of `server/` because the CLI path needs them too. `server/execution_coordinator.py` is the Web-side adapter onto it. `kernel/supervisor.py` owns worker identity/lifecycle and ABA-safe watchdog recovery *without* ever reading a protocol frame — a `Kernel` keeps its single synchronous reader.
- **`openai4s/kernel/env_generations.py`** — environments as a transaction, surfaced as `openai4s env plan|apply|list|rollback|recover`. `openai4s setup` installs in place; a generation is built fresh, verified, and only then pointed at via an atomic `os.replace` of a pointer file. An applied generation is immutable, so an artifact's environment provenance can name one. `preinstall.py` supplies the per-interpreter freeze that provenance attributes.
- **`harness/`** (+ `workflows/` and `openai4s/benchmark/`) — two different gates that are easy to confuse. `harness/` replays scripted scenarios with injected faults into a normalized event trace and diffs it against a golden; it is stdlib-only, versioned, and deliberately outside the production import graph. `workflows/` holds the 11 science-workflow benchmark manifests (34 cases) that `openai4s/benchmark/` executes against the *real* Store, kernel manager, host dispatcher and compute manager — only the LLM, the network, and the package manager are injected. In both, a declared `failure` / `permission_denied` / `recovered` / `provenance` outcome fails when the run *succeeds*: a benchmark scoring "no exception" measures nothing about the half of the system whose job is to refuse. Rule of thumb — a regression assertion about a specific contract goes in `tests/`; a replayable scenario, fake provider, or golden trajectory goes in `harness/`.
- **`openai4s/share/` + `server/share_*.py`** — the read-only session snapshot plus the outbound relay tunnel (`openai4s share`, `openai4s relay` on a VPS). **`openai4s/telemetry/`** — consent-gated, revocable; revoking destroys the identity with it, and the offline suite must never reach the real endpoint. **`openai4s/adapters/jupyter/`** — an optional KernelSpec bridge onto the same kernel managers; it is not a second outer loop (no tool batches, no `finalize_response`, no Artifact capture), and `bridge.py` imports `ipykernel`/ZeroMQ only when Jupyter actually launches it.
- **`scripts/`** — the release/verification tooling CI calls (`release_pipeline.py`, `verify_release_artifacts.py`, `release_import_smoke.py`, `capture_response_*.py`, `check_directory_readmes.py`, `check_css_tokens.py`, `source_secret_scan.py`, `connector_canary.py`, the macOS DMG build).
- **`Dockerfile` + `compose.yaml` + `deploy/`** — containerized deployment, deliberately outside the wheel (`deploy*` is not in `packages.find`, and nothing at runtime reads it). The image binds `0.0.0.0` because that is the only address a published port reaches, which forces the access token on and turns the DNS-rebind `Host` allowlist off — so the token is the whole control, and what you publish the port *to* is the real exposure decision. One trap is documented rather than papered over: an unprivileged container cannot give bubblewrap its namespaces, so `auto` degrades and a cell can then read `<data_dir>/access-token`. Credentials go through the broker's env-injection backend (`OPENAI4S_SECRET_STORE=env`, `OPENAI4S_SECRET_ENV=1`, `OPENAI4S_SECRET_LLM_LLM_API_KEY`), which `resolve_setting` now answers with no settings row at all — before that it short-circuited on the empty row and the injected variable was silently dead. Selecting a backend the container has also removes the `SecretStoreUnavailable` traceback `auto` prints ahead of the banner on every boot. `docs/docker.md` is the operator guide; `scripts/container_smoke.sh` is the gate.
- Other roots: `mcp_client.py` + `mcp_servers/` (MCP), `prompts.py` (system prompts), `replay.py` (trajectory replay), `doctor.py` / `diagnostics.py` (support surfaces), `egress.py`, `evidence.py`, `pkgscan.py`, `jobs.py`, `config.py` (dataclass `Config` + zero-dep `.env` loader), and the top-level `openai4s_worker_runtime/` package shipped alongside the wheel.
## Conventions & gotchas
- **Never add a hard third-party import to the core.** Optional science libs (numpy/pandas/matplotlib, the `science` extra) must be guarded by `try/except ImportError` at every in-tree use site. The kernel inherits whatever is in the active venv, so agent *cells* can use anything installed — but the engine itself cannot.
- **Config resolution is layered:** each of api_key / base_url / model resolves *per-provider var → generic `OPENAI4S_LLM_*` var → provider default* (e.g. `OPENAI4S_CLAUDE_API_KEY` → `OPENAI4S_LLM_API_KEY` → default). The daemon boots with no key set — the model is configured from the UI (Customize → Models) or `.env`.
- Ports/data via env: `OPENAI4S_HOST` (`127.0.0.1`), `OPENAI4S_PORT` (`8760`), `OPENAI4S_DATA_DIR` (`~/.openai4s`). Posture knobs worth knowing before you debug one of them: `OPENAI4S_KERNEL_SANDBOX` (`auto|enforce|off`), `OPENAI4S_EGRESS`, `OPENAI4S_SECRET_STORE`, `OPENAI4S_UNATTENDED_APPROVAL`, `OPENAI4S_NOTEBOOK_REPL`, `OPENAI4S_WEBUI` (`legacy` escape hatch), `OPENAI4S_DEFAULT_ENV`, `OPENAI4S_ALLOW_NETWORK`, `OPENAI4S_PROVENANCE_OFF`.
- **Every maintained directory needs a bilingual doc pair**, checked by `scripts/check_directory_readmes.py` in CI: `README.md` + `README_zh.md` (or `CONTENTS.md` + `CONTENTS_zh.md` where a `README.md` would collide with a tool, as in `.github/`), and the pair must list the directory's direct files. `openai4s/server/webui/vendor/` and `tests/fixtures/` are excluded — their *parent* README describes the boundary instead. The pinned `skills/bioskills/` collection keeps and checks its bilingual root boundary pair, while mechanically imported descendants are exempt. Adding any other package or skill directory without a pair fails the lint job even though pytest stays green. The same applies to translated sections of the root `README.md`/`README_zh.md`.
- **Branch names are enforced by CI** on every PR: `main`, `next`, or `<prefix>/<name>` with prefix one of `feat|fix|docs|test|refactor|chore|ui|harness|science|release|hotfix` (lowercase, 2–81 chars after the slash).
- **pre-commit excludes** `openai4s/server/webui/vendor/` (minified 3Dmol/fonts), `skills/bioskills/` (byte-exact pinned upstream payloads), and `tests/fixtures/` (byte-exact captured data) — never reformat those. ruff's rule set is pinned explicitly in `pyproject.toml` (`[tool.ruff.lint] select = ["E", "F"]`) and it ignores `E501,F401,E722,E402,E741`. **The `select` is load-bearing, not decoration** — without it the rule set is whatever the pinned ruff calls its default, and that default has moved: 0.0.274's was effectively `E` + `F`, while 0.16.0's is 413 rules. A routine version bump then silently becomes an unreviewed adoption of ~350 rules. Adopt further families one per PR, deliberately. **`[tool.black] target-version` is load-bearing for the same reason** — with no `[tool.black]` section black autodetects a target per file (26.5.1 reaches `py315`), which makes the formatting target a property of whichever interpreter runs the hook and puts black's own AST-equivalence safety check out of reach on any older one: it prints a warning and rewrites the file anyway. Keep it at the `requires-python` floor. The `mypy-core` hook is `always_run` and reads its file list from `pyproject.toml` (`agent/{actions,engine,finalize,models,ports}.py` + `host_dispatch.py` + `orchestration/{models,ports}.py`) — that boundary is strict (`disallow_untyped_defs`), the rest of the graph is not.
- **Data-only trees must stay in `[tool.setuptools.packages.find]`.** `skills*`, `envs*`, and `workflows*` are shipped as namespace packages so an installed `openai4s` still resolves them. When `workflows*` was missing, an installed `openai4s benchmark` found zero manifests and reported zero failures across zero workflows with exit code 0 — a silent pass on the thing that decides whether a release is good.
- The daemon is a **singleton** keyed by pidfile; `openai4s serve` refuses to start if one is already running. Bind stays on `127.0.0.1` — expose via SSH tunnel, never `0.0.0.0` on an untrusted network (see `docs/security.md`).
- **Edit the compatibility/composition facades surgically, never wholesale-rewrite them:** `server/gateway.py`, `host_dispatch.py`, `store.py`, `sdk/host.py`, `kernel/worker.py`, and `kernel/manager.py`. Put new algorithms in their owning service/repository/tool class; these facades pack routing, compatibility, schema, and transport contracts that a rewrite can silently drop. The PR template asks you to confirm this explicitly. New workbench UI goes in `frontend/`, not legacy `app.js`.
- **Don't autoclose matplotlib figures in `worker.py`** — the gateway is responsible for `savefig`-ing and closing unsaved figures after each cell so it can capture them as artifacts. Autoclosing in the worker would make figures vanish before capture.
- Workbench UI source is `frontend/`. `npm run dev` for live Vite on `:5173` (proxy `/api` `/ws` `/static` → `:8760`); `npm run build` emits `openai4s/server/webui/dist/`, which must be committed with the source. `OPENAI4S_WEBUI=legacy` serves the frozen `webui/index.html` + `app.js` hatch. Satellite pages remain classic scripts under `webui/`.
## Verify after changes
Tests are the floor, not the ceiling — much of what matters here is runtime behavior a unit test won't exercise.
- `uv run pytest` for the offline suite; scope kernel/engine work with `tests/test_kernel.py` / `tests/test_agent.py` and run them explicitly after protocol changes — but always finish with the **whole** suite. Per-module runs miss cross-test collisions (a global `Popen` patch in one file breaks the next file that spawns a real subprocess), and `pre-commit run --files X` can pass while `--all-files` fails.
- **The response capture is split and reassembled, not captured once.** `capture_response_schemas.py` runs the suite under `-n auto --maxprocesses=4 --dist loadfile` like every other gate. Each xdist worker atomically publishes its un-elided shapes during `pytest_sessionfinish`, before xdist can report that worker successful, together with the run ID and expected worker count. The script merges shares *after* pytest exits, through the same `merge` call `Recorder.observe` makes; it rejects a missing or mixed-run share before writing the capture. `tests/test_response_capture_assembly.py` asserts both completeness and equality with the single-process result.
- **Match the gate to what you touched:** a gateway route or serializer → `capture_response_schemas.py --check` *and* `capture_response_contract.py --check`; agent core, `host_dispatch.py`, or the orchestration contract → `uv run mypy`; scenario/fault/trace code → `python3 -m harness.cli run --tier pr --offline`; a new directory → `check_directory_readmes.py`; `style.css` custom properties → `scripts/check_css_tokens.py` (also a lint job step); packaging or resource files → `uv build` + `scripts/verify_release_artifacts.py dist`.
- For anything touching the kernel, host RPC, gateway streaming, or the web UI, **drive it end-to-end in a real browser** against a running `./start.sh` (the UI streams turns over WebSocket; behavior like figure capture, provenance, and live-Notebook kernel sharing only surfaces at runtime). A one-shot `uv run openai4s run "…" -v` is the fastest Code-as-Action smoke test without the UI. Workbench source changes also need `npm test --prefix frontend` and `npm run build --prefix frontend` with the resulting `openai4s/server/webui/dist/` committed.
- **Green on macOS is not green in CI.** CI is Linux: `sh` execs where macOS forks, there is no Seatbelt but there is bubblewrap, and platform branches you never take locally are the ones that run there. Force the Linux branch locally when you change sandbox, subprocess, or platform-detection code.
## Docs
`docs/architecture.md` (dual loop, host API) · `docs/skills.md` · `docs/compute.md` (BYOC/`host.fold`) · `docs/webapp.md` + `docs/webapp-api.md` · `docs/configuration.md` · `docs/security.md` · `docs/jupyter.md` (KernelSpec bridge) · `docs/webshare.md` · `docs/platforms.md` (per-OS support tiers) · `docs/science-connectors.md` · `docs/release-validation.md` · `docs/backend-extension-guide.md`. Governance — branch/PR/review/release policy and the numbered harness invariants — lives in `.github/CONTRIBUTING.md`, not here.