[tools] gopls/doc/design: add gopls CLI prototype design docs

3 views
Skip to first unread message

Hyang-Ah Hana Kim (Gerrit)

unread,
Apr 19, 2026, 11:19:23 PMApr 19
to goph...@pubsubhelper.golang.org, Hyang-Ah Hana Kim, golang-co...@googlegroups.com

Hyang-Ah Hana Kim has uploaded the change for review

Commit message

gopls/doc/design: add gopls CLI prototype design docs

Add design documentation for the gopls CLI prototype:
- gopls-cli-prototype.md: design and implementation notes for the
session-pool + agent-friendly CLI; documents all subcommands,
flags (--body, --dry-run, --severity, --preserve), and the
edit-producing rename flow.
- gopls-cli-migration.md: migration plan from legacy top-level
commands to the new CLI suite, including parity gaps (now shipped)
and phased rollout strategy.
- gopls-cli-reference.md: target reference for the CLI commands.

For #gopls-cli
Change-Id: I5648d521303f626d60cd32cf1eee6c31e1d4a8f3

Change diff

diff --git a/gopls/doc/design/gopls-cli-migration.md b/gopls/doc/design/gopls-cli-migration.md
new file mode 100644
index 0000000..6bc9582
--- /dev/null
+++ b/gopls/doc/design/gopls-cli-migration.md
@@ -0,0 +1,301 @@
+---
+title: "gopls CLI migration plan"
+---
+
+## Status
+
+**Draft.** Companion to [gopls-cli-prototype.md](gopls-cli-prototype.md)
+and a concrete proposal for resolving
+[golang/go#63693](https://github.com/golang/go/issues/63693) ("make a
+decision about the gopls CLI").
+
+This doc proposes the end-state CLI surface, explains how today's
+two CLI surfaces (legacy top-level and the `gopls cli` prototype)
+collapse into it, and sequences the work. The target reference —
+sample help text for every end-state command — lives in
+[gopls-cli-reference.md](gopls-cli-reference.md).
+
+Two items are explicitly out of scope for this plan's MVP and
+tracked separately: **interactive refactorings**
+(`gopls.modify_tags`, `gopls.implement_interface` via
+`command/resolve`) and the **configuration schema** for
+`format`/`imports`/`check`/`vet`. Both are discussed briefly
+below but deferred.
+
+## Context: two CLI surfaces today
+
+The gopls binary hosts commands under two paths:
+
+- **Legacy top-level** (`gopls definition`, `gopls rename`,
+ `gopls check`, ...): hand-use tools adopted over time by scripts
+ and agents. Neither LSP-faithful nor agent-friendly.
+- **`gopls cli` prototype** (`gopls cli def`, ...): agent-friendly
+ defaults (symbol lookup, 1-based UTF-8, pull diagnostics, session
+ pooling), but still a ~1:1 translation of LSP verbs under a `cli`
+ prefix. See [gopls-cli-prototype.md](gopls-cli-prototype.md).
+
+Neither serves agent and script use cases cleanly.
+
+## End-state: two CLI surfaces, one binary
+
+The binary hosts two CLI surfaces with disjoint purposes:
+
+- **Top-level** commands (`gopls def`, `gopls rename`, ...) shaped
+ around user/agent use cases, not LSP verbs. Stable, documented,
+ agent-friendly. This is the surface agents and scripts should
+ use.
+- **`gopls cli <verb>`** — LSP methods exposed 1:1 as subcommands,
+ for debugging, conformance checks, and reproducible invocation
+ in bug reports. Best-effort output, not a supported integration
+ point. See [Low-level LSP primitives under `gopls cli`](#low-level-lsp-primitives-under-gopls-cli).
+
+### Design principles
+
+- **Answer-shaped, not coordinate-shaped.** `gopls def --body`
+ returns location + decl source; `gopls refs --context=N` includes
+ surrounding source; `gopls hover` returns signature + doc.
+ Follow-up reads the agent would otherwise make are folded in.
+- **Use cases, not LSP verbs.** `gopls rename` validates and applies
+ in one command (no separate `prepare-rename`); `gopls callers` /
+ `gopls callees` wrap `prepareCallHierarchy` +
+ `incomingCalls`/`outgoingCalls`.
+- **Human/Agent-friendly defaults.** 1-based UTF-8 positions; clamping of
+ out-of-range coordinates; pull diagnostics; session pooling; JSON
+ via `--json`; terse text by default.
+- **Bounded iteration: one pass.** `gopls check --fix` applies
+ quickfixes in a single pass and reports remaining diagnostics;
+ `gopls codeaction` applies one action at a position. If fixes
+ introduce new diagnostics, the agent drives the loop.
+
+### Proposed surface
+
+| Command | Purpose |
+|----------------------------------------------------------------|------------------------------------------------------------------|
+| `gopls def SYMBOL --in FILE [--body]` | Locate a symbol; optionally include decl source |
+| `gopls refs SYMBOL --in FILE [--context=N]` | Find references; optionally with surrounding source |
+| `gopls impl SYMBOL --in FILE [--context=N]` | Find implementations |
+| `gopls hover SYMBOL --in FILE [--body]` | Signature + doc comment; optionally full body |
+| `gopls symbols FILE [--signatures]` | Top-level decls; optionally with signatures |
+| `gopls wsymbols QUERY` | Workspace symbol search |
+| `gopls callers SYMBOL --in FILE [--depth=N] [--context=N]` | Incoming call graph |
+| `gopls callees SYMBOL --in FILE [--depth=N] [--context=N]` | Outgoing call graph |
+| `gopls rename SYMBOL --in FILE --to NEW [-w\|-d\|-l\|--preserve\|--dry-run]` | Validate + apply rename |
+| `gopls check [FILE...] [--severity=S] [--fix] [-w\|-d\|-l\|--preserve]` | Pull diagnostics; `--fix` applies quickfixes |
+| `gopls vet [FILE...] [--severity=S] [--fix] [-w\|-d\|-l\|--preserve]` | Filtered to cmd/vet analyzers |
+| `gopls format FILE... [-w\|-d\|-l\|--preserve]` | Apply gofmt |
+| `gopls imports FILE... [-w\|-d\|-l\|--preserve]` | Organize imports |
+| `gopls codeaction FILE:LINE:COL [--kind KIND] [-w\|-d\|-l\|--preserve]` | List or apply a code action (quickfix, refactor, source.*) |
+
+Interactive refactorings (`gopls.modify_tags`,
+`gopls.implement_interface`) are deferred to a follow-up; see
+[Interactive refactoring on the CLI](#interactive-refactoring-on-the-cli).
+Full help text in [gopls-cli-reference.md](gopls-cli-reference.md).
+
+## Low-level LSP primitives under `gopls cli`
+
+`gopls cli <verb>` exposes LSP methods 1:1 as CLI subcommands. It is
+not a replacement for the top-level surface; it is a scriptable
+debug path.
+
+### Why keep a low-level surface
+
+- **Skill-side LSP conformance.** Agent skills that depend on
+ specific gopls responses can smoke-test them against a real server
+ without wiring an LSP client.
+- **Reproducible bug reports.** `gopls cli execute gopls.run_tests …`
+ is a one-line repro; `-rpc.trace` + a harness is not.
+- **Debugging without an editor.** Inspecting what gopls returns for
+ `semanticTokens/full` or `foldingRange` on a given file currently
+ requires either an editor plugin or a custom test binary.
+- **Preservation, at near-zero cost.** Several commands are in the
+ binary today (`semtok`, `execute`, `folding_range`, ...) and would
+ otherwise be deleted with no replacement; keeping them namespaced
+ under `cli` costs a dispatch entry each.
+
+### Contents
+
+| Subcommand | LSP method | Rationale |
+|---------------------------------|------------------------------------|-------------------------------------------------------|
+| `gopls cli execute CMD [ARGS]` | `workspace/executeCommand` | Raw access to every `gopls.*` command verb |
+| `gopls cli semtok FILE` | `textDocument/semanticTokens/full` | Debug semantic-token computation |
+| `gopls cli links FILE` | `textDocument/documentLink` | Inspect document links; niche |
+| `gopls cli codelens FILE` | `textDocument/codeLens` | Inspect per-file code lenses |
+| `gopls cli folding-range FILE` | `textDocument/foldingRange` | Inspect fold regions |
+| `gopls cli highlight POS` | `textDocument/documentHighlight` | Raw view of a method whose agent use case is `refs` |
+| `gopls cli signature POS` | `textDocument/signatureHelp` | Raw view of a method whose agent use case is `hover` |
+| `gopls cli prepare-rename POS` | `textDocument/prepareRename` | Raw view of a method whose agent use case is `rename --dry-run` |
+
+### Contract
+
+- **1:1 with LSP.** Each subcommand is a thin dispatcher around a
+ single LSP method. No answer-shape folding, no follow-up reads,
+ no quality-of-life enrichment. Agents should use the top-level
+ commands; `gopls cli` is for debugging.
+- **Output.** `--json` emits the server response verbatim (as
+ serialized by the protocol package). Text output is a minimal,
+ best-effort pretty-print; not guaranteed stable.
+- **Position convention.** 1-based UTF-8 bytes on input, matching
+ the rest of the CLI. `--json` output preserves LSP-native
+ coordinates (0-based UTF-16) as the spec requires; text output
+ converts to 1-based UTF-8. Users who want pure LSP coordinates
+ everywhere can read the JSON.
+- **Stability.** `gopls cli <verb>` output is best-effort faithful
+ to the LSP spec and the gopls server's implementation of it.
+ Shapes may change as the spec evolves or as internal types are
+ refactored. Not part of gopls' top-level CLI stability surface;
+ not a supported integration point for agent skills.
+
+### Primitives-only, not a full LSP mirror
+
+`gopls cli` hosts only the primitives above — it is not a 1:1
+mirror of every top-level command. No `gopls cli def`, `cli refs`,
+`cli hover` returning raw LSP types. The top-level surface is the
+one we commit to; duplicate entries add confusion ("which do I
+use?") with no user we can name today. Revisit if skill authors
+make the case for raw-LSP versions of existing top-level commands.
+
+## Configuration
+
+`format`, `imports`, `vet`, and `check` are only meaningfully
+better than their external equivalents (`gofmt`, `goimports`,
+`go vet`) when they apply the user's gopls settings. The current
+CLI ignores most of them, so `gopls format` / `gopls imports`
+degrade to `gofmt` / `goimports` with extra latency.
+
+**Proposed direction**: a documented config schema, searched as
+`./gopls.json` or `.gopls.json` (workspace) →
+`$XDG_CONFIG_HOME/gopls/config.json` (user) → built-in defaults,
+overridable via `--config PATH` or `GOPLS_CONFIG`. Editor plugins
+are encouraged to read the same files; reusing editor-specific
+configs directly is rejected as fragile. Piggybacking on
+`go env GOPLSCONFIG` is a possible alternative.
+
+Until config lands, `format` and `vet` are hard to justify over
+their external counterparts — the schema is a post-Phase-1 follow-up
+that graduates those commands from "equivalent with latency" to
+"strictly better inside a workspace." Coordination with the gopls
+team and editor-plugin owners required; tracked in
+[Open questions](#open-questions).
+
+## Migration
+
+### Mapping from today to end-state
+
+| Today | End-state |
+|---------------------------------|-------------------------------------------------------------------------------------------------------------|
+| `gopls definition` | Redesigned as `gopls def`; legacy shape deprecated one release, then deleted. |
+| `gopls references` | → `gopls refs` |
+| `gopls implementation` | → `gopls impl` |
+| `gopls rename` | Redesigned (validate + apply + dry-run). Same name. Legacy flag shape preserved where compatible. |
+| `gopls check` | Redesigned: pull-diagnostic, severity filter preserved. Same name. |
+| `gopls format`, `gopls imports` | Same names; agent-friendly defaults. |
+| `gopls codeaction` | Same name, same semantics; no rename. Listing and applying are one command: no `--kind` lists, `--kind K` applies. Covers quickfix, refactor, and `source.*` actions — not just fixes. |
+| `gopls call_hierarchy` | Split into `gopls callers` + `gopls callees`. |
+| `gopls prepare_rename` | Agent use case folded into `gopls rename --dry-run`; raw LSP view preserved as `gopls cli prepare-rename`. |
+| `gopls signature` | Agent use case covered by `gopls hover`; raw LSP view preserved as `gopls cli signature`. |
+| `gopls highlight` | Agent use case covered by `gopls refs`; raw LSP view preserved as `gopls cli highlight`. |
+| `gopls folding_range` | → `gopls cli folding-range` (debug only; no agent use case at top-level). |
+| `gopls semtok` | → `gopls cli semtok` (debug only). |
+| `gopls links` | → `gopls cli links` (niche). |
+| `gopls codelens` | → `gopls cli codelens`. Revisit a top-level command if agents want test-lens invocation. |
+| `gopls execute` | → `gopls cli execute`. Raw `workspace/executeCommand` for debugging. |
+| `gopls vulncheck` | Keep as-is (not code-intelligence). |
+| `gopls stats`, `remote`, `mcp` | Keep as-is (admin / peer commands). |
+| `gopls cli <op>` (prototype) | Prototype agent subcommands removed; namespace hosts low-level LSP primitives only (see [Low-level LSP primitives under `gopls cli`](#low-level-lsp-primitives-under-gopls-cli)). |
+
+### Phased rollout
+
+**Phase 0 — Fill in the remaining agent-utility additions under
+`gopls cli`.** The prototype already carries a working baseline
+(edit-flag-aware `rename`, `--dry-run`, `--severity`, `--preserve`,
+`--body`). Three end-state commands in the [Proposed surface](#proposed-surface)
+still need implementations:
+
+- `callers` / `callees` — wrap `prepareCallHierarchy` +
+ `incomingCalls` / `outgoingCalls`, with `--depth=N` and
+ `--context=N`.
+- `refs --context=N` — include surrounding source per reference.
+- `symbols --signatures` — include each top-level decl's signature.
+
+Each ships as its own CL under `cli`. The namespace is unannounced
+with no public users, so iteration is free. Config loading and
+interactive refactorings are separate follow-ups, not Phase 0 gates.
+
+**Phase 1 — Promote to top-level, narrow `cli`.** Single cut-over.
+Redesigned commands register at the top level; `gopls cli <op>` is
+narrowed in the same CL — every current agent subcommand
+(`def`, `refs`, `hover`, `rename`, `check`, ...) is removed, and
+the low-level primitives listed in
+[Low-level LSP primitives](#low-level-lsp-primitives-under-gopls-cli)
+are wired in (via direct move for those already present as legacy
+top-level commands, new implementation for any that are missing).
+Legacy top-level commands that keep their name get the new
+semantics; those with diverging names (e.g. `gopls definition` →
+`gopls def`) emit a one-line deprecation notice and dispatch to the
+new command. Legacy commands with no top-level replacement either
+move under `gopls cli` per the migration table or are deleted
+outright. Release notes announce the new surface, the `gopls cli`
+scope, and the #63693 resolution. Exit: top-level agent surface +
+`gopls cli` debug surface, both documented.
+
+## Open questions
+
+- **Configuration source of truth** ([Configuration](#configuration)).
+ Direction proposed but not committed; requires coordination with
+ the gopls team and editor-plugin owners. Post-Phase-1 follow-up,
+ not a Phase 1 blocker — Phase 1 ships with `format`/`vet`
+ equivalent to their external counterparts, and the schema work
+ graduates them to "strictly better inside a workspace."
+- **Interactive refactorings on the CLI.** Deferred to a follow-up
+ (see [Interactive refactoring on the CLI](#interactive-refactoring-on-the-cli)).
+ Open sub-questions for when that follow-up starts: protocol shape
+ for the Q&A loop (prompts on stderr vs full TTY form); wire
+ format for non-interactive inputs (stdin JSON vs `--answers FILE`
+ vs per-field `--set KEY=VAL`); whether to share the client-side
+ form logic with MCP or any other non-LSP surface.
+- **Deprecation window for legacy aliases.** Full deprecation runs
+ two gopls release cycles (~6 months), release-management's call
+ on dates. During the window, an opt-in env var
+ (e.g. `GOPLS_LEGACY_CLI=1`) selects legacy interpretation for
+ subcommand names that collide between surfaces (`gopls rename`,
+ `gopls check`, ...), so existing scripts keep working without
+ code changes while users migrate. Needs confirmation that an env
+ var is the right channel vs. a flag or detection heuristic.
+- **Telemetry for legacy aliases.** Accepted: emit a counter on
+ each deprecated-alias invocation so we have usage data before
+ deletion. Counter names and retention are an implementation
+ detail.
+- **Stability contract for `gopls cli`.** Accepted as stated in
+ [Contract](#contract). Operational question: land the `--help`
+ caveat and docs as soon as the prototype narrowing starts, not
+ at Phase 1 cut-over, so the window where users could pick up
+ prototype-form subcommands assuming stability stays small.
+
+## Interactive refactoring on the CLI
+
+Deferred to a post-MVP follow-up. `command/resolve` (see
+[integrating-interactive-refactoring.md](integrating-interactive-refactoring.md))
+assumes a client that renders forms and retries, which the CLI does
+not today provide.
+
+Intended direction when this lands: the CLI acts as an extended LSP
+client. It calls `command/resolve`, receives the server's
+`formFields`, prompts the user on stdin/stderr for each field, and
+sends back `formAnswers` to obtain the edit. Non-interactive
+callers (agents, scripts) pipe structured answers via stdin or
+`--answers FILE`. This avoids baking form fields into per-command
+CLI flag schemas — the server remains the source of truth for the
+schema, and the CLI has no hand-coded flag list to drift.
+
+Scope-wise this means implementing client-side form/retry logic in
+the CLI, which is a non-trivial addition and not required for the
+#63693 resolution. Tracked in [Open questions](#open-questions).
+
+## Out of scope
+
+- Session pooling, watcher ownership, diagnostic cache — server-side
+ and orthogonal; covered by the prototype doc.
+- MCP server surface. Migrating MCP tool definitions to share the new
+ top-level implementations is a separate effort this plan unblocks.
+- Non-code-intelligence commands (`stats`, `remote`, `mcp`,
+ `vulncheck`) — remain under their own rules.
diff --git a/gopls/doc/design/gopls-cli-prototype.md b/gopls/doc/design/gopls-cli-prototype.md
new file mode 100644
index 0000000..92abb34
--- /dev/null
+++ b/gopls/doc/design/gopls-cli-prototype.md
@@ -0,0 +1,643 @@
+---
+title: "gopls CLI prototype"
+---
+
+## Status
+
+**Prototype / experiment.** This document describes the design of the
+`gopls cli` subcommand suite and the supporting server-side machinery
+that makes CLI invocations fast (session pooling, pool-scoped file
+watcher, pull-diagnostic cache, per-connection capability profiles).
+It is shipped unannounced, behind flags, for evaluation and feedback.
+
+The `gopls cli` commands duplicate functionality already present in the
+legacy top-level commands (`gopls definition`, `gopls references`,
+`gopls rename`, `gopls check`, `gopls format`, `gopls imports`,
+`gopls vet`, `gopls codeaction`, `gopls fix`). Two command families
+coexist only while we validate the agent-friendly shape. The end state
+is a single CLI: the learnings here are expected to fold into the
+legacy commands, either by merging `gopls cli X` into `gopls X` or by
+deleting one side once the other covers the ground. See
+[Migration plan](#migration-plan).
+
+Tracking issue: [golang/go#63693](https://github.com/golang/go/issues/63693)
+("make a decision about the gopls CLI").
+
+## Goals
+
+1. Provide code intelligence (definition, references, hover, symbols,
+ implementations, rename, diagnostics, formatting, import
+ organization, code actions, quick fixes) to non-interactive clients
+ — shell scripts and AI coding agents — with sub-100ms warm-query
+ latency.
+2. Reuse gopls internals: the CLI is an LSP client that speaks the
+ same protocol as editors. No new wire protocol.
+3. Share state across clients: a gopls daemon can serve an editor, an
+ MCP client, and many short-lived CLI invocations against the same
+ workspace without reloading it.
+
+## Non-goals
+
+- Replace the editor protocol. This work does not modify the LSP
+ protocol that editors negotiate.
+- Provide a streaming / long-lived CLI session. Each `gopls cli`
+ invocation is one short connection.
+- Cover every LSP operation. The suite is scoped to the operations an
+ agent needs to read and safely transform Go source.
+
+## Background
+
+Agents and shell scripts need type-aware code navigation. The legacy
+`gopls definition` commands work, but each invocation creates a fresh
+`cache.Session` and pays 1–30 seconds of Initial Workspace Load (IWL)
+— even when connecting to a daemon via `-remote=auto`. The daemon
+creates and destroys a session per LSP connection
+(`internal/lsprpc/lsprpc.go`). Across tens of invocations per agent
+task, this dominates latency.
+
+Previous attempts (internal prototypes v2 and v3) introduced a custom
+length-prefixed JSON protocol that bypassed the LSP session lifecycle.
+Those attempts were rejected because a third wire format alongside LSP
+and MCP is a long-term maintenance burden.
+
+This prototype keeps the protocol standard and moves the optimization
+server-side. The CLI is a normal LSP client; the daemon keeps the
+heavy state (`cache.Session`, Views, type-check results, analysis
+results, file watcher) alive across connections and hands it to each
+new connection.
+
+## Architecture
+
+```
+┌────────────────────────────────────────────────────────────────┐
+│ gopls daemon process │
+│ (gopls serve -session.pool=true) │
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │
+│ │ LSP │ │ MCP │ │ LSP │ │
+│ │ (editor) │ │ (agents) │ │ (gopls cli / legacy) │ │
+│ └────┬─────┘ └────┬─────┘ └──────────┬───────────┘ │
+│ │ │ │ │
+│ └────────────────┴──────────────────────┘ │
+│ │ │
+│ ┌──────────────┴───────────────┐ │
+│ │ cache.Cache │ Session pool │
+│ │ + pooled cache.Sessions │ (keyed by root) │
+│ │ (Views, file watcher, │ • acquire on │
+│ │ type-check results, │ pool hit │
+│ │ diagnostic cache) │ • register on miss │
+│ │ │ • evict after idle │
+│ └──────────────────────────────┘ │
+└────────────────────────────────────────────────────────────────┘
+ ▲
+ │ Standard LSP over Unix socket / TCP
+ │ (-remote=auto, -remote=unix;path, ...)
+ │
+ ┌────────┴─────────┐
+ │ gopls cli def │ ← short-lived client
+ │ gopls cli refs │ (agent or shell)
+ └──────────────────┘
+```
+
+The CLI connects to the daemon as a regular LSP client. On connect,
+the daemon either finds a warm session for the workspace root or
+creates one. On disconnect, the session stays in the pool for the next
+connection; idle sessions are evicted after a configurable timeout.
+
+## Session pool
+
+The pool lives in `internal/lsprpc` and is keyed by workspace root.
+
+### Pool entry lifecycle
+
+Each pool entry wraps a `cache.Session` plus pool-scoped state
+(reference count, idle timer, shared file watcher, diagnostic cache,
+push-subscriber set).
+
+- **Acquire** — on LSP connect, the pool returns the entry for the
+ requested root if one exists, incrementing its reference count.
+- **Register** — on pool miss, after the new session finishes
+ initialization with its Views, it is registered in the pool. Races
+ are handled: if another connection registered first, the winner is
+ used and the loser session is discarded.
+- **Release** — on LSP disconnect, the reference count is decremented.
+ When the count reaches zero, an idle timer starts.
+- **Evict** — after the idle timeout, the pool closes the shared file
+ watcher (before shutting down the session, to drain in-flight
+ events), then shuts down the session and removes the entry.
+
+Default idle timeout: 5 minutes. Overridable with `-session.idle`.
+
+### Server integration: hooks
+
+The session pool lives in `internal/lsprpc`; the per-connection
+`*server.Server` lives in `internal/server`. To avoid a circular
+import, the pool integrates via two optional hooks set on the server
+at construction time:
+
+- **`SessionSwapHook`** — fires at the start of `addFolders` (during
+ the Initialize / Initialized handshake). When the hook returns a
+ pooled session, the server swaps its temporary session for the
+ warm one. The existing `HasView()` / `ErrViewExists` checks in
+ `addFolders` naturally cause IWL to be skipped for folders whose
+ Views already exist.
+- **`PostInitHook`** — fires at the end of `addFolders`, after the
+ Views have been created. On pool miss, this registers the newly
+ warmed session into the pool. Separating pool-miss registration
+ from the swap hook prevents a race where a second connection could
+ observe a not-yet-initialized entry.
+- **`onShutdown`** — optional callback that runs in place of
+ `session.Shutdown()` during server shutdown. The pool installs this
+ to release the session back to the pool (and start the idle timer)
+ rather than destroy it.
+
+The two-hook structure is an internal server concern and is not
+observable from LSP clients.
+
+## File watching across connections
+
+On baseline gopls, each `*server.Server` owns its own fsnotify watcher
+and tears it down on server shutdown. With session pooling, the
+`cache.Session` outlives the server, and a file edited on disk between
+two connections goes unobserved — the old watcher is gone, and no new
+connection is there yet to see the event. The pooled snapshot becomes
+stale without anyone noticing.
+
+### Pool-scoped watcher
+
+The file watcher is moved from `*server.Server` to the pool entry.
+It is created lazily on the first connection to the entry
+(`EnsureWatcher`), shared by any `*server.Server` attached to the
+same session, and closed before session shutdown during pool
+eviction. Its `onChange` closure captures the `*cache.Session` (not
+a `*server.Server`), so events continue to invalidate the snapshot
+after a connection shuts down.
+
+`WatchDir` is idempotent across connections via a pool-scoped
+`watchedDirs` set — each new connection re-requests the same
+directories during its own `updateServerSideWatcher` pass, and the
+pool deduplicates.
+
+Watcher mode (`fsnotify` vs `off`) is sticky for the pool entry's
+lifetime: the first-creator wins. Dynamic mode changes via
+`workspace/didChangeConfiguration` do not propagate to the pool-owned
+watcher. This is acceptable for the prototype.
+
+### Push-diagnostic fan-out
+
+A disk edit observed by the pool-scoped watcher invalidates the
+session snapshot, but the editor attached to the pooled daemon still
+needs a `publishDiagnostics` to see the change reflected in its UI.
+
+The pool entry maintains a set of push-diagnostic subscribers. Each
+`*server.Server` whose client wants push diagnostics registers a
+callback at `addFolders`. When a watcher event fires, the pool entry:
+
+1. Calls `session.DidModifyFiles` on the modifications.
+2. Iterates the subscriber set and calls each callback. The callback
+ on the `*server.Server` publishes `publishDiagnostics` for the
+ modified URIs and (if the client also has push diagnostics enabled)
+ kicks off a background `diagnoseChangedViews` goroutine.
+
+Multi-client topology is handled by the shared diagnostic cache (see
+[Shared diagnostic cache](#shared-diagnostic-cache)): the first
+goroutine across all attached servers pays the type-check and
+analysis cost; the rest publish from cache.
+
+## Diagnostics for short-lived clients
+
+A short-lived CLI connection cannot rely on push diagnostics: the
+client disconnects before the server's asynchronous
+`diagnoseSnapshot` completes. The prototype supports three cooperating
+mechanisms.
+
+### Pull diagnostics
+
+The server implements LSP 3.17 pull diagnostics:
+
+- `textDocument/diagnostic` — synchronous per-URI request-response.
+- `workspace/diagnostic` — synchronous workspace-wide request-response.
+
+Both are enabled when the client advertises pull-diagnostic
+capabilities and sets `pullDiagnostics: true` in
+`initializationOptions`. The `gopls cli check`, `cli vet`,
+`cli codeaction`, and `cli fix` subcommands opt into this via a
+capability profile (see [CLI capability profile](#cli-capability-profile)).
+
+`textDocument/diagnostic` existed before this work as a per-file
+handler; it has been extended to read from the shared diagnostic
+cache. `workspace/diagnostic` was a `notImplemented` stub before this
+work; it now iterates the session's views and emits one
+`WorkspaceFullDocumentDiagnosticReport` per compiled Go file, with
+URI dedup.
+
+Current scope limits:
+- Full reports only (no `resultId` / `Unchanged` incremental reports).
+- No partial-result streaming.
+- Go source files only (not `go.mod`, `go.work`, templates).
+
+### Push-subscriber gate
+
+With short-lived, pull-only CLI connections attached to a pooled
+session, the server should not run push-model diagnostic computation
+— no client will receive the results and the work is wasted.
+
+The pool entry counts push subscribers. Each connection registers as
+a push subscriber exactly once during `addFolders` if its client
+capabilities include push diagnostics; the count is decremented in
+`Shutdown`. `(*server).shouldComputeDiagnostics()` (defined in
+`internal/server/text_synchronization.go`) gates the
+modification-triggered workspace diagnose pass — when pooling is on
+and no connection wants push diagnostics, `diagnoseChangedViews` is
+not scheduled. CLI connections declare `wantsPushDiagnostics=false`
+in their init options and therefore never count.
+
+A re-entry guard on the subscribe side (`s.poolSubscribed`) ensures
+each server subscribes exactly once even when `addFolders` is
+re-entered via `DidChangeWorkspaceFolders` or a `DidOpen` fallback,
+so `Shutdown`'s single `Unsubscribe` keeps the count balanced.
+
+In parallel, the CLI's `skipDidOpen` profile (see
+[CLI capability profile](#cli-capability-profile)) avoids the
+redundant `DidOpen`/modification chatter entirely, eliminating the
+view invalidation that pool-hit CLI reads used to cause.
+
+### Shared diagnostic cache
+
+Per-snapshot diagnostic results live on the pool entry as a
+`DiagnosticCache` keyed by `(URI, *cache.View)`. Both the push path
+(`updateAndPublish`, `publishFileDiagnosticsLocked`,
+`findMatchingDiagnostics`) and the pull path (`Diagnostic`,
+`DiagnosticWorkspace`) go through the cache.
+
+Freshness rule: a new entry wins if it is for a newer snapshot, or
+for the same snapshot and marked `final=true`.
+
+The `final` flag distinguishes the fast pass (type-check only, no
+analysis) from the full pass (type-check plus `go/analysis`). When
+`settings.DiagnosticsDelay > 0`, `diagnoseSnapshot` stores a fast-pass
+entry and then a full-pass entry. Pull requires `final=true`, so a
+pull arriving inside the delay window does not silently drop analyzer
+diagnostics (staticcheck, etc.).
+
+Per-connection state (`publishedHash`, `mustPublish`, `orphanedAt`)
+remains per-`*server.Server`: those fields describe what a given
+server has fanned out on its own client wire. Only the heavy compute
+results are shared.
+
+Lock order: `server.diagnosticsMu` (outer) → `DiagnosticCache.mu`
+(inner, transient). Cache methods are self-contained and never
+re-enter server code.
+
+## CLI capability profile
+
+Each outgoing LSP connection from the `gopls` CLI selects a
+`clientProfile` before `app.connect()`. The profile controls:
+
+- `wantsPushDiagnostics` (sent in `initializationOptions`) — whether
+ this connection registers as a push subscriber on the pool entry
+ and receives `publishDiagnostics` notifications.
+- `WorkDoneProgress` client capability — off for short-lived CLI
+ connections (progress is noise).
+- `pullDiagnostics` client capability and init option — on for the
+ subcommands that consume diagnostics.
+- `skipDidOpen` — whether to send `textDocument/didOpen` before query
+ methods. Off for all `gopls cli` subcommands: CLI clients have no
+ unsaved buffers, disk is authoritative, and the pool-scoped file
+ watcher keeps the snapshot current across connections. The server's
+ query handlers read files via `snapshot.ReadFile`, which falls back
+ to disk when no overlay exists.
+
+The profile is selected per-subcommand in `cmd.cliProfileFor`:
+
+| Subcommand | Push diag | Pull diag | DidOpen | Edits out |
+|-------------------------------------------|:---------:|:---------:|:-------:|:--------------|
+| def, refs, hover, impl, symbols, wsymbols | off | off | skip | — |
+| rename | off | off | skip | WorkspaceEdit |
+| format, imports | off | off | skip | TextEdits |
+| check, vet | off | on | skip | — |
+| codeaction, fix | off | on | skip | WorkspaceEdit |
+
+The default profile (used by editor LSP clients and the legacy
+top-level commands) keeps the existing behavior: push diagnostics on,
+`WorkDoneProgress` on, no pull-diagnostic advertisement.
+
+## `gopls cli` subcommand suite
+
+The CLI subcommands are grouped under `gopls cli` and dispatched in
+`gopls/internal/goplscli/cmd/cli.go`. They share:
+
+- **Symbol-based lookup.** `def NewSession --in session.go` resolves
+ the symbol name to a position via `textDocument/documentSymbol` and
+ walks the tree (handling Go's `(T).Name` method naming) to find a
+ match. Agents know names, not coordinates.
+- **Position model.** CLI positions are 1-based line, 1-based UTF-8
+ byte column (matching `go/token.Position` and compiler output).
+ LSP positions are 0-based UTF-16. Conversion happens at the CLI
+ boundary. Out-of-range positions from hallucinated coordinates are
+ clamped to the nearest valid position rather than rejected.
+- **Terse output.** Default text mode prints one
+ `file:line:col[:symbol]` per line. `-json` emits a structured JSON
+ document.
+- **`params.Range` compatibility.** gopls's query methods accept
+ `params.Range`. The CLI sets both fields.
+
+### Read-only queries
+
+`def`, `refs`, `hover`, `impl`, `symbols`, `wsymbols`. All use the
+base CLI profile (no push, no progress, skip DidOpen).
+
+`def` and `hover` accept `--body`, which folds in the source of the
+declaration (located via `textDocument/documentSymbol` on the def
+file). Removes a follow-up file Read for the common agent flow of
+"jump to def, then look at the code." Falls back to empty body when
+no symbol matches the def position (e.g., for non-top-level
+definitions); the command still exits 0.
+
+### Edit-producing query: rename
+
+`gopls cli rename` returns a `WorkspaceEdit` that the CLI applies via
+the same `applyWorkspaceEdit` machinery as `cli fix`. It accepts the
+same `EditFlags` as `format`/`imports` (`-w` / `-d` / `-l` /
+`--preserve`), with the default being print-edited-content-to-stdout.
+
+`--dry-run` prints a terse per-file summary of the proposed edits and
+exits without modifying any files. Useful for review before applying.
+The summary printer iterates `DocumentChanges` first and falls back
+to `Changes`; gopls only ever writes to `DocumentChanges`, but
+servers in general populate either.
+
+### Diagnostics: check, vet
+
+`gopls cli check [FILE...]`:
+- No args → one `workspace/diagnostic` request, reports for every Go
+ file in the workspace in one round-trip.
+- Args → one `textDocument/diagnostic` request per file.
+- Output: `file:line:col: severity [source]: message`, sorted.
+
+`--severity=LEVEL` (`error`, `warning`, `info`, `hint`,
+case-insensitive) filters out diagnostics less severe than LEVEL.
+Matches the legacy `gopls check -severity` semantics.
+
+`gopls cli vet [FILE...]` is a source-filtered view of `check`:
+identical wire behavior, but filters returned diagnostics to those
+whose `Diagnostic.Source` matches an analyzer in the traditional
+`cmd/vet` suite. Membership is tracked by a new `inVet` field on
+`settings.Analyzer`; `settings.VetAnalyzerNames()` returns the set.
+A drift test fails if the `inVet` classification diverges from
+`go tool vet help`. `vet` also accepts `--severity`.
+
+### Edits: format, imports
+
+`gopls cli format FILE...` and `gopls cli imports FILE...` produce
+edits via `textDocument/formatting` and
+`textDocument/codeAction` (with `Only=[SourceOrganizeImports]`).
+They share the legacy `EditFlags` pattern:
+
+| Flag | Behavior |
+|-----------------|-----------------------------------------------|
+| (default) | Print edited content to stdout |
+| `-w, --write` | Overwrite file in place (only if changed) |
+| `-d, --diff` | Print unified diff |
+| `-l, --list` | Print filename only if file would change |
+| `--preserve` | With `-w`, copy the original to `<file>.orig` |
+| `--json` | Per-file `[{file, changed, newContent}, ...]` |
+
+The dispatcher routes format/imports out of the standard
+result→print pipeline because the edit flags control side effects
+that don't fit a single encoded result.
+
+### Code actions: codeaction, fix
+
+`gopls cli codeaction FILE:LINE:COL [--kind KIND]` lists available
+code actions at a position. It pulls `textDocument/diagnostic` first,
+filters diagnostics to those whose range covers the cursor, and
+passes them in `CodeActionParams.Context.Diagnostics`. The server's
+`codeActionsMatchingDiagnostics` path requires this field — without
+pulling diagnostics first, quickfix actions would not appear.
+`--kind` maps to `Context.Only` for server-side filtering.
+
+`gopls cli fix FILE:LINE:COL [--kind KIND] [-w|-d|-l]` picks one
+matching action and applies its `WorkspaceEdit`. If `action.Edit` is
+nil and `action.Data` is non-nil, `codeAction/resolve` is called
+first. Actions that require `workspace/applyEdit` round-trips are
+reported as unsupported — in-prototype scope is the inline-edit path.
+Edit application walks each `TextDocumentEdit` in
+`WorkspaceEdit.DocumentChanges`; file renames and creates are not yet
+supported.
+
+## Daemon lifecycle and flags
+
+### `-session.pool`
+
+`gopls serve -session.pool` enables session pooling on the daemon.
+Without this flag the daemon behaves as before: one `cache.Session`
+per LSP connection, destroyed on disconnect.
+
+### `-session.idle`
+
+Idle timeout for pooled sessions. Default 5 minutes. Short enough
+that warm sessions from one-off CLI invocations get reclaimed
+promptly; users who want longer retention can raise it.
+
+### `-remote=auto` auto-spawn
+
+Without configuration, `gopls -remote=auto cli <cmd>` would fail if
+no daemon were running — the auto-spawn path was never taken. The
+`connect()` helper now passes a `daemonArgs` callback to
+`ConnectToRemote` that builds the spawn argv with `-session.pool`
+and `-logfile=auto`. Users get the pool without having to remember
+the flag.
+
+### Client-side fast path for `-remote=<addr>`
+
+`main.go` calls `filecache.Get("nonesuch", ...)` at startup as a
+defensive ENOSPC / cache-corruption check (golang/go#67433). The
+check costs ~21ms per invocation. CLI processes invoked with
+`-remote=<addr>` forward to a daemon and never touch the local
+filecache; the daemon performs the same check at its own startup.
+The client-side call is gated off in remote mode. Measured impact:
+~16–19ms saved per warm CLI invocation.
+
+## Performance
+
+Measured on x/tools (~800 packages, GoWork view), Apple Silicon,
+Go 1.26.x. Each query is a separate CLI process connecting over a
+Unix socket. 11 runs per command, true median, one warm-up discarded.
+"v3" is the rejected custom-protocol prototype, retained as a
+latency baseline.
+
+### Cold start (first query, includes IWL)
+
+| Version | Time (ms) |
+|----------|-----------|
+| Prototype | 972–1166 |
+| v3 | 1406–1829 |
+| Legacy | 2046 |
+
+### Warm queries — `gopls cli`
+
+| Command | Prototype (ms) | v3 (ms) |
+|-----------|---------------:|--------:|
+| def | 70 | 40 |
+| refs | 78 | 42 |
+| hover | 69 | 40 |
+| symbols | 69 | 40 |
+| wsymbols | 77 | 54 |
+| impl | 72 | 41 |
+
+### Warm queries — legacy commands (`gopls -remote=auto <cmd>`)
+
+Session pooling applies to any LSP connection, not just `gopls cli`.
+The existing top-level commands benefit without any client changes.
+
+| Command | Prototype (ms) | Without pool (ms) | Speedup |
+|----------------|---------------:|------------------:|--------:|
+| definition | 70 | 1956 | 28x |
+| references | 99 | 2236 | 23x |
+| symbols | 75 | 715 | 10x |
+| workspace_sym | 86 | 1974 | 23x |
+| implementation | 82 | 1869 | 23x |
+
+### Per-query overhead
+
+The prototype pays ~28ms of LSP handshake overhead per connection
+(Initialize + Initialized + RegisterCapability + Shutdown
+round-trips) on top of ~15–20ms of Go process startup. v3 paid ~5ms
+(one custom-protocol round-trip). The prototype is ~28–30ms slower
+per warm query than v3; the tradeoff is that there is no third wire
+protocol to maintain.
+
+## Migration plan
+
+The `gopls cli` suite and the legacy `gopls <operation>` commands
+currently duplicate each other. The path to a single user-facing
+surface is the subject of a separate doc — see
+[gopls-cli-migration.md](gopls-cli-migration.md), which proposes an
+end-state where top-level commands (`gopls def`, `gopls rename`, ...)
+host the agent-friendly surface and `gopls cli <verb>` narrows to
+LSP primitives for debugging.
+
+The session-pooling work under `internal/lsprpc` and the capability
+profile under `internal/cmd` are not affected by that choice — they
+serve any LSP client equally.
+
+Until the migration lands, the `gopls cli` subcommands are
+undocumented on the public site, behind the implicit "experimental"
+label that covers unannounced CLI subcommands.
+
+## Follow-up work
+
+The prototype has landed end-to-end; this section tracks work we deferred
+on purpose so reviewers know what's in scope for later iterations.
+
+### Open decisions
+
+- **Migration plan.** Proposed in
+ [gopls-cli-migration.md](gopls-cli-migration.md); resolution blocks
+ the public announcement and user-facing docs.
+- **Persistent CLI connection** (§[Alternatives considered](#alternatives-considered),
+ option C). Revisit if the ~28ms per-query handshake shows up in
+ user reports.
+- **Pool key = root only** (§[Alternatives considered](#alternatives-considered),
+ option D). Add build-config (`GOOS`, tags, …) to the pool key if we
+ see clients with divergent configs sharing workspaces.
+
+### Scope limits to lift
+
+Each of these is called out in the prototype as a deliberate cut;
+none are required for correctness but lifting them expands coverage.
+
+- **Pull diagnostics: incremental reports.** Add `resultId` /
+ `Unchanged` support on `textDocument/diagnostic` and
+ `workspace/diagnostic` so push-capable editors can also benefit from
+ the shared diagnostic cache without recomputing.
+- **Pull diagnostics: partial-result streaming.** Stream
+ `workspace/diagnostic` results as views finish rather than holding
+ until the full pass completes.
+- **Pull diagnostics: non-Go files.** Extend `workspace/diagnostic`
+ coverage to `go.mod`, `go.work`, and template files — matching the
+ push path.
+- **Dynamic watcher-mode changes.** A `workspace/didChangeConfiguration`
+ that flips `fileWatcher` currently logs a warning but is ignored by
+ the pool-owned watcher. Either re-create the watcher on mode change
+ or document the restriction in user-facing settings.
+- **`gopls cli fix`: `workspace/applyEdit` round-trips.** Actions that
+ require the server to drive the edit (not return it inline) are
+ reported as unsupported. Wire up a client-side `ApplyEdit` handler so
+ these actions work end-to-end.
+- **`gopls cli fix`: file renames and creates.** Edit application walks
+ `TextDocumentEdit` entries only. Extend to `RenameFile` and
+ `CreateFile` in `WorkspaceEdit.DocumentChanges`.
+
+### Test coverage
+
+- **Eviction race.** Cover the window where the idle timer fires
+ concurrently with a new `acquire` for the same key.
+- **Multi-client pool sharing.** Verify that two concurrent connections
+ to the same root (e.g. editor + CLI, or CLI + CLI) land on the same
+ pool entry and share the diagnostic cache and pool-scoped watcher.
+
+### Cleanup
+
+- **Scrub internal-only references in code comments.** Several comments
+ reference internal design artifacts (`kb-gopls-skills/…`,
+ `research/FILE_WATCHER_AUDIT.md`) and staged-development labels
+ (`Stage 1`, `Stage 3c`, …) that aren't meaningful to readers of the
+ upstream tree. Replace with pointers to this design doc and
+ self-contained explanations.
+
+## Alternatives considered
+
+### A. Custom wire protocol (v2, v3)
+
+A length-prefixed JSON protocol with stateless one-shot connections.
+~5ms per-query overhead. Rejected twice by the gopls team because a
+third wire format alongside LSP and MCP is a long-term maintenance
+burden. The prototype pays ~28ms instead and avoids the protocol
+debt.
+
+### B. In-process library (no daemon)
+
+Each agent process links gopls internals directly. Zero IPC. Rejected
+because N agents × 500–800 MB resident. No state or cache sharing.
+
+### C. Persistent CLI connection
+
+Keep the CLI connected to the daemon across queries, amortizing the
+LSP handshake over a run. Rejected for the prototype because it adds
+connection-management complexity and the per-query overhead has not
+proven to be a problem in practice. Worth revisiting if user reports
+show the handshake is the bottleneck.
+
+### D. Pool keyed by full config
+
+The pool is keyed on workspace root only. Two clients with different
+`GOOS` or build tags share a pool entry and get whichever
+configuration arrived first. Acceptable because agent workflows
+almost never customize the build config; adding config to the pool
+key would multiply resident sessions. Future work if it matters.
+
+## Appendix: source map
+
+| Component | Path |
+|--------------------------------|----------------------------------------------------------------|
+| Session pool | `gopls/internal/lsprpc/pool.go` |
+| Pool / StreamServer wiring | `gopls/internal/lsprpc/lsprpc.go` |
+| Server hooks | `gopls/internal/server/server.go`, `general.go` |
+| Push-subscriber compute gate | `gopls/internal/server/text_synchronization.go` |
+| Diagnostic cache | `gopls/internal/server/diagnostics.go` |
+| `workspace/diagnostic` | `gopls/internal/server/diagnostics.go` |
+| Client profile, `initParams` | `gopls/internal/cmd/cmd.go`, `cli.go` |
+| `gopls serve` flags | `gopls/internal/cmd/serve.go` |
+| `-remote=auto` auto-spawn | `gopls/internal/cmd/cmd.go` (daemonArgs) |
+| Filecache smoke-test skip | `gopls/main.go` |
+| CLI subcommand dispatch | `gopls/internal/goplscli/cmd/cli.go` |
+| CLI rename | `gopls/internal/goplscli/cmd/rename.go` |
+| CLI `--body` fetch | `gopls/internal/goplscli/cmd/body.go` |
+| CLI check/vet | `gopls/internal/goplscli/cmd/check.go` |
+| CLI format/imports | `gopls/internal/goplscli/cmd/format.go` |
+| CLI codeaction/fix | `gopls/internal/goplscli/cmd/codeaction.go` |
+| Position conversion | `gopls/internal/goplscli/position.go` |
+| Symbol resolution | `gopls/internal/goplscli/resolve.go` |
+| `inVet` classification | `gopls/internal/settings/analysis.go` |
diff --git a/gopls/doc/design/gopls-cli-reference.md b/gopls/doc/design/gopls-cli-reference.md
new file mode 100644
index 0000000..bc3977c
--- /dev/null
+++ b/gopls/doc/design/gopls-cli-reference.md
@@ -0,0 +1,269 @@
+---
+title: "gopls CLI target reference"
+---
+
+## Status
+
+**Draft.** Sample help text companion to
+[gopls-cli-migration.md](gopls-cli-migration.md). Captures the
+target content and shape of `gopls help` and `gopls <cmd> --help`
+output after the migration's Phase 1 lands. The help-text generator
+that produces output this terse is a separate workstream; today's
+`gopls help` is significantly more verbose. This file is not a
+commitment to any particular rendering mechanism.
+
+## Top-level help
+
+```
+gopls — Go language server and CLI
+
+usage: gopls <command> [args]
+
+navigation
+ def SYMBOL --in FILE show where SYMBOL is defined
+ refs SYMBOL --in FILE find references to SYMBOL
+ impl SYMBOL --in FILE find implementations of SYMBOL
+ hover SYMBOL --in FILE show SYMBOL's signature and doc
+ symbols FILE list top-level symbols in FILE
+ wsymbols QUERY search workspace symbols
+ callers SYMBOL --in FILE list callers of SYMBOL
+ callees SYMBOL --in FILE list functions SYMBOL calls
+
+diagnostics
+ check [FILE...] [--fix] report diagnostics; --fix applies quickfixes
+ vet [FILE...] [--fix] cmd/vet-class diagnostics; --fix applies quickfixes
+
+edits and refactoring
+ rename SYMBOL --in FILE --to NEW rename a symbol
+ format FILE... apply gofmt
+ imports FILE... organize imports
+ codeaction POS [--kind KIND] list or apply a code action at POS
+
+server
+ serve run as an LSP server
+ mcp run as an MCP server
+
+other
+ version, help, bug, stats, remote, vulncheck
+
+global flags
+ --json emit JSON instead of text
+ -v, --verbose verbose stderr
+ --remote=auto connect to daemon (default)
+
+run `gopls help <command>` for command details.
+```
+
+## Shared conventions
+
+**Position** (any command taking `POS` or `SYMBOL --in FILE`):
+
+- `SYMBOL --in FILE` — preferred; agents know names.
+- `SYMBOL --in FILE:LINE` — disambiguate by line hint.
+- `FILE:LINE:COL` — for parsing compiler output.
+
+Lines and columns are 1-based UTF-8 bytes. Out-of-range coordinates
+clamp to the nearest valid position.
+
+**Edit flags** (apply to `rename`, `format`, `imports`, `codeaction`,
+and `check`/`vet` with `--fix`):
+
+```
+(default) print edited content to stdout
+-w write in place (only if changed)
+-d print unified diff
+-l print filenames that would change
+--preserve with -w, write .orig backup
+```
+
+**Output** — every command supports `--json` for structured output
+with a stable schema. Text output:
+
+- locations: `file:line:col`
+- locations with `--context=N`: `file:line:col` header, then N lines
+ before/after; blank line between matches.
+- diagnostics: `file:line:col: severity [source]: message`.
+- symbol listings: `name\tkind\tfile:line:col`.
+
+## Per-command
+
+### `def`, `impl` — locate
+
+```
+usage: gopls def SYMBOL --in FILE [flags]
+ gopls impl SYMBOL --in FILE [flags]
+ gopls <cmd> POS [flags]
+
+ --body (def only) include the full declaration source
+ --context=N (impl only) N lines of surrounding source per match
+ --json JSON output
+```
+
+`def` returns one location; `--body` folds the decl source into that
+response. `impl` returns many locations; per-match context via
+`--context=N` is the right shape for reading them.
+
+### `refs` — references
+
+```
+usage: gopls refs SYMBOL --in FILE [flags]
+ gopls refs POS [flags]
+
+ --context=N N lines of surrounding source per match
+ --json JSON output
+```
+
+Declaration is always included.
+
+### `hover` — signature and doc
+
+```
+usage: gopls hover SYMBOL --in FILE [flags]
+ gopls hover POS [flags]
+
+ --body include the full declaration source in addition to the signature
+ --json JSON output
+```
+
+### `symbols` — top-level symbols in a file
+
+```
+usage: gopls symbols FILE [flags]
+
+ --signatures include signatures for each symbol (default on)
+ --json JSON output
+```
+
+### `wsymbols` — workspace symbol search
+
+```
+usage: gopls wsymbols QUERY [flags]
+
+ --json JSON output
+```
+
+### `callers`, `callees` — call graph
+
+```
+usage: gopls callers SYMBOL --in FILE [flags]
+ gopls callees SYMBOL --in FILE [flags]
+ gopls <cmd> POS [flags]
+
+ --depth=N transitive depth (default 1)
+ --context=N N lines of surrounding source per call site
+ --json JSON output
+```
+
+### `check`, `vet` — diagnostics (with optional auto-fix)
+
+```
+usage: gopls check [FILE...] [flags]
+ gopls vet [FILE...] [flags]
+
+no args: report for the whole workspace.
+with args: report for each given file.
+
+ --severity=S filter: hint, info, warning, error (default: all)
+ --fix apply quickfix code actions to fixable diagnostics,
+ then report remaining diagnostics.
+ [edit flags: -w, -d, -l, --preserve] (only with --fix)
+ --json JSON output
+```
+
+Runs the user's configured analyzer set (including `staticcheck` if
+enabled). Matches the diagnostics shown in the editor. `gopls vet`
+is a severity/source filter over `gopls check`, scoped to the
+traditional `cmd/vet` analyzer set — different analyzer coverage
+than the standalone `go vet` binary.
+
+`--fix` applies diagnostic-associated quickfix code actions in a
+single pass. Actions not tied to a diagnostic (refactors,
+`source.organizeImports`, etc.) require `gopls codeaction` with an
+explicit position.
+
+### `rename` — validate and apply
+
+```
+usage: gopls rename SYMBOL --in FILE --to NEW [flags]
+ gopls rename POS --to NEW [flags]
+
+validates the rename is legal, then applies it. on invalid position,
+reports the problem without writing.
+
+ --to NEW new name (required)
+ --dry-run validate and print the edit without applying
+ [edit flags: -w, -d, -l, --preserve]
+ --json JSON output
+```
+
+### `format`, `imports` — apply formatter / organize imports
+
+```
+usage: gopls format FILE... [flags]
+ gopls imports FILE... [flags]
+
+ [edit flags: -w, -d, -l, --preserve]
+ --json JSON output
+```
+
+Applies the user's gopls formatter settings (e.g. `gofumpt`,
+`formatting.local`). For workspace projects, prefer `gopls imports`
+over `goimports` — it handles `go.work`, replace directives, and the
+module graph correctly. `gofmt`/`goimports` remain fine for ad-hoc
+use outside a workspace.
+
+### `codeaction` — list or apply a code action
+
+```
+usage: gopls codeaction POS [flags]
+
+with no --kind: list available actions at POS without applying.
+with --kind: apply one matching action.
+
+ --kind KIND code action kind (e.g. quickfix, refactor.extract,
+ source.organizeImports)
+ [edit flags: -w, -d, -l, --preserve]
+ --json JSON output
+```
+
+Covers all action kinds — refactors, source-level actions, and
+quickfixes. For bulk quickfix application across a file set, use
+`gopls check --fix` instead.
+
+## Low-level primitives: `gopls cli <verb>`
+
+A narrowed `gopls cli` namespace hosts LSP methods 1:1 for
+debugging, conformance checks, and reproducible bug reports. Not
+intended for agent skills — use the top-level commands instead.
+
+```
+gopls cli execute CMD [ARGS] # workspace/executeCommand
+gopls cli semtok FILE # textDocument/semanticTokens/full
+gopls cli links FILE # textDocument/documentLink
+gopls cli codelens FILE # textDocument/codeLens
+gopls cli folding-range FILE # textDocument/foldingRange
+gopls cli highlight POS # textDocument/documentHighlight
+gopls cli signature POS # textDocument/signatureHelp
+gopls cli prepare-rename POS # textDocument/prepareRename
+```
+
+`--json` emits the server response verbatim; text output is a
+best-effort pretty-print. Output shapes are not stable — see
+[gopls-cli-migration.md](gopls-cli-migration.md#low-level-lsp-primitives-under-gopls-cli)
+for the contract and rationale.
+
+## Deliberately absent
+
+Design choices not already captured in
+[gopls-cli-migration.md](gopls-cli-migration.md)'s migration table:
+
+- **No `gopls show` / `gopls explain` umbrella command.** `def` /
+ `hover` / `refs` each answer a different question and return a
+ different shape. Agents pick the one that matches the question.
+- **No `gopls fix-all`.** Iteration across diagnostics is the agent's
+ responsibility, not the CLI's.
+- **No `gopls cli def` / `cli refs` / ... duplicate of top-level
+ commands.** `gopls cli` holds only LSP methods without an
+ answer-shaped top-level equivalent (plus three inspection-path
+ exceptions for `highlight`, `signature`, `prepare-rename`).
+ Rationale in the migration doc.

Change information

Files:
  • A gopls/doc/design/gopls-cli-migration.md
  • A gopls/doc/design/gopls-cli-prototype.md
  • A gopls/doc/design/gopls-cli-reference.md
Change size: XL
Delta: 3 files changed, 1213 insertions(+), 0 deletions(-)
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: newchange
Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: I5648d521303f626d60cd32cf1eee6c31e1d4a8f3
Gerrit-Change-Number: 768861
Gerrit-PatchSet: 1
Gerrit-Owner: Hyang-Ah Hana Kim <hya...@gmail.com>
Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
unsatisfied_requirement
satisfied_requirement
open
diffy

Hyang-Ah Hana Kim (Gerrit)

unread,
Apr 19, 2026, 11:22:30 PMApr 19
to Hyang-Ah Hana Kim, goph...@pubsubhelper.golang.org, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
Open in Gerrit

Related details

Attention set is empty
Submit Requirements:
  • requirement is not satisfiedCode-Review
  • requirement satisfiedNo-Unresolved-Comments
  • requirement is not satisfiedReview-Enforcement
  • requirement is not satisfiedTryBots-Pass
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: tools
Gerrit-Branch: master
Gerrit-Change-Id: I5648d521303f626d60cd32cf1eee6c31e1d4a8f3
Gerrit-Change-Number: 768861
Gerrit-PatchSet: 1
Gerrit-Owner: Hyang-Ah Hana Kim <hya...@gmail.com>
Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
Gerrit-Comment-Date: Mon, 20 Apr 2026 03:22:26 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
unsatisfied_requirement
satisfied_requirement
open
diffy

Alan Donovan (Gerrit)

unread,
3:17 PM (8 hours ago) 3:17 PM
to Hyang-Ah Hana Kim, goph...@pubsubhelper.golang.org, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
Attention needed from Hyang-Ah Hana Kim

Alan Donovan added 17 comments

Patchset-level comments
Alan Donovan . resolved

Hi Hana, thanks for writing this up. Sorry, I thought I had mailed these notes out, but it turns out I got interrupted before getting the full way through, hence my confusing when we spoke last week. This partial pass allow us to make progress though.

File gopls/doc/design/gopls-cli-migration.md
Line 47, Patchset 1 (Latest):- **`gopls cli <verb>`** — LSP methods exposed 1:1 as subcommands,
Alan Donovan . unresolved

I like the idea of user-friendly and low-level debug/repro interfaces, but `cli` seems like the wrong name for the latter. I wonder how far a unified `gopls lsp textDocument/definition` subcommand that accepts JSON and prints JSON (using raw UTF-16 protocol.Locations, etc) would get us. Most of the queries (e.g. definition, hover, references, implementation, etc) fit neatly into this low-level model. Those that return edits or call ApplyEdits (e.g. rename, format, etc) would want an option to print the raw JSON, apply the change, or print the change as a diff. I think this might suffice for the low-level debugging interface, at least for a first version. Can you think of anything missing?

Line 54, Patchset 1 (Latest):- **Answer-shaped, not coordinate-shaped.** `gopls def --body`
Alan Donovan . resolved

<sounds of LLM taking massive bong rip>

;-)

Line 74, Patchset 1 (Latest):| `gopls def SYMBOL --in FILE [--body]` | Locate a symbol; optionally include decl source |
Alan Donovan . unresolved

Let's use the canonical names derived from LSP methods, minus textDocument/ (etc) prefix and converted to snake-case:

definition
implementation
references
workspace-symbols
incoming-calls
organize-imports
document-links
...

It will be easier to remember and find things that way.

Line 74, Patchset 1 (Latest):| `gopls def SYMBOL --in FILE [--body]` | Locate a symbol; optionally include decl source |
Alan Donovan . unresolved

What notation would this use? Something like go doc?

Does the `--in FILE` narrow the scope of the SYMBOL resolution?

Line 82, Patchset 1 (Latest):| `gopls rename SYMBOL --in FILE --to NEW [-w\|-d\|-l\|--preserve\|--dry-run]` | Validate + apply rename |
Alan Donovan . unresolved

These "edit flags" should be orthogonal and govern all commands that use ApplyEdits or WorkspaceEdits.

Line 82, Patchset 1 (Latest):| `gopls rename SYMBOL --in FILE --to NEW [-w\|-d\|-l\|--preserve\|--dry-run]` | Validate + apply rename |
Alan Donovan . unresolved

No flag; mandatory arguments should be positional.

Line 84, Patchset 1 (Latest):| `gopls vet [FILE...] [--severity=S] [--fix] [-w\|-d\|-l\|--preserve]` | Filtered to cmd/vet analyzers |
Alan Donovan . unresolved

Let's make this a --suite=vet option rather than a different command.

Line 87, Patchset 1 (Latest):| `gopls codeaction FILE:LINE:COL [--kind KIND] [-w\|-d\|-l\|--preserve]` | List or apply a code action (quickfix, refactor, source.*) |
Alan Donovan . unresolved

This should be a span denoting a protocol.Location.

Line 102, Patchset 1 (Latest):- **Skill-side LSP conformance.** Agent skills that depend on
Alan Donovan . resolved

whoa i am so high right now

;-)

Line 119, Patchset 1 (Latest):| `gopls cli execute CMD [ARGS]` | `workspace/executeCommand` | Raw access to every `gopls.*` command verb |
Alan Donovan . unresolved

This one exists in almost this form today.

Line 120, Patchset 1 (Latest):| `gopls cli semtok FILE` | `textDocument/semanticTokens/full` | Debug semantic-token computation |
Alan Donovan . unresolved

Using current `/*⇒5,identifier*/hello` notation?

Line 148, Patchset 1 (Latest):### Primitives-only, not a full LSP mirror


`gopls cli` hosts only the primitives above — it is not a 1:1
mirror of every top-level command. No `gopls cli def`, `cli refs`,
`cli hover` returning raw LSP types. The top-level surface is the
one we commit to; duplicate entries add confusion ("which do I
use?") with no user we can name today. Revisit if skill authors
make the case for raw-LSP versions of existing top-level commands.
Alan Donovan . unresolved

Primitives-only, not a full LSP mirror

I disagree with this. A raw JSON LSP client command would be easy to build and quite useful for debugging. A significant number of requests are little more than trivial wrappers around the basic protocol types.

Line 159, Patchset 1 (Latest):`format`, `imports`, `vet`, and `check` are only meaningfully

better than their external equivalents (`gofmt`, `goimports`,
Alan Donovan . unresolved

Not true. gopls imports uses the module cache. gopls' analysis driver has far more checkers.



### Mapping from today to end-state
Alan Donovan . unresolved

Let's just build the end state in the existing CLI tool. We don't need to preserve compatibility.

Line 206, Patchset 1 (Latest):### Phased rollout
Alan Donovan . unresolved

See my comments above.

Line 249, Patchset 1 (Latest):- **Interactive refactorings on the CLI.** Deferred to a follow-up
Alan Donovan . unresolved

We should have a think about what this might look like; I don't think it should be very hard to implement in full.

Open in Gerrit

Related details

Attention is currently required from:
  • Hyang-Ah Hana Kim
Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: I5648d521303f626d60cd32cf1eee6c31e1d4a8f3
    Gerrit-Change-Number: 768861
    Gerrit-PatchSet: 1
    Gerrit-Owner: Hyang-Ah Hana Kim <hya...@gmail.com>
    Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
    Gerrit-CC: Alan Donovan <adon...@google.com>
    Gerrit-Attention: Hyang-Ah Hana Kim <hya...@gmail.com>
    Gerrit-Comment-Date: Mon, 11 May 2026 19:17:38 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    unsatisfied_requirement
    open
    diffy

    Hyang-Ah Hana Kim (Gerrit)

    unread,
    8:27 PM (2 hours ago) 8:27 PM
    to Hyang-Ah Hana Kim, goph...@pubsubhelper.golang.org, Alan Donovan, golang...@luci-project-accounts.iam.gserviceaccount.com, golang-co...@googlegroups.com
    Attention needed from Alan Donovan

    Hyang-Ah Hana Kim added 3 comments

    File gopls/doc/design/gopls-cli-migration.md
    Line 102, Patchset 1 (Latest):- **Skill-side LSP conformance.** Agent skills that depend on
    Alan Donovan . resolved

    whoa i am so high right now

    ;-)

    Hyang-Ah Hana Kim

    This is because the prototype evolved from LSP primitive to a separate set of clis. :-P

    Line 148, Patchset 1 (Latest):### Primitives-only, not a full LSP mirror

    `gopls cli` hosts only the primitives above — it is not a 1:1
    mirror of every top-level command. No `gopls cli def`, `cli refs`,
    `cli hover` returning raw LSP types. The top-level surface is the
    one we commit to; duplicate entries add confusion ("which do I
    use?") with no user we can name today. Revisit if skill authors
    make the case for raw-LSP versions of existing top-level commands.
    Alan Donovan . unresolved

    Primitives-only, not a full LSP mirror

    I disagree with this. A raw JSON LSP client command would be easy to build and quite useful for debugging. A significant number of requests are little more than trivial wrappers around the basic protocol types.

    Hyang-Ah Hana Kim

    LSP spec is too broad. And I expect the shape of the cli for end user will be somewhat different from LSP's which may lead to confusion.

    For example, LSP go-to-definition takes position info, but the cli should take more human friendly input to be useful. My prototype took "symbol" (simple symbol text search to compute the position), but I found that's not sufficient to convince agents. (agents kept fall back to fuzzy grep that's more relaxed). So if we implement that feature, it's more than the primitive lsp.

    I think it's ok to implement lsp mirror for gopls developer's debugging purpose, but for most of non-editor tasks, that just took the space in the help message (extra token usage). And for debugging purpose, probably it's better to take a similar set of input as LSP's.

    Line 159, Patchset 1 (Latest):`format`, `imports`, `vet`, and `check` are only meaningfully
    better than their external equivalents (`gofmt`, `goimports`,
    Alan Donovan . unresolved

    Not true. gopls imports uses the module cache. gopls' analysis driver has far more checkers.

    Hyang-Ah Hana Kim

    IIRC `goimports` also used module cache (at least that's the biggest challenge the tool team was tasked to implement when go module was first introduced.)
    What I wanted to say is gopls's format, etc adds values when gopls's setting/configuration can be applied.
    But it's true that gopls has more diagnostics enabled by default than go vet. But staticcheck was good for most cases for me.

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Alan Donovan
    Submit Requirements:
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    • requirement is not satisfiedTryBots-Pass
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: tools
    Gerrit-Branch: master
    Gerrit-Change-Id: I5648d521303f626d60cd32cf1eee6c31e1d4a8f3
    Gerrit-Change-Number: 768861
    Gerrit-PatchSet: 1
    Gerrit-Owner: Hyang-Ah Hana Kim <hya...@gmail.com>
    Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
    Gerrit-CC: Alan Donovan <adon...@google.com>
    Gerrit-Attention: Alan Donovan <adon...@google.com>
    Gerrit-Comment-Date: Tue, 12 May 2026 00:26:58 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    Comment-In-Reply-To: Alan Donovan <adon...@google.com>
    unsatisfied_requirement
    open
    diffy
    Reply all
    Reply to author
    Forward
    0 new messages