# 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]
```
