time to cook

This commit is contained in:
ethernet 2026-07-03 15:09:27 -04:00
parent 05c01af68c
commit 5a346903d2
17 changed files with 49 additions and 676 deletions

View file

@ -1,164 +0,0 @@
name: Publish to PyPI
# Triggered by CalVer tag pushes from scripts/release.py (e.g. v2026.5.15)
# Can also be triggered manually from the Actions tab as an escape hatch.
on:
push:
tags:
- "v20*" # CalVer tags: v2026.5.15, v2026.5.15.2, etc.
workflow_dispatch:
inputs:
confirm_tag:
description: "Tag to publish (e.g. v2026.5.15). Must already exist."
required: true
type: string
# Restrict default token to read-only; each job escalates as needed.
permissions:
contents: read
# Prevent overlapping publishes (e.g. two same-day tags pushed quickly).
concurrency:
group: pypi-publish
cancel-in-progress: false
jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# On workflow_dispatch, check out the confirmed tag.
ref: ${{ inputs.confirm_tag || github.ref }}
fetch-tags: true
- name: Validate tag exists
if: github.event_name == 'workflow_dispatch'
run: |
if ! git tag -l "${{ inputs.confirm_tag }}" | grep -q .; then
echo "::error::Tag '${{ inputs.confirm_tag }}' does not exist in the repo"
exit 1
fi
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.13"
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- name: Build web dashboard
run: cd web && npm ci && npm run build
- name: Build TUI bundle
run: cd ui-tui && npm ci && npm run build
- name: Bundle TUI into hermes_cli
run: |
mkdir -p hermes_cli/tui_dist
cp ui-tui/dist/entry.js hermes_cli/tui_dist/entry.js
- name: Verify frontend assets exist
run: |
test -f hermes_cli/web_dist/index.html || { echo "ERROR: web_dist not built"; exit 1; }
test -f hermes_cli/tui_dist/entry.js || { echo "ERROR: tui_dist not built"; exit 1; }
- name: Bundle install scripts into wheel
run: |
mkdir -p hermes_cli/scripts
cp scripts/install.sh hermes_cli/scripts/install.sh
cp scripts/install.ps1 hermes_cli/scripts/install.ps1
- name: Build wheel and sdist
run: uv build --sdist --wheel
- name: Upload distribution artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-package-distributions
path: dist/
publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/hermes-agent
permissions:
id-token: write # OIDC trusted publishing
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-package-distributions
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
skip-existing: true
sign:
name: Sign and attach to GitHub Release
# Only runs on tag pushes — release.py creates the GitHub Release,
# and workflow_dispatch won't have a matching release to attach to.
if: startsWith(github.ref, 'refs/tags/')
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write # attach assets to the existing release
id-token: write # sigstore signing
steps:
- name: Download distribution artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: python-package-distributions
path: dist/
- name: Wait for GitHub Release to exist
env:
GITHUB_TOKEN: ${{ github.token }}
# release.py creates the GitHub Release after pushing the tag,
# but this workflow starts from the tag push — wait for it.
run: |
for i in $(seq 1 30); do
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $GITHUB_REF_NAME found"
exit 0
fi
echo "Waiting for release... ($i/30)"
sleep 10
done
echo "::warning::Release $GITHUB_REF_NAME not found after 5 minutes — skipping signature upload"
echo "skip_sign=true" >> "$GITHUB_ENV"
- name: Sign with Sigstore
if: env.skip_sign != 'true'
uses: sigstore/gh-action-sigstore-python@04cffa1d795717b140764e8b640de88853c92acc # v3.3.0
with:
inputs: >-
./dist/*.tar.gz
./dist/*.whl
- name: Attach signed artifacts to GitHub Release
if: env.skip_sign != 'true'
env:
GITHUB_TOKEN: ${{ github.token }}
# release.py already created the GitHub Release — just upload
# the Sigstore signatures alongside the existing assets.
run: >-
gh release upload
"$GITHUB_REF_NAME" dist/*.sigstore.json
--repo "$GITHUB_REPOSITORY"
--clobber

View file

@ -1,16 +0,0 @@
{
"id": "hermes-agent",
"name": "Hermes Agent",
"version": "0.18.0",
"description": "Self-improving open-source AI agent by Nous Research with ACP editor integration, persistent memory, skills, and rich tool support.",
"repository": "https://github.com/NousResearch/hermes-agent",
"website": "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp",
"authors": ["Nous Research"],
"license": "MIT",
"distribution": {
"uvx": {
"package": "hermes-agent[acp]==0.18.0",
"args": ["hermes-acp"]
}
}
}

View file

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="none">
<path d="M8 1.5v13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M8 3.25c-2.35-1.4-4.7-.95-6.25.35 1.85-.2 3.8.2 5.55 1.55" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 3.25c2.35-1.4 4.7-.95 6.25.35-1.85-.2-3.8.2-5.55 1.55" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.25c-2.3-1-3.05-2.65-1.35-4.15-2 .8-2.35 2.95-.35 4" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13.25c2.3-1 3.05-2.65 1.35-4.15 2 .8 2.35 2.95.35 4" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8" cy="1.8" r="1.1" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 882 B

View file

@ -1,3 +1,5 @@
"""Welcome banner, ASCII art, skills summary, and update check for the CLI.
Pure display functions with no HermesCLI state dependency.
@ -882,6 +884,7 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
from hermes_cli.config import (
detect_install_method,
format_unsupported_install_warning,
get_project_root,
is_unsupported_install_method,
get_project_root
)

View file

@ -548,12 +548,14 @@ def recommended_update_command() -> str:
#
# pip/PyPI and Homebrew are NOT an officially supported distribution method
# (see website/docs/getting-started/platform-support.md, "Unsupported"
# section). pip exists on PyPI for internal/CI reasons, not end-user installs;
# Homebrew is a legacy packaging path. Unlike NixOS/Homebrew "managed mode"
# (which hard-blocks config writes), this is a warn-don't-block deprecation
# notice surfaced everywhere the user might see install-method state: the CLI
# banner, the TUI/desktop session info panel, and ``hermes update``. NixOS
# stays fully supported (Tier 2) and must never hit this path.
# section). Hermes Agent is no longer published to PyPI or packaged for
# Homebrew — existing installs from either keep running on whatever version
# they last pulled, but will never see a newer one. Unlike NixOS/Homebrew
# "managed mode" (which hard-blocks config writes), this is a warn-don't-block
# deprecation notice surfaced everywhere the user might see install-method
# state: the CLI banner, the TUI/desktop session info panel, and
# ``hermes update``. NixOS stays fully supported (Tier 2) and must never hit
# this path.
PLATFORM_SUPPORT_DOCS_URL = "https://hermes-agent.nousresearch.com/docs/getting-started/platform-support"

View file

@ -1,14 +0,0 @@
Homebrew packaging notes for Hermes Agent.
Use `packaging/homebrew/hermes-agent.rb` as a tap or `homebrew-core` starting point.
Key choices:
- Stable builds should target the semver-named sdist asset attached to each GitHub release, not the CalVer tag tarball.
- `faster-whisper` now lives in the `voice` extra, which keeps wheel-only transitive dependencies out of the base Homebrew formula.
- The wrapper exports `HERMES_BUNDLED_SKILLS`, `HERMES_OPTIONAL_SKILLS`, and `HERMES_MANAGED=homebrew` so packaged installs keep runtime assets and defer upgrades to Homebrew.
Typical update flow:
1. Bump the formula `url`, `version`, and `sha256`.
2. Refresh Python resources with `brew update-python-resources --print-only hermes-agent`.
3. Keep `ignore_packages: %w[certifi cryptography pydantic]`.
4. Verify `brew audit --new --strict hermes-agent` and `brew test hermes-agent`.

View file

@ -1,48 +0,0 @@
class HermesAgent < Formula
include Language::Python::Virtualenv
desc "Self-improving AI agent that creates skills from experience"
homepage "https://hermes-agent.nousresearch.com"
# Stable source should point at the semver-named sdist asset attached by
# scripts/release.py, not the CalVer tag tarball.
url "https://github.com/NousResearch/hermes-agent/releases/download/v2026.3.30/hermes_agent-0.6.0.tar.gz"
sha256 "<replace-with-release-asset-sha256>"
license "MIT"
depends_on "certifi" => :no_linkage
depends_on "cryptography" => :no_linkage
depends_on "libyaml"
depends_on "python@3.14"
pypi_packages ignore_packages: %w[certifi cryptography pydantic]
# Refresh resource stanzas after bumping the source url/version:
# brew update-python-resources --print-only hermes-agent
def install
venv = virtualenv_create(libexec, "python3.14")
venv.pip_install resources
venv.pip_install buildpath
pkgshare.install "skills", "optional-skills"
%w[hermes hermes-agent hermes-acp].each do |exe|
next unless (libexec/"bin"/exe).exist?
(bin/exe).write_env_script(
libexec/"bin"/exe,
HERMES_BUNDLED_SKILLS: pkgshare/"skills",
HERMES_OPTIONAL_SKILLS: pkgshare/"optional-skills",
HERMES_MANAGED: "homebrew"
)
end
end
test do
assert_match "Hermes Agent v#{version}", shell_output("#{bin}/hermes version")
managed = shell_output("#{bin}/hermes update 2>&1")
assert_match "managed by Homebrew", managed
assert_match "brew upgrade hermes-agent", managed
end
end

View file

@ -576,7 +576,6 @@ def main(argv: list[str]) -> int:
REPO_ROOT / "plugins",
REPO_ROOT / "scripts",
REPO_ROOT / "acp_adapter",
REPO_ROOT / "acp_registry",
]
roots = [r for r in roots if r.exists()]
elif args.diff:

View file

@ -21,7 +21,6 @@ Usage:
"""
import argparse
import json
import re
import shutil
import subprocess
@ -34,11 +33,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
VERSION_FILE = REPO_ROOT / "hermes_cli" / "__init__.py"
PYPROJECT_FILE = REPO_ROOT / "pyproject.toml"
# ACP Registry manifest must stay version-locked with pyproject.toml.
# tests/acp/test_registry_manifest.py enforces this lockstep so the release
# bump touches both files atomically.
ACP_REGISTRY_MANIFEST = REPO_ROOT / "acp_registry" / "agent.json"
# ──────────────────────────────────────────────────────────────────────
# Git email → GitHub username mapping
# ──────────────────────────────────────────────────────────────────────
@ -1973,70 +1967,6 @@ def update_version_files(semver: str, calver_date: str):
)
desktop_pkg.write_text(pkg_text, encoding="utf-8")
# Update ACP Registry manifest + npm launcher (must stay version-locked
# with pyproject — enforced by tests/acp/test_registry_manifest.py).
_update_acp_registry_versions(semver)
def _update_acp_registry_versions(semver: str) -> None:
"""Bump the ACP Registry manifest's version + uvx package pin in lockstep
with pyproject.
Skips silently if the manifest is missing — older release branches predate
the ACP Registry assets.
"""
if ACP_REGISTRY_MANIFEST.exists():
manifest = json.loads(ACP_REGISTRY_MANIFEST.read_text(encoding="utf-8"))
manifest["version"] = semver
uvx = manifest.get("distribution", {}).get("uvx", {})
if "package" in uvx:
uvx["package"] = f"hermes-agent[acp]=={semver}"
# Preserve trailing newline + 2-space indent the file already uses.
ACP_REGISTRY_MANIFEST.write_text(
json.dumps(manifest, indent=2) + "\n", encoding="utf-8"
)
def build_release_artifacts(semver: str) -> list[Path]:
"""Build sdist/wheel artifacts for the current release.
Tries ``uv build`` first (matching the CI workflow), falls back to
``python -m build`` if uv is unavailable.
"""
dist_dir = REPO_ROOT / "dist"
shutil.rmtree(dist_dir, ignore_errors=True)
# Prefer uv build (matches CI workflow), fall back to python -m build.
uv_bin = shutil.which("uv")
if uv_bin:
cmd = [uv_bin, "build", "--sdist", "--wheel"]
else:
cmd = [sys.executable, "-m", "build", "--sdist", "--wheel"]
result = subprocess.run(
cmd,
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
print(" ⚠ Could not build Python release artifacts.")
stderr = result.stderr.strip()
stdout = result.stdout.strip()
if stderr:
print(f" {stderr.splitlines()[-1]}")
elif stdout:
print(f" {stdout.splitlines()[-1]}")
print(" Install uv or the 'build' package to attach sdist/wheel assets.")
return []
artifacts = sorted(p for p in dist_dir.iterdir() if p.is_file())
matching = [p for p in artifacts if semver in p.name]
if not matching:
print(" ⚠ Built artifacts did not match the expected release version.")
return []
return matching
def resolve_author(name: str, email: str) -> str:
"""Resolve a git author to a GitHub @mention."""
@ -2376,8 +2306,6 @@ def main():
# Commit version bump
add_files = [str(VERSION_FILE), str(PYPROJECT_FILE)]
if ACP_REGISTRY_MANIFEST.exists():
add_files.append(str(ACP_REGISTRY_MANIFEST))
add_result = git_result("add", *add_files)
if add_result.returncode != 0:
print(f" ✗ Failed to stage version files: {add_result.stderr.strip()}")
@ -2410,14 +2338,6 @@ def main():
print(" Continue manually after fixing access:")
print(" git push origin HEAD --tags")
# Build semver-named Python artifacts so downstream packagers
# (e.g. Homebrew) can target them without relying on CalVer tag names.
artifacts = build_release_artifacts(new_version)
if artifacts:
print(" ✓ Built release artifacts:")
for artifact in artifacts:
print(f" - {artifact.relative_to(REPO_ROOT)}")
# Create GitHub release
changelog_file = REPO_ROOT / ".release_notes.md"
changelog_file.write_text(changelog, encoding="utf-8")
@ -2427,7 +2347,6 @@ def main():
"--title", f"Hermes Agent v{new_version} ({calver_date})",
"--notes-file", str(changelog_file),
]
gh_cmd.extend(str(path) for path in artifacts)
gh_bin = shutil.which("gh")
if gh_bin:
@ -2452,9 +2371,9 @@ def main():
print(f" Tag was created locally. Create the release manually:")
print(
f" gh release create {tag_name} --title 'Hermes Agent v{new_version} ({calver_date})' "
f"--notes-file .release_notes.md {' '.join(str(path) for path in artifacts)}"
f"--notes-file .release_notes.md"
)
print(f"\n ✓ Release artifacts prepared for manual publish: v{new_version} ({tag_name})")
print(f"\n ✓ Release prepared for manual publish: v{new_version} ({tag_name})")
else:
print(f"\n{'='*60}")
print(f" Dry run complete. To publish, add --publish")

View file

@ -37,23 +37,26 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [ -z "$HERMES_PYTHON" ]; then
# ── Activate venv ───────────────────────────────────────────────────────────
VENV=""
for candidate in "$REPO_ROOT/.venv" "$REPO_ROOT/venv" "$HOME/.hermes/hermes-agent/venv"; do
if [ -f "$candidate/bin/activate" ]; then
VENV="$candidate"
break
VENV=""
for candidate in "$REPO_ROOT/.venv" "$REPO_ROOT/venv" "$HOME/.hermes/hermes-agent/venv"; do
if [ -f "$candidate/bin/activate" ]; then
VENV="$candidate"
break
fi
done
if [ -z "$VENV" ]; then
echo "error: no virtualenv found in $REPO_ROOT/.venv or $REPO_ROOT/venv" >&2
exit 1
fi
done
if [ -z "$VENV" ]; then
echo "error: no virtualenv found in $REPO_ROOT/.venv or $REPO_ROOT/venv" >&2
exit 1
PYTHON="$VENV/bin/python"
else
PYTHON=$HERMES_PYTHON
fi
PYTHON="$VENV/bin/python"
# ── Live-gateway plugin (computed before we drop env) ───────────────────────
EXTRA_PYTHONPATH=""
EXTRA_PYTEST_PLUGINS=""

View file

@ -1,90 +0,0 @@
"""Tests for ACP Registry metadata shipped with Hermes."""
from __future__ import annotations
import json
import re
import tomllib
from pathlib import Path
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[2]
MANIFEST = ROOT / "acp_registry" / "agent.json"
ICON = ROOT / "acp_registry" / "icon.svg"
FORBIDDEN_MANIFEST_KEYS = {"schema_version", "display_name"}
ALLOWED_DISTRIBUTIONS = {"binary", "npx", "uvx"}
def _manifest() -> dict:
return json.loads(MANIFEST.read_text(encoding="utf-8"))
def _pyproject_version() -> str:
data = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))
return data["project"]["version"]
def test_agent_json_matches_official_registry_required_fields():
data = _manifest()
assert FORBIDDEN_MANIFEST_KEYS.isdisjoint(data)
assert data["id"] == "hermes-agent"
assert re.fullmatch(r"[a-z][a-z0-9-]*", data["id"])
assert data["name"] == "Hermes Agent"
assert data["description"]
assert data["repository"] == "https://github.com/NousResearch/hermes-agent"
assert data["website"].startswith("https://hermes-agent.nousresearch.com/")
assert data["authors"] == ["Nous Research"]
assert data["license"] == "MIT"
assert set(data["distribution"]) <= ALLOWED_DISTRIBUTIONS
def test_agent_json_uses_uvx_distribution_without_local_command_fields():
data = _manifest()
assert set(data["distribution"]) == {"uvx"}
uvx = data["distribution"]["uvx"]
# Schema allows {package, args, env}; we use {package, args}.
assert set(uvx) <= {"package", "args", "env"}
assert "package" in uvx
assert uvx["package"] == f"hermes-agent[acp]=={data['version']}"
assert uvx["args"] == ["hermes-acp"]
# Old command-shape fields must not leak back in.
assert "type" not in data["distribution"]
assert "command" not in data["distribution"]
def test_agent_json_version_matches_pyproject():
assert _manifest()["version"] == _pyproject_version()
def test_agent_json_pins_uvx_package_to_pyproject_version():
"""The registry CI rejects ``@latest`` and floating pins; the manifest must
always reference the exact PyPI version listed in pyproject.toml."""
assert _manifest()["distribution"]["uvx"]["package"] == (
f"hermes-agent[acp]=={_pyproject_version()}"
)
def test_icon_svg_is_16x16_current_color():
root = ET.fromstring(ICON.read_text(encoding="utf-8"))
assert root.attrib["viewBox"] == "0 0 16 16"
assert root.attrib["width"] == "16"
assert root.attrib["height"] == "16"
def test_icon_svg_has_no_hardcoded_colors_or_gradients():
text = ICON.read_text(encoding="utf-8")
assert "linearGradient" not in text
assert "radialGradient" not in text
assert "url(#" not in text
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", text)
root = ET.fromstring(text)
for element in root.iter():
for attr in ("fill", "stroke"):
value = element.attrib.get(attr)
if value is not None:
assert value in {"currentColor", "none"}

View file

@ -1,113 +0,0 @@
"""Tests for the ACP Registry version-lockstep bump in scripts/release.py.
The official ACP Registry manifest must match ``pyproject.toml`` exactly —
``tests/acp/test_registry_manifest.py`` enforces this at lint time, and the
upstream registry CI rejects ``@latest`` / floating pins. The release script
is the single place that bumps the manifest in lockstep with pyproject; if
that bump ever silently breaks, weekly releases fail the manifest test
until someone hand-edits the JSON.
"""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
def _load_release_module(monkeypatch, tmp_root: Path):
"""Import scripts/release.py with REPO_ROOT pinned to a temp tree."""
spec = importlib.util.spec_from_file_location(
"_release_under_test",
Path(__file__).resolve().parents[2] / "scripts" / "release.py",
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
monkeypatch.setattr(module, "REPO_ROOT", tmp_root)
monkeypatch.setattr(
module, "ACP_REGISTRY_MANIFEST", tmp_root / "acp_registry" / "agent.json"
)
return module
def _write_manifest(root: Path, version: str) -> None:
manifest_dir = root / "acp_registry"
manifest_dir.mkdir(parents=True)
(manifest_dir / "agent.json").write_text(
json.dumps(
{
"id": "hermes-agent",
"name": "Hermes Agent",
"version": version,
"description": "test",
"distribution": {
"uvx": {
"package": f"hermes-agent[acp]=={version}",
"args": ["hermes-acp"],
}
},
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
def test_update_acp_registry_versions_bumps_manifest_and_pin(monkeypatch, tmp_path):
_write_manifest(tmp_path, "0.13.0")
module = _load_release_module(monkeypatch, tmp_path)
module._update_acp_registry_versions("0.14.0")
manifest = json.loads(
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
)
assert manifest["version"] == "0.14.0"
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"
# args stay untouched so we don't accidentally rewrite them.
assert manifest["distribution"]["uvx"]["args"] == ["hermes-acp"]
def test_update_acp_registry_versions_is_silent_when_manifest_missing(
monkeypatch, tmp_path
):
"""Older release branches predate the ACP Registry asset — must no-op."""
module = _load_release_module(monkeypatch, tmp_path)
# No fixture written; function should not raise.
module._update_acp_registry_versions("0.14.0")
def test_update_version_files_bumps_manifest_alongside_pyproject(
monkeypatch, tmp_path
):
"""End-to-end: update_version_files() is the function release.py actually
calls, so it must drive the manifest bump too."""
_write_manifest(tmp_path, "0.13.0")
(tmp_path / "pyproject.toml").write_text(
'[project]\nname = "hermes-agent"\nversion = "0.13.0"\n', encoding="utf-8"
)
version_dir = tmp_path / "hermes_cli"
version_dir.mkdir()
(version_dir / "__init__.py").write_text(
'__version__ = "0.13.0"\n__release_date__ = "2026-05-14"\n',
encoding="utf-8",
)
module = _load_release_module(monkeypatch, tmp_path)
monkeypatch.setattr(module, "VERSION_FILE", version_dir / "__init__.py")
monkeypatch.setattr(module, "PYPROJECT_FILE", tmp_path / "pyproject.toml")
module.update_version_files("0.14.0", "2026-05-21")
pyproject_text = (tmp_path / "pyproject.toml").read_text(encoding="utf-8")
assert 'version = "0.14.0"' in pyproject_text
manifest = json.loads(
(tmp_path / "acp_registry" / "agent.json").read_text(encoding="utf-8")
)
assert manifest["version"] == "0.14.0"
assert manifest["distribution"]["uvx"]["package"] == "hermes-agent[acp]==0.14.0"

View file

@ -17,7 +17,6 @@ Key implementation files:
- `acp_adapter/permissions.py`
- `acp_adapter/tools.py`
- `acp_adapter/auth.py`
- `acp_registry/agent.json`
## Boot flow
@ -31,8 +30,6 @@ hermes acp / hermes-acp / python -m acp_adapter
-> acp.run_agent(agent, use_unstable_protocol=True)
```
The Zed ACP Registry path launches the same adapter through `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`, pointed at the `hermes-agent` PyPI release.
Stdout is reserved for ACP JSON-RPC transport. Human-readable logs go to stderr.
## Major components
@ -149,7 +146,7 @@ Instead it reuses Hermes' runtime resolver:
- `acp_adapter/auth.py`
- `hermes_cli/runtime_provider.py`
So ACP advertises and uses the currently configured Hermes provider/credentials. It also always advertises a terminal setup auth method (`hermes-setup`, args `--setup`) so first-run registry clients can open Hermes' interactive model/provider configuration before starting a normal ACP session.
So ACP advertises and uses the currently configured Hermes provider/credentials. It also always advertises a terminal setup auth method (`hermes-setup`, args `--setup`) so first-run clients can open Hermes' interactive model/provider configuration before starting a normal ACP session.
## Working directory binding

View file

@ -44,7 +44,7 @@ PRs to fix them will _not_ be accepted, and any code that keeps compatibility wi
- installs via the AUR (we might upstream patches if it helps out &lt;3)
- macOS on x86 (Intel) processors
- installs via `pypi` (e.g. `uv tool install hermes-agent`, `pip install hermse-agent`, etc.)
- installs via `brew` (`brew install hermes-agent`)
- installs via `pypi` (e.g. `uv tool install hermes-agent`, `pip install hermse-agent`, etc.) — Hermes Agent is no longer published to PyPI, so these installs will not receive further updates
- installs via `brew` (`brew install hermes-agent`) — Hermes Agent is no longer packaged for Homebrew, so these installs will not receive further updates
If you are using an unsupported distribution method, please read the [the installation guide](./installation.md) to learn how to switch to a supported one.

View file

@ -45,14 +45,6 @@ This installs the `agent-client-protocol` dependency and enables:
- `hermes-acp`
- `python -m acp_adapter`
For Zed registry installs, Zed launches Hermes through the official ACP Registry entry. That entry uses a `uvx` distribution that runs:
```bash
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
Make sure `uv` is available on `PATH` before using the registry install path.
## Launching the ACP server
Any of the following starts Hermes in ACP mode:
@ -89,7 +81,7 @@ hermes acp --setup-browser # interactive (prompts before ~400 MB downl
hermes acp --setup-browser --yes # accept the download non-interactively
```
This is the standalone command. The Zed registry's terminal-auth flow (`hermes acp --setup`) also offers the browser bootstrap as a follow-up question after model selection, so most users never need to run `--setup-browser` directly.
This is the standalone command. Hermes' terminal-auth flow (`hermes acp --setup`) also offers the browser bootstrap as a follow-up question after model selection, so most users never need to run `--setup-browser` directly.
What it does:
@ -126,19 +118,7 @@ If you want to define Hermes manually, add it through VS Code settings under `ac
### Zed
Zed v0.221.x and newer installs external agents through the official ACP Registry.
1. Open the Agent Panel.
2. Click **Add Agent**, or run the `zed: acp registry` command.
3. Search for **Hermes Agent**.
4. Install it and start a new Hermes external-agent thread.
Prerequisites:
- Configure Hermes provider credentials first with `hermes model`, or set them in `~/.hermes/.env` / `~/.hermes/config.yaml`.
- Install `uv` so the registry launcher can run `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`.
For local development before the registry entry is available, use a custom agent server in Zed settings:
Zed supports external agents via a custom agent server entry in Zed settings:
```json
{
@ -152,32 +132,11 @@ For local development before the registry entry is available, use a custom agent
}
```
Configure Hermes provider credentials first with `hermes model`, or set them in `~/.hermes/.env` / `~/.hermes/config.yaml`.
### JetBrains
Use an ACP-compatible plugin and point it at:
```text
/path/to/hermes-agent/acp_registry
```
## Registry manifest
The source copy of Hermes' official ACP Registry metadata lives at:
```text
acp_registry/agent.json
acp_registry/icon.svg
```
The upstream registry PR copies those files into the top-level `hermes-agent/` directory in `agentclientprotocol/registry`.
The registry entry uses a `uvx` distribution that points directly at the `hermes-agent` PyPI release:
```text
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
The registry CI verifies that the pinned version exists on PyPI, so the manifest's `version` and uvx `package` pin must always match `pyproject.toml`. `scripts/release.py` keeps them in lockstep automatically.
Use an ACP-compatible plugin and point it at the local `hermes acp` command (see the VS Code / Zed `command`/`args` examples above — the same shape applies).
## Configuration and credentials
@ -188,7 +147,7 @@ ACP mode uses the same Hermes configuration as the CLI:
- `~/.hermes/skills/`
- `~/.hermes/state.db`
Provider resolution uses Hermes' normal runtime resolver, so ACP inherits the currently configured provider and credentials. Hermes also advertises a terminal auth method (`--setup`) for first-run registry clients; this opens Hermes' interactive model/provider setup.
Provider resolution uses Hermes' normal runtime resolver, so ACP inherits the currently configured provider and credentials. Hermes also advertises a terminal auth method (`--setup`) so first-run clients can open Hermes' interactive model/provider setup.
## Session behavior
@ -239,11 +198,9 @@ The ACP bridge maps these options onto Hermes' internal approval semantics — `
Check:
- In Zed, open the ACP Registry with `zed: acp registry` and search for **Hermes Agent**.
- For manual/local development, verify the custom `agent_servers` command points to `hermes acp`.
- Verify the custom `agent_servers` (Zed) or equivalent editor config points to `hermes acp`.
- Hermes is installed and on your PATH.
- The ACP extra is installed (`pip install -e '.[acp]'`).
- `uv` is installed if launching from the official Zed registry entry.
### ACP starts but immediately errors
@ -264,11 +221,7 @@ ACP mode uses Hermes' existing provider setup. Configure credentials with:
hermes model
```
or by editing `~/.hermes/.env`. Registry clients can also trigger Hermes' terminal auth flow, which runs the same interactive provider/model setup.
### Zed registry launcher cannot find uv
Install `uv` from the official uv installation docs, then retry the Hermes Agent thread from Zed.
or by editing `~/.hermes/.env`. ACP clients can also trigger Hermes' terminal auth flow, which runs the same interactive provider/model setup.
## See also

View file

@ -17,7 +17,6 @@ ACP 适配器将 Hermes 的同步 `AIAgent` 封装为异步 JSON-RPC stdio 服
- `acp_adapter/permissions.py`
- `acp_adapter/tools.py`
- `acp_adapter/auth.py`
- `acp_registry/agent.json`
## 启动流程
@ -31,8 +30,6 @@ hermes acp / hermes-acp / python -m acp_adapter
-> acp.run_agent(agent, use_unstable_protocol=True)
```
Zed ACP Registry 路径通过 `uvx --from 'hermes-agent[acp]==<version>' hermes-acp` 启动同一适配器,指向 `hermes-agent` PyPI 发布包。
stdout 保留用于 ACP JSON-RPC 传输。人类可读的日志输出至 stderr。
## 主要组件
@ -149,7 +146,7 @@ ACP 不实现自己的认证存储。
- `acp_adapter/auth.py`
- `hermes_cli/runtime_provider.py`
因此 ACP 通告并使用当前配置的 Hermes provider/凭据。它还始终通告一个终端 setup 认证方法(`hermes-setup`,参数 `--setup`),以便首次运行的 registry 客户端在启动正常 ACP 会话前可以打开 Hermes 的交互式模型/provider 配置。
因此 ACP 通告并使用当前配置的 Hermes provider/凭据。它还始终通告一个终端 setup 认证方法(`hermes-setup`,参数 `--setup`),以便首次运行的客户端在启动正常 ACP 会话前可以打开 Hermes 的交互式模型/provider 配置。
## 工作目录绑定

View file

@ -45,14 +45,6 @@ pip install -e '.[acp]'
- `hermes-acp`
- `python -m acp_adapter`
对于 Zed registry 安装,Zed 通过官方 ACP Registry 条目启动 Hermes。该条目使用 `uvx` 发行版运行:
```bash
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
使用 registry 安装路径前,请确保 `uv` 已在 `PATH` 中可用。
## 启动 ACP 服务器
以下任意命令均可以 ACP 模式启动 Hermes:
@ -87,7 +79,7 @@ hermes acp --setup-browser # 交互式(下载约 400 MB 前会提示
hermes acp --setup-browser --yes # 非交互式接受下载
```
这是独立命令。Zed registry 的终端认证流程(`hermes acp --setup`)在模型选择后也会将浏览器引导作为后续问题提供,因此大多数用户无需直接运行 `--setup-browser`。
这是独立命令。Hermes 的终端认证流程(`hermes acp --setup`)在模型选择后也会将浏览器引导作为后续问题提供,因此大多数用户无需直接运行 `--setup-browser`。
具体操作:
@ -124,19 +116,7 @@ hermes acp --setup-browser --yes # 非交互式接受下载
### Zed
Zed v0.221.x 及更新版本通过官方 ACP Registry 安装外部 agent。
1. 打开 Agent 面板。
2. 点击 **Add Agent**,或运行 `zed: acp registry` 命令。
3. 搜索 **Hermes Agent**。
4. 安装后启动新的 Hermes 外部 agent 线程。
前提条件:
- 先通过 `hermes model` 配置 Hermes provider 凭据,或在 `~/.hermes/.env` / `~/.hermes/config.yaml` 中设置。
- 安装 `uv`,以便 registry 启动器可以运行 `uvx --from 'hermes-agent[acp]==<version>' hermes-acp`。
在 registry 条目可用之前进行本地开发时,在 Zed 设置中使用自定义 agent 服务器:
Zed 通过 Zed 设置中的自定义 agent 服务器条目支持外部 agent:
```json
{
@ -150,32 +130,11 @@ Zed v0.221.x 及更新版本通过官方 ACP Registry 安装外部 agent。
}
```
先通过 `hermes model` 配置 Hermes provider 凭据,或在 `~/.hermes/.env` / `~/.hermes/config.yaml` 中设置。
### JetBrains
使用兼容 ACP 的插件并将其指向:
```text
/path/to/hermes-agent/acp_registry
```
## Registry 清单
Hermes 官方 ACP Registry 元数据的源文件位于:
```text
acp_registry/agent.json
acp_registry/icon.svg
```
上游 registry PR 将这些文件复制到 `agentclientprotocol/registry` 中的顶层 `hermes-agent/` 目录。
Registry 条目使用直接指向 `hermes-agent` PyPI 发行版的 `uvx` 发行版:
```text
uvx --from 'hermes-agent[acp]==<version>' hermes-acp
```
Registry CI 会验证固定版本是否存在于 PyPI,因此清单的 `version` 和 uvx `package` 固定版本必须始终与 `pyproject.toml` 匹配。`scripts/release.py` 会自动保持它们同步。
使用兼容 ACP 的插件,并将其指向本地的 `hermes acp` 命令(参见上面 VS Code / Zed 的 `command`/`args` 示例——形式相同)。
## 配置与凭据
@ -186,7 +145,7 @@ ACP 模式使用与 CLI 相同的 Hermes 配置:
- `~/.hermes/skills/`
- `~/.hermes/state.db`
Provider 解析使用 Hermes 的正常运行时解析器,因此 ACP 继承当前配置的 provider 和凭据。Hermes 还为首次运行的 registry 客户端提供终端认证方法(`--setup`);这将打开 Hermes 的交互式模型/provider 设置。
Provider 解析使用 Hermes 的正常运行时解析器,因此 ACP 继承当前配置的 provider 和凭据。Hermes 还提供终端认证方法(`--setup`),以便首次运行的客户端打开 Hermes 的交互式模型/provider 设置。
## 会话行为
@ -237,11 +196,9 @@ ACP 桥接将这些选项映射到 Hermes 的内部审批语义——`allow_alwa
检查:
- 在 Zed 中,使用 `zed: acp registry` 打开 ACP Registry 并搜索 **Hermes Agent**。
- 对于手动/本地开发,验证自定义 `agent_servers` 命令是否指向 `hermes acp`。
- 验证自定义 `agent_servers`(Zed)或等效的编辑器配置是否指向 `hermes acp`。
- Hermes 已安装且在 PATH 中。
- ACP 扩展已安装(`pip install -e '.[acp]'`)。
- 如果从官方 Zed registry 条目启动,`uv` 已安装。
### ACP 启动后立即报错
@ -262,14 +219,10 @@ ACP 模式使用 Hermes 现有的 provider 设置。通过以下方式配置凭
hermes model
```
或编辑 `~/.hermes/.env`。Registry 客户端也可以触发 Hermes 的终端认证流程,该流程运行相同的交互式 provider/模型设置。
### Zed registry 启动器找不到 uv
从官方 uv 安装文档安装 `uv`,然后从 Zed 重试 Hermes Agent 线程。
或编辑 `~/.hermes/.env`。ACP 客户端也可以触发 Hermes 的终端认证流程,该流程运行相同的交互式 provider/模型设置。
## 另请参阅
- [ACP 内部机制](../../developer-guide/acp-internals.md)
- [Provider 运行时解析](../../developer-guide/provider-runtime.md)
- [工具运行时](../../developer-guide/tools-runtime.md)
- [工具运行时](../../developer-guide/tools-runtime.md)