docs(AGENTS): update repository guidelines for clarity and structure

This commit is contained in:
2026-10-03 15:19:13 +02:00
committed by Jan Wyzula
parent 1e3b344f68
commit d2e1bf8dfd
2 changed files with 229 additions and 186 deletions
+227 -152
View File
@@ -1,207 +1,282 @@
# Repository Guidelines — `ophyd_devices`
`ophyd_devices` is the **hardware abstraction layer** for
[BEC (Beamline Experiment Control)](https://github.com/bec-project/bec). It extends
[ophyd](https://github.com/bluesky/ophyd) with device support for hardware that the standard EPICS
implementation does not cover — motion controllers, detectors, shutters, undulators, monochromators —
plus a full simulation framework so BEC can run end-to-end with no hardware attached.
`ophyd_devices` provides reusable ophyd hardware support and simulation devices for
[BEC](https://github.com/bec-project/bec). Prefer focused changes, follow existing local patterns,
and verify the smallest relevant test scope.
This file is a quick-reference for AI coding agents (and new contributors). User-facing documentation
lives at <https://bec.readthedocs.io>; general ophyd concepts are documented at
<https://blueskyproject.io/ophyd/>.
This file is an agent-oriented operating manual. User-facing documentation lives at
<https://bec.readthedocs.io> and is authored in the separate `bec_docs` repository.
Treat `pyproject.toml` as the source of truth for dependencies, scripts, and tool configuration.
## Project Structure & Module Organization
## Core Rules
`ophyd_devices/` is the importable package:
- Use `PSIDeviceBase` when a device needs BEC lifecycle or business logic. A device that merely
exposes signals can use plain `ophyd.Device`, regardless of the communication backend.
- Separate reusable device control from beamline-specific business logic in `on_*` hooks.
- Prefer repository statuses, signals, and helpers when a BEC-aware counterpart exists.
- Return promptly from device-server calls. Represent unfinished work with a status.
- Implement safe interruption through `on_stop()` and register cancellable statuses.
- Use `bec_signals.py` for BEC live data, progress, and file events.
- Check the relevant device protocols when changing interfaces.
- Add an example configuration for each new reusable device family.
- Do not edit `ophyd_devices/devices/device_list.md`; CI generates it.
- Follow existing local patterns before introducing a new abstraction.
- Keep diffs focused and preserve unrelated local changes.
- Add regression tests for bug fixes.
- Do not commit, push, open a PR, or post a review unless explicitly requested.
| Path | What goes there |
| --- | --- |
| `ophyd_devices/interfaces/base_classes/` | The classes you should inherit from: `PSIDeviceBase`, `PSIPositionerBase`, `PSIPseudoDeviceBase`, `PSIPseudoMotorBase`. Start here before writing a device. |
| `ophyd_devices/interfaces/protocols/` | `typing.Protocol` definitions (`BECDeviceProtocol`, `BECPositionerProtocol`, `BECFlyerProtocol`, …) describing what BEC expects from a device. Useful as a checklist and in `isinstance` tests. |
| `ophyd_devices/interfaces/device_config_templates/` | Templates for generating device configuration entries. |
| `ophyd_devices/devices/` | Concrete device implementations (`psi_motor.py`, `undulator.py`, `optics_shutter.py`, `dxp.py`, `areadetector/`, `panda_box/`, …), plus the generated `device_list.md`. |
| `ophyd_devices/sim/` | The simulation framework: `SimPositioner`, `SimCamera`, `SimMonitor`, `SimWaveform`, `SimFlyer`, and the `sim_data.py` data generators behind them. |
| `ophyd_devices/utils/` | Shared helpers: `bec_signals.py`, `bec_scaninfo_mixin.py`, `controller.py`, `socket.py`, `psi_device_base_utils.py` (`FileHandler`, `TaskHandler`), `static_device_test.py`. |
| `ophyd_devices/configs/` | Example device configuration YAML files, including the simulation config used for local BEC runs. |
| `ophyd_devices/npoint/`, `rt_lamni/`, `sls_devices/`, `smaract/` | Vendor- and facility-specific integrations. |
| `tests/` | The test suite (flat; one `test_<area>.py` per area). |
## First Read
`ophyd_devices/devices/device_list.md` is **generated by CI** on pushes to `main` — do not edit it by hand.
Read the affected implementation and its tests. For unfamiliar areas, start here:
## Local Environment Overlay
- `ophyd_devices/interfaces/base_classes/psi_device_base.py` — device lifecycle and BEC integration
- `ophyd_devices/interfaces/base_classes/psi_positioner_base.py` — motion
- `ophyd_devices/interfaces/base_classes/psi_pseudo_device_base.py` — pseudo devices
- `ophyd_devices/interfaces/base_classes/psi_pseudo_motor_base.py` — pseudo motors
- `ophyd_devices/interfaces/protocols/bec_protocols.py` — expected device contracts
- `ophyd_devices/utils/psi_device_base_utils.py` — statuses, tasks, and file helpers
- `ophyd_devices/utils/bec_signals.py` — live data publishing
- `tests/conftest.py` and `ophyd_devices/tests/utils.py` — reusable test helpers
- `README.md` — project overview
If a file named **`AGENTS_PERSONAL.md`** exists next to this one, read it and treat it as an extension
of this file. It carries machine-specific setup — interpreter and environment manager, local paths,
private workflow conventions — and **its instructions take precedence over the generic
"Development Environment" section below**. Everything else in this file still applies.
## Repo Layout
That file is intentionally untracked and personal to one developer's machine. Do not commit it, do not
reference it from committed files, and do not assume it exists — if it is absent, follow this file as
written.
- `ophyd_devices/interfaces/` — base classes, protocols, and device configuration templates
- `ophyd_devices/devices/` — concrete devices and the generated device list
- `ophyd_devices/sim/` — simulation devices and data generators
- `ophyd_devices/utils/` — shared signals, statuses, controllers, and other helpers
- `ophyd_devices/configs/` — example device configurations
- `tests/` — unit tests
## Development Environment
Related but separate repositories:
Requires **Python 3.11+** (CI tests 3.11, 3.12, 3.13).
- `bec` — core messaging, scans, services, and the client
- `bec_widgets` — GUI widgets
- `bec_docs` — published documentation
- beamline plugin repositories — beamline-specific devices, scans, and widgets
```bash
python -m venv .venv
source .venv/bin/activate # macOS/Linux only; see "Platform Notes"
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
```
## Local Overlay
The `dev` extra pulls in `bec-server`, which is what the device-server-facing tests exercise. Verify the
environment resolves to this checkout:
If `AGENTS_PERSONAL.md` exists beside this file, read it as an extension of these instructions.
Its machine-specific environment and workflow guidance takes precedence over the corresponding
sections here. Keep it local and untracked; do not copy its contents into committed files.
```bash
python -c "import ophyd_devices; print(ophyd_devices.__file__)"
```
## Common Task Routing
No EPICS IOC or hardware is needed for development: unit tests mock connections, and the `sim/` devices
provide a full working beamline in software.
If you change:
## Writing a Device
- `ophyd_devices/interfaces/base_classes/*`: inspect relevant protocols and affected concrete and
simulation devices; test staging, subscriptions, movement, and stop behavior.
- `ophyd_devices/utils/psi_device_base_utils.py`: check timeout handling and composition across
status subclasses.
- `ophyd_devices/utils/bec_signals.py`: check device-server consumers and report compatibility
risks for `bec` and `bec_widgets`.
- `ophyd_devices/sim/*`: check the affected device and tests consuming its scan data.
- `ophyd_devices/devices/*` or vendor integrations: add targeted device tests; include an example
configuration and validation notes for a new reusable device family.
- `ophyd_devices/configs/*`: run `ophyd_test --config <changed-config>` and inspect its report.
- Documentation or templates: verify referenced paths, commands, examples, and metadata.
A broad unit test run is unnecessary when executable behavior is unchanged.
**Inherit from a `PSI*` base class, not from `ophyd.Device` directly.** `PSIDeviceBase` wires up the
subscription types BEC's device manager expects (`readback`, `value`, `done_moving`, `motor_is_moving`,
`progress`, `file_event`, `device_monitor_1d`, `device_monitor_2d`), gives you `scan_info` and
`device_manager`, and provides the `FileHandler` / `TaskHandler` utilities. A bare `ophyd.Device` will
appear to work locally and then misbehave inside a running BEC deployment.
Reusable hardware support belongs here. Devices specific to one beamline belong in its plugin
repository. Route core messaging, scans, and service changes to `bec`, GUI behavior to
`bec_widgets`, and published documentation to `bec_docs`.
```python
from ophyd import Component as Cpt, EpicsSignal, EpicsSignalRO
## Writing A Device
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
### Control and business logic
The device's base control class defines how to communicate with the hardware: signal
definitions, commands, protocol handling, and device state. Keep this layer reusable across
beamlines and implement it in `ophyd_devices`. Plain ophyd control classes may be composed into
or combined with a class that uses `PSIDeviceBase` when business logic is needed.
class MyDetector(PSIDeviceBase):
"""One-line description; this text reaches the generated device list."""
A device that only exposes a collection of signals needs no business-logic layer; plain
`ophyd.Device` is enough. For example, `SLSOperatorMessages` in
`ophyd_devices/devices/sls_devices.py` groups operator messages and their dates using dynamic
components without scan-specific behavior. This applies equally to EPICS and other communication
backends; the need for business logic determines the base class, not the transport.
acquire = Cpt(EpicsSignal, "ACQ", kind="omitted")
readback = Cpt(EpicsSignalRO, "VAL", kind="hinted")
The `on_*` hooks typically describe business logic: how a beamline uses that control interface
during a scan. For example, the control class exposes acquisition and trigger-mode commands;
a beamline's `on_stage()` chooses the trigger mode and acquisition settings for its experiment,
and `on_trigger()` starts acquisition through the control interface.
def on_stage(self) -> None:
... # prepare for a scan
Implement bespoke hook behavior in a subclass in the beamline plugin repository. Keep shared
lifecycle behavior generic, and make hooks call reusable control methods rather than duplicating
PV definitions or communication code. This lets beamlines share the same hardware support while
choosing their own acquisition and scan behavior.
def on_complete(self) -> None:
... # wait for acquisition to finish
### Base classes and hooks
def on_unstage(self) -> None:
... # release resources
```
Rules of thumb:
- **Where `ophyd_devices` provides a counterpart to an `ophyd` class, always import the
`ophyd_devices` one.** Several ophyd classes are subclassed here to add BEC behaviour, and the plain
ophyd version silently loses it. The status classes in `ophyd_devices/utils/psi_device_base_utils.py`
— `StatusBase`, `Status`, `DeviceStatus`, `MoveStatus`, `SubscriptionStatus`, `AndStatus` — add
timeout diagnostics that report which device and which call is stuck, and the `&` operator for
composing statuses. The same module adds BEC-only statuses with no ophyd equivalent
(`CompareStatus`, `ExceptionStatus`, `TransitionStatus`, `TaskStatus`), and signals that publish to
BEC live in `ophyd_devices/utils/bec_signals.py`.
- When lifecycle or business logic is needed, use `PSIDeviceBase` to integrate the control class
with BEC's lifecycle, scan information, statuses, and subscriptions. Do not add it solely to
expose a collection of signals.
- Use the lifecycle hooks provided by the base class rather than replacing its wrappers:
`on_init`, `on_connected`, `on_stage`, `on_pre_scan`, `on_trigger`, `on_complete`,
`on_kickoff`, `on_unstage`, `on_stop`, and `on_destroy`.
- When implementing a device inheriting from `PSIDeviceBase`, copy the entire
"Beamline Specific Implementations" section from
`ophyd_devices/interfaces/base_classes/psi_device_base.py`, including its separator, all ten
hooks, signatures, and docstrings. Keep the hooks together in that order, including unused
hooks, so readers can quickly identify the device's business logic.
- Put helper methods outside that section under a separate separator, for example:
```python
from ophyd_devices.utils.psi_device_base_utils import DeviceStatus, MoveStatus # yes
from ophyd.status import DeviceStatus, MoveStatus # no
########################################
# Beamline Specific Implementations #
########################################
# All ten on_* hooks belong here.
########################################
# Helper Methods #
########################################
```
The same applies to anything re-exported from `ophyd_devices/__init__.py`. Importing straight from
`ophyd` stays correct only for what has no counterpart here — `Component`, `EpicsSignal`, `Kind`,
`PositionerBase`, and friends.
- **Never block.** Long-running work goes through `TaskHandler`, and completion is reported with a
`DeviceStatus`. A device that blocks in `stage()` or `trigger()` stalls the whole device server.
- **Set `kind` deliberately.** `hinted` signals are recorded by default; `omitted` and `config` are not.
A wrong `kind` either loses data or floods every scan file.
- **Always implement `stop()`** so the device can be interrupted mid-scan and leaves the hardware safe.
- **Emit BEC data through `ophyd_devices/utils/bec_signals.py`** rather than inventing a message shape.
- **Check your device against the protocols** in `interfaces/protocols/bec_protocols.py` — they are the
contract BEC relies on.
- **Add an example configuration** under `ophyd_devices/configs/` when adding a new device family.
- When replacing a hook in a subclass, preserve any required parent behavior with `super()`;
copied hook stubs must not silently disable an inherited implementation.
- Constructors and `on_init()` must not communicate with devices. Use `on_init()` only for local
initialization; do not read hardware state or send commands there.
- `on_connected()` is the first hook allowed to communicate with devices and send instructions,
such as setting hardware defaults. The device server calls it once an enabled device has
connected; plain instantiation, unit tests, and `ophyd_test` do not, so call it explicitly in
tests that rely on it. It is not a per-scan hook. Use the scan lifecycle hooks for
scan-specific instructions and access scan parameters through `self.scan_info.msg`.
- Check the relevant protocols when adding or changing a device interface. Read the base-class
implementation for hook return semantics; the protocol signatures alone do not describe them.
### Validating a device configuration
### Completion and interruption
`ophyd_test` statically analyses a device configuration YAML, and can optionally connect to the hardware:
- Acquisition, staging, completion, and movement must return promptly to the device server.
Represent unfinished work with a status; do not wait, sleep, or poll on the calling thread.
- Import statuses from `ophyd_devices.utils.psi_device_base_utils`, including `DeviceStatus`,
`MoveStatus`, and `StatusBase`. These provide BEC timeout diagnostics and status composition.
Prefer repository helpers whenever a BEC-aware counterpart exists.
- Hooks such as `on_stage()` and `on_complete()` may return a status or `None`. Return `None`
only when there is no outstanding work. In particular, `complete()` converts `None` into
an already-finished status; return a pending status while acquisition or file writing continues.
- Register statuses that must fail on interruption with `self.cancel_on_stop(status)`.
Implement hardware interruption in `on_stop()` and make repeated calls safe. Cancelling a
status does not by itself stop hardware or a background task.
- Preserve base-class stop and destroy behavior. Release subscriptions, threads, sockets, and
other owned resources in `on_destroy()`; worker code must be able to exit on interruption.
### Signals and configuration
- Set signal `kind` deliberately. `normal` and `hinted` signals appear in `read()`; `hinted`
also selects default BEC scan readouts. `config` signals appear in `read_configuration()`;
`omitted` signals appear in neither. Treat changes to kinds and names as data-interface changes.
- Use the signals in `ophyd_devices/utils/bec_signals.py` for BEC live data, progress, and file
events. Reuse their message formats instead of publishing custom Redis messages.
- Add an example configuration under `ophyd_devices/configs/` for a new reusable device family.
- Give each device class a docstring with a useful first-line description. CI uses it to generate
`ophyd_devices/devices/device_list.md`; do not edit that generated file by hand.
### USER_ACCESS
- Prefer methods over properties for functionality exposed through `USER_ACCESS`.
- Give exposed methods verb-based names that describe the operation, such as `set_velocity()`
or `get_velocity()`, rather than noun-only names such as `velocity()`.
## Validation
Add regression tests for bug fixes. New device tests must cover instantiation, the relevant
protocol, and safe `stop()` behavior. For asynchronous changes, also exercise completion,
failures, and interruption while work is pending.
Use mocked EPICS and sockets for unit tests. Reuse `get_mock_scan_info` from
`ophyd_devices/tests/utils.py` and existing fixtures; use simulation devices when a working device
is needed. Keep tests independent of execution order.
Run the smallest relevant test target first, with `--random-order` when available:
```bash
ophyd_test --config ./ophyd_devices/configs/ophyd_devices_simulation.yaml
ophyd_test --config /path/to/beamline_config.yaml --connect --timeout-per-device 30
python -m pytest --random-order tests/test_psi_device_base.py
```
Reports are written to `./device_test_reports` by default. Run this before proposing a configuration
change for a real beamline.
## Testing
For a full unit test or coverage run:
```bash
python -m pytest --random-order ./tests
```
`--random-order` matches CI and is how order-dependent test pollution gets caught.
Coverage, as CI measures it:
```bash
coverage run --source=./ophyd_devices --omit=*/ophyd_devices/tests/* -m pytest --random-order ./tests
python -m pytest --random-order tests
coverage run --source=./ophyd_devices --omit='*/ophyd_devices/tests/*' \
-m pytest --random-order tests
coverage report
```
**Conventions:**
`ophyd_test` writes reports to `./device_test_reports` by default. Use `--connect` only when the
user explicitly requests hardware validation and the target is reachable. Report whether device
validation used mocks, simulation, or real hardware; include the model and firmware when known.
- Name files `test_<area>.py` and tests after behaviour — `test_positioner_reports_done_after_move()`,
not `test_positioner_3()`.
- Mock EPICS and sockets. Use `get_mock_scan_info` from `ophyd_devices/tests/utils.py` and the fixtures
in `tests/conftest.py` rather than constructing scan metadata by hand.
- Prefer building on the `sim/` devices when you need a working device in a test.
- Every new device class needs at least: it instantiates, it satisfies the relevant protocol, and its
`stop()` is safe to call.
## Running BEC Locally
## Coding Style & Naming Conventions
Full service validation needs Redis, usually at `localhost:6379`, and an environment with BEC
services installed. Unit tests normally use mocks and need no hardware.
- Python 3.11+, 4-space indentation, **100-character** line limit.
- **Black** and **isort** are the source of truth (settings in `pyproject.toml`). CI fails on any diff:
When service management is part of the requested validation, start services and open the client:
```bash
black --line-length=100 --skip-magic-trailing-comma .
isort --line-length=100 --profile=black --multi-line=3 --trailing-comma .
```
```bash
bec-server start
bec
```
- **Pylint** runs in CI and reports a score; do not introduce new warnings. Beamline-idiomatic names
(`scanID`, `RID`, `pointID`, `*_1D`, `*_2D`) are explicitly allowed via `[tool.pylint.basic]`.
- `snake_case` for modules, functions, and test files; `PascalCase` for device classes. Device class
names should read as the hardware they represent (`SimPositioner`, `PSIMotor`, `DelayGenerator645`).
Run the client in another shell. Restart the device server after changing code it has already
loaded; otherwise a running session may use stale code.
## Style And Change Hygiene
- Use Python 3.11-compatible syntax, four-space indentation, and a 100-character line limit.
- Use f-strings and `pathlib`.
- **Docstrings are not optional on device classes** — the first line is picked up by the generated
device list and is what beamline scientists read when choosing a device.
- Type-annotate new public functions and methods; document public APIs.
- Follow existing naming and docstring conventions.
- Run Black and isort on changed Python files using `pyproject.toml` configuration.
- Avoid formatting unrelated files and introducing new Pylint warnings.
## Development Environment
Use an existing suitable environment when available. To create one, use Python 3.11 or newer:
```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install -e '.[dev]'
python -c "import ophyd_devices; print(ophyd_devices.__file__)"
```
Verify imports resolve to the checkout under test. Separate worktrees need separate editable
installations; do not repoint another task's environment. The `dev` extra includes `bec-server`.
## Platform Notes
Code must run on **macOS and Linux**. Windows is not supported or tested.
Keep code portable across macOS and Linux. Windows is unsupported and untested.
## Related Repositories
## Commit And PR Notes
- [`bec`](https://github.com/bec-project/bec) — core library and services; `bec_lib` is a direct
dependency, and the device server here is driven by `bec_server`.
- [`bec_widgets`](https://github.com/bec-project/bec_widgets) — GUI toolkit that displays these devices.
- Beamline plugin repositories — beamline-specific devices that do not belong in this shared repository.
Branch from `main` for new work. Continue an existing PR on its current branch.
A device used at exactly one beamline belongs in that beamline's plugin repository. This repository is
for hardware support that is reusable across beamlines and facilities.
## Commit & Pull Request Guidelines
The manual PR template lives at `.github/PULL_REQUEST_TEMPLATE/pull_request_template.md`.
When writing a description:
- **Do not commit or push unless explicitly asked to.** Leave the working tree for the human to review.
- **Never open, update, or merge a pull request.** Submitting the change is the human contributor's
step. An agent's work ends at a reviewed working tree — or at a local commit on a branch, when a
commit was explicitly requested.
- Branch from `main` with a descriptive name such as `feat/panda-position-capture` or
`fix/undulator-timeout`.
- **Conventional Commits are mandatory** — `<type>(<scope>): <summary>`, e.g.
`fix(psi_motor): report done_moving after limit hit`. Allowed types: `build`, `chore`, `ci`, `docs`,
`feat`, `fix`, `perf`, `refactor`, `style`, `test`. `feat` triggers a minor release, `fix` and `perf` a
patch release; breaking changes need `!` or a `BREAKING CHANGE:` footer.
- Commit messages are parsed by python-semantic-release and become the published `CHANGELOG.md`. Keep
them to a single clean subject line.
- The pull request itself needs a clear description, linked issues, and test evidence, and for a new
device it must state which hardware the device was tested against — or say explicitly that it has
only been tested in simulation. Put that in your summary so whoever opens the PR can carry it over.
- Lead with the concrete problem and resulting behavior. For a bug fix, explain the trigger and
before/after result. Describe the final change for a reviewer who has not followed the work.
- Keep detail proportional to the change. Replace prompts and remove unused sections; avoid a
file-by-file recap. Put lengthy examples in a `<details>` block and label before/after output.
- Use `Closes #123` for resolved issues and `Related to #123` for partial work. Link companion PRs
and `bec_docs` updates, including any required merge or deployment order.
- Give exact test commands or manual steps and expected outcomes. Separate instructions for
reviewers from checks already performed; report results and material limitations honestly.
- Explain compatibility changes, affected consumers, defaults, migrations, and remaining limits.
Include hardware/simulation validation and configuration-check results where applicable.
- State whether documentation was updated or why it is unnecessary. Add design tradeoffs and
follow-ups only when they help assess the change. Update the description when scope changes.
Use Conventional Commit titles: `<type>(<scope>): <summary>`. Allowed types are `build`, `chore`,
`ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `style`, and `test`. Mark breaking changes with
`!` or a `BREAKING CHANGE:` footer.
For reviews, use `.github/pull_request_review_template.md`. Separate findings introduced or
exposed by the change from inherited issues and optional suggestions. Identify the reviewed
revision, give concrete evidence, and state validation limits. Return the review in chat unless
posting to GitHub is explicitly requested.
+2 -34
View File
@@ -1,37 +1,5 @@
# CLAUDE.md — `ophyd_devices`
# `ophyd_devices`
@AGENTS.md
The guidelines above are imported from [`AGENTS.md`](AGENTS.md) (single source of
truth). The points that matter most in day-to-day work:
- **Check for `AGENTS_PERSONAL.md` first.** If it exists, it extends `AGENTS.md` with
machine-specific environment setup and takes precedence over the generic venv/pip instructions there.
It is untracked and personal — never commit it, and never assume it exists.
- **Inherit from `PSIDeviceBase` / `PSIPositionerBase` / `PSIPseudoDeviceBase`**
(`ophyd_devices/interfaces/base_classes/`), never from `ophyd.Device` directly — the base classes wire
up the subscriptions, `scan_info`, and task/file handling that BEC's device server expects.
- **Import the `ophyd_devices` counterpart, never the plain `ophyd` one, wherever one exists.** The
status classes in `ophyd_devices/utils/psi_device_base_utils.py` (`StatusBase`, `Status`,
`DeviceStatus`, `MoveStatus`, `SubscriptionStatus`, `AndStatus`) subclass ophyd's to add timeout
diagnostics and `&` composition, and the module adds BEC-only `CompareStatus`, `ExceptionStatus`,
`TransitionStatus`, `TaskStatus`; BEC-publishing signals live in `ophyd_devices/utils/bec_signals.py`.
Importing from `ophyd` directly silently drops that behaviour. Plain `ophyd` imports are correct only
where there is no counterpart (`Component`, `EpicsSignal`, `Kind`, …).
- **Never block the device server.** Long work goes through `TaskHandler` and reports completion with a
`DeviceStatus`. Always implement a safe `stop()`.
- **Set `kind` deliberately** (`hinted` / `config` / `omitted`) — it decides what lands in the scan file.
Emit BEC data through `ophyd_devices/utils/bec_signals.py`, and check the device against the protocols
in `interfaces/protocols/bec_protocols.py`.
- **Docstring every device class** — the first line feeds the generated
`ophyd_devices/devices/device_list.md`, which is CI-generated and must not be hand-edited.
- **Tests**: `python -m pytest --random-order ./tests`. Mock EPICS and sockets; build on the `sim/`
devices; use `get_mock_scan_info` and the `tests/conftest.py` fixtures.
- **Validate configs** with `ophyd_test --config <file.yaml>` (add `--connect` for real hardware).
- **Format before finishing**: `black --line-length=100 --skip-magic-trailing-comma .` and
`isort --line-length=100 --profile=black --multi-line=3 --trailing-comma .`.
- **A device used at only one beamline belongs in that beamline's plugin repo**, not here.
- **Do not commit or push unless explicitly asked, and never open a pull request.** If you do commit,
write a single Conventional Commits line — it is parsed into the published changelog. Opening the PR
is the human's step; leave them the summary and test output they need for it, including whether a new
device was tested against real hardware or only in simulation.
Follow the shared repository instructions in `AGENTS.md`, including its local-overlay guidance.