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
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.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
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.
- **`gopls cli <verb>`** — LSP methods exposed 1:1 as subcommands,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?
- **Answer-shaped, not coordinate-shaped.** `gopls def --body`<sounds of LLM taking massive bong rip>
;-)
| `gopls def SYMBOL --in FILE [--body]` | Locate a symbol; optionally include decl source |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.
| `gopls def SYMBOL --in FILE [--body]` | Locate a symbol; optionally include decl source |What notation would this use? Something like go doc?
Does the `--in FILE` narrow the scope of the SYMBOL resolution?
| `gopls rename SYMBOL --in FILE --to NEW [-w\|-d\|-l\|--preserve\|--dry-run]` | Validate + apply rename |These "edit flags" should be orthogonal and govern all commands that use ApplyEdits or WorkspaceEdits.
| `gopls rename SYMBOL --in FILE --to NEW [-w\|-d\|-l\|--preserve\|--dry-run]` | Validate + apply rename |No flag; mandatory arguments should be positional.
| `gopls vet [FILE...] [--severity=S] [--fix] [-w\|-d\|-l\|--preserve]` | Filtered to cmd/vet analyzers |Let's make this a --suite=vet option rather than a different command.
| `gopls codeaction FILE:LINE:COL [--kind KIND] [-w\|-d\|-l\|--preserve]` | List or apply a code action (quickfix, refactor, source.*) |This should be a span denoting a protocol.Location.
- **Skill-side LSP conformance.** Agent skills that depend onwhoa i am so high right now
;-)
| `gopls cli execute CMD [ARGS]` | `workspace/executeCommand` | Raw access to every `gopls.*` command verb |This one exists in almost this form today.
| `gopls cli semtok FILE` | `textDocument/semanticTokens/full` | Debug semantic-token computation |Using current `/*⇒5,identifier*/hello` notation?
### 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.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.
`format`, `imports`, `vet`, and `check` are only meaningfully
better than their external equivalents (`gofmt`, `goimports`,Not true. gopls imports uses the module cache. gopls' analysis driver has far more checkers.
## Migration
### Mapping from today to end-stateLet's just build the end state in the existing CLI tool. We don't need to preserve compatibility.
- **Interactive refactorings on the CLI.** Deferred to a follow-upWe should have a think about what this might look like; I don't think it should be very hard to implement in full.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
- **Skill-side LSP conformance.** Agent skills that depend onwhoa i am so high right now
;-)
This is because the prototype evolved from LSP primitive to a separate set of clis. :-P
### 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.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.
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.
`format`, `imports`, `vet`, and `check` are only meaningfully
better than their external equivalents (`gofmt`, `goimports`,Not true. gopls imports uses the module cache. gopls' analysis driver has far more checkers.
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.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |