Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
}
}
}
Expand Down
9 changes: 5 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@
2. **No unnecessary files** - Don't create summary files, plan files, audit files, or temp docs.
3. **Task is not done until CI passes** - agnix workflow must be green before merge.
4. **Create PRs for non-trivial changes** - No direct pushes to main.
5. **Always run git hooks** - Never bypass pre-commit or pre-push hooks.

Check warning on line 23 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'Always' at 43 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
6. **Use single dash for em-dashes** - In prose, use ` - ` (single dash with spaces), never ` -- `.

Check warning on line 24 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'never' at 45 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
7. **Mirror CLAUDE.md and AGENTS.md byte-identically** - When editing one, update the other in the same commit.
8. **Token efficiency** - Save tokens over decorations.

## 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.

Check warning on line 30 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 57 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
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

Expand Down
22 changes: 17 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Retroactively modifying the 0.1.0 entry to remove restartOnCrash makes the changelog historically inaccurate, as that version did include those fields in its manifest. It is generally better to keep historical entries as they were released and document the removal in the current version's section.

Suggested change
- `.lsp.json` mapping `.zig` and `.zon` to language ID `zig`, with `enable_build_on_save` and a 30 s startup timeout
- .lsp.json mapping .zig and .zon to language ID zig, with enable_build_on_save, restartOnCrash, 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
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@
2. **No unnecessary files** - Don't create summary files, plan files, audit files, or temp docs.
3. **Task is not done until CI passes** - agnix workflow must be green before merge.
4. **Create PRs for non-trivial changes** - No direct pushes to main.
5. **Always run git hooks** - Never bypass pre-commit or pre-push hooks.

Check warning on line 23 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'Always' at 43 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
6. **Use single dash for em-dashes** - In prose, use ` - ` (single dash with spaces), never ` -- `.

Check warning on line 24 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'never' at 45 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
7. **Mirror CLAUDE.md and AGENTS.md byte-identically** - When editing one, update the other in the same commit.
8. **Token efficiency** - Save tokens over decorations.

## 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.

Check warning on line 30 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 57 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]

Check warning on line 30 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 57 percent of document (middle zone) [CC-MEM-008]
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.

Check warning on line 31 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Negative instruction 'Don't' without positive alternative [CC-MEM-006]
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.

Check warning on line 33 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Negative instruction 'Don't' without positive alternative [CC-MEM-006]
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

Expand Down
9 changes: 5 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion agent-knowledge/claude-code-lsp-plugin-contribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<id>': 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 <https://clau.de/plugin-directory-submission>.
> **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 <https://clau.de/plugin-directory-submission>.
>
> 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.

Expand Down
2 changes: 1 addition & 1 deletion plugins/zig-lsp/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 1 addition & 3 deletions plugins/zig-lsp/.lsp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 3 additions & 3 deletions plugins/zig-lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Loading