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. Mibyan
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 successfulwrite_file or patch:
- Mibyan captures a baseline of current diagnostics for the file.
- Performs the write.
- Re-queries the language server, filters out diagnostics that were already in the baseline, and surfaces only the new ones.
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.
Workspace trust
Many language servers run code that the project itself ships: pyright executes the configured Python interpreter, typescript-language-server loads the project’snode_modules/typescript, svelte-language-server loads
svelte.config.js, rust-analyzer runs cargo check (build scripts,
proc-macros) on every save, and servers such as jdtls,
kotlin-language-server, elixir-ls, zls or haskell-language-server
evaluate the project’s build files (Gradle, mix.exs, build.zig,
Cabal/Stack) when they start. That is fine for your own project, but
not for a repository the agent has just cloned.
Mibyan therefore treats every workspace as untrusted unless it is:
- the git worktree of a directory you pointed Mibyan at: where you
launched it (
cd my-app && mibyan), the worktreemibyan -wcreated, the project a Desktop or TUI session is opened in, or a gateway’sterminal.cwd, or - a directory listed under
lsp.trusted_workspaces(or any directory below one).
cd in the agent’s terminal does not move the session’s workspace.
Cron jobs and Kanban workers get no automatic trust, because the agent
can choose their workdir or workspace; list the directories they should
trust under lsp.trusted_workspaces. A checkout nested inside a trusted
worktree has its own .git, so it is not trusted, and neither is a git
repository at or above your home directory (a dotfiles repo there would
otherwise trust everything below it). Trust covers the whole directory
you pointed Mibyan at, including anything later cloned into it, and
lasts until Mibyan exits.
In an untrusted workspace Mibyan denies by default: only the
servers below start, each with settings that keep it on Mibyan-side
tools. Every other server is skipped, including 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 (it loads the vueCompilerOptions.plugins
a project’s tsconfig.json names) and any server you declare under lsp.servers. The diagnostics
log records skipped: untrusted workspace …; add it to lsp.trusted_workspaces, and mibyan lsp status marks those servers
[trusted workspaces only].
On a local backend, the post-write shell linters that would use the
checkout’s own toolchain are skipped the same way whenever the terminal’s
current directory is untrusted:
npx tsc (it runs
the repository’s node_modules/.bin/tsc, or installs from the
registry its .npmrc names) and rustfmt --check (rustup honours the
repository’s rust-toolchain.toml). Sandboxed backends (Docker, SSH,
Modal, …) are unchanged.
Diagnostics that need the project’s dependencies (for example
unresolved-import warnings) may be less precise until you trust the
workspace.
Supported languages
For “manual” entries, install the server through whatever toolchain
manager makes sense for that language (rustup, ghcup, opam, brew,
…). Mibyan auto-detects the binary on PATH or in
<mibyan_HOME>/lsp/bin/.
PowerShell
PowerShellEditorServices isn’t a single binary — it’s a PowerShell module bundle launched by apwsh (PowerShell 7+) or powershell
host. Setup:
- Install PowerShell so
pwsh(or Windowspowershell) is on PATH. - Download the latest release zip from PowerShellEditorServices releases and extract it.
- Point Mibyan at the extracted bundle — the directory that contains
PowerShellEditorServices/Start-EditorServices.ps1. Either:- set
lsp.servers.powershell.command: ["/path/to/bundle"]inconfig.yaml, or - extract it to
<mibyan_HOME>/lsp/PowerShellEditorServices, or - export
PSES_BUNDLE_PATH=/path/to/bundle.
- set
mibyan 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.
Laravel Blade
.blade.php templates go to laravel-lsp
(Blade, Eloquent, Facades) while plain .php files stay with
intelephense. Install it once with Composer and make sure the binary
is on PATH (or pin it with lsp.servers.laravel-lsp.command):
laravel-lsp lsp (stdio). There is no
auto-install recipe; mibyan lsp status shows manual-only until the
binary is found.
A few servers are installed alongside a peer dependency that npm
won’t auto-pull. typescript-language-server and @vue/language-server
require the typescript SDK importable from the same node_modules
tree — Mibyan installs typescript@6 (the last JavaScript-based line;
TypeScript 7 is the Go port and ships no tsserver.js) together with
the server when you run mibyan lsp install typescript /
mibyan lsp install vue-language-server or auto-install fires on first use.
Vue is pinned to @vue/language-server@2, started with
vue.hybridMode: false so it hosts its own TypeScript service. The 3.x
line only works behind a client-hosted tsserver tunnel (the VS Code /
Neovim setup) that Mibyan’s generic client does not run, so it never
publishes diagnostics. If an earlier Mibyan installed 3.x, the log shows a
one-time vue-language-server: ... 3.x warning; delete
<mibyan_HOME>/lsp/node_modules/@vue and <mibyan_HOME>/lsp/bin/vue-language-server*,
then run mibyan lsp install vue-language-server (the recipe co-installs the
TypeScript SDK).
CLI
mibyan 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.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. Servers and the npm /go installauto-installers start from Mibyan’ scrubbed child environment (no gateway tokens or provider API keys), so a server that needs one of those gets it only through this key.initialization_options: {...}— merged into the LSPinitializationOptionspayload sent in theinitializehandshake. Server-specific; consult the language server’s docs.
Custom servers
Anylsp.servers key that is not a built-in server id declares
your own language server. It needs command and extensions; the
other keys are optional. Custom servers are matched before the
built-ins, so they can also take over an extension Mibyan already
handles.
mibyan lsp status lists it as
installed. A malformed entry is logged and skipped without
affecting the other servers.
Installation locations
Wheninstall_strategy: auto, Mibyan installs binaries into
<mibyan_HOME>/lsp/bin/. NPM packages land in
<mibyan_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 Mibyan-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. Each edit waits twice (a pre-edit
baseline snapshot for the delta, then the post-edit re-check), so a
server that never answers costs at most 2 × wait_timeout per edit.
The very first request against a workspace also pays the spawn and the
server’s initial program build; give large projects room with
lsp.warmup_timeout (only that first, cold request uses it — the
steady-state budget is unchanged) rather than raising wait_timeout,
which would let every later edit block for the cold-build duration.
A server that fails for a workspace — spawn error, or a request that
outran its budget — marks that (server, root) pair broken and every
later request for it is skipped (logged once per root at INFO). By
default the pair stays broken until mibyan lsp restart or process
exit; lsp.broken_retry_seconds: N retries it after N seconds so one
transient stall does not cost the workspace its diagnostics for good.
A root you never want served — one monorepo whose server cannot finish
in any budget — goes in lsp.exclude_roots; other workspaces keep
their servers.
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 are also released when their workspace goes away, even if they
are not idle: removing a Mibyan-managed worktree (mibyan -w session
end, Kanban task cleanup, a delegated subagent’s pruned worktree) shuts
down that tree’s language servers before git worktree remove runs, and
the periodic sweep shuts down any server whose project root no longer
exists on disk (deleted outside Mibyan). The sweep is part of the idle
reaper, so idle_timeout: 0 also disables deleted-root reaping; the
worktree-removal release always runs. A multi-root server only drops the
vanished folder and keeps serving its sibling roots.
Servers that support multi-root workspaces (currently pyright) run as a
single process per Mibyan 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
Setlsp.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:
Troubleshooting
mibyan lsp status shows a server as “missing”
The binary isn’t on PATH and isn’t in <mibyan_HOME>/lsp/bin/. Run
mibyan lsp install <server_id> to attempt an auto-install, or
install the binary manually through the language’s normal toolchain.
Backend warnings section in mibyan 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 mibyan lsp status shows a Backend warnings section, install
the named tool through your OS package manager:
~/.mibyan/logs/agent.log.
Server starts but never returns diagnostics
Check ~/.mibyan/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 mibyan 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.
