dotnet-read
Skill2 starsChanged 53 days agoMITView on GitHub
Use when reading or navigating C#/.NET code — find or search for a class, interface, method, property or field; locate a symbol's declaration and read it; look up who calls something or what implements it; trace a call tree or a path between two functions; see what is callable at a line; inspect a base chain or project references; find a dependency cycle; or see what a commit or branch changed. Replaces Grep, Glob, find, cat and whole-file Read on .cs files with the plugin's Roslyn-backed MCP tools plus a ranged Read, and names the one right tool and call shape for each question.
In this repository10 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- dotnet-architectureSkill
- dotnet-consistencySkill
- dotnet-exploreSkill
- dotnet-initSkill
- dotnet-performanceSkill
- dotnet-reviewSkill
- dotnet-selfevalSkill
- dotnet-writeSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 4 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
Every question below is answered by a Roslyn-backed MCP tool that reads the compiler's own model.
- Very specific: line 25
Resolve the prefix once per session rather than assuming it: Run ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__workspace_status").
- Distinctive: line 123
All three columns are terse codes, each with its legend stated once per response:
- Very specific: line 147
When dispatch is the question, ask get_references or get_symbol's referenceCounts — both expand to the interface members a symbol …
- Distinctive: line 282
If I change this, how far does the change ripple?
- Very specific: line 290
Never for "how does A reach B": When both ends are named, that is get_call_slice, and the difference is not small: a callees tree from a …
- Distinctive: line 304
If there is no path, how close does each end get?
- Very specific: line 350
Use it instead of opening every .csproj.
- Distinctive: line 372
Use it instead of reading a textual diff and inferring.
- Very specific: line 494
Every tool that records telemetry takes an optional taskId.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Reading and navigating C# with dotnet-toolkit
Every question below is answered by a Roslyn-backed MCP tool that reads the compiler's own model.
Grep, Glob, find, cat, sed and whole-file Read on a .cs file are wrong here for
finding things, not just slower — they cannot see interface, virtual or delegate dispatch, count
comment and string matches as real hits, silently under-report when output is truncated, and hand
back one fragment of a partial class with no signal that the rest exists. Reading the text once it
is located is a different matter: no tool here returns source. get_symbol answers where
(declarationSites, doc comment included), and a ranged Read over that range fetches the text —
that is the intended pair, and a ranged read draws no hook note. A PreToolUse hook adds an advisory
when Read or a shell read takes a whole compiled .cs file, but does not block it —
Grep/Glob are watched by no hook at all. The router below, not hook enforcement, is what keeps
you on the right tool.
Tool names carry a host-dependent prefix. In Claude Code they are
mcp__plugin_dotnet-toolkit_dotnet__<tool>, because the plugin qualifies the server it ships. Codex
registers the same server under the bare name dotnet and namespaces by that instead, so the prefix
differs there. Every fully-qualified name below is written in the Claude Code form.
Resolve the prefix once per session rather than assuming it. Run
ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__workspace_status"); if that returns nothing,
search for the bare tool name (ToolSearch("workspace_status")) and read the prefix off whatever
comes back, then apply that same prefix to every name in this skill. One probe settles it for the
whole session. Do not guess a prefix and do not fall back to Grep/Read because a qualified name
missed — the tools are there under a different label, and the fallback is the failure this skill
exists to prevent.
Step 0 — call workspace_status before any read
First MCP call of the session, always. It is free, takes no arguments, records no telemetry, and gives you two things nothing else can:
- Readiness. Until the workspace is loaded, semantic tools answer from the syntax tier or refuse
outright. A zero-hit from
get_references,get_call_hierarchy,get_call_slice,get_type_hierarchyorget_semantic_diffagainst a cold workspace is workspace state, not absence — never report it as "nothing uses this". pluginRoot. The plugin's install directory, and the only supported way to reach a file that ships with the plugin. Join it yourself:- a tool manual →
<pluginRoot>/docs/tools/<tool>.md - a coding standard →
<pluginRoot>/standards/<name>.md
Never write
${CLAUDE_PLUGIN_ROOT}into a path. The harness substitutes it into.mcp.jsonargs, hook commands and skill content, but not into a rule or an agent definition, so it stays literal and the read fails.Readthe joined path — the read guards only ever note a.csfile.- a tool manual →
Re-call it whenever a response comes back stale or degraded (see Workspace readiness below).
Step 1 — load the tool by its exact name
Schemas are deferred: a tool must pass through ToolSearch once per session before it can be called.
Pick the tool from the tables below, then load it by exact name:
ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__get_references")Never describe the task to ToolSearch and let it rank candidates. The mechanism is lexical
(regex or BM25 over tool names, descriptions and argument descriptions), not semantic. Measured here:
11 of the then 18 tools failed to appear in the top 5 for a natural-phrasing query — "who calls this method"
never surfaced get_references at all. The tables below pick the tool; ToolSearch only fetches it.
The schema is then enough for an ordinary call. Reach for a manual only for an advanced selector, an unfamiliar response field, or error recovery.
The tools
search_index — find symbols when you don't know the exact name
Answers to:
- Where is the class/interface/method/property/field called X?
- What symbols exist matching these several terms at once?
- Which types implement IFoo, or live under this folder?
- Which symbols are
public,static,abstract,async, extension methods,IDisposable? - Which symbols have (or lack) an XML
<summary>,<returns>,<remarks>? - How is the codebase laid out by namespace or by file?
- Roughly what will fetching this symbol cost me?
- Does anything actually use this — is it dead code?
- Did any of my search terms find nothing at all?
One call, every term. Terms are OR-ed and ranked together:
search_index(query: "fee ledger TryBuy TrySell") answers for all four in one round trip. Each term
gets only a shallow floor share of limit, so always read termsWithNoHits — a term the result
never covered is named there, and an absent term is never evidence of an absent symbol.
query matches identifier text, not structure. query: "class", "partial class" or "nested"
searches for a symbol literally named that — it is never how you ask "list every class" or "list every
partial/nested type", and returns nothing when this repo has no symbol named class. "Is a class" is
the kinds filter; "is partial/static/public/…" is modifiers; "is nested" has no filter at all — read
it off shape's N count. If you don't yet know a real identifier or domain noun to search for, Read
this project's README.md (or CLAUDE.md if there is no README.md) before the first search_index
call — both are plain Markdown, not .cs, so this skill's tools don't apply to them.
A hit is one symbol, not one declaration. A type declared partial across several files collapses
to a single ranked row, whose file names only the fragment that ranked. parts: N (with a
partsHint) appears when N > 1 and says so; absent means one file. Never read a ranked page as an
enumeration of declaration sites — get_symbol's declarationSites is the call that enumerates
them, and on a 13-file partial a benchmarked MCP route read the hits instead and answered 5.
A hit's lines (@from-to) is the signature span only — it excludes a leading /// doc comment,
so it is the line to quote for "where is this", not the range to Read for the whole declaration.
get_symbol's declarationSites is the range to read.
Names only. The index holds identifiers and doc summaries, not bodies: a string literal, a log
message, a comment or a local variable is not findable here. That one question is a Grep, and the
only one on C# where it is the right tool. A type you can only describe is often that question
too: <summary> text is indexed, <remarks> and body comments are not, so when the phrase that
discriminates it ("full pivoting", "re-runs elimination") lives in prose, one Grep over the tree
lands on the doc comment where two name-guessing searches return nothing — measured on FluidScript
(2026-09-20), where the raw route won that question outright. Grep the phrase, then get_symbol on
the type it sits above.
include picks each hit's columns, comma-separated, and replaces the default set exactly as
get_symbol's include does. Default: shape,refs,modifiers. Also available: summary
(a hasSummary bool) and summary:full (the text, capped at 160 chars). Commas, never hyphens.
All three columns are terse codes, each with its legend stated once per response:
| Column | Reads | Letters |
shape | what fetching it costs | Params Members Nested Lines Outline Doclines Commentlines Attributes |
refs | what already uses it | Rcallers Ecallees Implementations Voverrides Tests |
modifiers | what it is | lowercase: p=public i=internal t=protected x=private s=static a=abstract v=virtual o=override y=async … |
Uppercase-with-digits is shape/refs; lowercase-without is modifiers, so the columns never blur
together even though both reuse letters.
refs answers "is this used anywhere" without a second call, in one batched lookup for the whole
page. Read its zeroes and its silences precisely:
- A digit is a measured fact.
R0means nothing calls this — the dead-code answer the column exists to give. A named type getsIinstead ofR, because call edges bind to members. - A missing letter means that fact cannot apply to this symbol's kind, or is zero and not worth
three bytes. Only
R(on a member) andI(on a type) are emitted at zero. - A missing whole column is "not measured", never "zero". The project's edges were never indexed
— typically because it failed to load in MSBuild, which once hid a method that had 5 callers.
refsHintsays so when it happens, and reading that silence as "unused" is how live code gets deleted. Rcounts calls written against the symbol itself. One made through an interface is recorded against the interface member, so a method reached only by dispatch can legitimately readR0. When dispatch is the question, askget_referencesorget_symbol'sreferenceCounts— both expand to the interface members a symbol implements.
A describe-the-operation lookup compares summaries, never takes the top hit. When the question
names what a type does rather than what it is called, ask with include: "shape,refs,modifiers,summary:full"
so every hit's summary comes back in the same call, and read the top few before choosing. Two probes
issued near-identical queries on FluidScript (2026-09-20); the one that took the top-ranked hit named
the consumer of the thing asked for, the one that read the runner-up's summary got it. The verb in a
summary ("derives", "computes", "builds") is what settles it; ranking is by name tokens and cannot.
Read shape before deciding the next call. A type with a large M wants
get_symbol(include: "members"), never a whole-type Read. A member with a large L wants
get_symbol(include: "bodyOutline") to map it, then a Read of the one region. Anything small wants
get_symbol for declarationSites and one Read of that range.
Manual: <pluginRoot>/docs/tools/search_index.md
get_symbol — locate a symbol you can name
Answers to:
- Where exactly is this symbol declared — every file and line range, partials included?
- What XML docs, attributes, members, interfaces and base type does it have?
- How many callers, tests, implementations and overrides does it have?
- What is its control-flow structure, without reading the whole body?
- Is the name I used ambiguous, or misspelled?
- Is expanding to its references worth it?
It returns no source text. declarationSites carries file, startLine (where a leading ///
doc comment begins), signatureLine (where the declaration itself begins; present only when it
differs) and endLine (the closing brace), one entry per part of a partial. The text is one
ranged read away:
Read(file, offset: startLine, limit: endLine - startLine + 1)That pair — get_symbol to locate, Read of that range to fetch — is the whole reading path. It
costs less than a whole-file Read on anything but a tiny file, misses no part of a partial, and the
ranged read draws no hook note. Do not Read a whole file to find a symbol you could have named.
Read startLine..endLine once — it already includes the /// block. Reading from
signatureLine and then a second time from startLine "to get the doc comment" is two reads for one
span; both MCP routes on PandaAI (2026-09-20) paid it. And a range past ~100 lines is a sign to
outline first: get_symbol(include: "bodyOutline") maps a long member's landmarks so the read is
the one region, not three 150-line slices of it — the MCP ceiling on PandaAI lost ~5k tokens to
exactly that, on a question the outline would have settled in one slice.
Report a location from signatureLine, not startLine. signatureLine comes first in the row
and is where the declaration begins, matching what search_index reports; startLine is the read
offset and takes in the doc comment. Answering "declared at line 3" when the class keyword is on
line 7 is the mistake it exists to prevent — on FluidScript (2026-09-20) it was 42 lines, the length
of the doc comment.
include is a plain comma list of component names that replaces the default (xmlDoc,
referenceCounts):
include: "members" a type's surface — navigate by member list, not by reading the type
include: "bodyOutline" landmarks in a long member, then Read one region
include: "members,baseType,interfaces" a type's shape from outside
include: "mechanicalFacts" does it throw / await / lock / write, without reading itmembers is capped at 40, with totalMembers/truncated past the cap.
A single-symbol lookup always carries xmlDoc, whatever include says — a batch keeps include
literal. The doc comment is the one line you are about to act on: an <remarks> saying a method
exists to satisfy an analyzer rule, or is not thread-safe, or is obsolete. Read it before deciding
anything about the symbol. On FluidScript (2026-09-20) every route declared a method "safe to
delete" on the strength of zero references, while its own remarks said the build depends on it.
Zero references answers "is this called", never "is this needed".
Manual: <pluginRoot>/docs/tools/get_symbol.md
get_references — who calls it, and from where
Answers to:
- Who calls this method, and at which file and line?
- What uses this type — as a field, parameter, return type, or construction site?
- What implements this interface, or overrides this virtual?
- How many text-only matches would a grep have given me falsely?
- Are any of these use sites tests?
- Are there more references than came back, and how do I page to them?
- Is a 0-caller result actually safe to treat as dead code?
direction is callers (default) | implementations | overrides. Each item carries a
symbolId, a displayString, and sites — {file, line, snippet}, one row per file+line. On a
named type there are no call sites, so callers returns the members that reference it.
totalItems counts referencing symbols; totalSites counts the sites themselves and is present
only when the two differ. Asked "how many call sites", report totalSites — hand-tallying the rows
across items is how a benchmarked route delivered 18 of the 19 sites its own response contained.
A 0-caller result on a known reflection entry point is not evidence it is unused. entryPointHint
(present only in that exact shape) says so directly — a [Fact]/[Theory]/[Test]/[TestMethod]
method, an [McpServerTool]/[McpServerPrompt]/[McpServerResource] handler, an ASP.NET routing
attribute ([HttpGet], [Route], …), [JsonConverter], [ModuleInitializer], or a static Main is
invoked by its own framework's reflection scan or routing table, which leaves no call-site edge for
this or any static reference search to find. Two separate blind benchmarks of this tool caught this
exact shape producing a confidently wrong "safe to delete" verdict — first on a [Fact] method, later
on an [McpServerTool] one on an unrelated repo — before the hint generalized past tests; don't
re-derive that caveat from memory when the field already states it. For a routed action the hint
also says where the callers are — outside the compilation — and that is a Grep of the route
template over the client code (ClientApp/, a TypeScript tree, HttpClient callers) before any
"safe to delete" verdict. On PandaAI (2026-09-20) the MCP route stopped at the hint with a correct
"no" and no caller; the raw route's whole-tree grep found the React consumer. The compiler's view
ends at the process boundary, and this is the one delete-safety question where that boundary is
crossed.
Do not read a large dispatchKind: virtual count as a large real fan-in, and do not report an
indirect row as a caller of this member. Roslyn cascades: a call written against a base or
interface declaration is reported as a caller of everything that overrides or implements it. Those
rows are marked indirect: true and carry via — the member the call actually binds to — and the
envelope splits directItems from indirectItems and says so once in dispatchNote. Asked "where is
this called with the receiver typed as Pipe", the answer is the rows without indirect, and
nothing else needs reading; on FluidScript (2026-09-20) a route reported all eight rows where three
were right. indirectItems means "could land here", not "does".
Check targetDisplayString when a count surprises you. A bare-name or suffix symbol handle can
bind to something you never meant, and the wrong answer looks exactly as authoritative as the right
one. That field names what actually resolved; targetSymbolId is a hash and confirms nothing on
sight.
An item's symbolId is a fetch target; the text at a site is a Read around sites[].line, or a
get_symbol on the item for its whole declaration span — and only for the sites you will change.
Manual: <pluginRoot>/docs/tools/get_references.md
get_call_hierarchy — the multi-level call tree, and blast radius
Answers to:
- Who eventually calls this, up to the entry points?
- What does this eventually call, several hops down?
- If I change this, how far does the change ripple?
- How many distinct symbols does it reach, per depth?
- Was the tree truncated, and where?
- Is there recursion in this path?
direction: "callers"|"callees", maxDepth (default 3). includeTree: false returns only
blastRadius — the cheapest possible answer to "how much does changing this ripple".
Never for "how does A reach B". When both ends are named, that is get_call_slice, and the
difference is not small: a callees tree from a well-connected root is the most expensive response
this server produces — on FluidScript (2026-09-20) two independent probes each paid 13–18k tokens for
a three-hop path inside one file that get_call_slice answers in a few hundred. The hierarchy tool
is for an open-ended question with one known end.
Manual: <pluginRoot>/docs/tools/get_call_hierarchy.md
get_call_slice — is there a path from X to Y
Answers to:
- Does control flow reach from this function to that one?
- What is the shortest path between them, and through what?
- If there is no path, how close does each end get?
Both endpoints must already be named. Use it instead of walking the graph with repeated
get_references calls.
Manual: <pluginRoot>/docs/tools/get_call_slice.md
get_scope — what is callable at this exact line
Answers to:
- What can I call at this file/line/column?
- Which extension methods apply to this receiver?
- Does a helper for this already exist before I write one?
- What is in scope when I don't yet know the receiver's type?
- Which members are inherited rather than declared here?
Grep cannot answer this at all: an extension method shares no text with its call site. Different from
get_symbol(include: "members"), which is a type's static declared list with no position involved.
Manual: <pluginRoot>/docs/tools/get_scope.md
get_type_hierarchy — the full base chain and every implementer
Answers to:
- What is this type's complete base chain, up to
object? - Which interfaces does it implement, directly and inherited?
- Which types derive from it or implement it?
One hop further than get_symbol's single containingType/baseType. Don't guess a hierarchy from
a one-hop fetch.
A derived entry carries isAbstract/isSealed when true, so "which of these are concrete" is
answered from the same call — the derived list mixes abstract intermediates in with concrete leaves,
and reporting all of them as implementers is a measured wrong answer, not a rounding error.
Manual: <pluginRoot>/docs/tools/get_type_hierarchy.md
get_project_graph — which project references which
Answers to:
- What does this project reference, and what references it?
- What is the solution's whole project dependency graph?
Use it instead of opening every .csproj.
Manual: <pluginRoot>/docs/tools/get_project_graph.md
detect_circular_dependencies — reference loops
Answers to:
- Is there a real dependency cycle between projects?
- Which projects form it?
Manual: <pluginRoot>/docs/tools/detect_circular_dependencies.md
get_semantic_diff — what a commit or branch actually changed
Answers to:
- Which symbols were added, removed or changed between two refs?
- Which version layers moved — declaration, body, or both?
- What is the API impact of this change?
- Was this commit formatting-only?
Use it instead of reading a textual diff and inferring. Formatting- and comment-only commits correctly report no change.
Manual: <pluginRoot>/docs/tools/get_semantic_diff.md
get_retrieval_metrics — where the tokens went
Answers to:
- How many tokens has this session spent on retrieval?
- Which tool, symbol or task cost the most?
- What did one specific call cost? (snapshot
groupBy: "tool"either side and subtract)
It reports this server process and nothing else, always — the raw telemetry is cleared when the server starts and again when it stops, and there is no argument that reads another session. A number here is this session's, never a month of accumulated history.
Pass a taskId on the calls you want to isolate — it is the only thing separating concurrent callers,
since every agent talking to this server process shares one ambient session id.
Manual: <pluginRoot>/docs/tools/get_retrieval_metrics.md
workspace_status, reload_workspace, ping, set_output_format
Answers to:
- Is the workspace ready, is indexing done, where is
pluginRoot? →workspace_status - Which projects failed to load, and why? →
workspace_status - A tool just failed, or results look frozen at an older revision? →
workspace_status. It printsknowledgeCache:when the SQLite cache is damaged (symbol search and reference counts are then disabled for the session, and the cache is rebuilt on the next server start), andmsbuild:when no installed SDK satisfies the repo'sglobal.json— the state that otherwise reaches you as an unresolvedMicrosoft.Build.Framework. Both lines are absent when nothing is wrong. - I pulled/checked out/rebased — how do I refresh? →
reload_workspace(scope: "all"). AnEdit/Writeneeds nothing: therefresh-cs-fileshook re-indexes what was written, and theindex:line sayssymbols refreshed Ns agoand how many files are still queued - Is the server answering at all? →
ping - How do I get plain JSON instead of TOON? →
set_output_format(format: "compact"|"json")
Manual (all four): <pluginRoot>/docs/tools/server.md
The cheap-route table
Each row is a route actually observed being taken, and the cheaper route that answers the identical question. This table is why this skill exists: these findings used to live only inside individual tool manuals, which are not loaded unless something already went looking — so the anti-pattern was committed before the note that prevents it was ever read.
| Anti-pattern (route taken) | Cheap route |
search_index for an exact name, then get_symbol on the hit "to confirm it" | Nothing — the hit already carries file and lines (the signature span), and that is the answer to "where is X declared". get_symbol is for what the hit does not carry: declarationSites with the doc comment, every part of a partial, members, docs, reference counts. Measured on FluidScript (2026-09-20): three of ten questions were answered by the search hit alone and the MCP route spent a second call on each anyway |
search_index(pathPrefix: "<one exact .cs file>") to browse a known file's symbols | get_symbol(symbol: "TypeName", include: "members") — no ranking needed, and you get signatures and docs for the same tokens |
search_index, then get_references (or get_symbol) only to learn whether anything uses it | Nothing — the refs column is already there by default. R0 is the dead-code answer, I/V the dispatch one. Measured: that route took 3 calls where a single grep took 1. The one case still worth a second call is a symbol reached only through an interface, which reads R0 — get_symbol(include: "referenceCounts") is dispatch-aware |
search_index for a symbol you found by describing it, then get_symbol just to read its <summary> and location | search_index(include: "shape,refs,modifiers,summary:full") — the location was already in the search hit, and the doc comes back with it. Measured: 2 calls where 1 answers |
get_symbol on a hit just to learn whether it is public, static, or overridable | The modifiers column, already on the hit — it comes from the index the search already read, so it costs nothing extra |
Whole-file Read of a .cs file to see one method | get_symbol for declarationSites, then Read(file, offset: startLine, limit: endLine - startLine + 1) — the same text, none of the rest of the file |
get_symbol(include: "all") to "see everything" about a member | The default include, or bodyOutline for a long body (measured on a 117-line method: all 2,133 tokens vs 192); all adds members/mechanicalFacts costs a navigation question never needs |
search_index("fee"), search_index("ledger"), search_index("TryBuy") … one call per term | search_index("fee ledger TryBuy TrySell") — one round trip, and cross-term ranking you otherwise lose |
search_index(query: "class"), query: "partial class", query: "nested" to enumerate a structural shape | kinds/modifiers for "is a class"/"is partial"; shape's N count for "is nested" — query still needs a real identifier or domain term, from the README/CLAUDE.md if you don't have one yet |
Re-fetching a symbol with get_symbol that this session already fetched and that hasn't changed | Reuse the held declarationSites; after an edit of your own the lines have moved by exactly your edit, and a Read anchored on text you know is cheaper than a locate |
get_references for a caller list you only need as names | get_call_hierarchy(maxDepth: 1) — measured ~1/3 the tokens at 8 callers, ~1/6 at 25 (395 vs 2,255), ~1/8 at 105: the gap widens with fan-in because get_references pays per site and the hierarchy pays per caller. Reach for get_references when you need the {file, line, snippet} sites, and pay for it deliberately |
get_references for an open-ended multi-level tree on a high-fan-in symbol | get_call_hierarchy(maxDepth: 1) — at 105 callers it measured ~1/8 the tokens |
Repeated get_references walking outward to see whether X reaches Y | get_call_slice(from: "X", to: "Y") |
get_call_hierarchy(direction: "callees") to find the path from X to a named Y | get_call_slice(from: "X", to: "Y") — measured 18k tokens against a few hundred for a three-hop path (FluidScript, 2026-09-20); the tree answers a question nobody asked |
search_index with an exact identifier you already hold, "to get the symbolId first" | get_symbol, get_references, get_call_hierarchy all take the name directly — a fully-qualified name, a unique suffix, or Name(ParamType) for an overload. search_index is for a name you do not have. On PandaAI (2026-09-20) both MCP probes opened 7 of 10 questions with a search they already knew the answer to, ~6–8k tokens each |
search_index(query: "TypeName", kinds: "method") to count or list a type's members | get_symbol(include: "members") — a ranked search is not an enumeration: it ranks by name tokens, so same-file members of other types rank too, and a partial's parts merge only in members. On FluidScript (2026-09-20) the search route counted 122 methods of a type that declares 119 |
Chaining get_references by hand three levels up | get_call_hierarchy |
get_call_hierarchy with the full tree just to size a change | get_call_hierarchy(includeTree: false) — blastRadius alone, and it is byte-identical to the one the full tree carries (verified: same totalUniqueNodes, same perDepth). Measured at maxDepth: 3 on an 11-caller root: 46 vs 1,020 tokens, 22×; at maxDepth: 1 on a 147-caller root, 44 vs 397. The tree is the expensive part and you were not going to read it |
Guessing a base chain from get_symbol's one-hop containingType | get_type_hierarchy |
| Grepping for a helper to find out whether one already exists | get_scope at the line you are standing on — it includes inherited and extension methods |
Batch get_symbol(symbols: [...]) used reflexively as "obviously cheaper" at any size | What decides it is whether shared can hoist, not n alone. A homogeneous batch (same kind, one include) hoists those fields out of every entry and is a wash-to-slight-win from about n=5 — measured 1,291 tokens against ~1,310 for the five separate fetches, so take it for the 4 saved calls. A mixed-kind batch hoists almost nothing and the per-entry results[i] nesting costs more than it saves until roughly n=8–10 |
search_index (at any limit) to enumerate the files a partial type is declared in, reading the returned hits as the list | get_symbol — its declarationSites returns every fragment, and is the only call that does. A partial type is one hit; parts: N on it says how many files, never which. Measured on a 13-file partial: two ranked searches reported 5 files and rated themselves "fairly sure", while one get_symbol had all 13 |
Hand-tallying sites rows across get_references items to answer "how many call sites" | totalSites — the tool's own count over the whole result, present whenever it differs from totalItems. The hand tally lost one site of 19 on a response that carried all 19 |
get_symbol(include: "members") on a very large or many-fragment partial type | Check the hit's shape first: members is capped at 40 — a M245 type reports the first 40 plus totalMembers: 245, truncated: true, not all 245 (measured 11,367 tokens for the old uncapped response on a 13-file partial). Past the cap, reach for search_index(pathPrefix: "<the one fragment file>", kinds: "method property field") |
search_index(query: "ISolver", implements: "ISolver") treated as broken because it once returned nothing, then get_symbol, get_references, get_symbol … | It is the one-call listing of direct implementers (since 2026-09-20 — before that the interface's own name ranked only the interface, and a full probe on FluidScript spent five calls where one answers). An empty page always carries implementsHint saying which zero it is: the name resolved to no interface (spelling), or it resolved and a different query excluded every implementer. Direct only: an implementer that inherits the interface through a base class is invisible here and to any grep ": ISolver" — get_references(direction: "implementations") or get_type_hierarchy walk the chain, and on PandaAI (2026-09-20) that was the one implementer the raw route missed |
| Reading a whole long member to see one region | get_symbol(include: "bodyOutline") to map it, then Read over the one landmark's startLine–endLine |
Opening every .csproj to trace references | get_project_graph |
Reading git diff and inferring what changed | get_semantic_diff |
| Guessing why code looks deliberate | git log -p / git blame on the file — the toolkit keeps no change log; the repository's history is the record |
Workspace readiness — limitedBy
limitedBy names what the answer could not draw on. It applies to every retrieval tool.
- absent — fully informed. Silence is the healthy case.
index_only— the syntax tier answered. Reference counts and semantic resolution are unavailable, not zero.stale— the file changed on disk since the workspace read it. The next sweep (within a couple of seconds) clears it by itself; re-call after that, since line spans will have moved. Never anchor an edit on astaleresponse's lines.degraded— projects failed to load. Results may be silently wrong, not merely thin. Callworkspace_status, fix the build, thenreload_workspace. Never report a finding from a degraded workspace without saying so.
get_references needs live semantics and returns error: "workspace_loading" until ready. After a
large git operation, call reload_workspace; after an ordinary edit, nothing — the refresh hook and
the sweep keep the index current.
Reading responses
Responses are TOON by default — the same field names as the JSON documented in each tool manual,
more compact. set_output_format(format: "compact"|"json") switches for the session.
Switch formats the moment TOON is costing you accuracy. TOON is the default because it is the cheapest encoding of the identical data, not because reading it is a requirement — every field name is the one the manual documents either way. A response you parse tentatively is the expensive outcome, because the next call is built on a field you guessed at:
| Symptom | Call | Why this and not a retry |
| The response's structure is not unambiguous to you — nesting, array headers, or where one row ends | set_output_format(format: "compact") | Compact JSON is explicit about structure and still drops whitespace, so it costs far less than full json. One call, holds for the session |
| Compact JSON is still hard to follow | set_output_format(format: "json") | Full indentation. The most expensive format, and worth it against guessing |
| The shape is clear but a field's meaning is not | Read the tool's manual at <pluginRoot>/docs/tools/<tool>.md | A format switch cannot answer a semantics question — the same field comes back under the same name |
Re-issuing an identical call in the same format returns identical bytes. If a response disappointed you, change the question, the arguments, or the format — never repeat it unchanged.
Absent is not zero. An absent field carries no information: an absent tests means "not
computed", not "no tests". A null is dropped rather than written as "field":null, so check for the
key's absence, not its value. The one deliberate exception is search_index's shape column, whose
legend says so: an absent letter means zero or not-applicable, never "not computed".
Every tool that records telemetry takes an optional taskId. No tool takes a sessionId.
When to hand off
- The set of symbols the task touches isn't known yet → invoke
dotnet-exploreinstead of fanning outsearch_index/get_referenceshere. It spends the wide responses in its own context and hands backsymbolIds, use sites and blast radius. Skip it when the symbol is already known. - About to change a
.csfile → invokedotnet-write. It owns the locate-read-edit-build loop, the standards step, and how the index follows your edits. - Judging code quality → invoke
dotnet-review. Never review C# inline.
Non-C# files (.csproj, .json, .md, .cmd) are ordinary Read/Grep territory — this skill
does not apply to them.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

