# Agent Guidelines for hermes-chatto-plugin ## Tests `adapter.py` imports from `gateway.*`, which lives in the Hermes Agent rather than this repo. Without its source on the `PYTHONPATH`, collection dies with `ModuleNotFoundError: No module named 'gateway'` before a single test runs. So fetch the Hermes Agent and keep it around before running the tests: ```bash git clone https://github.com/NousResearch/hermes-agent.git /tmp/hermes PYTHONPATH=/tmp/hermes uv run --with pyyaml pytest -q ``` `gateway.config` needs `pyyaml`, which is not among our dev dependencies. If a checkout already exists somewhere, point at that path instead of cloning again. ## Typed access **Work against the declared types. Do not reach for values dynamically when a typed attribute exists.** `chattolib` ships dataclasses with `py.typed` for everything it returns — `Message`, `Asset`, `AssetUpload`, `Room`, `User`. Read their fields directly, so a wrong name is a loud `AttributeError` at the first call and a type checker can see it before that: ```python upload_id = upload.upload_id # yes upload_id = str(getattr(cast(Any, upload), "id", "")) # no ``` The second form cost us every single file upload. `AssetUpload` has no `id` field — it is called `upload_id` — but `cast(Any, ...)` disables the check and the `""` default turns the mismatch into an empty string. The result was an error message blaming the server (`CreateUpload returned no upload ID`) for a name we got wrong ourselves, and it survived in production because it never raised. So: no `cast(Any, ...)` on a value whose type is known, and no `getattr` with a fallback default to paper over a field you are not sure about — look the field up in `vendor/common/chattolib/types.py` instead. `getattr` is fine for genuinely optional or duck-typed things (a handler that may not be set, a payload from an untyped source), not for dodging a type. Tests should exercise real library types too, not mocks shaped like them. The upload bug was invisible because the tests replaced `_upload_asset` wholesale with an `AsyncMock`, so no test ever touched a real `AssetUpload`. ## Capabilities **When you extend what the adapter can do, keep the startup banner current.** `_capabilities()` in `adapter.py` logs what the plugin supports at registration, so starting it up tells you what is available instead of making you read the source. Whether a capability counts is derived from real overrides: it only counts when `ChattoAdapter` replaces the `BasePlatformAdapter` method. The label for it, however, lives in `_CAPABILITY_LABELS` and is *not* discovered automatically. So when you add an overridden `BasePlatformAdapter` method: 1. Add an entry to `_CAPABILITY_LABELS` (method name → wording that means something to someone who has not read the code). Skip this and the plugin can do the thing but says so nowhere. 2. Check whether the `platform_hint` in `register()` needs to mention it. That hint goes into the system prompt and is the only way the model learns what the channel can do — without it the agent falls back to shelling out. 3. Add a test in `TestRegistration` if the capability is user-visible. The reverse holds too: drop a method and its line disappears from the log on its own, but its `_CAPABILITY_LABELS` entry should go with it. ## Shell Scripts **Always run `shellcheck` after editing any shell scripts.** ```bash shellcheck path/to/script.sh ``` Or validate all shell scripts in the project: ```bash find . -name "*.sh" -exec shellcheck {} \; ``` ### Configuration Project-specific shellcheck rules are defined in `.shellcheckrc`. Default severity is `error` to catch all issues. ### Why - Prevents syntax errors and common pitfalls (e.g., missing quotes, unsafe variable expansions) - Ensures portability across different shell environments - Maintains code quality and security standards ### Integration Consider adding a pre-commit hook for automatic validation: ```yaml # .pre-commit-config.yaml repos: - repo: https://github.com/koalaman/shellcheck-precommit rev: v0.9.0 hooks: - id: shellcheck args: [--severity=error] ```