diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 857d5a0..a081a1b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -16,6 +16,11 @@ body: and host facts, and every `hermes talk doctor` outcome — no values, no logs, no prompts, no transcripts, no audio. Nothing else in this template is worth more than that one file. + + If the session connected fine before and a provider now refuses, + drops events, or changed shape under us, the + [provider compatibility report](https://github.com/TheSmokeDev/hermes-talk/issues/new?template=provider_compatibility_report.yml) + is the better form — it asks for exactly the events that pin the break. - type: textarea id: diagnostics_bundle attributes: @@ -33,6 +38,8 @@ body: - /talk inside a session - Discord voice channel - Dashboard tab + - hermes realtime --provider hermes-talk/… (the core contract lane) + - hermes talk check / doctor / diagnostics / setup validations: required: true - type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index fba36b8..13f5991 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,11 @@ blank_issues_enabled: true contact_links: + - name: Security vulnerability — report privately + url: https://github.com/TheSmokeDev/hermes-talk/security/advisories/new + about: Credential leakage, auth-store writes, tool-authority bypass, redaction failures, supply chain. Never in a public issue — SECURITY.md has the classes and the 72-hour acknowledgement promise. + - name: Questions, ideas, design sketches — Discussions + url: https://github.com/TheSmokeDev/hermes-talk/discussions + about: Not sure it is a bug, or want to talk through a provider, surface, or tool before writing it? Start here. - name: Make a diagnostics bundle first (hermes talk diagnostics --bundle) url: https://github.com/TheSmokeDev/hermes-talk/blob/main/docs/OPERATING.md#7-hermes-talk-diagnostics--the-redacted-support-bundle about: One redacted file — versions, variable names, device/host facts, doctor outcomes — turns "it doesn't work" into something reproducible. @@ -9,3 +15,6 @@ contact_links: - name: Operating manual — install, upgrade, verify, every knob, troubleshooting url: https://github.com/TheSmokeDev/hermes-talk/blob/main/docs/OPERATING.md about: Most "is this broken?" questions are answered by the verify runbook. + - name: Contributing guide — priorities, paths, setup, the merge bar + url: https://github.com/TheSmokeDev/hermes-talk/blob/main/CONTRIBUTING.md + about: Read before opening a PR. First response within 24 hours. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d642c26 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,81 @@ +name: Feature request +description: Something the voice should be able to do, hear, or reach that it cannot today. +labels: [enhancement] +body: + - type: markdown + attributes: + value: | + The problem first, the proposal second. The ranked list of what we + take first is in + [CONTRIBUTING.md](https://github.com/TheSmokeDev/hermes-talk/blob/main/CONTRIBUTING.md#what-we-want-most); + a new provider or a new surface has a mapped path there. Check the + [open issues](https://github.com/TheSmokeDev/hermes-talk/issues) and + the [Current boundaries](https://github.com/TheSmokeDev/hermes-talk#current-boundaries) + section of the README first — several gaps are already named and + tracked. + - type: textarea + id: problem + attributes: + label: What you are trying to do that you cannot today + description: The situation on the call, in your words. What did you say or want to say, and what happened instead? + placeholder: | + On a Discord call I want to hand a task to an agent and have it + answer in the channel's text chat instead of out loud, because… + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed behaviour + description: What the voice should do, say, or reach. If a spoken sentence is involved, write the sentence — the wording is part of the design here. + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - A new realtime provider (behind talk_realtime.py) + - A new surface (another room the session can be in) + - A new or changed talk tool + - Session / relay behaviour (turn-taking, barge-in, announcements) + - Delegation, steering, approvals (background work) + - Auth / credentials + - Dashboard tab + - doctor / check / diagnostics / setup + - Docs + - Other + validations: + required: true + - type: checkboxes + id: surfaces + attributes: + label: Surfaces it applies to + options: + - label: Terminal (hermes talk) + - label: /talk inside a session + - label: Discord voice channel + - label: Dashboard tab + - label: hermes realtime (core contract lane) + - type: checkboxes + id: providers + attributes: + label: Providers it applies to + options: + - label: OpenAI Realtime + - label: xAI Grok + - label: Gemini Live + - label: All of them / provider-neutral + - type: textarea + id: alternatives + attributes: + label: Alternatives you considered + description: Including "do it in Hermes core instead" — some things belong upstream, and saying so early saves a PR. + validations: + required: false + - type: checkboxes + id: contribution + attributes: + label: Contribution + options: + - label: I would like to build this myself and open the PR diff --git a/.github/ISSUE_TEMPLATE/provider_compatibility_report.yml b/.github/ISSUE_TEMPLATE/provider_compatibility_report.yml new file mode 100644 index 0000000..530621b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/provider_compatibility_report.yml @@ -0,0 +1,111 @@ +name: Provider compatibility report +description: A live provider works — or stopped working — on your setup. A pass is as useful as a fail. +title: "[provider] / on hermes-talk : PASS | FAIL" +labels: [provider] +body: + - type: markdown + attributes: + value: | + The test suite is offline by design, so a provider changing its wire + under us reaches us through you. This form collects the evidence in + a fixed shape. The how-to, and the table it mirrors, is + [docs/PROVIDER-RECEIPT.md](https://github.com/TheSmokeDev/hermes-talk/blob/main/docs/PROVIDER-RECEIPT.md). + + **Do not paste audio, transcripts, prompts, task results, or any + credential.** `hermes talk check --json` and `hermes talk doctor --json` + redact by construction and are all the detail a report needs. + - type: dropdown + id: verdict + attributes: + label: Verdict + options: + - PASS — a full turn worked (connect, SessionReady, a spoken reply, a tool round-trip) + - PARTIAL — connected, but one or more events below failed + - FAIL — the session never reached SessionReady + validations: + required: true + - type: dropdown + id: provider + attributes: + label: Provider + options: + - openai + - grok + - gemini + validations: + required: true + - type: input + id: model + attributes: + label: Model + description: As `hermes talk doctor` reports it (the `model` check), e.g. `gpt-realtime-2.1`, `grok-voice-latest`, `gemini-3.1-flash-live-preview`. + validations: + required: true + - type: dropdown + id: auth_lane + attributes: + label: Credential lane + description: Which lane the doctor's `auth` check names — never the credential itself. + options: + - Codex CLI OAuth (ChatGPT subscription) + - xAI OAuth (SuperGrok / X Premium+) + - API key (TALK_*_API_KEY or the shared variable) + - Not sure — the doctor `auth` check is pasted below + validations: + required: true + - type: dropdown + id: surface + attributes: + label: Surface + options: + - Terminal (hermes talk) + - /talk inside a session + - Discord voice channel + - Dashboard tab + - hermes realtime --provider hermes-talk/… (the core contract lane) + - hermes talk check only + validations: + required: true + - type: input + id: versions + attributes: + label: Versions + description: "hermes-talk version, Hermes host version, Python, OS — e.g. `hermes-talk 0.16.0 · hermes 0.21.0 · Python 3.12.6 · Windows 11`. `hermes plugins list` and `hermes --version` have the first two." + validations: + required: true + - type: checkboxes + id: events + attributes: + label: Events observed + description: Tick what you saw happen. Leave unticked what did not — that is the finding. + options: + - label: Connected (the session opened; `check` reports `session_ready`) + - label: SessionReady (the provider acknowledged the session setup) + - label: SpeechStarted (the provider heard you start talking) + - label: A spoken reply came back (ResponseStarted → OutputAudio → ResponseFinished) + - label: FunctionCall round-trip (say "status report" — the tool ran and the reply used its result) + - label: Barge-in (you spoke over the reply and playback cut) + - label: A clean hang-up (Ctrl+C / leave ended the session without an error) + - type: textarea + id: check_json + attributes: + label: hermes talk check --json (or doctor --json) + description: Paste the report. `check` proves the live path with per-step pass/fail; `doctor --json` if the session cannot be reached at all. Both carry no tokens and no paths — still read what you paste. + render: json + validations: + required: true + - type: textarea + id: wire_error + attributes: + label: The provider's error, verbatim (if any) + description: The error TYPE and MESSAGE the provider or the plugin printed — e.g. a close code, an HTTP status, a `ProviderFailure` reason. Redact anything that looks like a key or a session id. + render: text + validations: + required: false + - type: textarea + id: notes + attributes: + label: Anything else + description: "Last version it worked on, if you know it. Regional or account-tier facts that might matter (free-tier key, enterprise account, a proxy in the path). Names of knobs you have set — never their values." + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e4c444e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,52 @@ + + +## What and why + + + +Fixes # + +## How to test + + + +```bash + +``` + +## Platforms + + + +- [ ] Windows +- [ ] Linux +- [ ] macOS + +## Live receipt (required when a provider lane, credential resolution, or the delegation path is touched) + + + +```json + +``` + +## Checklist + +- [ ] One logical change; tests ride with it, not behind it +- [ ] `pytest -q` and `ruff check .` pass locally on the pinned ruff (`pip install -e ".[dev]"` or `uv run --extra dev …`) +- [ ] Commits follow Conventional Commits with a hermes-talk scope (`fix(audio): …`, `feat(realtime): …`, `docs: …`) +- [ ] No auth-store writes outside the documented Codex refresh; tokens reach only the provider's own host +- [ ] Nothing secret in logs, receipts, spoken sentences, or test fixtures +- [ ] Every spoken sentence I added or changed claims only what an artifact proves +- [ ] Docs updated where behaviour changed (README, `docs/OPERATING.md`, `docs/VOICE-COMMANDS.md`) — or N/A +- [ ] `CHANGELOG.md` `[Unreleased]` entry added (name yourself — merged work is credited) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7540802..3ed187a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,35 @@ named rather than smoothed. ## [Unreleased] ### Added +- The contributor experience, written down. `CONTRIBUTING.md` now ranks + what we take first (bug fixes on live lanes, then provider and host + compatibility, security hardening, cross-platform, new providers behind + the contract, new surfaces, docs), maps the common paths — a new realtime + provider, a new surface, a new talk tool, a fix, a docs change — to the + exact files each one touches and ships with, and states the merge bar + (live-verified before merge on a provider wire; offline tests and fake + sessions otherwise; no auth-store writes; tokens only to the provider's + own host; nothing secret in logs or receipts), the branch and + Conventional-Commit scope conventions derived from the history, a + first-response-within-24-hours review promise, and how merged work is + credited. A pull request template carries the same contract (what and + why, how to test, platforms, a `check --json` receipt when a lane is + touched, `Fixes #N`). Two new issue forms — a feature request that asks + for the problem before the proposal and which surfaces and providers it + reaches, and a **provider compatibility report** that collects a + provider's PASS/PARTIAL/FAIL as a fixed table (provider, model, + credential lane, versions, which of `SessionReady` / `SpeechStarted` / + `FunctionCall` round-trip / barge-in were observed, the `check --json` + report, the wire error verbatim) with no audio, transcripts, or secrets; + `docs/PROVIDER-RECEIPT.md` is the how-to behind it and says how + maintainers act on each verdict. The bug form gained the core-contract + lane and the check/doctor/diagnostics commands as places a bug can + happen, and the issue chooser now links private security reporting and + Discussions. Labels `provider`, `surface`, `security`, and `docs` join + `good first issue` / `help wanted`. The README credits the people who + showed up: @kvnloo, @TheAngryPit, @webdevtodayjason. Ported idea from + bielcarpi/hermes-live-voice's provider-compatibility receipt (MIT) — idea + only, no text. - `SECURITY.md`: supported versions (the latest PyPI release and `main`), private reporting through GitHub security advisories (private vulnerability reporting is enabled on the repository), a 72-hour acknowledgement target, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f257d29..3c21c68 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,72 @@ # Contributing Small plugin, strong opinions. This page is everything you need to get a -change from clone to green. +change from clone to merged — what we want most, where each kind of change +starts, how to set up, and what a PR has to carry to land. + +The promise on our side: **a first response within 24 hours, usually the +same day.** Merged work is credited by name in [CHANGELOG.md](CHANGELOG.md) +and in the README's Contributors line. Not sure where a change belongs? +Open a [Discussion](https://github.com/TheSmokeDev/hermes-talk/discussions) +or an issue first — a ten-line sketch saves a two-day PR. + +## What we want most + +Ranked. When two contributions compete for review time, the higher one +goes first. + +1. **Bug fixes on live lanes.** A session that dies, a barge-in that does + not cut playback, a tool call that never lands, a spoken receipt that + claims more than an artifact proves. Reproduce against `main`, name the + line where it manifests, fix the class (sibling adapters and surfaces + included), ship the regression test in the same PR. +2. **Provider and host compatibility.** The suite is offline, so a wire + change at OpenAI, xAI, or Google — or a Hermes host API moving under us — + reaches us as a broken user. A filled-in + [provider receipt](docs/PROVIDER-RECEIPT.md) is a contribution on its + own; the fix that follows it is a better one. +3. **Security hardening**: the auth store, token routing, redaction. The + invariants are spelled out in [SECURITY.md](SECURITY.md): a raw + credential is spent on exactly one upstream endpoint and appears nowhere + else; nothing but the documented Codex refresh writes a credential + store; `doctor`, `check`, and `diagnostics` redact by construction. + Report a *break* privately (see SECURITY.md); a *hardening* PR is + welcome in the open. +4. **Cross-platform.** CI is ubuntu + windows × Python 3.11–3.13; macOS is + covered by users, not runners. Audio devices, paths, file permissions, + process handling, encodings — anything that behaves differently across + the three. +5. **New realtime providers behind the contract.** A fourth lane on + [`talk_realtime.py`](talk_realtime.py), self-hosted or hosted. The + path is below. +6. **New surfaces.** Another room the same session can be in — the Discord + channel and the dashboard tab are the two that exist. +7. **Docs.** A sentence that stopped being true, a knob with no row in the + [operating manual](docs/OPERATING.md), a voice command with no card in + [docs/VOICE-COMMANDS.md](docs/VOICE-COMMANDS.md). + +Not wanted, even when well built: a new dependency for something the +standard library does, a knob cached at import time, a `TALK_*` variable +that is inferred instead of set, a spoken sentence that outruns its +receipt, and a mock that can make `hermes talk check` go green. + +## Common contribution paths + +| I want to… | Start here | It touches | It ships with | +|---|---|---|---| +| **Report that a provider works, or broke** | [docs/PROVIDER-RECEIPT.md](docs/PROVIDER-RECEIPT.md) | nothing — it is a filled-in table | the [provider compatibility issue form](.github/ISSUE_TEMPLATE/provider_compatibility_report.yml) | +| **Fix a bug** | the module in the [layout table](#layout--flat-modules-on-purpose) that owns the symptom; [`tests/fake_realtime.py`](tests/fake_realtime.py) to script the failing transcript | the owning module, its siblings if the class repeats | a regression test that fails on `main` | +| **Add a realtime provider** | the neutral contract in [`talk_realtime.py`](talk_realtime.py) (`RealtimeSession`, the event and command types); [`talk_gemini_realtime.py`](talk_gemini_realtime.py) as the freshest complete adapter; its [spec](docs/plans/gemini-provider/01-spec.md) as the shape of a good pre-implementation write-up | `talk__realtime.py`; `TALK_PROVIDERS` and the model/voice knobs in [`talk_config.py`](talk_config.py); `resolve_provider_lane()` and `_realtime_session()` in [`talk_cli.py`](talk_cli.py); the `provider`/`auth`/`model`/`voice` checks in [`talk_doctor.py`](talk_doctor.py); a core lane in [`talk_core_provider.py`](talk_core_provider.py) with its capabilities declared, never assumed; `py-modules` and `known-first-party` in [`pyproject.toml`](pyproject.toml); [`.env.example`](.env.example) | `tests/test__realtime.py` against scripted server events, new cases in [`tests/test_doctor.py`](tests/test_doctor.py) and [`tests/test_core_provider.py`](tests/test_core_provider.py); README Providers row, an [operating manual](docs/OPERATING.md) knob row, a CHANGELOG entry; a [receipt](docs/PROVIDER-RECEIPT.md) from a live session in the PR | +| **Add a surface** | the audio-device shape in [`talk_audio.py`](talk_audio.py) (`DuplexAudio`: `start`, `stop`, `read_input_chunk`, `queue_playback`, `drain_playback`, `playback_pending`, `played_ms`); [`talk_discord.py`](talk_discord.py) is the second implementation of the same methods in a different room | a new device module; wiring in [`talk_cli.py`](talk_cli.py); if speakers other than the operator can be in the room, authority in [`talk_operator_auth.py`](talk_operator_auth.py), fail-closed | tests against a fake device (see [`tests/test_discord.py`](tests/test_discord.py)); a README Surfaces row; the spoken receipts the surface commits to | +| **Add a talk tool** | [`talk_tools.py`](talk_tools.py): a schema dict, a `_handle_` returning bounded plain text, an entry in `_HANDLERS`, the advertised list in `default_talk_tools()` | one of `READ_ONLY_TALK_TOOLS` / `MUTATING_TALK_TOOLS` in [`talk_operator_auth.py`](talk_operator_auth.py) — unclassified means unreachable, on purpose | [`tests/test_tools.py`](tests/test_tools.py) and [`tests/test_operator_auth.py`](tests/test_operator_auth.py); a card row in [docs/VOICE-COMMANDS.md](docs/VOICE-COMMANDS.md) stating what the reply commits to | +| **Fix or extend the docs** | [README.md](README.md), [docs/OPERATING.md](docs/OPERATING.md), [docs/VOICE-COMMANDS.md](docs/VOICE-COMMANDS.md) | prose only | nothing else — but every relative link must resolve, and a claim about behaviour must match the code on `main` | + +The tool contract that makes a live call survivable, from +[`talk_tools.py`](talk_tools.py): an unknown tool name raises +`TalkToolError` (a client bug); a known tool that fails *returns* the +failure as text, so the model says what broke instead of the session dying +on a stack trace. Outputs are plain text the model will read aloud — +nothing formatted for a screen. ## Setup @@ -16,13 +81,27 @@ project-local `.venv`: ```bash uv sync --extra dev # --extra audio too, for a real mic -uv run pytest -q -uv run ruff check . +uv run --extra dev pytest -q +uv run --extra dev ruff check . ``` Either way you get the pinned `ruff==0.16.5` from the dev extra — the pin is load-bearing, see below. +**Why `--extra dev` on every `uv run`:** pytest and the pinned ruff live in +the `dev` extra, not in the dependencies. `uv run` only guarantees the base +dependencies are present, so on a fresh clone — or after anything that +re-syncs the environment — a bare `uv run pytest` can find no pytest at +all. Naming the extra on the run is the form that works regardless of what +you synced before. + +You need **Python 3.11–3.13** and **git**. `node` on `PATH` is needed for +the two dashboard-transport tests in +[`tests/test_dashboard_js.py`](tests/test_dashboard_js.py), which drive the +hand-authored bundle in `dashboard/dist/` through `node -e`; GitHub's +runners have it. A Hermes install is **not** needed for the suite — only +for the live proof. + ## Tests and lint ```bash @@ -49,6 +128,27 @@ staying inert. CI never sees it (no Hermes there). A project-local venv side-steps it; until the tests are hermetic, those twelve are the known baseline on such a box and anything else red is yours. +### The live proof — `hermes talk check` + +Green tests prove the policy; they cannot prove a wire. Anything that +touches a provider adapter, credential resolution, or the delegation path +also needs one live pass on a real Hermes install: + +```bash +hermes talk check # doctor + one real provider turn + one bounded Hermes run +hermes talk check --json # the same, as a report you can paste into the PR +hermes talk check --provider grok # a lane other than TALK_PROVIDER, this process only +``` + +To run **your branch** live: push it to your fork, then install that exact +commit over the released plugin and restart the gateway — +`hermes plugins install --force --ref <40-char sha> /hermes-talk --enable` +(a running gateway keeps executing the old code until it restarts; the +[operating manual](docs/OPERATING.md#upgrade) names both traps). `check` is +not read-only: it spends one short provider turn and one short agent run +and changes nothing else. Its report carries no tokens and no paths, which +is what makes it safe to paste. + ## Layout — flat modules, on purpose Top-level `talk_*` modules, no package nesting. Hermes loads plugins by @@ -64,6 +164,8 @@ fails the release if a declared module is missing from the wheel. | `talk_auth.py` | The three credential lanes, resolved fail-closed | | `talk_grok_auth.py` | Grok (xAI) credential resolution — subscription login or key, fail-closed | | `talk_doctor.py` | Read-only detect/decide/verify diagnostics and renderers | +| `talk_check.py` | `hermes talk check` — the live proof: doctor, one provider turn, one bounded Hermes run | +| `talk_diagnostics.py` | `hermes talk diagnostics` — the redacted, default-deny support bundle | | `talk_setup.py` | Interactive missing-decision prompts, per-write confirmation, and post-write doctor verification | | `talk_realtime.py` | Typed provider-neutral session setup, events, commands, lifecycle states, and adapter protocol | | `talk_openai_realtime.py` | OpenAI ephemeral-mint/WebSocket adapter and neutral-to-wire translation | @@ -93,6 +195,51 @@ fails the release if a declared module is missing from the wheel. | `talk_vault.py` | Vault recall — the durable-notes lookup a voice session can make | | `talk_providers.py` | Optional REST TTS/STT providers | +The dashboard tab lives beside them: `dashboard/plugin_api.py` (routes), +`dashboard/dist/index.js` (the hand-authored browser transport — source, +not a build artifact), `dashboard/manifest.json`. + +## Code style + +- **Ruff decides.** `E F I UP B SIM RUF BLE`, line length 100, target + `py311` — all in `pyproject.toml`. Run the pinned version; the rule set + and the version are pinned separately, and each pin alone still lets a + ruff release turn the build red. Bump both together, in their own PR. +- **Knobs resolve at call time.** Every `TALK_*` setting is read when it + is needed, in `talk_config.py`, never bound at import or cached in a + module global. Tests rely on flipping the environment between calls. +- **Fail closed, never infer.** `TALK_PROVIDER` names the lane; nothing + guesses it from which keys happen to exist. The same rule for speaker + authority, tool classification, and every `doctor` decision. +- **A credential is spent on exactly one endpoint.** It goes in the header + or URL that endpoint requires and nowhere else — not a log line, not a + receipt, not a spoken sentence, not a test fixture. Anything printed + passes through `talk_doctor.SECRET_PATTERNS` or `talk_core_provider.redact`. +- **A spoken sentence claims only what an artifact proves.** "queued" is a + queue write; "landed" follows a delivery artifact. Receipt states are + contracts, and a wrong sentence is a real bug. +- **Broad `except` clauses are house style at the voice boundary** — a live + call must not die on a stack trace — and each one states its reason in + a comment. Don't "fix" them; do justify new ones. Whether the reason + also needs a `noqa: BLE001` depends on the pinned ruff and, surprisingly, + on what the module calls its logger: since 0.16 ruff drops BLE001 when + the handler logs the exception through a name it treats as a logger + (`logger` yes, `_log` no), and a directive it no longer needs fails + `RUF100`. Let `ruff check .` decide rather than copying a neighbouring + handler. +- **Cross-platform, always.** Open files with `encoding="utf-8"`; build + paths with `pathlib`; guard POSIX-only process calls behind + `sys.platform`; treat an audio device as optional (the `[audio]` extra + is not installed on CI). Windows is a first-class runner here, not a + best-effort port. +- **One version surface.** Bump `pyproject.toml`; `plugin.yaml` and + `dashboard/manifest.json` must match it and + [`tests/test_repository_hygiene.py`](tests/test_repository_hygiene.py) + fails the suite when they drift. +- **Comments explain intent, trade-offs, and API quirks** — not what the + next line obviously does. The codebase leans on them; keep the density, + not the noise. + ## How changes ship here Every release goes through adversarial review before it tags: the suite @@ -107,20 +254,127 @@ the fix is usually the sentence, sometimes the code, never the standard. ## Pull requests +### Branch names + +``` +fix/description # bug fixes +feat/description # new capability +docs/description # documentation +test/description # tests only +ci/description # workflows, pins, release plumbing +chore/description # dependency bumps, housekeeping +``` + +### Commit messages + +[Conventional Commits](https://www.conventionalcommits.org/): +`(): `, imperative, lower-case, no trailing +period. Types are the branch prefixes above plus `perf` and `refactor`. +The scope is the area the change lands in — these are the ones the +history already uses: + +| Scope | Covers | +|---|---| +| `audio` | `talk_audio.py` — devices, echo gate, AEC | +| `auth` | `talk_auth.py`, `talk_grok_auth.py`, credential lanes | +| `bridge` | the capability bridge and approval bridge — `talk_approvals.py`, `talk_host.py`, `talk_apiserver.py` | +| `catalog` | `talk_capabilities.py` | +| `cli` | `talk_cli.py`, the `hermes talk` command surface | +| `core` | the Hermes core contract lanes — `talk_core_provider.py`, `talk_core_realtime.py`, `talk_core_session.py` | +| `discord` | `talk_discord.py` | +| `grok`, `gemini`, `openai` | one provider adapter | +| `identity` | `talk_identity.py` | +| `lifecycle` | `talk_lifecycle.py` | +| `memory` | `talk_transcript.py`, `talk_vault.py` — what a session remembers | +| `realtime` | `talk_realtime.py`, the neutral contract itself | +| `setup` | `talk_setup.py` | +| `steer` | `talk_steer.py`, the receipt ledger | +| `talk` | the session as a whole — `__init__.py`, relay, several surfaces at once | +| `tools` | `talk_tools.py`, `talk_operator_auth.py` | + +A module with no scope yet takes its `talk_` suffix (`fix(check): …`, +`fix(relay): …`). Repo-wide changes go without a scope: `ci: …`, +`docs: …`, `test: …`, `chore: …`. + +``` +fix(audio): preserve speech through echo cancellation +feat(realtime): report server-cancelled tool calls as a typed event +docs: README leads with what works today +``` + +### What the PR carries + +The [pull request template](.github/PULL_REQUEST_TEMPLATE.md) asks for +exactly this: + +- **What and why** — the symptom or the gap, and why this approach. If a + spoken sentence changes, quote before and after. +- **How to test** — the command and the expected output, so a reviewer + reproduces it without asking. +- **Platforms** — where you ran it (`Windows 11`, `Ubuntu 24.04`, + `macOS 15`, …). CI covers ubuntu + windows; say so if that is all. +- **The live receipt, when a lane is touched** — a + `hermes talk check --json` excerpt (or `hermes talk doctor --json` when + the change cannot reach a live turn) with the provider, host, and plugin + versions and the step statuses. Both reports redact by construction. +- **`Fixes #N`** when there is an issue. GitHub closes it on merge. + +- One logical change per PR; tests ride with the change, not behind it. - Keep the suite green on ubuntu + windows × 3.11–3.13 (CI runs exactly `pytest -q` and `ruff check .`). CodeQL and dependency review run on the PR as well; every third-party action in `.github/workflows/` is pinned to a commit SHA with its version in a comment — keep it that way when you bump one. -- One logical change per PR; tests ride with the change, not behind it. -- Broad `except` clauses are house style at the voice boundary (a live - call must not die on a stack trace) — each states its reason. Don't - "fix" them; do justify new ones. Whether the reason also needs a - `noqa: BLE001` depends on the pinned ruff and, surprisingly, on what the - module calls its logger: since 0.16 ruff drops BLE001 when the handler - logs the exception through a name it treats as a logger (`logger` yes, - `_log` no), and a directive it no longer needs fails `RUF100`. Let - `ruff check .` decide rather than copying a neighbouring handler. -- Bug reports: the issue form asks for your `talk_status` output — that - one paste answers version, auth lane, agent lane, and audio in one go. - `hermes talk doctor --json` is the fuller receipt. + +### The merge bar + +- **Live-verified before merge for anything on a provider wire** — a + `check` receipt from the author, and a second one from a maintainer when + the lane is one they can reach. +- **Offline tests and fake sessions for everything else** — a change that + cannot be exercised against a scripted transcript or a stub host is not + done yet. +- **No auth-store writes** beyond the one documented Codex refresh. +- **Tokens go only to the provider's own host** — a credential resolved for + one lane never reaches another lane's endpoint, a proxy, or a log. +- **Nothing secret in logs or receipts** — `doctor`, `check`, and + `diagnostics` output, spoken sentences, and test fixtures included. +- **The wording matches the artifact.** A review may ask you to change a + sentence rather than the code. + +### Review and credit + +First response within 24 hours, typically the same day. Review is +adversarial by design (see above): expect at least one round that tries +to refute the change, and expect it to be about the change, not about +you. If a round stalls on our side, ping the thread — that is a fair ask. + +When it merges, the CHANGELOG entry names you, and the README's +Contributors line credits what you did. Substantial external work is +merged so that authorship survives in the git history — we do not +reimplement a good PR to avoid crediting it. + +## Reporting issues + +- **A bug**: the [bug form](.github/ISSUE_TEMPLATE/bug_report.yml) asks for + `hermes talk diagnostics --bundle` first — one redacted file that answers + version, auth lane, agent lane, audio, and every doctor outcome in one + paste. In a session, "status report" (the `talk_status` tool) gives the + short form; `hermes talk doctor --json` is the fuller receipt. +- **A provider that works or broke**: the + [provider compatibility form](.github/ISSUE_TEMPLATE/provider_compatibility_report.yml), + filled from [docs/PROVIDER-RECEIPT.md](docs/PROVIDER-RECEIPT.md). +- **A feature**: the [feature form](.github/ISSUE_TEMPLATE/feature_request.yml) + — the problem first, the proposal second, and which surfaces and + providers it applies to. +- **A vulnerability**: privately, per [SECURITY.md](SECURITY.md). Never in + a public issue, never with a credential, transcript, or audio attached. + +Issues tagged `good first issue` are scoped so that the fix and its test +fit in one sitting; `help wanted` marks work we would merge but are not +building ourselves right now. + +## License + +By contributing you agree that your contribution is licensed under the +[MIT License](LICENSE), like the rest of the repository. diff --git a/README.md b/README.md index 0448c89..7fb1080 100644 --- a/README.md +++ b/README.md @@ -814,15 +814,35 @@ voice transport. ## Contributing -Clone, `pip install -e ".[dev]"` (or `uv sync --extra dev`), `pytest -q`, -`ruff check .` — the whole thing is in [CONTRIBUTING.md](CONTRIBUTING.md), -including the one test trap on a box that has Hermes installed. +`uv sync --extra dev` (or `pip install -e ".[dev]"`), `pytest -q`, `ruff check .` +— offline, no keys, seconds. Priorities, the path for each kind of change +(a provider, a surface, a tool, a fix), the merge bar, and the one test trap +on a box that has Hermes installed: [CONTRIBUTING.md](CONTRIBUTING.md). +First response within 24 hours; +[`good first issue`](https://github.com/TheSmokeDev/hermes-talk/issues?q=is%3Aopen+label%3A%22good+first+issue%22) +fits in one sitting; a provider that works or broke for you is a +contribution too ([docs/PROVIDER-RECEIPT.md](docs/PROVIDER-RECEIPT.md)). Contributors adapting The Homie's v1.7.0 capability-plugin lessons to Hermes should use the [capability-kernel port plan](docs/CAPABILITY-KERNEL-PORT.md). It maps the reusable safety and lifecycle contracts onto Hermes-owned APIs; it does not claim that hot lifecycle support already exists here. +### Contributors + +[@kvnloo](https://github.com/kvnloo) — PulseAudio WebRTC echo cancellation +on Linux, and the fix that stopped quiet words being clipped during +playback ([#81](https://github.com/TheSmokeDev/hermes-talk/pull/81)); +semantic turn-detection controls across the three lanes +([#107](https://github.com/TheSmokeDev/hermes-talk/pull/107), in review). +[@TheAngryPit](https://github.com/TheAngryPit) — a renderer-owned Realtime +transport for the Hermes desktop app that keeps core as the single chat +authority ([#80](https://github.com/TheSmokeDev/hermes-talk/pull/80), in +review). [@webdevtodayjason](https://github.com/webdevtodayjason) — +field-tested feedback from a second live consumer on the upstream +`RealtimeVoiceProvider` contract these lanes register on +([hermes-agent#81404](https://github.com/NousResearch/hermes-agent/pull/81404)). + ## License MIT diff --git a/docs/PROVIDER-RECEIPT.md b/docs/PROVIDER-RECEIPT.md new file mode 100644 index 0000000..23ed888 --- /dev/null +++ b/docs/PROVIDER-RECEIPT.md @@ -0,0 +1,106 @@ +# Provider compatibility receipt + +The test suite is offline by design: every realtime session in `tests/` runs +against a scripted transcript, never a socket. That is what keeps CI free of +secrets and flakes — and it means a provider changing its wire under us (a +renamed event, a new close code, a model retired, an auth flow tightened) +reaches us through a user, not a red build. This page is how that user +turns "it stopped working" into evidence we can act on in one pass. A +**pass** is worth filing too: it tells the next person which provider, +model, host, and plugin versions are known to work together right now. + +File it through the +[provider compatibility form](https://github.com/TheSmokeDev/hermes-talk/issues/new?template=provider_compatibility_report.yml). +The form's fields are the table below. + +## What a receipt is not + +No audio. No transcripts. No prompts or task results. No credential, no +session id, no filesystem path. `hermes talk check --json` and +`hermes talk doctor --json` are built to carry none of those, which is why +they are the body of the report — still read what you paste. If you are +unsure whether a value is a secret, leave it out and say so; a report with +a gap is fixable, a leaked key is not. + +## Producing it — about two minutes + +1. **The mechanical half.** + + ```bash + hermes talk check --json # the configured lane + hermes talk check --json --provider grok # another live lane, this process only + ``` + + `check` runs the doctor checks, opens a **real** session on the provider + through the same adapter and credential path the voice uses (connect → + `SessionReady` → one text turn → `ResponseFinished`), then hands one + bounded task to a real Hermes agent. Each step reports `pass`, `fail`, + or `skip` with its duration. The `provider_session` step's details carry + `auth_source`, `session_ready`, `response_finished`, `audio_bytes`, and + `transcript_chars` — counts, never content. If the session cannot be + reached at all, paste `hermes talk doctor --json` instead; it is + read-only and names the check that refused. + +2. **The human half — one short call.** Start the session on the surface + you use (`hermes talk`, `/talk`, `/talk join`, the dashboard tab) and: + + | Do | Tick if | Which event it proves | + |---|---|---| + | Say anything | the reply starts | `SpeechStarted` → `ResponseStarted` → `OutputAudio` | + | Wait for the reply to finish | it finishes cleanly | `ResponseFinished` | + | Say "status report" | the reply quotes the version, auth lane, agent lane, or audio state | `FunctionCall` round-trip (the `talk_status` tool ran and its result came back through the model) | + | Start talking over a reply | playback cuts | barge-in (`CancelResponse` / `TruncateOutput`; on Gemini Live there is no client-side cancel on the wire, so the plugin drops playback locally — tick it if playback stopped) | + | Hang up (Ctrl+C, `/talk leave`, close the tab) | no error is printed or spoken | `SessionTerminated` | + + Something did not happen? Leave it unticked. The unticked row is the + finding. + +3. **The error, verbatim, if there was one** — the type and the message the + provider or the plugin printed (a WebSocket close code, an HTTP status, + a `ProviderFailure` reason). Redact anything that looks like a key or a + session id. + +## The table + +| Field | Where it comes from | Example | +|---|---|---| +| Verdict | your call: PASS / PARTIAL / FAIL | `PARTIAL` | +| Provider | `TALK_PROVIDER`, or the doctor's `provider` check | `grok` | +| Model | the doctor's `model` check | `grok-voice-latest` | +| Credential lane | the doctor's `auth` check — the lane, never the credential | `xAI OAuth` | +| Surface | where you ran the call | `Discord voice channel` | +| hermes-talk version | `hermes plugins list` | `0.16.0` | +| Hermes host version | `hermes --version` | `0.21.0` | +| Python / OS | `python --version`, your OS | `3.12.6 / Windows 11` | +| Events observed | the checklist above | `SessionReady, SpeechStarted, ResponseFinished` — no `FunctionCall` | +| `check --json` (or `doctor --json`) | the command's output | the report, pasted whole | +| Provider error, verbatim | the console or the spoken failure | `ProviderFailure: close code 1008 policy violation` | +| Last version it worked on | if you know it | `0.15.0` | + +## How maintainers use it + +- The `provider` label goes on every receipt; the + [label search](https://github.com/TheSmokeDev/hermes-talk/issues?q=label%3Aprovider) + is the running compatibility record — newest receipt per provider/model + pair is the current word on it. +- **A FAIL or PARTIAL** is reproduced with `hermes talk check --provider + ` on the same plugin version. The unticked events point at the + adapter function: `SessionReady` is the setup acknowledgement translation + in `talk__realtime.py`; `SpeechStarted`/`SpeechStopped` are the + turn-detection events; `FunctionCall` is the tool-call translation and + `SubmitToolResult` its return path; barge-in is `CancelResponse` / + `TruncateOutput`. The fix ships with a regression test whose scripted + server event is the wire shape from the receipt — the report becomes the + fixture. +- **A PASS on a version pair we had not seen** is acknowledged and closed; + the issue stays searchable as the receipt. If it is the first pass on a + new host or model version, the README's provider row or the operating + manual is updated to say so. +- **A receipt that names an upstream break** (the Hermes host API moved, + not the provider) is redirected to + [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent/issues) + with a link back, so the reporter is not left in limbo. + +A receipt from the PR author is required for any change on a provider +wire — see the [merge bar](../CONTRIBUTING.md#the-merge-bar). The same +table, filled in, is what the PR template's "Live receipt" section wants.