Language Server Protocol (LSP)
Hermes запускает полноценные языковые серверы — pyright, gopls,
rust-analyzer, typescript-language-server, clangd и ещё около двадцати —
как фоновые подпроцессы и передаёт их семантическую диагностику в
проверку после записи, которой пользуются write_file и patch. Стоит
агенту отредактировать файл, и он сразу видит ровно те ошибки, что внесла
правка: не только синтаксис, но и ошибки типов, неопределённые имена,
недостающие импорты и семантические проблемы уровня всего проекта,
которые ловит языковой сервер.
Такую же архитектуру используют сильнейшие кодовые агенты. Hermes несёт её в себе целиком: не нужен редактор-хост, не нужно ставить плагины, не нужно держать отдельный демон.
Когда срабатывает LSP
Запуск LSP завязан на обнаружение git-воркспейса. Если рабочий каталог агента (или редактируемый файл) находится внутри git-репозитория, LSP работает по этому воркспейсу. Когда ни то ни другое не лежит в git-репо, LSP остаётся спящим — это удобно для шлюзов сообщений, где cwd указывает на домашний каталог пользователя и диагностировать попросту нечего.
Проверка устроена слоями: сначала синтаксический разбор в процессе (микросекунды), затем — диагностика LSP, когда синтаксис чистый. Сбойный или отсутствующий языковой сервер никогда не сломает запись: любой путь отказа LSP молча откатывается к результату одной только синтаксической проверки.
Если конкретно, при каждом успешном write_file или patch:
- Hermes снимает базовый срез текущей диагностики по файлу.
- Выполняет запись.
- Заново опрашивает языковой сервер, отсеивает диагностику, которая уже была в базовом срезе, и показывает только новую.
Агент получает примерно такой вывод:
{
"bytes_written": 42,
"dirs_created": false,
"lint": {"status": "ok", "output": ""},
"lsp_diagnostics": "LSP diagnostics introduced by this edit:\n<diagnostics file=\"/path/to/foo.py\">\nERROR [42:5] Cannot find name 'foo' [reportUndefinedVariable] (Pyright)\nERROR [50:1] Argument of type \"str\" is not assignable to \"int\" [reportArgumentType] (Pyright)\n</diagnostics>"
}
Поле lint несёт результат синтаксической проверки (микросекундный разбор
в процессе через ast.parse, json.loads и т. п.); поле
lsp_diagnostics — семантическую диагностику от настоящего языкового
сервера. Два канала, независимые сигналы: файл, чистый по синтаксису, но с
семантическими проблемами, агент увидит как lint: ok плюс заполненный
lsp_diagnostics.
Доверие к воркспейсу
Многие языковые серверы выполняют код, который поставляет сам проект:
pyright запускает настроенный интерпретатор Python,
typescript-language-server загружает node_modules/typescript проекта,
svelte-language-server загружает svelte.config.js, rust-analyzer при каждом
сохранении запускает cargo check (скрипты сборки, proc-макросы), а такие
серверы, как jdtls, kotlin-language-server, elixir-ls, zls или
haskell-language-server, при старте вычисляют файлы сборки проекта (Gradle,
mix.exs, build.zig, Cabal/Stack). Для вашего собственного проекта это
нормально, а для репозитория, который агент только что склонировал, — нет.
Поэтому Hermes считает любой воркспейс недоверенным, если он не относится к одному из двух случаев:
- git-worktree каталога, на который вы направили Hermes: там, откуда вы его
запустили (
cd my-app && hermes), worktree, созданныйhermes -w, проект, в котором открыта сессия Desktop или TUI, либоterminal.cwdшлюза; - каталог из списка
lsp.trusted_workspaces(или любой каталог внутри него).
Команда cd в терминале агента воркспейс сессии не меняет. Задачи cron и воркеры
Kanban автоматического доверия не получают, потому что агент сам может выбрать их
рабочий каталог или воркспейс; каталоги, которым они должны доверять, перечислите
в lsp.trusted_workspaces. Чекаут внутри доверенного worktree имеет собственный
.git, поэтому доверенным не считается; не считается им и git-репозиторий в вашем
домашнем каталоге или выше (иначе репозиторий с дотфайлами сделал бы доверенным всё,
что лежит ниже). Доверие распространяется на весь каталог, на который вы направили
Hermes, включая всё, что позже будет в него склонировано, и действует до выхода из Hermes.
В недоверенном воркспейсе Hermes по умолчанию запрещает: стартуют только
перечисленные ниже серверы, и каждый с настройками, которые оставляют его на
инструментах стороны Hermes. Все остальные серверы пропускаются: rust-analyzer, gopls,
jdtls, kotlin-language-server, elixir-ls, zls, clojure-lsp,
haskell-language-server, lua-language-server, terraform-ls, prisma,
astro, vue-language-server (он загружает vueCompilerOptions.plugins, названные
в tsconfig.json проекта) и любой сервер, который вы объявили в lsp.servers. Журнал
диагностики записывает skipped: untrusted workspace …; add it to lsp.trusted_workspaces, а hermes lsp status помечает такие серверы
[trusted workspaces only].
| Сервер | Недоверенный воркспейс |
|---|---|
| pyright | VIRTUAL_ENV или управляемый Hermes Python, но не .venv/venv проекта |
| typescript-language-server | tsserver.path закреплён на TypeScript рядом с сервером; если его нет, сервер пропускается |
| svelte-language-server | isTrusted: false (ни svelte.config.js, ни svelte/prettier проекта) |
| bash-language-server, yaml-language-server, dockerfile-ls, intelephense | без изменений: код проекта они не запускают (yaml-language-server может подтягивать JSON-схемы, которые названы в файле) |
| clangd | без изменений: Hermes никогда не передаёт --query-driver, поэтому компилятор проекта не запускается |
На локальном бэкенде шелл-линтеры после записи, которые использовали бы собственный
тулчейн чекаута, пропускаются так же, когда текущий каталог терминала недоверенный:
npx tsc (он запускает node_modules/.bin/tsc репозитория или ставит пакет из
реестра, указанного в его .npmrc) и rustfmt --check (rustup учитывает
rust-toolchain.toml репозитория). Песочные бэкенды (Docker, SSH, Modal и т. д.)
остаются без изменений.
Диагностика, которой нужны зависимости проекта (например, предупреждения о неразрешённых импортах), может быть менее точной, пока вы не сделаете воркспейс доверенным.
lsp:
trusted_workspaces:
- ~/code/my-app
Поддерживаемые языки
| Язык | Сервер | Автоустановка |
|---|---|---|
| Python | pyright-langserver | npm |
| TypeScript / JavaScript / JSX / TSX | typescript-language-server | npm |
| Vue | @vue/language-server | npm |
| Svelte | svelte-language-server | npm |
| Astro | @astrojs/language-server | npm |
| Go | gopls | go install |
| Rust | rust-analyzer | вручную (rustup) |
| C / C++ | clangd | вручную (LLVM) |
| Bash / Zsh | bash-language-server | npm |
| YAML | yaml-language-server | npm |
| Lua | lua-language-server | вручную (релизы на GitHub) |
| PHP | intelephense | npm |
Laravel Blade (.blade.php) | laravel-lsp | вручную (composer) |
| OCaml | ocaml-lsp | вручную (opam) |
| Dockerfile | dockerfile-language-server-nodejs | npm |
| Terraform | terraform-ls | вручную |
| Dart | dart language-server | вручную (dart sdk) |
| Haskell | haskell-language-server | вручную (ghcup) |
| Julia | julia + LanguageServer.jl | вручную |
| Clojure | clojure-lsp | вручную |
| Nix | nixd | вручную |
| Zig | zls | вручную |
| Gleam | gleam lsp | вручную (gleam install) |
| Elixir | elixir-ls | вручную |
| Prisma | prisma language-server | вручную |
| Kotlin | kotlin-language-server | вручную |
| Java | jdtls | вручную |
| PowerShell | PowerShellEditorServices (хост pwsh) | вручную (zip релиза) |
Для пунктов с пометкой «вручную» ставьте сервер тем менеджером инструментов,
который уместен для этого языка (rustup, ghcup, opam, brew, …). Hermes сам
находит бинарник в PATH или в <HERMES_HOME>/lsp/bin/.
PowerShell
PowerShellEditorServices — не один бинарник, а набор модулей PowerShell,
который запускается хостом pwsh (PowerShell 7+) или powershell.
Порядок настройки:
- Установите PowerShell, чтобы
pwsh(илиpowershellв Windows) оказался в PATH. - Скачайте свежий zip релиза со страницы релизов PowerShellEditorServices и распакуйте его.
- Укажите Hermes путь к распакованному набору — каталогу, где лежит
PowerShellEditorServices/Start-EditorServices.ps1. Любым из способов:- пропишите
lsp.servers.powershell.command: ["/path/to/bundle"]вconfig.yaml, - распакуйте набор в
<HERMES_HOME>/lsp/PowerShellEditorServices, - экспортируйте
PSES_BUNDLE_PATH=/path/to/bundle.
- пропишите
hermes lsp status покажет installed, как только найдёт pwsh; если набора
модулей нет, в логах один раз появится предупреждение со ссылкой на скачивание.
Laravel Blade
Шаблоны .blade.php уходят в laravel-lsp
(Blade, Eloquent, Facades), а обычные файлы .php остаются за intelephense.
Поставьте его один раз через Composer и проследите, чтобы бинарник был в PATH
(или закрепите путь через lsp.servers.laravel-lsp.command):
composer global require laravel/lsp
export PATH="$HOME/.config/composer/vendor/bin:$PATH"
Hermes запускает его как laravel-lsp lsp (stdio). Рецепта автоустановки нет;
пока бинарник не найден, hermes lsp status показывает manual-only.
Часть серверов идёт в комплекте с зависимостью-соседом, которую npm не
подтянет сам. typescript-language-server и @vue/language-server требуют
SDK typescript, импортируемый из того же дерева node_modules, — поэтому
Hermes ставит typescript@6 (последняя линейка на JavaScript; TypeScript 7 —
это порт на Go, и tsserver.js в нём нет) вместе с сервером, когда вы
выполняете hermes lsp install typescript /
hermes lsp install vue-language-server или когда автоустановка срабатывает
при первом обращении.
Vue закреплён на @vue/language-server@2 и запускается с
vue.hybridMode: false, чтобы он сам держал свой сервис TypeScript. Линейка 3.x
работает только через туннель к tsserver, который поднимает клиент (так
устроено в VS Code / Neovim), а универсальный клиент Hermes такого туннеля не
запускает — поэтому диагностику 3.x не публикует никогда. Если 3.x поставила
более ранняя версия Hermes, в логе один раз появится предупреждение
vue-language-server: ... 3.x; удалите <HERMES_HOME>/lsp/node_modules/@vue и
<HERMES_HOME>/lsp/bin/vue-language-server*, затем выполните
hermes lsp install vue-language-server (рецепт заодно ставит SDK TypeScript).
CLI
hermes lsp status # состояние сервиса + статус установки по серверам
hermes lsp list # реестр, опционально --installed-only
hermes lsp install <id> # сразу установить один сервер
hermes lsp install-all # попробовать каждый сервер с известным рецептом
hermes lsp restart # снести запущенные клиенты
hermes lsp which <id> # вывести вычисленный путь к бинарнику
С hermes lsp status лучше всего и начинать — команда показывает, какие
языки уже сегодня получат семантическую диагностику, а каким недостаёт
установленного бинарника.
Конфигурация
Значения по умолчанию подходят для типичных установок; настраивать нечего, если бинарники лежат в PATH.
# config.yaml
lsp:
# Главный переключатель. Отключение пропускает всю подсистему: серверы
# не запускаются, фоновый цикл событий не работает.
enabled: true
# Сколько ждать диагностику после каждой записи.
wait_mode: document # "document" или "full"
# Максимум секунд ожидания сервера на каждом из двух ожиданий, которые
# делает правка: базовый срез до правки и перепроверка после неё.
# Сообщаются только *свежие* диагностики — те, что получены для содержимого
# после правки; если сервер не уложился в бюджет, правка отчитается «нет
# данных LSP», а не старыми ошибками до правки. Поднимайте значение для
# медленных серверов на больших проектах (tsserver, rust-analyzer в процессе
# индексации).
wait_timeout: 5.0
# Бюджет на ПЕРВЫЙ запрос к воркспейсу, чей сервер ещё не запущен: внутри
# него проходят запуск, инициализация и начальная сборка программы
# сервером (tsserver на проекте в 10 тыс. файлов может потребовать минуту).
# Как только клиент поднялся, снова действует wait_timeout.
# 0 = без дополнительной форы (то же, что wait_timeout).
warmup_timeout: 0
# После сбоя сервера на воркспейсе (ошибка запуска или запрос вышел за
# бюджет) пара (сервер, корень) пропускается. 0 = до конца жизни процесса
# (до `hermes lsp restart`); N = повторная попытка через N секунд, чтобы
# одна случайная заминка не глушила воркспейс навсегда. Пропуски пишутся в
# лог один раз на корень на уровне INFO с временем повтора.
broken_retry_seconds: 0
# Корни воркспейсов, где языковые серверы не запускаются вовсе, — glob-
# шаблоны, сверяемые с вычисленным корнем проекта (~ раскрывается; голый
# путь совпадает и со всем, что под ним). Нужно для того самого огромного
# монорепозитория, чей сервер не укладывается в бюджет, тогда как все
# остальные воркспейсы сохраняют диагностику, — в отличие от
# servers.<id>.disabled, который выключает сервер везде. Должен быть
# списком; любая другая форма пишет предупреждение и пропускает LSP для
# всех воркспейсов, пока её не исправят.
exclude_roots: []
# exclude_roots: ["~/work/huge-monorepo", "/srv/checkouts/*/vendor"]
# Каталоги, проекты в которых языковой сервер может загружать код
# (см. «Доверие к воркспейсу» выше). ~ раскрывается; считается всё,
# что лежит под записью. Worktree каталога, где вы запустили Hermes
# или открыли сессию, всегда доверенный.
trusted_workspaces: []
# trusted_workspaces: ["~/code/my-app"]
# Что делать с отсутствующими бинарниками серверов.
# auto — установить через npm/pip/go install в <HERMES_HOME>/lsp/bin
# manual — использовать только бинарники, уже лежащие в PATH
install_strategy: auto
# Node package manager for the npm-based servers: npm (default), pnpm
# or yarn. Installs still land in <HERMES_HOME>/lsp/node_modules; a
# manager that is configured but not installed — or a value outside
# npm|pnpm|yarn — skips the install with a warning instead of silently
# using npm, so a pnpm/yarn supply-chain policy (minimumReleaseAge,
# allowBuilds, …) is never bypassed. Yarn Berry (2+): its default PnP
# linker writes no node_modules/.bin, so set `nodeLinker: node-modules`
# in <HERMES_HOME>/lsp/.yarnrc.yml. pnpm 11 blocks git-hosted transitive
# deps by default (ERR_PNPM_EXOTIC_SUBDEP); @vue/language-server 2.x pulls
# one in, so under pnpm that server is skipped with the pnpm error in the
# log — install it once with npm, or relax block-exotic-subdeps in
# <HERMES_HOME>/lsp/.npmrc if your policy allows it.
package_manager: npm
# Сколько секунд живёт неиспользуемый клиент языкового сервера.
# Простаивающие серверы гасятся автоматически и поднимаются заново при
# следующей операции с подходящим файлом. Значение 0 отключает сбор
# простаивающих и держит серверы живыми весь срок жизни процесса. Значения
# меньше 30 с поднимаются до 30, чтобы проход сборщика никогда не убил
# клиента посреди операции.
idle_timeout: 600
# Переопределения по серверам (всё опционально).
servers:
pyright:
disabled: false
command: ["/abs/path/to/pyright-langserver", "--stdio"]
env: { PYRIGHT_LOG_LEVEL: "info" }
initialization_options:
python:
analysis:
typeCheckingMode: "strict"
typescript:
disabled: true # пропустить TS, даже если расширения совпали
Ключи на уровне сервера
disabled: true— полностью пропустить этот сервер, даже когда его расширения совпадают с файлом.command: [bin, ...args]— закрепить свой путь к бинарнику. Минует автоустановку.env: {KEY: value}— дополнительные переменные окружения для запускаемого процесса. Серверы и автоустановщики npm /go installстартуют из очищенного дочернего окружения Hermes (без токенов шлюза и API-ключей провайдеров), поэтому сервер, которому нужен один из них, получит его только через этот ключ.initialization_options: {...}— вливается в полезную нагрузку LSPinitializationOptions, которую отправляют в рукопожатииinitialize. Зависит от сервера; сверяйтесь с документацией конкретного языкового сервера.
Собственные серверы
Любой ключ в lsp.servers, который не совпадает с id встроенного сервера,
объявляет ваш собственный языковой сервер. Ему нужны command и extensions,
остальные ключи необязательны. Собственные серверы сопоставляются раньше
встроенных, так что могут перехватить и расширение, которое Hermes уже
обслуживает.
lsp:
servers:
panache:
command: ["panache-lsp", "--stdio"] # поиск в PATH или абсолютный путь / путь с ~
extensions: [".pnch"] # или базовые имена вроде "Justfile"
root_markers: ["panache.toml"] # ближайший каталог с одним из них; по умолчанию — корень воркспейса
language_id: "panache" # languageId для didOpen; по умолчанию выводится из расширения
description: "Panache markdown"
env: { PANACHE_LOG: "warn" } # те же необязательные ключи, что у встроенных
initialization_options: {}
Собственные серверы никогда не ставятся автоматически: положите бинарник в PATH
(или укажите абсолютный путь), и hermes lsp status покажет его как
installed. Некорректная запись пишется в лог и пропускается, не затрагивая
остальные серверы.
Куда ставятся бинарники
При install_strategy: auto Hermes ставит бинарники в
<HERMES_HOME>/lsp/bin/. NPM-пакеты попадают в
<HERMES_HOME>/lsp/node_modules/, а симлинки на бинарники — уровнем выше.
Go-бинарники приходят через go install с GOBIN, направленным в
промежуточный каталог.
Ничего и никогда не ставится в /usr/local/, ~/.local/ или другое общее
место — промежуточный каталог целиком принадлежит Hermes и удаляется при
сбросе профиля.
Характеристики производительности
LSP-серверы запускаются лениво, при первом обращении. Правка
Python-файла в проекте, где ещё не было .py-трафика, поднимет pyright;
запуск занимает 1–3 секунды для большинства серверов (у rust-analyzer на
холодном проекте может уйти 10+). Последующие правки в том же воркспейсе
переиспользуют уже работающий сервер.
Слой LSP добавляет несколько миллисекунд к чистым записям, когда
диагностики нет. Когда диагностика есть, бюджет ожидания — wait_timeout
секунд; обычно pyright/tsserver отвечают за десятки миллисекунд, а
rust-analyzer в разгар индексации — за несколько секунд. Каждая правка ждёт
дважды (базовый срез до правки для вычисления разницы, затем перепроверка после
неё), так что сервер, который вообще не отвечает, обходится максимум в
2 × wait_timeout на правку. Самый первый запрос к воркспейсу вдобавок
оплачивает запуск и начальную сборку программы сервером; большим проектам
давайте запас через lsp.warmup_timeout (его использует только этот первый,
холодный запрос — бюджет установившегося режима не меняется), а не через
увеличение wait_timeout, из-за которого каждая следующая правка могла бы
блокироваться на время холодной сборки.
Сервер, который дал сбой на воркспейсе, — ошибка запуска или запрос, вышедший
за бюджет, — помечает пару (server, root) сломанной, и все последующие
запросы к ней пропускаются (с одной записью в лог на корень на уровне INFO). По
умолчанию пара остаётся сломанной до hermes lsp restart или завершения
процесса; lsp.broken_retry_seconds: N повторяет попытку через N секунд, чтобы
одна случайная заминка не лишала воркспейс диагностики навсегда. Корень,
который вы вообще не хотите обслуживать, — монорепозиторий, чей сервер не
укладывается ни в какой бюджет, — вносите в lsp.exclude_roots; остальные
воркспейсы сохраняют свои серверы.
Диагностики проходят проверку на свежесть: результат засчитывается,
только если сервер выдал его для содержимого текущей правки (push
publishDiagnostics в момент изменения или после него либо ответ на
pull-запрос, пришедший после). Медленные серверы, ещё не успевшие
перепроверить файл, дают для этой правки «нет данных» — но никогда не выдают
вчерашние ошибки за текущие.
Серверы живут, пока ими пользуются, и гасятся после lsp.idle_timeout секунд
(по умолчанию 600) без файловой активности — долгоживущий шлюз, который трогает
много worktree, больше не копит по одному процессу языкового сервера на рабочее
пространство навсегда. Погашенный сервер поднимается автоматически при
следующей подходящей операции с файлом. Поставьте idle_timeout: 0, чтобы
отключить сбор и держать индекс каждого сервера прогретым весь срок жизни
процесса.
Серверы освобождаются и тогда, когда исчезает их воркспейс, даже если они не
простаивают: удаление worktree, которым управляет Hermes (конец сессии
hermes -w, очистка задачи Kanban, подрезанный worktree делегированного
субагента), гасит языковые серверы этого дерева до запуска
git worktree remove, а периодический проход гасит любой сервер, чей корень
проекта больше не существует на диске (удалён в обход Hermes). Этот проход —
часть сборщика простаивающих, поэтому idle_timeout: 0 отключает и сбор по
удалённым корням; освобождение при удалении worktree срабатывает всегда.
Многокорневой сервер отбрасывает только исчезнувшую папку и продолжает
обслуживать соседние корни.
Серверы с поддержкой многокорневых рабочих пространств (пока это pyright) работают одним процессом на процесс Hermes: первый же проект на Python поднимает сервер, а каждый следующий корень проекта — например, соседние git-worktree, которые правят параллельные субагенты, — подключается к тому же серверу дополнительной папкой рабочего пространства, а не поднимает ещё одну копию.
Отключение
Поставьте lsp.enabled: false в config.yaml, чтобы выключить всю
подсистему. Проверка после записи откатится к синтаксическому разбору в
процессе (ast.parse для Python, json.loads для JSON и т. д.), который
работает без изменений ещё с прежних версий.
Чтобы отключить один язык, не трогая весь слой:
lsp:
servers:
rust-analyzer:
disabled: true
Устранение неполадок
hermes lsp status показывает сервер как «missing»
Бинарника нет ни в PATH, ни в <HERMES_HOME>/lsp/bin/. Запустите
hermes lsp install <server_id>, чтобы попробовать автоустановку, либо
поставьте бинарник вручную через обычный тулчейн этого языка.
Раздел Backend warnings в hermes lsp status
Некоторые серверы — это тонкая обёртка вокруг внешнего CLI, который и
выдаёт настоящую диагностику: они нормально запускаются и принимают
запросы, но никогда не отдают ошибок, если соседний бинарник отсутствует.
Самый частый случай — bash-language-server, который делегирует
диагностику shellcheck. Когда в hermes lsp status появляется раздел
Backend warnings, поставьте указанный инструмент через пакетный менеджер
вашей ОС:
apt install shellcheck # Debian / Ubuntu
brew install shellcheck # macOS
scoop install shellcheck # Windows
То же предупреждение один раз записывается в лог при запуске сервера в
~/.hermes/logs/agent.log.
Сервер запускается, но не возвращает диагностику
Загляните в ~/.hermes/logs/agent.log и поищите записи [agent.lsp.client] —
туда попадают и stderr языкового сервера, и ошибки протокола. Некоторым
серверам (особенно rust-analyzer) нужно сначала закончить индексацию всего
проекта, прежде чем они начнут выдавать диагностику по файлам; первая правка
после старта сервера может завершиться без диагностики, а последующие уже её
подхватят.
Сервер упал
Упавший сервер попадает в набор «сломанных» и до конца сессии повторно не
пробуется. Выполните hermes lsp restart, чтобы очистить набор; следующая
правка поднимет его заново.
Правка файла вне любого git-репозитория
По задумке LSP работает только внутри git-репозитория. Если проект ещё не
инициализирован, выполните git init, чтобы включить диагностику LSP. Иначе
сработает запасной вариант — синтаксическая проверка в процессе.