diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 2767a84..eed8233 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "zig-lsp", "description": "Zig language server for Claude Code via ZLS", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "agent-sh", "email": "aviarchi1994@gmail.com", @@ -38,9 +38,7 @@ "inlay_hints_show_parameter_name": true, "prefer_ast_check_as_child_process": true }, - "startupTimeout": 30000, - "restartOnCrash": true, - "maxRestarts": 3 + "startupTimeout": 30000 } } } diff --git a/AGENTS.md b/AGENTS.md index 5e8a94e..cf83daa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,10 +27,11 @@ None. This is a config-only plugin - no slash commands, no agents, no skills. ## Plugin shape - non-negotiables -1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. -2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. -3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. -4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. +1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop. Both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Loaded 1 LSP server(s) from plugin: zig-lsp`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. +2. **Don't include `restartOnCrash` or `maxRestarts` in `.lsp.json` or the `marketplace.json` `lspServers` block.** The plugins reference docs them, but the loader rejects them with `"restartOnCrash is not yet implemented. Remove this field from the configuration."` and the entire LSP server fails to initialize. `startupTimeout` works; the other lifecycle fields don't. +3. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. +4. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. +5. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. ## Validation diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e81a7d..6df79ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,23 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.1.1] - 2026-04-26 + ### Fixed -- **`.lsp.json` restored at plugin root.** Empirical testing on Claude Code v2.1.119 showed third-party LSP plugins need both: an `lspServers` block in `marketplace.json` (matches official-plugin shape) AND a `.lsp.json` file at the plugin root (this is what the runtime loader actually reads from the cached install directory). The previous round of changes deleted `.lsp.json` based on an incorrect inference from the official LSP plugins' minimal source layout — those plugins use a separate hardcoded loader path that doesn't apply to third parties. With `.lsp.json` shipped, `claude --debug` logs `Loaded 1 LSP server(s) from plugin: zig-lsp`. Without it, the loader silently skips the entry. +- **Removed `restartOnCrash` and `maxRestarts`** from `.lsp.json` and `marketplace.json`'s `lspServers` block. The Claude Code 2.1.x runtime documents these fields but rejects them at load time with `"restartOnCrash is not yet implemented. Remove this field from the configuration."`, causing the entire LSP server to fail initialization. With these fields present, `claude --debug` showed `Failed to initialize LSP server plugin:zig-lsp:zig` and `.zig` files got `No LSP server available`. Without them, ZLS spawns and `documentSymbol` / `hover` / post-edit diagnostics work end-to-end. `startupTimeout: 30000` is kept (loader accepts it). +- **`.lsp.json` restored at plugin root** (carried from Unreleased). Empirical testing on Claude Code v2.1.119 confirmed third-party LSP plugins need `.lsp.json` in the plugin source so it lands in the cached install dir; without it the loader silently skips the entry. Marketplace.json's `lspServers` field alone is empirically inert for non-`claude-plugins-official` marketplaces. ### Changed -- `agent-knowledge/claude-code-lsp-plugin-contribution.md` correction block expanded with the two-path picture (official hardcoded vs third-party `.lsp.json`) and the empirical evidence table. -- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` "non-negotiables" updated: `.lsp.json` and marketplace `lspServers` are both required, kept byte-equivalent on server config. +- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` plug-shape rules now explicitly forbid `restartOnCrash` / `maxRestarts` until the runtime ships them; reflect the corrected two-file shape (`.lsp.json` + marketplace `lspServers`). +- `plugins/zig-lsp/README.md` configuration table no longer lists `restartOnCrash` / `maxRestarts`; gotchas section notes that a crashed ZLS stays dead until session restart. +- `agent-knowledge/claude-code-lsp-plugin-contribution.md` correction block adds the empirical row showing the loader rejects the unimplemented fields, and notes a third valid config location (`lspServers` inline in `.claude-plugin/plugin.json` — the path `michelsciortino/dart-lsp` uses; verified working on 2.1.119). - Top-level `README.md` repo layout reflects `.lsp.json` back in the plugin source. +### Empirical verification +Tested end-to-end against `cockpit/src-tauri/sidecars/agent-probe-zig` (Zig 0.17, ~900 LOC `main.zig`). After `0.1.1` install: +- `Loaded 1 LSP server(s) from plugin: zig-lsp` in debug log +- ZLS spawned, `didOpen` succeeded, `publishDiagnostics` returned 0 issues +- `documentSymbol` returned 30+ top-level functions with line numbers +- `hover` on a struct identifier returned the full type definition + ## [0.1.0] - 2026-04-26 ### Added - Initial `zig-lsp` plugin for Claude Code, dispatching to user-installed `zls` - `agent-sh` marketplace shell hosting `zig-lsp` (and ready for future LSP plugins) -- `.lsp.json` mapping `.zig` and `.zon` to language ID `zig`, with `enable_build_on_save`, `restartOnCrash`, and a 30 s startup timeout +- `.lsp.json` mapping `.zig` and `.zon` to language ID `zig`, with `enable_build_on_save` and a 30 s startup timeout - `agent-knowledge/` research notes covering Claude Code's LSP plugin model, the `.lsp.json` schema, the contribution flow, and `zls` operator details (24 sources) - `.agnix.toml` (target `claude-code`, `agent-knowledge/` excluded) and a CI workflow running `agent-sh/agnix@v0.20.1` on push and PR -[Unreleased]: https://github.com/agent-sh/zig-lsp/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/agent-sh/zig-lsp/compare/v0.1.1...HEAD +[0.1.1]: https://github.com/agent-sh/zig-lsp/releases/tag/v0.1.1 [0.1.0]: https://github.com/agent-sh/zig-lsp/releases/tag/v0.1.0 diff --git a/CLAUDE.md b/CLAUDE.md index 5e8a94e..cf83daa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,10 +27,11 @@ None. This is a config-only plugin - no slash commands, no agents, no skills. ## Plugin shape - non-negotiables -1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. -2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. -3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. -4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. +1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop. Both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Loaded 1 LSP server(s) from plugin: zig-lsp`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. +2. **Don't include `restartOnCrash` or `maxRestarts` in `.lsp.json` or the `marketplace.json` `lspServers` block.** The plugins reference docs them, but the loader rejects them with `"restartOnCrash is not yet implemented. Remove this field from the configuration."` and the entire LSP server fails to initialize. `startupTimeout` works; the other lifecycle fields don't. +3. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. +4. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. +5. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. ## Validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59340b4..edea13a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,10 +45,11 @@ Update `CHANGELOG.md` in the same PR. Keep entries under the `## [Unreleased]` h ## Plugin shape - non-negotiables 1. **Ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`) AND keep an `lspServers` block inline in `.claude-plugin/marketplace.json`. Both are required for a third-party LSP plugin: the runtime loader reads `.lsp.json` from the user's cached install (this is what makes the LSP register on Claude Code v2.1.119); the marketplace `lspServers` field is metadata for the marketplace UI and matches what official plugins ship. Official LSP plugins (typescript-lsp, rust-analyzer-lsp) load via a separate hardcoded code path inside the Claude Code binary and don't ship `.lsp.json` themselves; that path doesn't apply to third parties. -2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. -3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - `build.zig.zon` needs LSP support. -4. Keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal on the server config. If they drift, behavior depends on which file the loader picks up first. -5. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit. +2. **Do not include `restartOnCrash` or `maxRestarts`.** The plugins reference docs them, but the loader rejects them with `"restartOnCrash is not yet implemented. Remove this field from the configuration."` and the entire LSP server fails to initialize. `startupTimeout` works; the other lifecycle fields don't. Re-add when Claude Code ships them. +3. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. +4. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - `build.zig.zon` needs LSP support. +5. Keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal on the server config. If they drift, behavior depends on which file the loader picks up first. +6. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit. ## Style diff --git a/agent-knowledge/claude-code-lsp-plugin-contribution.md b/agent-knowledge/claude-code-lsp-plugin-contribution.md index d370d69..15857e2 100644 --- a/agent-knowledge/claude-code-lsp-plugin-contribution.md +++ b/agent-knowledge/claude-code-lsp-plugin-contribution.md @@ -26,8 +26,14 @@ > | Plugin in `agent-sh` marketplace, only `lspServers` in marketplace.json (no `.lsp.json`) | Silently skipped. `Checking plugin zig-lsp` appears in debug log, no `Loaded ... LSP server(s)` line follows. | > | Same plugin replicated under `claude-plugins-official` marketplace cache | Loads. Hardcoded path. | > | Plugin in `agent-sh` marketplace WITH `.lsp.json` at plugin root → cached in install dir | **Loads.** `Loaded 1 LSP server(s) from plugin: zig-lsp`. | +> | Plugin with `lspServers` inline in `.claude-plugin/plugin.json` (no `.lsp.json` and no marketplace.json `lspServers`) | **Also loads.** This is a third valid path the docs don't cover; `michelsciortino/dart-lsp` uses it. | +> | Manifest with `restartOnCrash: true` or `maxRestarts: N` | **Initialization fails.** Loader logs `[ERROR] LSP server '': restartOnCrash is not yet implemented. Remove this field from the configuration.` The entire LSP server is rejected, not just the field. `startupTimeout` works fine. | > -> **Updated key facts (canonical):** A working third-party LSP plugin ships `.lsp.json` at the plugin root + an `lspServers` block in `.claude-plugin/marketplace.json` (kept byte-equivalent on server config) + a `plugin.json` for identity metadata + `README.md`. Binary name `zls`. Extensions `.zig` and `.zon` → language `"zig"`. Submission to the official marketplace is form-gated at . +> **Updated key facts (canonical, as of v2.1.119):** +> - Three working config locations for a third-party LSP plugin: (a) `.lsp.json` at plugin root (recommended; what we ship), (b) `lspServers` inline in `.claude-plugin/plugin.json`, (c) `lspServers` inline in `.claude-plugin/marketplace.json` for plugins under `claude-plugins-official` only. +> - **Do not include `restartOnCrash` or `maxRestarts`** in any of these locations until Claude Code's loader implements them. `startupTimeout` is fine. +> - Plugin needs at minimum `.claude-plugin/plugin.json` (identity), `README.md`, and one of the three config locations above. +> - Binary name `zls`. Extensions `.zig` and `.zon` → language `"zig"`. Submission to the official marketplace is form-gated at . > > The sections below are kept verbatim with the original `.lsp.json`-only framing as historical record. Refer to `CONTRIBUTING.md` and `CLAUDE.md` in this repo for the corrected, currently-accurate guidance. diff --git a/plugins/zig-lsp/.claude-plugin/plugin.json b/plugins/zig-lsp/.claude-plugin/plugin.json index f11e707..c553002 100644 --- a/plugins/zig-lsp/.claude-plugin/plugin.json +++ b/plugins/zig-lsp/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "zig-lsp", "description": "Zig language server for Claude Code (requires zls in PATH)", - "version": "0.1.0", + "version": "0.1.1", "author": { "name": "agent-sh", "email": "aviarchi1994@gmail.com", diff --git a/plugins/zig-lsp/.lsp.json b/plugins/zig-lsp/.lsp.json index e0176ef..6161e6c 100644 --- a/plugins/zig-lsp/.lsp.json +++ b/plugins/zig-lsp/.lsp.json @@ -15,8 +15,6 @@ "inlay_hints_show_parameter_name": true, "prefer_ast_check_as_child_process": true }, - "startupTimeout": 30000, - "restartOnCrash": true, - "maxRestarts": 3 + "startupTimeout": 30000 } } diff --git a/plugins/zig-lsp/README.md b/plugins/zig-lsp/README.md index 6d0aacb..49abf47 100644 --- a/plugins/zig-lsp/README.md +++ b/plugins/zig-lsp/README.md @@ -80,12 +80,12 @@ Defaults set by this plugin: | `inlay_hints_show_parameter_name` | `true` | Inline parameter names at call sites | | `prefer_ast_check_as_child_process` | `true` | Faster, more accurate parser-level errors | | `startupTimeout` | `30000` ms | Conservative — ZLS usually starts in under 3 s | -| `restartOnCrash` | `true` | Recover from rare crashes mid-session | -| `maxRestarts` | `3` | Cap restart loop on persistent failures | + +`restartOnCrash` and `maxRestarts` are documented in the Claude Code plugin reference but are not yet implemented in the loader on Claude Code 2.1.x — including them in the manifest fails plugin initialization with `"restartOnCrash is not yet implemented. Remove this field from the configuration."` They're omitted here until the runtime ships them. ## Gotchas -- **`zls` and `zig` must be the same version.** A mismatch crashes ZLS on startup. With `restartOnCrash: true` you'll burn through `maxRestarts` per session. ZVM keeps them in lockstep — recommended. +- **`zls` and `zig` must be the same version.** A mismatch crashes ZLS on startup, and Claude Code currently does not auto-restart the server (`restartOnCrash` is unimplemented). A crashed ZLS stays dead until the session restarts. ZVM keeps `zls` and `zig` in lockstep — recommended. - **`comptime`-heavy code:** ZLS cannot fully evaluate complex `comptime` expressions. The build-on-save step covers most of this gap. - **`build.zig.zon`:** the package manifest is mapped to the `zig` language so you get LSP support inside it. Without this mapping, `.zon` files would be opaque. - **Windows:** the binary is `zls.exe`, but the `command` field is just `"zls"` — the OS resolves the extension. Make sure the directory containing `zls.exe` is on `PATH`.