Skip to main content

Language Server Protocol (LSP)

Nastech runs full language servers — pyright, gopls, rust-analyzer, typescript-language-server, clangd, and ~20 more — as background subprocesses and feeds their semantic diagnostics into the post-write lint check used by write_file and patch. When the agent edits a file, it sees exactly the errors that edit introduced — not just syntax errors, but type errors, undefined names, missing imports, and project-wide semantic issues the language server detects.

This is the same architecture top-tier coding agents use. Nastech ships it self-contained: no editor host required, no plugins to install, no separate daemon to manage.

When LSP runs​

LSP is gated on git workspace detection. When the agent's working directory (or the file being edited) is inside a git repository, LSP runs against that workspace. When neither is in a git repo, LSP stays dormant — useful for messaging gateways where the cwd is the user's home directory and there's no project to diagnose.

The check is layered: in-process syntax check first (microseconds), then LSP diagnostics second when syntax is clean. A flaky or missing language server can never break a write — every LSP failure path falls back silently to the syntax-only result.

Concretely, on every successful write_file or patch:

  1. Nastech captures a baseline of current diagnostics for the file.
  2. Performs the write.
  3. Re-queries the language server, filters out diagnostics that were already in the baseline, and surfaces only the new ones.

The agent sees output like:

{
"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>"
}

The lint field carries the syntax-check result (microsecond in-process parse via ast.parse, json.loads, etc.); the lsp_diagnostics field carries the semantic diagnostics from the real language server. Two channels, independent signals — the agent sees a syntax-clean file with semantic problems as lint: ok plus a populated lsp_diagnostics.

Supported languages​

LanguageServerAuto-install
Pythonpyright-langservernpm
TypeScript / JavaScript / JSX / TSXtypescript-language-servernpm
Vue@vue/language-servernpm
Sveltesvelte-language-servernpm
Astro@astrojs/language-servernpm
Gogoplsgo install
Rustrust-analyzermanual (rustup)
C / C++clangdmanual (LLVM)
Bash / Zshbash-language-servernpm
YAMLyaml-language-servernpm
Lualua-language-servermanual (GitHub releases)
PHPintelephensenpm
OCamlocaml-lspmanual (opam)
Dockerfiledockerfile-language-server-nodejsnpm
Terraformterraform-lsmanual
Dartdart language-servermanual (dart sdk)
Haskellhaskell-language-servermanual (ghcup)
Juliajulia + LanguageServer.jlmanual
Clojureclojure-lspmanual
Nixnixdmanual
Zigzlsmanual
Gleamgleam lspmanual (gleam install)
Elixirelixir-lsmanual
Prismaprisma language-servermanual
Kotlinkotlin-language-servermanual
Javajdtlsmanual
PowerShellPowerShellEditorServices (pwsh host)manual (release zip)

For "manual" entries, install the server through whatever toolchain manager makes sense for that language (rustup, ghcup, opam, brew, …). Nastech auto-detects the binary on PATH or in <NASTECH_HOME>/lsp/bin/.

PowerShell​

PowerShellEditorServices isn't a single binary — it's a PowerShell module bundle launched by a pwsh (PowerShell 7+) or powershell host. Setup:

  1. Install PowerShell so pwsh (or Windows powershell) is on PATH.
  2. Download the latest release zip from PowerShellEditorServices releases and extract it.
  3. Point Nastech at the extracted bundle — the directory that contains PowerShellEditorServices/Start-EditorServices.ps1. Either:
    • set lsp.servers.powershell.command: ["/path/to/bundle"] in config.yaml, or
    • extract it to <NASTECH_HOME>/lsp/PowerShellEditorServices, or
    • export PSES_BUNDLE_PATH=/path/to/bundle.

nastech lsp status reports installed once pwsh is found; if the bundle is missing you'll see a one-time warning in the logs with the download link.

A few servers are installed alongside a peer dependency that npm won't auto-pull. The current case is typescript-language-server, which requires the typescript SDK importable from the same node_modules tree — Nastech installs both packages together when you run nastech lsp install typescript or auto-install fires on first use.

CLI​

nastech lsp status # service state + per-server install status
nastech lsp list # registry, optionally --installed-only
nastech lsp install <id> # eagerly install one server
nastech lsp install-all # try every server with a known recipe
nastech lsp restart # tear down running clients
nastech lsp which <id> # print resolved binary path

nastech lsp status is the best starting point — it shows which languages will get semantic diagnostics today and which need a binary installed.

Configuration​

The defaults work for typical setups; nothing to set if the binaries are on PATH.

# config.yaml
lsp:
# Master toggle. Disabling skips the entire subsystem — no servers
# spawn, no background event loop runs.
enabled: true

