Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx skills add amusleh-spotware-com/terse-sharp --skill assetsgit clone --depth 1 https://github.com/amusleh-spotware-com/terse-sharpWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/skills/amusleh-spotware-com/terse-sharp/assets)<a href="https://agentmods.dev/skills/amusleh-spotware-com/terse-sharp/assets"><img src="https://agentmods.dev/badge/skills/amusleh-spotware-com/terse-sharp/assets.svg" alt="Measured on agentmods" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5.1 | $0.00112 | $0.30152 |
| Opus 5 | $0.00056 | $0.15076 |
| Sonnet 5 | $0.00022 | $0.06030 |
| Haiku 4.5 | $0.00011 | $0.03015 |
Grade A, and why
terse-sharp scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 3d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 1,135 lines — stays where its author put it; the contents beside it link to each section on GitHub.
TerseSharp
TerseSharp answers C# and XAML questions semantically, from a Roslyn workspace that is already
loaded. Reading a .cs file whole, or grepping for a type name, costs 10-30x more tokens and returns
matches that are not references.
Route every question by its target
Every C#/.NET question has a tool, and the table below names it. Read the left column, take the
tool on the right, call it — that is the whole working rule, and it is the one thing to remember from
this document. It holds for .cs, .razor, .cshtml, .csproj, .props, .targets,
.sln/.slnx/.slnf, .xaml, .axaml, .paml, .resx and .resw, and for every question about
C# symbols, references, diagnostics, builds, tests or the working tree.
The rules that keep it that way — what the guard denies, what to do when a tool errors, and the tripwires — are the hard gate directly below the table.
The whole surface — one row per job
Read the Job column for what you want, the Instead of column for the built-in it retires, and call what is in Use. Every tool the server advertises is in this table exactly once.
| Job | Instead of | Use | Why |
|---|---|---|---|
| Workspace | — | workspace_status |
solution, worktree, branch, project and document counts, plus advertised=<n> tools <t> tokens for what this session's tools/list really costs - verbose=true splits that total into toolDescriptions, parameterDescriptions, schemaFrame and names; its last line is terse=<version>, the one place the running binary names itself — read it before claiming what a tool does or does not do. verbose=true adds the whole surface beside the narrowed one, advertised=20 tools 6000 tokens of 88 tools 25857 |
| Workspace | Bash: terse doctor |
workspace_status(verbose: true) |
the six self-check lines an agent acts on, in-server and without the ~40 s shell-out: roslyn (the SDK's Roslyn against the one terse carries — the check that explains a dead Razor generator), assets, guard coverage, memory (what every live terse server holds), shadow (whether an analyzer was mapped IN PLACE rather than from the shadow cache) and phases |
| Workspace | globbing for *.sln |
load_workspace(path, discover: true) |
lists every solution and project under a directory without loading one; auto-discovery only walks up from the working directory |
| Workspace | — | load_workspace |
one call per solution; targetFramework: picks the framework every semantic tool answers from, reload: true forces a re-read you should almost never need |
| Workspace | — | list_workspaces |
every loaded solution with its git branch and worktree, and the absolute path unload_workspace takes |
| Workspace | — | unload_workspace(path) |
releases the MSBuild file locks; addressed by the solution path, not a worktree name (workspace= is an alias for path=) |
| Workspace | — | list_projects(filter) |
name, language, document count; the name it prints is exactly what build, run_tests, list_tests and clean accept as project= |
| Workspace | one project_properties call per project for "which projects set X" |
list_projects(properties: "IsTestProject,TargetFramework") |
each line gains name=value from MSBuild's evaluated set, so a Directory.Build.props value is answered rather than missed, and an undefined one reads (unset); refused beside path= |
| Workspace | reading a .csproj to learn whether an edit is gated |
list_projects(path: "src/Foo.cs") |
which project compiles that file, from the evaluated EnableDefaultItems the edit path reads; no project compiling it is exactly when a write is not gated |
| Navigate | Read a .cs file |
get_file_outline(path) |
every type and member with signatures and line ranges, no bodies; usings: true adds the file's own using directives, parameterNames: false prints parameter types without their names for about an eighth fewer tokens |
| Navigate | read_text a .cs file no project compiles |
get_file_outline(path) |
a path inside the workspace root that belongs to no project — a fixture tree kept outside the solution — is parsed from its own text, not refused, and the answer ends HEURISTIC parsed from the file's own text. Outside the root it is still refused |
| Navigate | Read several .cs files |
get_file_outline(paths: [...]) |
up to 10 in one response, each under its own path line; an unresolved path is reported inline as NOT_FOUND, never a failed call |
| Navigate | outlining a 45-member file to find five members | get_file_outline(path, contains: "Total") |
keeps only the matching members, under their declaring type, with an N of M members line so the omission is never silent; get_type_outline takes it too. An unfiltered outline of a type with more than 40 members answers its member COUNT, not its members - 104 members - contains= or all=true; contains= or all: true opens it |
| Navigate | read_text a whole .cs file |
it already answers the outline | a .cs path with no startLine, endLine, tail, section or verbose returns get_file_outline's answer plus a steer, because the text is ~3x the tokens; pass verbose: true or a line range for the text |
| Navigate | Read a whole class's source |
get_symbol_source(symbolId) on a type id |
answers get_type_outline's member list plus a steer to one member, not the whole file's text; verbose: true opts back into the source. A type with ONE declaring reference whose rendered source - the declaration plus its doc comment, which is what the response carries - is at most 4 lines and 200 characters answers that source instead, because withholding something shorter than the steer saves nothing |
| Navigate | Read to see one method |
get_symbol_source(symbolId) |
that member only, dedented; verbose: true for it verbatim, comments: false to drop doc and inline comments when you are orienting rather than editing |
| Navigate | Read to see several methods |
get_symbol_source(symbolIds: [...]) |
all of them in one response; an id that does not resolve is reported NOT_RESOLVED <id> carrying the nearest ids when the miss is close, never a failed call |
| Navigate | one get_type_outline call per type |
get_type_outline(symbolIds: [...]) |
up to 20 types in one response, each under its own header line; an id that does not resolve is NOT_RESOLVED inline, never a failed call |
| Navigate | Read to learn a class's API |
get_type_outline(symbolId) |
member list, no bodies; parameterNames: false there too |
| Navigate | — | get_symbol(symbolId) |
signature, kind, accessibility, location and XML doc of one symbol |
| Navigate | one get_symbol call per symbol |
get_symbol(symbolIds: [...]) |
several described in ONE response, each under its own block, with an id that does not resolve reported inline as NOT_RESOLVED rather than failing the call - the batch shape get_symbol_source and get_type_outline already take |
| Navigate | a name an outline printed that answers SaturatedName or AmbiguousSymbol |
get_symbol_source(symbolId, path: "src/Trading/OrderService.cs") |
path= resolves the name inside that file first and only falls back to the solution when the file holds no match — get_symbol and get_type_outline take it too, symbolIds= scopes every id in the batch, and a path= naming no document answers DocumentNotFound instead of being ignored |
| Navigate | Grep for a type or member name |
search_symbols(query) |
declarations only; CamelHump (OSvc finds OrderService); production declarations first, and when the test half also matches it is folded to one N more in test projects - scope=test line |
| Navigate | asking the model whether a framework or NuGet member exists | search_symbols · get_type_outline · get_symbol · get_symbol_source |
a name no source declaration matches falls back to the referenced assemblies: JsonSerializer and System.Threading.Lock answer real signatures tagged System.Runtime 10.0.0.0 instead of 0 symbols/NOT_RESOLVED. Exact type name only - no CamelHump, no substring - and no source, so members come with no line ranges. A kind= that is not a type kind, or any scope=, declines the fallback and says so rather than answering off-filter |
| Navigate | a name the tests declare dozens of times | search_symbols(query, scope: "src") |
keeps one half of the solution - src for the production projects, test for the ones referencing a test framework; an unknown value is refused rather than searching everything |
| Navigate | a common name that buries the one declaration you meant | search_symbols(query, path: "src/Trading/OrderBook.cs") |
matches inside that file are answered first and the whole solution is searched only when it declares none - the path= get_symbol, get_symbol_source and get_type_outline already take; a path naming no document answers DocumentNotFound, and a fallback says so as NOTE path= declared no match |
| Navigate | Grep to find callers |
find_usages(symbolId) |
real references, one line per file, each marked src or test; a usage inside generated code is tagged gen — real, but never edit it |
| Navigate | Grep for implementers |
find_implementations(symbolId) |
resolved through the interface |
| Navigate | the get_file_outline → get_symbol_source pair, when learning what a symbol IS |
explore_symbol(symbolId) |
signature, doc, location, usages split src/test, implementations and XAML sites, in one call |
| Navigate | judging a rename before doing it | impact_of(symbolId) |
every affected file, XAML site and recompiling project |
| Navigate | searching for the tests a change can break | impact_of(symbolId, tests: true) |
the test classes referencing it, each a ready run_tests test= argument; DIRECT references only, so it narrows a run and never replaces one |
| What grep cannot reach | "where is IFoo registered?" |
find_registrations(query) |
open generics, factories and Add* extensions defeat grep; a registration inside an Add* helper is also reported at the call site as via AddTrading(); symbol= and name= are aliases for query= |
| What grep cannot reach | "what endpoints exist?" | list_endpoints() |
every ASP.NET Core Map* with the member it sits in |
| Files | "find the file called X" | find_files(name: "orderrouter") |
a plain file-name substring, case-insensitive, no glob to get right; combines with glob=, which selects first, and a glob that matched nothing names it |
| Files | ls in a directory outside the workspace |
find_files(glob, root: "C:/Users/me/AppData/Local/terse-analyzers") |
any absolute directory, tagged outside-workspace, with full paths on its paths=[...] line; refused beside tracked=true, which needs a repository this tool did not load |
| Files | one find_files call per glob |
find_files(globs: [...]) |
up to 10 globs in ONE response, each under its own header line with its own count, so a glob that matched nothing is visible rather than indistinguishable from one never asked |
| Files | Glob / ls |
find_files(glob) |
bin, obj, .git, .claude, .vs, .idea, artifacts, TestResults, node_modules and directory symlinks excluded |
| Files | globbing a whole tree to learn its shape | find_files(glob, depth: 2) |
everything below the 2nd path segment folds into one src/TerseSharp.Core/** x94 files row - 11 rows here against 367; the count line still counts every file, and a single-match directory stays its file |
| Files | ls -l / Get-Item for a size or a timestamp |
find_files(glob, stamps: true) |
each record gains the file's UTC last-write time and byte length, so "when was this written, and how big is it?" needs no shell; glob takes a concrete path as readily as a pattern, so one file's size is one call |
| Files | Bash: git ls-files to tell a checked-in file from a scratch one |
find_files(glob, tracked: true) |
only the files git tracks, so build output and another session's untracked notes drop out; the bare git ls-files is denied by the guard, every flagged form is not |
| Files | Grep in non-code files |
search_text(query) / search_regex(query) |
tagged HEURISTIC once for the whole response, not per record - these two answer nothing else; the count line counts matching lines, at most one per line, and a zero result proves absence only in the files it searched |
| Files | grep -n -e A -e B -e C / one search per literal |
search_text(queries: ["I175", "I176", "I177"]) |
up to 10 literals in one pass over the same file set; every record carries q1..qN for the position of its literal in queries=, which a regex alternation cannot tell you. A line matching several is one record tagged q1,q3 in query order, so a tag absent from a record means that literal is absent from that line. No legend is echoed back — you passed the array |
| Files | a search that keeps hitting a folder you do not want | search_text(query, exclude: ".research/**") |
dropped after glob= has selected, so one call answers what two used to |
| Files | Grep -C3 / a search then a read |
search_text(query, context: 3) |
the surrounding lines arrive on the hit's own record, indented — no follow-up read_text |
| Files | a text hit, then "which declaration is that line in?" | search_text(query, containers: true) / search_regex(query, containers: true) |
names the C# declaration each hit sits in — Type.Member, from syntax — between the position and the matched line, so the record is an id get_symbol_source takes and no outline plus ranged read follows. Only a .cs file carries one; refused beside countOnly:. A search whose glob restricts it to .cs files and that did NOT pass it ends by naming it, because that is the parameter that turns hits into ids |
| Files | grep -w / a short literal that drags in every longer identifier |
search_text(query, word: true) |
keeps a literal only where the characters either side are neither a letter, a digit nor _; search_regex answers it with \b and does not declare it |
| Edits | change a declaration's attributes — a tool [Description], an [Obsolete] — without re-sending it |
edit_text(path, force: true, oldText: "<short unique fragment>") |
the sanctioned attribute edit: an anchor costs ~30 tokens where replace_symbol re-sends the whole declaration (~1 175 tokens for one [Description]). Not compile-gated - edit_text writes straight through, so analyze the file after |
| Files | grep -o |
search_regex(query, matchesOnly: true) |
prints the matched span instead of the whole line; compose with unique: true for "which distinct values of this shape exist". search_text refuses it — a literal's matched span is the literal you passed |
| Files | grep -c / "is X in these files at all?" |
search_text(query, countOnly: true) |
ONE line per file — path and match count, plus q1=N per queries= entry — and no matched text. Refused beside matchesOnly=, unique=, context= |
| Files | grep -r in a log folder outside the repo |
search_text(query, root: "C:/logs") |
an absolute directory outside every workspace, tagged outside-workspace |
| Files | sort | uniq -c over repeated log lines |
search_text(query, unique: true) |
identical matching lines collapse to the first record plus x<count> |
| Files | Bash: git show <ref>:<path> |
read_text(path, ref: "main") · get_file_outline(path, ref: "main") |
the file as it was at that ref, with the same gutter, line ranges, tail=, section= and maxChars as the working tree, and a whole .cs answering its outline; one path, and a ref that does not exist is refused rather than answered from the working tree |
| Files | Read a non-.cs file |
read_text(path) |
line ranges, bounded response; a line number is printed only where the numbering jumps, so a contiguous read carries one — verbose: true numbers every line; a clipped read ends with next: startLine=… |
| Files | Read several files |
read_text(paths: [...]) |
up to 10 in one response, each under its own path line with its own count and next: note; an unresolved path is NOT_FOUND inline, and maxChars is one budget shared across the batch that names the entry it clipped |
| Files | tail -n 200 log.txt |
read_text(path, tail: 200) |
the last N lines, so the end of a huge log is addressable |
| Edit text | a read-modify-write on a tree another session is also editing | read_text(path, stamp: true) then edit_text(path, …, ifUnchangedSince: "<that stamp>") |
refused ERROR EditConflict when the file's last-write time moved after the stamp, naming both times, before anything is written, write_text ref= included - the one race no other read can detect. It carries ONE file's stamp, so a call writing more than one file is refused rather than checked against a file the stamp did not come from |
| Files | wc -c file for a size you want while reading |
read_text(path, bytes: true) |
ends the answer with bytes=N, on every shape it returns and once per paths= entry |
| Files | guessing what a budgeted document costs before its test runs | read_text(path, tokens: true) |
ends the answer with tokens=N for the whole file whatever range was read - the count the shipped-doc budgets assert - on every shape it returns and once per paths= entry |
| Files | a file whose lines are enormous | read_text(path, maxChars: 20000) |
maxLines cannot bound those; the clip still names the line to continue from, and says line N was cut mid-way when the budget ran out inside a line — raise maxChars for that line, because a line range cannot resume at a character offset |
| Files | Read a whole .md to find a section |
read_text(path, headings: true) then read_text(path, section: "## Commands") |
the heading map with line ranges and each heading's GitHub anchor slug, then only that section. maxLines= bounds the map and maxLevel: 2 drops every ###, so a 179-section changelog answers its shape in a dozen lines; anchors stay the ones GitHub assigns over the whole document, and maxLevel= without headings=true is refused |
| Files | reading a whole .md whose content is one long table |
read_text(path, columns: "Finding,Tool") |
one line per table row, those columns only - what headings=true cannot give a file with nothing to narrow by. section= scopes it to that section's tables and is named in a refusal, maxLines= and maxChars= bound the rows - a projection that runs out of its CHARACTER budget truncates and ends with next: search_regex matchesOnly=true unique=true, the one bounded call that answers the same question, instead of spilling a whole large table; a column no table under the read declares is refused naming the real ones even when the others matched, and headings=/startLine=/endLine=/tail= beside it are refused rather than silently winning |
| Files | a projection whose first column is PROSE, when you only want the row ids | read_text(path, columns: "Finding", cellChars: 60) |
caps every projected CELL and counts what it clipped - a backlog whose Finding cells total ~7 000 characters answers in ~600. Refused without columns=, and below 8 |
| Files | Bash: git checkout -- <path> after a bad write |
write_text(path, ref: "HEAD") |
restores the file from that ref through the same compile gate as any write - the way back for a .csproj or .md that undo_last_change cannot cover. A write of markup carrying < and no raw < warns and names this call, because HTML-escaped markup is not markup |
| Files | a scratch .cs probe outside every workspace root |
write_text(path, content, force: true) |
an absolute path under no loaded root is written with force=true, tagged outside-workspace and never compile-gated, because no project of this workspace compiles it - the read half already worked |
| Files | Bash: rm file · Bash: rmdir |
write_text(path, delete: true) |
containment-checked; a .cs document goes through the compile gate and is covered by undo_last_change. The same call on a DIRECTORY removes it when it is empty, and refuses a non-empty one naming what it still holds |
| Edit text | Edit a .md section |
edit_text(path, section: "## Commands", newText: …) |
no oldText, so no read-then-match round trip |
| Edit text | re-reading a file to see what an edit landed | edit_text(path, oldText, newText, context: 2) |
the N lines around each change in their POST-edit state, numbered, 1-10 - the confirm-read the one-line answer otherwise costs. Not a diff; default 0 |
| Edit text | reading a section out of one file and writing it into another | edit_text(path, section: "## Open", toPath: "other.md") |
cuts the section and lands it in the other file as one write, answered as one changed-line count per file - the whole section text never crosses the wire. place=prepend puts it at the top of the target, anything else appends; occurrence= picks the source section; both paths must be markdown, both must already exist, and naming the same file twice is refused |
| Edit text | anchoring on ### Added to add a changelog entry |
edit_text(path, section: "### Added", occurrence: 1, place: "prepend", newText: …) |
writes inside the section — prepend under its heading, append after its last non-blank line. A heading that repeats needs occurrence=: the refusal names occurrence=1..N and each candidate's start line, so the index is picked with no re-read. read_text takes it too, and refuses it without a section=. Only with section=; supply your own blank lines |
| Edit text | one edit_text row= call per row when closing a whole backlog |
edit_text(path, rows: [{row, newText}, ...], toPath: "IMPROVEMENTS-ARCHIVE.md") |
up to 25 rows cut and landed in order as ONE write per file; an identifier matching nothing or several refuses the batch, so a partial move cannot happen |
| Edit text | closing a backlog row: cutting one table row out of one markdown file and appending it to another | edit_text(path, row: "I286", toPath: "IMPROVEMENTS-ARCHIVE.md", newText: "| … |") |
the row is matched by its first cell, so its old text never crosses the wire; newText= is what lands in the target - omit it to move the row verbatim. An identifier matching no row is refused saying so; one matching several names each candidate's LINE NUMBER and, when one exists, the longer identifier that resolves, so the retry needs no re-read; and row= without toPath= is refused rather than dropped |
| Edit text | three or more edit_text calls on the same file |
edit_text(path, edits: [{oldText, newText}, …]) |
applied in order as one write, at most 10; an entry whose anchor fails is reported with its own code and remedy and the others still land, so one bad anchor never costs the batch. A partly refused batch leads with what changed and lists each refusal as REFUSED <path>: <code> - <message>; remedy: …, so only a leading ERROR means re-send; a malformed entry is named — edits[1] is the entry that failed to bind |
| Edit text | one edit_text call per file across several files |
edit_text(edits: [{oldText, newText, path}, …]) |
an entry may name its own path and its own force, so one .cs attribute edit rides in a markdown batch, and the top-level path may then be omitted entirely; entries are grouped by file, applied as one write each, and answered one line per changed file. A path-less entry with no top-level path is refused by index. At most 10 per file and 25 in total |
| Edit text | one write_text call per new file |
write_text(files: [{path, content}, …]) |
up to 10 in one call, and every .cs document among them shares one compile gate — so a type and the consumer it breaks land together instead of the first write being rolled back alone |
| Edit text | two entries of one edits= batch addressing occurrence 1 and 2 of the SAME anchor |
edit_text refuses them up front |
naming both by the caller's own index, before ANY file of the batch is written - entries apply in order, so once occurrence=1 lands the anchor matches once. Lengthen each anchor, or send the second on its own, where it is occurrence=1 |
| Edit text | an anchor that deliberately repeats — a table of near-identical rows | edit_text(path, oldText: "| row |", occurrence: 3) |
picks the Nth match instead of forcing you to lengthen the anchor; a multi-match refusal lists the candidate lines with their numbers, so occurrence= is picked from the refusal and needs no re-read, and an out-of-range value names the range it could have picked |
| Edit text | Edit/Write a non-.cs file |
edit_text · write_text |
line endings normalized before matching; an ambiguous match is refused and a miss names the file's closest lines |
| Edit text | re-reading a file because an anchor copied from get_symbol_source did not match |
edit_text already handles it |
that payload is dedented, and it still matches: the anchor is compared line by line allowing one uniform whitespace prefix, newText is re-indented by it, and a NOTE says so. A multi-line anchor matching nothing gets the closest REGION and its range, so the retry is a corrected anchor, not a re-read |
| Edit text | Write a new .cs file |
write_text(path, content, force: true) |
no symbol tool creates a file; the write is compile-gated whenever a project globs it, the new type is resolvable on the very next call, and two interdependent new files land in either order |
| Edit text | rewriting a whole .cs file |
write_text(path, content, force: true) |
compile-gated like replace_symbol when the file is already a document: rolled back on a new error unless allowErrors: true |
| Edit code | Edit a .cs file |
replace_symbol_body · replace_symbol · add_member · delete_symbol |
addressed by symbol, immune to line drift, compile-gated; add_member and replace_symbol take several declarations in one edit |
| Edit code | a new body that calls a private helper you have not written yet | replace_symbol(symbolId, declaration, add: [...]) |
the new members land in the containing type inside the same compile-gated edit, so the callee-after-caller CompileRegression never happens; targets must share one containing type, and an enum container is refused, never walked past |
| Edit code | a signature change that breaks its callers | replace_symbol(symbolIds: [...], declarations: [...]) |
one declaration per symbol, paired positionally, applied as one compile-gated edit across every file they live in — the way to land a signature change together with the callers it breaks instead of paying a CompileRegression and a retry |
| Edit code | renaming a member and rewriting its body in one edit | replace_symbol(symbolIds: [...], declarations: [...], rename: true) |
accepts a declaration whose name differs from the symbol it is paired with instead of refusing the batch; references are not rewritten, so the gate rolls it back when a caller breaks - rename_symbol is what makes them follow; every rename it applies is reported as NOTE renamed: Add -> Append |
| Edit code | adding an enum member | add_member(typeSymbolId: "T:…MyEnum", declaration: "Retry") |
an enum id takes enum members; replace_symbol and delete_symbol work on one too |
| Edit code | adding a sibling type to an existing file | add_member(path: "Foo.cs", declaration: "public sealed record Bar(int X);") |
appended to that file's namespace as one compile-gated edit — no whole-file rewrite, no forced text edit |
| Edit code | find-and-replace a name | rename_symbol(symbolId, newName) |
solution-wide, incl. interfaces, overrides, doc crefs and XAML |
| Edit code | reverting an edit you regret | undo_last_change |
up to ten solution snapshots per workspace; a snapshot dropped by an external change is reported rather than overwritten |
| Refactor | hand-writing an interface from a class | extract_interface(symbolId) |
the members you name, with their doc comments, plus the : IFoo on the type |
| Refactor | cut-and-paste between files | move_type_to_file · move_type_to_namespace |
the type, its usings and every reference, as one compile-gated edit |
| Refactor | editing a signature and every call site by hand | change_signature(symbolId, …) |
reorders, adds and removes parameters and updates the callers |
| Projects | editing a .csproj by hand |
project_set_property · project_properties · project_add_reference · project_remove_reference · project_create |
CPM-aware, containment-checked |
| Projects | editing PackageReference by hand |
package_list · package_add · package_remove |
central package management aware: the version lands in Directory.Packages.props |
| Projects | Bash: dotnet list package --vulnerable |
package_list(vulnerable: true) · package_list(outdated: true) |
the resolved graph, including transitive packages - the question the project file cannot answer; needs a restore |
| Projects | "which properties does this project really have?" | project_properties(project) |
MSBuild's evaluated properties with the file that set each, so a Directory.Build.props value is answered instead of 0 properties |
| Projects | editing a .sln/.slnx by hand |
solution_add_project · solution_remove_project |
the solution file only, no MSBuild evaluation |
| Projects | "which projects does this solution contain?" for a solution that is not loaded | solution_projects(path: …) |
reads the .slnx, .sln or .slnf directly and loads nothing, so a fixture-scoped question does not cost a load_workspace that makes every later un-hinted call ambiguous |
| Git | Bash: git log / git show --stat |
history |
commits touching a path, one line each - short sha, date, author, subject - baseRef= for a ref or a range, contains= for git's pickaxe (only the commits whose diff added or removed that literal), message= for the subject grep, and commit=<sha> for one commit's per-file stat. git blame stays on the shell: it ran once in 683 sessions |
| Git | Bash: git describe |
history(describe: true) |
HEAD's position in one line - tag=, ahead=, sha=, dirty= - which is the MinVer question a release asks; creating or verifying a tag stays on the shell |
| Git | Bash: git tag --list / git tag -l "v*" |
history(tags: true) |
every tag newest version first, one line each - name, the short sha it names, its date - bounded by maxResults; refused beside baseRef=, path=, contains=, message= or commit= rather than ignoring them, and creating, annotating or deleting a tag stays on the shell |
| Git | Bash: git diff --cached for its hunk text or its declarations |
diff_symbols(staged: true) · diff_text(staged: true) |
the index against HEAD, which is what a pre-commit review asks; without it a bare diff compares the working tree against the INDEX, so a fully staged change set answers nothing |
| Git | Bash: git diff --cached --name-only / git status --untracked-files=no |
changed_files(staged: true) · changed_files(untracked: false) |
staged=true reads the INDEX against HEAD - or against baseRef= - which is what a pre-commit check asks; untracked=false answers tracked changes only |
| Git | Bash: git status / git diff --stat |
changed_files |
one line per file - path, +added -deleted, status letter; untracked files included, path= scopes it to one pathspec on a shared tree, and exclude= drops what a pathspec cannot leave out - exclude: ".research/**" for another session's notes; an excluded file is not counted |
| Git | Bash: git diff to decide what to review |
diff_symbols |
every hunk mapped onto the declaration containing it, answered as symbol ids you feed straight to get_symbol_source, several hunks inside ONE declaration folded onto one record carrying the id once and its line ranges after it - EXACT inside one declaration, HEURISTIC with the raw line range otherwise, and it ends by naming the exact diff_text path=… call for the hunks it could not map |
| Git | Bash: git diff for the hunk text itself |
diff_text(path: …) |
the raw unified diff: whitespace, a non-.cs file, a pure deletion, and whatever diff_symbols mapped only HEURISTIC. It costs about a response line per changed line, so bound it - path= scopes it, paths=[...] takes up to 10 pathspecs in the same git invocation, maxLines= caps it at 3000 - raised from 1000, which truncated 85% of real calls - and a truncated answer names the exact maxLines= that returns the rest |
| Build and test | Bash: dotnet build / msbuild |
build |
deduplicated diagnostics, no MSBuild spew; a successful build is one line whatever it warned about, a failed one lists errors only |
| Build and test | Bash: dotnet build -c Release |
build(configuration: "Release") |
configuration and targetFramework map to -c and -f on build, run_tests, rerun_failed and list_tests |
| Build and test | Bash: dotnet build -p:Name=Value |
build(properties: ["Name=Value"]) |
properties maps to one -p: per entry on the same four tools, applied after -c and -f; an entry that is not Name=Value is refused before anything runs |
| Build and test | Bash: dotnet test / vstest |
run_tests |
a green run is one line, and a run that spanned several projects appends Name:total/durationMs per project so "which tier is slow" costs no second run; a failure carries its message, expected/actual and one source frame. A solution is built once, then each test assembly runs directly where its runner allows, skipping the MSBuild and VSTest host dotnet test pays per project |
| Build and test | one run_tests call per test project |
run_tests(projects: [...]) |
at most 10, run concurrently; the timeout applies to each project, and one that timed out is named instead of the merged run being reported as passed. Naming the same project twice is refused - two invocations of one assembly race each other and fail tests that pass alone |
| Build and test | bounding parallelism inside one test assembly | run_tests(runSettings: ["xUnit.MaxParallelThreads=1"]) |
VSTest RunSettings overrides, passed through as one trailing -- Name=Value block - the layer parallel deliberately does not touch. xUnit.StopOnFail, MSTest.Parallelize.Workers and NUnit.NumberOfTestWorkers live here too; an entry that is not Name=Value is refused before anything runs |
| Build and test | re-running what broke | rerun_failed |
replays the previous failures only |
| Build and test | re-verifying SOME of what broke, after re-pointing the rest | rerun_failed(tests: [...], exclude: [...]) |
filters the remembered failure list instead of replaying it whole - tests= keeps the entries whose name contains one of them, exclude= drops them, both capped at 10. A filtered re-run always ends NOTE partial rerun - N of M remembered failure(s) re-run, naming what it did not verify, and a filter matching nothing is refused rather than quietly replaying everything |
| Build and test | dotnet test --list-tests |
list_tests(contains) |
names without running |
| Build and test | Bash: dotnet clean |
clean |
freed-byte counters, also removes obj, releases the workspace's file locks; path= sweeps a .slnx/.sln/.slnf/project that is not loaded |
| Analyse | one analyze call per touched file |
analyze(paths: [...]) |
up to 10 files, directories or globs in one pass, so the end-of-task per-file sweep is one call; an entry carrying a comma or a brace is refused by name rather than mis-scoped. A batch that saturates the 10-path cap ends with next: analyze changed=true, which answers the same end-of-task sweep over every modified file in ONE call - take it rather than sending a second batch |
| Analyse | dotnet format whitespace / an IDE inspection |
analyze |
compiler + every referenced analyzer + dead code, down to info |
| Analyse | running analyze → format → cleanup → analyze at the end of a task |
gate |
the same four calls in the mandated order, answering one verdict line - clean analyzed=N fixed=M remaining=0, where analyzed counts the documents in scope - and keeping only the diagnostics still unfixed, each carrying the declaration it sits in exactly as analyze does |
| Analyse | dotnet format style / dotnet format analyzers |
cleanup fix=style|analyzers|all |
applies the referenced analyzers' code fixes, compile-gated, UNFIXED <id> for what no fixer covers |
| Analyse | dotnet format --verify-no-changes |
format verify=true · cleanup verify=true |
one verdict line (clean or VERIFY_FAILED n), no diff; each named file carries the step that would change it - whitespace, fixers or fixers+whitespace - and a mode that also reformats names the byte-equivalent CI pair, so the verdict says whether CI would really be red |
| Analyse | one format call per touched file |
format(paths: [...]) |
up to 10 files, directories or globs in ONE pass, exactly as analyze takes them; an entry carrying a comma or a brace is refused by name rather than mis-scoped |
| Analyse | formatting only what you touched | format changed=true · cleanup changed=true |
files modified since the workspace loaded, so a sweep stops rewriting files the task never opened; the change set survives the unload-and-reload a locked build performs |
| Analyse | reading build output for a consumer you broke | get_diagnostics |
the solution-wide warning and error sweep a per-file pass cannot see; it takes severity= as an alias for minSeverity=, exactly as analyze does |
| XAML | Read a .xaml file |
xaml_outline(path) |
element tree with x:Name/x:Key, no attributes |
| XAML | Grep a .xaml file |
xaml_find(query) · xaml_names() · xaml_resources() |
by element, attribute or content; x:Name declarations; every x:Key with its dictionary |
| XAML | hunting a resource through App.xaml |
xaml_resolve(key) |
every declaration with its scope, one call; a key with no keyed declaration lists the implicit styles targeting it, HEURISTIC, and names no winner |
| XAML | "why does this control look like that" | xaml_styles(typeName) |
implicit and keyed styles with the BasedOn chain, capped by maxResults (100) |
| XAML | eyeballing a {Binding} |
xaml_bindings(path, validate: true) |
each path type-checked through Roslyn |
| XAML | Read a .xaml.cs to see what the markup wires |
xaml_codebehind(path) |
x:Class plus every handler |
| XAML | "is this element translated" | xaml_localization() |
every x:Uid joined to its .resx/.resw entry |
| XAML | guessing whether the markup is sound | xaml_validate() |
duplicate x:Key/x:Name, and resources that resolve to no declaration anywhere under the root |
| XAML | Edit a .xaml file |
xaml_set_property · xaml_add_element · xaml_remove_element |
addressed by element, formatting preserved, an unparseable result refused |
| Localization | Read a .resx/.resw |
resx_get(path, cultures) |
every key with its value per culture; absent ones print MISSING, values=false lists keys only |
| Localization | Glob for resource files |
resx_files() |
every family with its cultures, counts, missing total and designer |
| Localization | Grep a resource key |
resx_find(query) |
key, value or comment, across every family |
| Localization | "is this key still used" | resx_usages(key) |
designer property through Roslyn, plus GetString, localizer, x:Uid, Razor, with composedLookups= so an empty answer is never claimed as proof |
| Localization | "which strings are untranslated" | resx_validate() |
RESX001 missing · RESX002 placeholder mismatch · RESX003 unused (includeUnused only) · RESX004 duplicate · RESX005 orphan · RESX006 empty · RESX007 trimmed whitespace · RESX008 unsorted · RESX009 stale designer |
| Localization | one resx_set call per key |
resx_set(entries: "Key=Value\nOther=Second") |
every key in one pass, and files: [{path, entries}, …] writes up to 10 culture FILES in one, which same-file entries= cannot; a bad line, an empty entry, or a top-level key/value/entries beside it is refused, never dropped |
| Localization | Edit a .resx/.resw |
resx_set · resx_remove · resx_rename |
one <data> element rewritten; header, order, indentation, line endings and BOM kept, and resx_set creates a missing culture file from the neutral header |
| Razor | Read a .razor or .cshtml file |
razor_outline(path) |
directives, component tree and @code members, each component resolved to its type |
| Razor | "how do I use this component" | razor_component(name) |
every [Parameter], which are [EditorRequired], from source or a referenced package |
| Razor | Grep a tag, directive or route in markup |
razor_find(query, kind) |
component, element, attribute, directive, expression or route |
| Razor | "is this @bind real" |
razor_bindings(path, validate: true) |
each @bind/@on/@ref/asp-for resolved against the component type |
| Razor | Read a .razor.cs |
razor_codebehind(path) |
the partial class behind the component and the members it declares |
| Razor | "what breaks at render" | razor_validate() |
unknown parameter, duplicate route, unregistered @inject — none of which the compiler reports |
| Razor | Edit a .razor file |
razor_set_attribute · razor_add_element · razor_remove_element · razor_set_directive |
element-addressed, formatting preserved, compile-gated through the Razor generator |
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 3d ago Changed · +1 lines 4be9efeb7ebe
- 8d ago First seen · 1,134 lines · 112 tokens per session scan A acc45d72b47d
terse-sharp is a skill published in the GitHub repository amusleh-spotware-com/terse-sharp (2 stars, last pushed 3d ago), licensed MIT. It adds 112 tokens to every session and 30,152 once invoked, about $0.0006 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.
Other skills, from other repositories
dependency-audit
Scan .NET project dependencies for vulnerabilities, outdated packages, and license issues. Use before PRs, after adding packages, or on a regular schedule.
rider-search
Routing rules for code search in JetBrains Rider projects — use the Rider MCP symbol/reference/file tools instead of Bash grep. Use whenever searching for a symbol, definition, function, variable, type, or finding usages/references in a C#/.NET or Unreal C++ codebase open in Rider.
C# Patterns
Use this skill when working in C# projects (.NET) and you want patterns for async correctness, clear layering, and safe dependency injection.
.NET Patterns
Use this skill when working in .NET projects (C#) and you want clean layering, safe async usage, and maintainable dependency injection patterns.
generate-code-cs
Generate the code from typespec for C#. Parameter: C# SDK repository root location .
author-test
Generate a test given sample. Parameters: C# SDK repository root; Package name: one of Azure.AI.Projects, Azure.AI.Projects.Agents or Azure.AI.Extensions.OpenAI; the sample to use as a starting point for the test.