fix(lsp): start a worktree's Go language server with GOWORK=off (#521) - #542
Merged
Merged
Conversation
In a git worktree under an enclosing go.work that lists the main checkout's directory for the module, gopls resolved that go.work, in which the worktree is not a module. workspace_symbols answered from the main checkout (or not at all), and the worktree's files were never type-checked, so a post-write diagnostics pass labelled "authoritative" reported clean code that go build rejected. #506 already makes this per-directory GOWORK=off decision for git hooks, run_task and mutation_test. The pool now applies it too, keyed on the root, when it spawns a Go language server: - tools.GoWorkBypass (renamed from goWorkBypass) is the one decision; the pool lives in internal/cli, above internal/tools, so nothing moves layer. - An explicit GOWORK always wins: inherited, [lsp.go] env, the go env file or $GOROOT/go.env (all read by the decision), and gopls's own `env` setting in [lsp.go] initialization_options, which gopls applies over its process environment. - Only the Go server is considered; GOWORK means nothing to the others. - The daemon log says when a server starts with GOWORK=off, and session_start (full and brief) shows a "Go LSP:" line naming the go.work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Plumb-Session: giant-bison
atlas-from-plumb
enabled auto-merge (rebase)
September 30, 2026 19:18
golimpio
approved these changes
Sep 30, 2026
golimpio
left a comment
Contributor
There was a problem hiding this comment.
Re-approve after updating with main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #521
Defect
In a git worktree of a Go module, under an enclosing
go.workthat lists the main checkout's directory for that module, gopls answers about the main checkout instead of the worktree. This is the #505 layout, e.g.plumb-ops/go.workhasuse ./plumb, and the worktree is atplumb/.claude/worktrees/<name>.workspace_symbolsreturns nothing, or the main checkout's copy.authoritative. I reproduced this live in this worktree before the fix: writing a file with an unusedosimport returned[diagnostics: authoritative post-write pass] ✓ … no new errors.Cause
#506 makes the per-directory
GOWORK=offdecision for the git child (hooks),run_taskandmutation_test(internal/tools/git_gowork.go). The pool never applied it when it spawned a language server. So gopls resolved the enclosinggo.work, and the worktree is not a module in it.Fix
goWorkBypassis now exported astools.GoWorkBypass. The spawner isinternal/cli/pool.go(presentation), which sits aboveinternal/tools(application), so nothing had to move layer.internal/lsp.Supervisoronly receives the env.goLSPEnv(internal/cli/pool_gowork.go) applies the decision to the Go server's env, keyed on the workspace root. Only the Go server gets it, becauseGOWORKmeans nothing to other servers.GOWORKyou set always wins. The decision already reads the inherited environment,[lsp.go] env(both are in the env it is handed), the go env file and$GOROOT/go.env. On top of that, aGOWORKin gopls's ownenvsetting ([lsp.go.initialization_options] env) is honoured, because gopls applies it over its process environment.pool: starting the Go language server with GOWORK=off: …withrootandgo_work.session_start, full and brief, showsGo LSP: runs with GOWORK=off — <go.work> lists another copy of this module (set GOWORK in [lsp.go] env to override).poolEntry.goWorkOff→pool.goWorkOffFor→routingProxy.GoWorkOff→connSession.lspGoWorkOff→SessionStart.WithLSPGoWorkOff.docs/configuration.md, the git-child environment section) and a CHANGELOG entry under 0.20.4 → Fixed.Diagnostics labelling
The label now tells the truth for this layout, because the server loads the worktree's module. I did not add a separate "outside every loaded module" check. It can't be detected cleanly:
authoritativemeans the server re-published for the file after the write, and it did: gopls published an empty set for a file outside its loaded packages, with no "no active builds" message to match on.With this fix the auto-detected worktree case is gone: the integration test below shows gopls now publishes the unused-import error for the worktree's file. What remains is an explicit
GOWORKthe user chose, which plumb honours by design.Verification
Red first, on origin/main.
TestPoolSpawn_GoWorkOffForWorktreeUnderEnclosingGoWorkfailed withworktree server GOWORK = "" (set=false), want "off". The stand-in server is/bin/shrecording its own env, spawned throughpool.acquireLang.TestIntegration_PoolGoplsWorktreeUnderGoWork(real gopls,//go:build integration) failed withworkspace/symbol SharedIssue521 from the worktree root = [.../main/a.go], want exactly [.../main/.claude/worktrees/wt/b.go]. That is the LSP in a git worktree under an enclosing go.work answers from the main checkout #521 symptom exactly.Green after.
c.go. WithGOWORK=offmutated out, the worktree half fails:no unused-import diagnostic published for …/wt/c.go.Left-alone direction.
TestPoolSpawn_GoWorkLeftAlonecovers four cases:go.work;GOWORK;[lsp.go] env GOWORK;initialization_options.env.GOWORK.In every case the env is untouched and the pool reports nothing.
TestGoLSPEnv_OnlyTheGoServerchecks that a non-Go server never getsGOWORK=off.TestSessionStart_GoWorkOffLinechecks the full and brief renders, both present and absent.mutation_test: 9 mutants, all KILLED.
internal/cli: revertGOWORK=off; drop the Go-only gate; ignore gopls's env setting; spawn with the unmodified env at the call site; stop recordinggoWorkOffon the entry.internal/tools: drop the line from the brief render; drop it from the full render; render it when nothing was switched off.CI-faithful run.
GOWORK=off GOTMPDIR=$PWD/.testcache go test ./internal/tools/... ./internal/cli/... ./internal/arch/... ./internal/mcp/... ./internal/tui/... -count=1passed. The integration tests passed with-tags integration, including the existingTestIntegration_PoolHibernateWake.Lint and checks.
golangci-lint run ./...reports 0 issues.make check-size check-brief check-changelogpasses.Not unit-tested: the one-line accessor glue (
routingProxy.GoWorkOff,connSession.lspGoWorkOff), which follows the existingDiagModechain.🤖 Generated with Claude Code