# How long to wait for diagnostics after each write.
wait_mode: document # "document" or "full"
# Max seconds to wait for the server to re-check the file after an
# edit. Only *fresh* diagnostics (produced for the post-edit
# content) are ever reported; if the server doesn't finish within
# this budget, the edit reports "no LSP data" rather than stale
# errors from before the edit. Raise this for slow servers on big
# projects (tsserver, rust-analyzer mid-indexing).
wait_timeout: 5.0

# How to handle missing server binaries.
# auto — install via npm/pip/go install into <NASTECH_HOME>/lsp/bin
# manual — only use binaries already on PATH
install_strategy: auto

# How long an unused language-server client stays alive (seconds).
# Idle servers are shut down automatically and respawned on the next
# relevant file operation. Set to 0 to disable idle reaping and keep
# servers alive for the life of the process. Values below 30s are
# clamped to 30 so a sweep can never reap a client mid-operation.
idle_timeout: 600

# Per-server overrides (all optional).
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 # skip TS even when its extensions match

Per-server keys​

  • disabled: true — skip this server entirely even when its extensions match a file.
  • command: [bin, ...args] — pin a custom binary path. Bypasses auto-install.
  • env: {KEY: value} — extra env vars passed to the spawned process.
  • initialization_options: {...} — merged into the LSP initializationOptions payload sent in the initialize handshake. Server-specific; consult the language server's docs.

Installation locations​

When install_strategy: auto, Nastech installs binaries into <NASTECH_HOME>/lsp/bin/. NPM packages land in <NASTECH_HOME>/lsp/node_modules/ with bin symlinks one level up. Go binaries come from go install with GOBIN pointed at the staging dir.

Nothing is ever installed to /usr/local/, ~/.local/, or any other shared location — the staging dir is fully Nastech-owned and is removed when you reset the profile.

Performance characteristics​

LSP servers are lazy-spawned on first use. Editing a Python file in a project that's never seen .py traffic spawns pyright; the spawn takes 1-3 seconds for most servers (rust-analyzer can take 10+ on a cold project). Subsequent edits in the same workspace re-use the running server.

The LSP layer adds a few milliseconds to clean writes when no diagnostics are emitted. When diagnostics are emitted, the wait budget is wait_timeout seconds — typically the server responds in tens of milliseconds for pyright/tsserver and a few seconds for rust-analyzer mid-indexing.

Diagnostics are freshness-gated: a result only counts when the server produced it for the content of the current edit (a publishDiagnostics push at/after the change, or a pull request answered after it). Slow servers that haven't re-checked yet result in "no data" for that edit — never in yesterday's errors being re-reported as current.

Servers are kept alive while they're being used and shut down after lsp.idle_timeout seconds (default 600) with no file activity — a long-running gateway that touches many worktrees no longer accumulates one language-server process per workspace forever. A reaped server is respawned automatically on the next relevant file operation. Set idle_timeout: 0 to disable reaping and hold every server's index warm for the life of the process.

Servers that support multi-root workspaces (currently pyright) run as a single process per Nastech process: the first Python project spawns it, and every further project root — for example sibling git worktrees edited by parallel subagents — is attached to that same server as an additional workspace folder instead of starting another copy.

Disabling​

Set lsp.enabled: false in config.yaml to disable the entire subsystem. The post-write check falls back to the in-process syntax check (ast.parse for Python, json.loads for JSON, etc.) which ships unchanged from earlier versions.

To disable a single language without disabling the whole layer:

lsp:
servers:
rust-analyzer:
disabled: true

Troubleshooting​

nastech lsp status shows a server as "missing"

The binary isn't on PATH and isn't in <NASTECH_HOME>/lsp/bin/. Run nastech lsp install <server_id> to attempt an auto-install, or install the binary manually through the language's normal toolchain.

Backend warnings section in nastech lsp status

Some servers ship as thin wrappers around an external CLI for actual diagnostics — they spawn cleanly and accept requests but never emit errors when the sidecar binary is missing. The most common case is bash-language-server, which delegates diagnostics to shellcheck. When nastech lsp status shows a Backend warnings section, install the named tool through your OS package manager:

apt install shellcheck # Debian / Ubuntu
brew install shellcheck # macOS
scoop install shellcheck # Windows

The same warning is logged once at server spawn time in ~/.nastech/logs/agent.log.

Server starts but never returns diagnostics

Check ~/.nastech/logs/agent.log for [agent.lsp.client] entries — both stderr from the language server and protocol errors land there. Some servers (rust-analyzer especially) need to finish a project-wide index before they emit per-file diagnostics; the first edit after server start may complete with no diagnostics, with subsequent edits picking them up.

Server crashed

A crashed server is added to the broken-set and won't be retried for the rest of the session. Run nastech lsp restart to clear the set; the next edit re-spawns.

Editing a file outside any git repo

By design, LSP only runs inside a git repository. If the project isn't yet initialized, run git init to enable LSP diagnostics. Otherwise the in-process syntax-only fallback applies.