Skip to content

0.5.0

Choose a tag to compare

@github-actions github-actions released this 20 Sep 08:21
· 189 commits to main since this release

csmesh 0.5.0

Everything an agent branches on without reading the payload — exit codes, budget caps, whether an
answer is complete — got checked against what the tool actually does. Several of them were wrong.

Before you upgrade

Run csmesh index, then csmesh install.

review no longer answers from an index that predates HEAD, and a graph built by the previous binary
is flagged rather than silently trusted. csmesh install refreshes the generated agent files —
AGENTS.md, GEMINI.md, .github/copilot-instructions.md, and the per-assistant skill copies — which
keep the old budgets until it runs, leaving an agent working from numbers this release moved.

If you rely on a default --budget, pass one explicitly to pin the old behaviour.

Wiring sites on path and trace

Hops now say where they were wired, not just where the target lives:

Caller.Run  Caller.cs:7
  -> IGreeter.Greet  Services.cs:3
    -> Greeter.Greet  [impl, di-bound]  Services.cs:4  @ Wiring.cs:7

The @ site is the registration or dispatch line — the other half of "who does this actually call",
and the reason you no longer have to go back to grep to find the binding. It shortens to @ line 41
when the site is in the target's own file.

Interface-dispatch hops carry no site of their own, so the binding is found at render time from the
service's DiBinding edge. When the service has more than one binding, the suffix says which of how
many — @ Wiring.cs:7 (1 of 2 bindings) — rather than presenting one registration as the answer.
When the binding came from an assembly scan rather than an explicit registration, its score and origin
print with it: @ Startup.cs:41 ?0.55 assembly-scan.

Source generators are indexed, not just Razor

csmesh reads compiler-generated sources from obj/**/generated for every generator, not only the
Razor one. On this repo that closed the last 40 unresolved call sites: 38 JsonSerializer calls taking
a JsonSerializerContext.Default member, and the two calls downstream of them whose receiver type
never bound.

If your code consumes System.Text.Json source generation, [GeneratedRegex], [LibraryImport] or
[LoggerMessage], those call chains were previously missing from the graph and are now present —
provided the output exists on disk. A cold checkout that has never been built still resolves less
completely than a built one, and the docs now say so instead of framing it as a Blazor concern.

Exit codes

  • review exits 4 when the index predates HEAD. Previously it compared the merge-base to itself,
    found nothing, and exited 0 — a gate passing the very change it exists to catch. Exit 4 already
    means "the index cannot answer this"; the remedy is the same csmesh index.
  • review --accept under that gap exits 64 and writes nothing, rather than recording findings
    from the wrong side into .csmesh/accepted.txt where every later review would inherit them.
  • An unhandled fault exits 70, not 64. The catch-all reported a crash with the same code as a
    mistyped flag, so an agent retried its arguments instead of reporting the fault.
  • A contended index write exits 75, after exponential backoff, instead of 70. Another process
    holding the graph file is a wait-and-retry condition, not a crash, and 70 meaning both destroyed the
    signal.
  • changes rows carry [STALE] like every other command.

Budget defaults, re-sized

Sized against the worst reasonable answer rather than the median. Every figure below was measured by
re-running the command at a high budget and reading the real output size — the numbers in the
telemetry log were themselves clipped by the caps being judged.

command was now sized against
impl 300 600 an interface with 20 implementations: ~507 tokens
path 400 500 a 12-hop chain: ~445
context 800 900 a base entity type: ~880
entrypoints 700 800 a 29-project endpoint list: ~738, plus six recorded overflows at the old default
where 400 600 a broad term whose full candidate list wanted 535
map 700 850 a real map wanted 1463; 700 was clipping every measurement taken through it
silence 700 300 the largest answer measured was ~127
unresolved 600 700 —

Unchanged: trace 600, blast-radius 800, cycles 800, diff 800, changes 800, review 800.

A raised cap costs nothing for an answer that already fit; it only lifts the ceiling.

map and unresolved are explicit samples

Both print their top rows and a closing footer naming how many groups or rows were withheld and the
flag that narrows to them. A capped answer exits 0 with that footer — a top-N section is complete as
designed.

map used to skip sections silently and exit 0, so a map that dropped half its content reported
success. It now says what it dropped, and exits 2 only when the budget runs out before the caps do.

One marker, inside the budget

The forced OVER BUDGET paragraph is gone. A truncated answer ends with a single line:

INCOMPLETE: nearly complete -- 26 token(s) over (600 of ~626). re-run: csmesh trace Queries.Trace --budget 666
INCOMPLETE: much larger than 600 (5 of 118 shown). narrow with --under or raise --budget to 1250

It names the re-run command that actually fits, and it is written inside a reserved slice of the
budget rather than forced past it — so out_tokens can no longer exceed the cap it reports against.
An incomplete answer is marked at every budget, including ones too small to hold much else.

Warnings that used to vanish

Stale-index and version-gap notes drew from the same pool as the rows, so on a tight budget the note
telling you the index cannot see your change was the first thing dropped. They now have their own
reserve, bounded at 60 tokens. A separator line that does not fit no longer ends a query before its
closing footer.

Honest numbers

Percentages are floored. doctor reporting 6161 of 6163 resolved printed 100.0%; it now prints
99.9% with the raw count beside it, and 100.0% means complete. The same applies to the telemetry
hit rate.

where states in its own output that ranking is reach-weighted, not exact-name-first: an exact name
nothing calls can rank below a substring the entrypoints reach.

Telemetry

The recorded budget is now the cap the writer actually got — it used to log 600 regardless, so every
command with a different default was logged wrong. New fields: reserved_tokens (what pre-query notes
consumed) and would_be_tokens (the size the answer wanted, a lower bound recorded on overflow).
opencode is detected from the markers it exports, and a host that exposes no session id is labelled as
such rather than reported as zero sessions.

Under the hood

Golden fixtures now cover every dispatch shape the indexer claims to support — INotificationHandler,
ICommandHandler, IQueryHandler, IHandleMessages, SendAsync, PublishAsync, Invoke — plus a
case that deliberately does not resolve, so the unresolved section proves something. Snapshots compare
as sorted sequences with confidence, source and site retained, so a regression reads as "this edge lost
its confidence" rather than a wall of renumbered ids.

Two guards now read the documentation and compare it to the code: the exit-code tables in the README,
the npm README, SKILL.md and the site must equal the emitted Exit constants, and the documented
default-budget line must equal what the writer resolves per command. The first one caught a missing
exit 75 on its first run.


📦 Installation & Upgrades

Global .NET Tool

dotnet tool update --global CsMesh

Automatic One-Line Install Script

Linux & macOS:

curl -fsSL https://raw.githubusercontent.com/nRafinia/CsMesh/main/install.sh | sh

Windows (PowerShell):

irm https://raw.githubusercontent.com/nRafinia/CsMesh/main/install.ps1 | iex

Quick Setup for AI Assistants

# Install skills and register MCP server in one command:
csmesh install --all
csmesh index