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:

  1. Hermes снимает базовый срез текущей диагностики по файлу.
  2. Выполняет запись.
  3. Заново опрашивает языковой сервер, отсеивает диагностику, которая уже была в базовом срезе, и показывает только новую.

Агент получает примерно такой вывод:

{
  "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].

СерверНедоверенный воркспейс
pyrightVIRTUAL_ENV или управляемый Hermes Python, но не .venv/venv проекта
typescript-language-servertsserver.path закреплён на TypeScript рядом с сервером; если его нет, сервер пропускается
svelte-language-serverisTrusted: 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

Поддерживаемые языки

ЯзыкСерверАвтоустановка
Pythonpyright-langservernpm
TypeScript / JavaScript / JSX / TSXtypescript-language-servernpm
Vue@vue/language-servernpm
Sveltesvelte-language-servernpm
Astro@astrojs/language-servernpm
Gogoplsgo install
Rustrust-analyzerвручную (rustup)
C / C++clangdвручную (LLVM)
Bash / Zshbash-language-servernpm
YAMLyaml-language-servernpm
Lualua-language-serverвручную (релизы на GitHub)
PHPintelephensenpm
Laravel Blade (.blade.php)laravel-lspвручную (composer)
OCamlocaml-lspвручную (opam)
Dockerfiledockerfile-language-server-nodejsnpm
Terraformterraform-lsвручную
Dartdart language-serverвручную (dart sdk)
Haskellhaskell-language-serverвручную (ghcup)
Juliajulia + LanguageServer.jlвручную
Clojureclojure-lspвручную
Nixnixdвручную
Zigzlsвручную
Gleamgleam lspвручную (gleam install)
Elixirelixir-lsвручную
Prismaprisma language-serverвручную
Kotlinkotlin-language-serverвручную
Javajdtlsвручную
PowerShellPowerShellEditorServices (хост pwsh)вручную (zip релиза)

Для пунктов с пометкой «вручную» ставьте сервер тем менеджером инструментов, который уместен для этого языка (rustup, ghcup, opam, brew, …). Hermes сам находит бинарник в PATH или в <HERMES_HOME>/lsp/bin/.

PowerShell

PowerShellEditorServices — не один бинарник, а набор модулей PowerShell, который запускается хостом pwsh (PowerShell 7+) или powershell. Порядок настройки:

  1. Установите PowerShell, чтобы pwsh (или powershell в Windows) оказался в PATH.
  2. Скачайте свежий zip релиза со страницы релизов PowerShellEditorServices и распакуйте его.
  3. Укажите 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: {...} — вливается в полезную нагрузку LSP initializationOptions, которую отправляют в рукопожатии 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. Иначе сработает запасной вариант — синтаксическая проверка в процессе.

перевод и редактура:

обновлено

⌕ ESC