diff --git a/.gitignore b/.gitignore index 467575a81..7d8c7ae20 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,14 @@ web/playwright-report/ # plaintext Gitea credential and absolute /Users paths, so it is neither portable nor safe to # commit. See ersatztv#711 for the related merge-gate gap. .codex/ + +# serena's per-project state, written by `activate_project` (ersatztv#799): project.yml, +# project.local.yml, a language-server cache, and memories/. +# +# This deliberately rejects serena's own versioning model. Its nested .serena/.gitignore excludes +# only `cache` and `project.local.yml`, and project.local.yml says project.yml "is intended to be +# versioned" — but activation here is per DIRECTORY, and every worktree generates a project.yml +# whose project_name is that worktree's folder (e.g. `781-tooling`). A committed copy would name +# the wrong project in every checkout but the one that produced it. memories/ is ignored with it: +# it is serena's own written notes, and this repo's durable knowledge lives in docs/ instead. +.serena/ diff --git a/docs/README.md b/docs/README.md index 664bbe5ca..154afc50e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,7 @@ doc below, or that changes which sections a task signal points to.** | Writing or editing any doc, or answering a review finding in prose | `docs/decisions/records/docs/no-session-narrative.md` — the doc records the END STATE; the path to it goes in the commit message. Apply the who-benefits test, and read the carve-out before you cut (dated measurements, stated snapshot boundaries and tested-and-rejected results stay) | | Adding / changing / deleting a guard file | `docs/guard-inventory.md` — every guard's row is machine-checked by `scripts/tests/test_guard_inventory.py`, so a new guard must acquire a row before the suite goes green, and a row graded `MUTATION` must also acquire a declared clause in `scripts/tests/mutation_manifest.py` | | Writing code that reads live Gitea/remote state and then acts on it | `docs/decisions/records/process/check-and-use-pins-a-version.md`, then `docs/remote-state-inventory.md` — a new executable under `scripts/` (**excluding `scripts/tests/`**), `.claude/hooks/`, `.husky/` or `.gitea/workflows/` must acquire a row there before `scripts/tests/test_remote_state_inventory.py` goes green | -| Finding every site that references a symbol (multi-site fix/sweep) | `docs/local-lsp-tooling.md` — which surface answers, and why a delegated agent must be pointed at the `csharp-lsp` MCP tools rather than the `LSP` tool | +| Finding every site that references a symbol (multi-site fix/sweep) | `docs/local-lsp-tooling.md` — which of the three surfaces answers, and why a delegated agent must be pointed at an MCP server (`csharp-lsp`, or `serena` after an `activate_project`) rather than the `LSP` tool, which no subagent has been observed to reach | | Live local run / Playwright-MCP verification | `docs/e2e-local.md` + `scripts/e2e-local.sh` | | Adding/changing a UI-E2E browser flow | `docs/e2e-local.md` → "UI-E2E harness" + `scripts/e2e-ui.sh` | | What does a test suite cover | `docs/testing.md` | @@ -77,11 +77,11 @@ bounds, what's mined per issue): `docs/handoffs/chicorytv-issue-queue.md` → "K - **`docs/spa-conventions.md`** — playbook for adding a screen to the ChicoryTV React SPA. - **`docs/e2e-local.md`** (+ `scripts/e2e-local.sh`) — how to run a live local instance for manual or Playwright-MCP verification. -- **`docs/local-lsp-tooling.md`** — the code-intelligence surfaces (the `LSP` tool's three servers and - the `csharp-lsp` MCP server): how each is configured, which one a **subagent** can actually reach, - the traps (a cold server answers the first query with a confidently partial result), and - `scripts/check-local-lsp.sh` to verify the preconditions. Read before briefing an agent to find - every site referencing a symbol. +- **`docs/local-lsp-tooling.md`** — the code-intelligence surfaces (the `LSP` tool's three servers, + the `csharp-lsp` MCP server, and `serena`): how each is configured, which ones a **subagent** can + actually reach, the traps (a cold server answers the first query with a confidently partial result; + serena needs an `activate_project` per directory), and `scripts/check-local-lsp.sh` to verify the + preconditions. Read before briefing an agent to find every site referencing a symbol. - **`docs/testing.md`** — testing map: what each `*.Tests` project / `web` suite covers, golden-file nets, the timezone-independence rule, how to run subsets, the per-PR verification gate. diff --git a/docs/defect-shapes-773.md b/docs/defect-shapes-773.md index fa0637f02..11801fe8d 100644 --- a/docs/defect-shapes-773.md +++ b/docs/defect-shapes-773.md @@ -485,56 +485,110 @@ list and the measured exclude matrix: `ci.python-lint-ruff-config-committed`. ### 5.3 Configured vs actually invoked -Measured over the session transcript corpus (811 files under -`~/.claude/projects/-Users-timothy-ersatztv/`, this session excluded). +**Measured 2026-08-28 (#781/#799).** Population derived from config, not listed by hand: +`~/.claude/settings.json` `enabledPlugins`, `~/.claude.json` `mcpServers` (plus its per-project +`projects[].mcpServers`), the project `.mcp.json`, and `.claude/settings.local.json`. Corpus: **486 +transcripts** under `~/.claude/projects/` via `rglob` (a top-level glob sees 209 and manufactures +false zeros), this session excluded. Positive control on the same query shape: 22,927 `Bash`, 1,420 +`Read`, 1,116 `Edit`. -| Tool | Configured | Actually invoked | Verdict | +**A count is only meaningful against the surface a thing is actually used through**, so each row +below names its surface. Do not reason from a surface *count* — enumerate the event shapes and say +which you measured, because the whole failure here is an audit that checked one and reported zeros +for the rest. Measured: MCP tool calls (`mcp____*`), `Skill` invocations, `Agent` +`subagent_type`, user-typed slash commands (``), `LSP` tool calls, hooks (which appear +as `hookInfos` inside a `stop_hook_summary` record and *never* as a tool call), and CLIs invoked +inside `Bash`. Measured at zero everywhere: MCP **resource** tools (`ListMcpResourcesTool`, +`ReadMcpResourceTool`). **Not** separately identified: MCP prompts, and the `attributionSkill` +records that also appear in transcripts — so a zero below is a zero on the shapes listed, not on +every shape that exists. + +| Entry | Scope / surface | Measured | Verdict | |---|---|---|---| -| `gitea` MCP | project `.mcp.json` **and** user scope (duplicate, identical values) | Heavy, same-day | Keep; de-duplicate the config | -| `ssh-mcp` | project `.mcp.json` | 16 transcripts, same-day | Keep | -| `mempalace` MCP | user scope | 10 transcripts, **last hit 2026-07-25** (19 days) | Keep, but see below | -| **`LSP` (all three servers)** | 3 plugins enabled + `csharp-lsp` in `enabledMcpjsonServers` | **0 calls in 811 transcripts** | Broken *and* unused — both C# and TS servers repaired 2026-08-14 (#777); the zero is the baseline a future measurement is compared against | -| `context7` | enabled, `CONTEXT7_API_KEY` set | 0 calls | Dead | -| `playwright` | enabled | 145 calls, last 2026-07-25 | Keep | -| `superpowers` | enabled | 29 transcripts, last 2026-08-04 | Keep | -| `feature-dev`, `ralph-loop`, `security-guidance` | enabled | 0 skill invocations | Dead as configured | -| `codex` plugin | enabled | 0 *skill* invocations — but the documented workflow is `codex exec` via Bash, which this search cannot see | Not dead; measured the wrong surface | -| `nuget`, `docker-mcp` | in `.mcp.json`, explicitly **disabled** | 0 (expected) | Correctly off | +| `gitea` MCP | user `~/.claude.json` | **975 calls**, 105 transcripts, last 2026-08-28 | Keep — heaviest tool in the corpus | +| `codex` plugin | `codex exec` inside `Bash`, plus `SessionStart`/`SessionEnd`/`Stop` hooks | **109–126 `Bash` inputs invoke it at command position** across ~21–26 transcripts, last 2026-08-28 — 109 under a quote- and heredoc-aware parse, rising toward 126 if wrappers such as `nohup` are admitted. The range is stated rather than resolved: three successive parsers gave three answers, which is the point at which a hand-written command-position grammar stops being worth another round. **258** inputs merely *contain* the string — `pgrep -f "codex exec"` polling loops and heredoc bodies mention it — so a substring count overstates it roughly twofold | Keep — heavily used, and invisible to a skill-name search | +| `playwright` | plugin MCP | 64 calls, last 2026-08-27 | Keep | +| `security-guidance` | **hooks** (`SessionStart`, `UserPromptSubmit`, `PostToolUse`, `Stop`) | **1,086 execution records** in `hookInfos` naming `security_reminder_hook`, across 78 transcripts / 23 UTC days, last 2026-08-28 | Keep — **not** dead; it exposes no tool at all | +| `ssh-mcp` | project `.mcp.json` | 16 calls, 9 transcripts, last 2026-08-22 | Keep | +| `superpowers` | `Skill` | 13 invocations (`systematic-debugging` 6, `brainstorming` 5, …), last 2026-08-26 | Keep | +| `mempalace` | user `~/.claude.json` | 11 calls, 7 transcripts, last 2026-08-27 | Keep — in active use | +| `feature-dev` | **`Agent` subagent_type** | `feature-dev:code-reviewer` ×2, last 2026-08-21 | Keep — **not** dead; used via agents, not skills | +| `serena` | plugin MCP | **Disabled until 2026-08-14T12:31Z**; 0 invocations in the 14 days since it was enabled; **demonstrated working 2026-08-28** | Keep — see #799 and `docs/local-lsp-tooling.md` | +| `LSP` tool (`csharp-lsp`, `pyright-lsp`, `typescript-lsp` plugins) | `LSP` | 13 calls, last 2026-08-14 — all §5.1/#777 probes | Repaired but unadopted; no post-repair use | +| `csharp-lsp` MCP server | project `.mcp.json` | **0** since it became startable on 2026-08-14 | Reachable, unadopted | +| `ralph-loop` | **hooks** (`Stop`) + slash command | Its `Stop` hook ran **1,086 times** across 78 transcripts / 23 days — the same triple as `security-guidance` at this snapshot, consistent with both registering a `Stop` hook in their `hooks.json`. The `/ralph-loop` command itself: **0** | Keep — the hook executes and exits when no loop state exists; the *feature* is unused, the plugin is not inert | +| `context7` | plugin MCP | **0**, and it was enabled across the whole retained window | No observed use in the window. **Deliberately kept** — operator decision 2026-08-28 | +| `code-review`, `code-simplifier`, `commit-commands`, `claude-md-management`, `skill-creator`, `frontend-design` | `Skill` / `Agent` / slash command | **0** — but all six were **disabled** until 2026-08-14T12:41Z, so the zero covers only the 14 days since | Not yet exercised rather than rejected. **Deliberately kept** — operator decision 2026-08-28 | +| `nuget` | project `.mcp.json`, explicitly disabled | 0 (expected); `command` names `/usr/local/share/dotnet/dotnet`, **verified absent** | Entry removed 2026-08-28 | +| `docker-mcp` | project `.mcp.json`, explicitly disabled | 0 (expected) | Correctly off | +| `homeassistant` | `~/.claude.json` `projects[/Users/timothy/server-management]` | 129 calls | Not in this repo's population — scoped to another project | -The zero for `LSP` is verified rather than assumed: the same query shape returns 23,661 `Bash` calls -and 5,102 `Read` calls over the same corpus, so the search was demonstrably capable of finding hits. -**Re-derive it with that positive control**, not on its own — `rg -c` over multiple files prints -`path:count`, so the obvious way to total it sums the paths and returns zero for everything. +`claude-in-chrome` also appears in the corpus (154 calls) and is **not** configured anywhere in these +files: it is harness-provided. An audit deriving its population from config alone will not see it. -**Re-measured 2026-08-14 (#781), and the table above needs two corrections plus seven additions.** -The population here was assembled **by hand**; deriving it from the config files instead -(`~/.claude/settings.json` `enabledPlugins`, `~/.claude.json` `mcpServers`, the project `.mcp.json`, -`.claude/settings.local.json`) surfaces **seven enabled plugins the table omits** — `serena`, -`code-review`, `code-simplifier`, `commit-commands`, `claude-md-management`, `skill-creator`, -`frontend-design`. An audit whose completeness rests on remembering every entry is exactly the -failure `testing.guard-derives-population-from-source` exists to prevent, applied to an audit. +**Why "retire what is enabled and never invoked" was not carried out.** #781 assumed a zero means +useless. Across three re-measurements the zeros here split **six** ways, and none of them is grounds +for removal until you know which one you are looking at: -The corpus is also larger than counted: **964** transcripts via `rglob`, not 811 — a top-level-only -glob sees 209 here and manufactures false zeros. Positive control on the same query: 24,762 `Bash`, -5,275 `Read`, 4,037 `Edit`. +1. **Broken** — `csharp-ls` and the `csharp-lsp` MCP server named a dotnet root this machine does not + have (#777). Unreachable, not unwanted. +2. **Measured on the wrong surface** — `codex` (used through `Bash`), `security-guidance` and + `ralph-loop` (hooks, which never appear as a tool call), `feature-dev` (agents, not skills). Four + separate instances of one mistake, and each looked like a confident zero. +3. **Not enabled** — `serena` was `false` until 2026-08-14T12:31Z, and six of the plugins above until + 12:41Z the same day. *Installed* is not *enabled*: `installedAt` says March for all of them, and + only `enabledPlugins` says whether a thing could have been used at all. +4. **Enabled but never called** — `serena` since 2026-08-14, and the `csharp-lsp` MCP server since it + became startable. This is the only shape that resembles the issue's premise. +5. **Evidence aged out** — the corpus is a rolling window: 964 transcripts on 2026-08-14, **486** on + 2026-08-28, reaching back only to 2026-07-29. A zero can mean the calls expired, and an older + measurement's counts are not reproducible from a later corpus. +6. **No observed use in the window** — `context7`. Note the wording: the corpus starts 2026-07-29 and + these plugins date to March, so the evidence cannot distinguish "never used" from "not used + lately", and it cannot support "excess" as a historical claim. -| Row above | Re-measured | -|---|---| -| `mempalace` "last hit 2026-07-25 (19 days)" | **31 calls, last seen 2026-08-14.** In active use; the gap was a snapshot artifact, not disuse. | -| `codex` "0 skill invocations" | **113 `codex exec` calls across 17 sessions** — the third-heaviest tool in the corpus. The caveat was right; this is the number. | -| `LSP` "0 calls" | Not zero. The handful of calls are from sessions *testing whether the LSP works* — including §5.1's own probes. | -| `gitea` "duplicate, identical values" | Duplicated but **not identical** — the user-scope entry carries `"type": "stdio"`. **Resolved:** the *project* copy was removed, not the user one, because `server-management` and `homelab-docs` have no `.mcp.json` and depend on user scope. | -| *(absent)* `serena` | Enabled; starts and initializes standalone (Serena 1.28.1) but its tools never appear in a session and `ToolSearch` cannot resolve them. Cause unproven → **#799**. | -| *(absent)* six further plugins | Three (`claude-md-management`, `skill-creator`, `frontend-design`) were enabled **on 2026-08-14**, so their zeros mean "not yet tried". The rest are reachable and simply unused. | +Establishing *which* of the six applies is the precondition for acting, and doing so cost three +sessions here. What was demonstrably wrong with this configuration was **duplication** and +**breakage**; excess was never shown. -**The issue's framing does not survive the measurement, and that is the finding.** "Retire what is -enabled and never invoked" treats a zero as evidence of uselessness. These zeros split **four** ways: -**broken** (`csharp-lsp` MCP and two LSP servers — #777), **unreachable** (`serena`), **just enabled** -(three plugins), and **measured on the wrong surface** (`codex`). Establishing *why* a counter is zero -is a precondition for acting on it — and on this evidence almost nothing should be removed. What was -actually wrong was **duplication** and **breakage**, not excess. The one entry still reading as -coverage it does not provide is `nuget`, which names the same dead `/usr/local/share/dotnet` path -#777 fixed elsewhere; it is inert only because it is explicitly disabled. +**`serena` was disabled, and #799's observation was correct (#799).** The plugin was `false` in +`~/.claude/settings.json` until **2026-08-14T12:31Z**, when a *concurrent* session enabled it — +`~/.claude/settings.json.bak-doctor`, written moments earlier, still records `serena: false`. Its +tools appear in **no transcript before 2026-08-14T12:42:54Z**, then in 6 transcripts that day and +186 across the 10 days since, always in a harness-emitted availability listing rather than prose +(counts as of the 2026-08-28 snapshot; the corpus is live and grows). The #799 session had started +at 12:01Z **and never reloaded**, so its tool set never contained them; its `ToolSearch` probe at +13:41Z correctly reported them absent, and that probe *did* carry a positive control — two other +`ToolSearch` queries resolved in the same session minutes before. + +**Enabling a plugin did not reach a session that was already running; a reload did.** Both sessions +started *before* the 12:31Z toggle, so start time alone separates nothing — what separates them is +the reload. The enabling session ran `/reload-plugins` at **12:54:32.694Z**, printed +`Reloaded: 17 plugins`, and its next tool delta added serena **19 seconds later at 12:54:51.402Z**. +The #799 session ran for 3h54m, emitted exactly one tool delta (12:01:27Z, no serena), and never +reloaded. Its author read `serena: true` from the settings file and `serena: absent` from their own +tool set, and both were true simultaneously. + +Two sessions are not a proof of the harness's contract, so treat this as the operating rule rather +than a mechanism: **do not assume a plugin a sibling session enabled is available to yours — reload +or restart, then confirm the tool resolves.** + +Two things follow for any future audit. **Read enablement as of the evidence, not as of now** — an +audit that compares *current* config against *historical* usage will call a plugin unused for a +window in which it was switched off. And **date transcript evidence by record timestamp, never by +file mtime**: mtime is when a session last wrote, which can be hours or days after the event being +proved, and here it is what made a first-appearance look like a pre-existing state. + +The `activate_project` requirement is a separate, real trap, reachable only once the tools are +present: without it the first query returns a "no active project" error naming the projects it does +know. Both of #799's phrasings resolve today, and the demonstrated query is in +`docs/local-lsp-tooling.md`. + +`.mcp.json` is **gitignored**, so none of its content is reviewable from this repo and the +de-duplication recorded below happened on one machine only. Two scopes defined `gitea` (the user +entry additionally carrying `"type": "stdio"`); the **project** copy was removed rather than the user +one, because `server-management` and `homelab-docs` have no `.mcp.json` and depend on user scope. +Verified afterwards by starting the surviving server: 54 tools. **Hooks are the good news.** All 13 scripts in `.claude/hooks/` are wired from either `.claude/settings.json` or `.husky/*`, and no settings entry points at a missing path — there are @@ -668,14 +722,15 @@ This issue is analysis; it spawns implementation rather than doing it. | Issue | Detector / finding | Covers | Priority | |---|---|---|---| -| #774 | **A** — a guard derives its population from the authoritative source and asserts set equality | Family A, 27 records (39%) | high | -| #775 | **B** — every guard ships a mutation proof: delete that guard alone, see red | Family B, 9 records (13%) | high | -| #776 | Make hooks report that they fired — PreToolUse/PostToolUse execution is unobservable | the whole hook layer | high — **DONE**, §5.4 is measured | -| #777 | `csharp-lsp` and `typescript-lsp` are broken; the "workflow agents must use csharp-lsp" note is stale | partial mitigation for Family A | medium | -| #778 | **D** — pin a version or use compare-and-set for read-then-write against live remote state | Family D, 5 records | medium | -| #779 | **F + G** — test the deny path with the production config value; assert full-replace field lists | fail-open + destructive-replace, ~8 records | medium | -| #780 | Reconcile Python tooling — commit a ruff config and enforce it, or stop claiming we do | environment divergence in our own toolchain | medium | -| #781 | Retire enabled-but-never-invoked plugins/MCP servers; de-duplicate the `gitea` server | config that reads as coverage | low | +| #774 | **A** — a guard derives its population from the authoritative source and asserts set equality | Family A, 27 records (39%) | high — **CLOSED** | +| #775 | **B** — every guard ships a mutation proof: delete that guard alone, see red | Family B, 9 records (13%) | high — **CLOSED** | +| #776 | Make hooks report that they fired — PreToolUse/PostToolUse execution is unobservable | the whole hook layer | high — **CLOSED**, §5.4 is measured | +| #777 | `csharp-lsp` and `typescript-lsp` are broken; the "workflow agents must use csharp-lsp" note is stale | partial mitigation for Family A | medium — **CLOSED**, §5.1 | +| #778 | **D** — pin a version or use compare-and-set for read-then-write against live remote state | Family D, 5 records | medium — **CLOSED** | +| #779 | **F + G** — test the deny path with the production config value; assert full-replace field lists | fail-open + destructive-replace, ~8 records | medium — **CLOSED** | +| #780 | Reconcile Python tooling — commit a ruff config and enforce it, or stop claiming we do | environment divergence in our own toolchain | medium — **CLOSED**, §5.2 | +| #781 | Retire enabled-but-never-invoked plugins/MCP servers; de-duplicate the `gitea` server | config that reads as coverage | low — **DONE**, §5.3 is re-measured from a derived population; the retirement half was answered *no* on evidence | +| #799 | `serena` is enabled but never invoked — establish why, then use it or disable it | one row of §5.3 | low — **DONE**, it had been *disabled* until 2026-08-14 and is now adopted (`docs/local-lsp-tooling.md`) | **Detector C (dedup by construction) has no issue of its own on purpose.** It is not a thing to build; it is the shape the fixes in #774 and #778 should take when they find two copies of one rule — with diff --git a/docs/local-lsp-tooling.md b/docs/local-lsp-tooling.md index 25ea59a49..b53336609 100644 --- a/docs/local-lsp-tooling.md +++ b/docs/local-lsp-tooling.md @@ -1,4 +1,4 @@ -# Local code-intelligence tooling (LSP + the csharp-lsp MCP server) +# Local code-intelligence tooling (LSP, the csharp-lsp MCP server, serena) What is available for "find every site that references this symbol", how each surface is configured, and how to verify it rather than assume it. Run `scripts/check-local-lsp.sh` to check all of it at @@ -11,7 +11,7 @@ not cover the residue where the population is *sites in code* — #403 (5 of 6 d (a by-id handler covering 4 of 10 media types). Find-all-references is the tool for that residue, and it is overwhelmingly a C# problem here. -## The two surfaces, and which one a subagent can reach +## The three surfaces, and which one a subagent can reach This distinction is the whole reason both halves are documented together. @@ -19,6 +19,7 @@ This distinction is the whole reason both halves are documented together. | --- | --- | --- | | The **`LSP` tool** (Claude Code plugins) | `csharp-ls`, `typescript-language-server`, `pyright-langserver` | **No dispatched subagent has been observed to reach it.** `ToolSearch` returns "No matching deferred tools found" for `select:LSP` in a subagent while the same query resolves in the main session — Claude Code 2.1.232, agent types `general-purpose` and `Explore`, 2026-08-14, plus the independent observation in `docs/defect-shapes-773.md` §5.1. Three observations on one harness version: treat it as measured behaviour to design around, not an architectural guarantee. | | The **`csharp-lsp` MCP server** (`.mcp.json`) | wraps `csharp-ls`; serves `csharp_references`, `csharp_diagnostics`, `csharp_hover`, `csharp_definition`, `csharp_symbols`, `csharp_completions`, `csharp_set_workspace`, … (16 tools) | **Main session and subagents.** Subagents demonstrably call MCP tools here: 326 MCP calls from inside subagent turns across the transcript corpus, spanning four servers (`gitea` 288, `playwright` 26, `ssh-mcp` 6, `mempalace` 6). Not yet demonstrated for `csharp-lsp` specifically — that server only became startable on 2026-08-14 — so this is the general MCP boundary, evidenced, rather than a per-server measurement. | +| The **`serena` MCP server** (plugin `serena@claude-plugins-official`) | its own bundled Roslyn C# server; its generated config lists 73 selectable language-server ids, one or more chosen per project (first match wins per file) | **Main session** demonstrated. Subagent use is *expected* (it is MCP, so the boundary above applies) but **not yet demonstrated** — every serena call on record is from a main session. Requires an explicit `activate_project` before the first query — see below. | So an instruction telling *delegated agents* to use C# code intelligence must point them at the **MCP tools**, never at the `LSP` tool. Briefing harder does not help: an agent that cannot resolve @@ -153,6 +154,72 @@ The alternative is to install the .NET 8 SDK and build upstream unmodified. Eith not fine is leaving it undocumented, because `scripts/check-local-lsp.sh` now *starts* the server, so a wrong pin or a missing patch surfaces as a failed smoke test rather than a false pass. +### The `serena` MCP server + +`serena` is an enabled plugin (`serena@claude-plugins-official`) declaring one MCP server: + +```json +"serena": { + "command": "uvx", + "args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"] +} +``` + +Nothing about it needs configuring in this repo. It answers the same find-all-references question as +`csharp-lsp`, and being MCP it is *expected* to be reachable from dispatched subagents — the gap the +`LSP` tool leaves — though that has not yet been demonstrated for serena specifically. + +**Once it is available, it still requires an explicit project activation.** This was *not* the cause +of #799 — that session never had serena's tools at all (see `docs/defect-shapes-773.md` §5.3) — it is +a second prerequisite you meet only after the tools resolve. A fresh server has no active project, +and the first real call fails with a message naming the projects it does know: + +``` +No active project. Ask the user to provide the project path or to select a project +from this list of known projects: ['adversarial-reviewer', 'server-management', ...] +``` + +That error is the tell, and it is the system reporting this prerequisite rather than the caller +inferring it from an empty result. Activate by absolute path, once per directory: + +``` +mcp__plugin_serena_serena__activate_project project=/Users/timothy/ersatztv +``` + +Demonstrated on 2026-08-28 against a worktree of this repo (`/Users/timothy/ersatztv-wt/781-tooling` +— the path above is the canonical checkout, which is the one to activate for ordinary work) — +`find_symbol` on `ChannelPlaylist` +(`ErsatzTV.Core/Iptv/ChannelPlaylist.cs`) returns the class and its constructor, and +`find_referencing_symbols` on `ErsatzTV.Core.Iptv/ChannelPlaylist` returns references across **7 +files** spanning `ErsatzTV.Core.Tests`, `ErsatzTV/Controllers`, `ErsatzTV/Formatters` and +`ErsatzTV.Application` — the cross-project spread that makes it useful for the sites-in-code +residue. + +Traps specific to it: + +- **Activation is per directory, not per repository, and it is a registration — not a session + setting.** A git worktree is a different path, so it registers as a *separate* serena project + named after the worktree directory. Activate each directory you work in the first time; a running + server also has one *active* project at a time, so switching directories means activating again + even if both are already registered. +- **It writes `.serena/` into the project root** — `project.yml`, `project.local.yml` and a language + server cache. Gitignored here since #799; activating in a checkout that predates that entry leaves + an untracked directory. +- **It starts its own Roslyn server**, `Microsoft.CodeAnalysis.LanguageServer.dll` under + `~/.serena/language_servers/`, independent of `csharp-ls`. That is a *second* C# language server + competing for the same cores as a `dotnet build` or a `csharp-lsp` solution load. Its warm-up cost + has **not** been characterised: the one cold run on record (2026-08-28) activated in 1.7 s and + answered `find_symbol` in 6.8 s and `find_referencing_symbols` in 13.1 s, which is far short of + `csharp-ls`'s multi-minute solution load. One observation is not a warm-up profile — treat the + cold-query trap below as applying until someone measures it. +- **A truncated answer degrades into a summary, and says so only in its first line.** + `find_referencing_symbols` with `max_answer_chars: 4000` on `ChannelPlaylist` returned *"The answer + is too long (5758 characters)"* followed by a "References without surrounding lines" list. Re-run + at `max_answer_chars: 60000`, the full answer names the **same 7 files and the same 17 reference + entries across 16 distinct symbols** (`GetChannelPlaylistHandler/Handle` is referenced twice) — so + on this query the degraded form lost only the `content_around_reference` snippets, not coverage. One comparison is not a guarantee for every query: raise the limit and diff the two + before treating a truncated result as exhaustive. + ## Traps - **A query issued before the project graph is loaded is answered anyway, and answered wrongly.**