agentleFS
AttemainioAttemainio/dotnet-toolkit/skills/dotnet-read/SKILL.md

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.

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
  1. Very specific:

    Every question below is answered by a Roslyn-backed MCP tool that reads the compiler's own model.

    line 8
  2. Very specific:

    Resolve the prefix once per session rather than assuming it: Run ToolSearch("select:mcp__plugin_dotnet-toolkit_dotnet__workspace_status").

    line 25
  3. Distinctive:

    All three columns are terse codes, each with its legend stated once per response:

    line 123
  4. Very specific:

    When dispatch is the question, ask get_references or get_symbol's referenceCounts — both expand to the interface members a symbol …

    line 147
  5. Distinctive:

    If I change this, how far does the change ripple?

    line 282
  6. Very specific:

    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 …

    line 290
  7. Distinctive:

    If there is no path, how close does each end get?

    line 304
  8. Very specific:

    Use it instead of opening every .csproj.

    line 350
  9. Distinctive:

    Use it instead of reading a textual diff and inferring.

    line 372
  10. Very specific:

    Every tool that records telemetry takes an optional taskId.

    line 494

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. 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_hierarchy or get_semantic_diff against a cold workspace is workspace state, not absence — never report it as "nothing uses this".
  2. 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.json args, hook commands and skill content, but not into a rule or an agent definition, so it stays literal and the read fails. Read the joined path — the read guards only ever note a .cs file.

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:

ColumnReadsLetters
shapewhat fetching it costsParams Members Nested Lines Outline Doclines Commentlines Attributes
refswhat already uses itRcallers Ecallees Implementations Voverrides Tests
modifierswhat it islowercase: 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. R0 means nothing calls this — the dead-code answer the column exists to give. A named type gets I instead of R, 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) and I (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. refsHint says so when it happens, and reading that silence as "unused" is how live code gets deleted.
  • R counts 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 read R0. When dispatch is the question, ask get_references or get_symbol's referenceCounts — 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 it

members 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 prints knowledgeCache: 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), and msbuild: when no installed SDK satisfies the repo's global.json — the state that otherwise reaches you as an unresolved Microsoft.Build.Framework. Both lines are absent when nothing is wrong.
  • I pulled/checked out/rebased — how do I refresh? → reload_workspace(scope: "all"). An Edit/Write needs nothing: the refresh-cs-files hook re-indexes what was written, and the index: line says symbols refreshed Ns ago and 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 symbolsget_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 itNothing — 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 locationsearch_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 overridableThe 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 methodget_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 memberThe 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 termsearch_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 shapekinds/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 changedReuse 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 namesget_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 symbolget_call_hierarchy(maxDepth: 1) — at 105 callers it measured ~1/8 the tokens
Repeated get_references walking outward to see whether X reaches Yget_call_slice(from: "X", to: "Y")
get_call_hierarchy(direction: "callees") to find the path from X to a named Yget_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 membersget_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 upget_call_hierarchy
get_call_hierarchy with the full tree just to size a changeget_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 containingTypeget_type_hierarchy
Grepping for a helper to find out whether one already existsget_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 sizeWhat 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 listget_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 typeCheck 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 regionget_symbol(include: "bodyOutline") to map it, then Read over the one landmark's startLine–endLine
Opening every .csproj to trace referencesget_project_graph
Reading git diff and inferring what changedget_semantic_diff
Guessing why code looks deliberategit 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 a stale response's lines.
  • degraded — projects failed to load. Results may be silently wrong, not merely thin. Call workspace_status, fix the build, then reload_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:

SymptomCallWhy this and not a retry
The response's structure is not unambiguous to you — nesting, array headers, or where one row endsset_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 followset_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 notRead the tool's manual at <pluginRoot>/docs/tools/<tool>.mdA 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-explore instead of fanning out search_index/get_references here. It spends the wide responses in its own context and hands back symbolIds, use sites and blast radius. Skip it when the symbol is already known.
  • About to change a .cs file → invoke dotnet-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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.