agentsclimarketplace

Codanna review

Skill antono/agent-skills/codanna-review

Install
npx -y skills add antono/agent-skills --skill codanna-review

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Diff-driven code review for a branch or PR using codanna's call graph plus difftastic's structural diff. Use when the user asks to "review this PR / branch / diff", "audit callers after my changes", "check what I missed", or otherwise wants to find untouched callers of functions whose signatures or bodies changed between two refs.

SKILL.md

17.2 KB, as published. Nobody here has run it

Codanna Review

Diff-driven review: given two refs (or a GitHub PR), find every function that really changed (per difftastic's structural diff), enumerate its callers via codanna, and flag callers whose own bodies were not updated.

Workflow diagram & rationale: README.md

Scripts

scripts/walk-review.sh — end-to-end probe script implementing steps 2–13 for Rust projects. Usage: REPO=/path/to/repo bash scripts/walk-review.sh <base> <head> [output.md].

When to use

  • "Review this branch / PR / diff."
  • "Did I miss any callers after changing function X?"
  • "What's the blast radius of this signature change?"
  • Audit before merge: which callers were updated, which weren't.

Don't use for:

  • Exploring an unfamiliar codebase → codanna-explore.
  • Regex-only searches → rg directly.
  • Pure stylistic / formatter review → out of scope (we explicitly drop those).

Inputs

Accept either:

  1. Two refs: base and head. Default base = main; default head = HEAD. Three-dot semantics (base...head) — compares against merge-base, matching GitHub/GitLab.
  2. GitHub PR: URL or owner/repo#N. Resolve via gh pr view <id> --json baseRefName,headRefName,baseRefOid,headRefOid 2>/dev/null | toon, then gh pr checkout <id> (or git fetch origin pull/<id>/head:<local-name>) if refs aren't already local.

Tool surface

PurposeToolInvocation
Index statusget_index_info MCPcodanna mcp get_index_info
Symbols in a filesearch_symbols MCPcodanna retrieve search "" --module <file> --kind function,method,struct,trait,class --limit 200
Exact symbolfind_symbol MCPcodanna retrieve symbol <name>
Callersfind_callers MCPcodanna retrieve callers <name|symbol_id:N>
Blast radius (high-severity only)analyze_impact MCPcodanna mcp analyze_impact <name>
Structural diffdifft$DIFFT --check-only --exit-code <old> <new> and DFT_UNSTABLE=yes $DIFFT --display=json <old> <new>
Regex sanity passrg (fallback grep)rg -n '<symbol>'
JSON validate / compacttoon… --json 2>/dev/null | toon

$DIFFT probe (do once per session):

if command -v difft >/dev/null 2>&1; then DIFFT="difft"
else DIFFT="nix run nixpkgs#difftastic --"
fi

If nix is also missing, set a NO_DIFFT=1 flag and skip steps 4b/5a — the workflow degrades to line hunks (severity will report "unknown" for affected files). Warn the user once before the first nix run (cold start can take several seconds). Do not suggest nix profile install — out of scope.

Workflow

1. Resolve input → two local refs

  • Two refs given: skip ahead.
  • PR input: gh pr view <id> --json baseRefName,headRefName,baseRefOid,headRefOid 2>/dev/null | toon. Verify both refs are local with git rev-parse --verify; if not, git fetch origin pull/<id>/head or gh pr checkout <id>.
  • Confirm: git merge-base <base> <head> succeeds.

2. Index check + freshness

  • get_index_info. If symbol_count: 0, bootstrap per codanna-explore step 2a. Refuse to proceed without an index.
  • The semantic_search.enabled: false flag is misleading — semantic tools load on demand. Don't bail on it.
  • Precondition 1: git rev-parse HEAD must equal git rev-parse <head>. If not: "codanna's working tree is at <X> but you're reviewing <head> — symbol ranges will be misaligned. Run git checkout <head> first." Refuse to continue.
  • Precondition 2 — force a fresh index. Codanna's incremental indexing (codanna index <file> without --force) leaves stale entries with old line numbers, even for files that have since changed. Confirm with the user, then run codanna index <workspace_root> --force once before step 6. Use workspace_root from .codanna/settings.toml, not a subdir — --force on a subdir wipes the rest of the index. (Observed in pass 2: incremental reindex left get_context at line 9 when it had moved to line 22.)

3. File enumeration

git diff --name-only --find-renames <base>...<head>

Result is the candidate file list. Group by --find-renames output: R<score>\t<old>\t<new> lines are renames — track old → new for later caller mapping.

4. Cosmetic filter (two tiers)

4a — Whitespace pre-filter (skip if NO_DIFFT):

for F in <files>; do
  $DIFFT --check-only --exit-code \
    <(git show <base>:"$F" 2>/dev/null) \
    <(git show <head>:"$F" 2>/dev/null) >/dev/null
  # exit 0 = pure whitespace, drop. exit 1 = tokens changed, keep.
done

Record dropped files for the report footer ("N files: formatter-only").

4b — Comment-only filter (uses step 5a JSON): after step 5a parses the JSON, if every changes[] entry across all chunks has highlight: "comment", drop the file as cosmetic. Add to the same footer count.

5. Extract change spans

5a — AST spans (preferred):

Critical: feed difftastic real files with the right extension — process substitutions <(…) produce FIFOs whose names lack extensions, so difftastic falls back to language: "Text" and loses syntax awareness. Use a tempdir:

tmp=$(mktemp -d)
ext="${F##*.}"
git show "<base>:$F" > "$tmp/old.$ext"
git show "<head>:$F" > "$tmp/new.$ext"
DFT_UNSTABLE=yes $DIFFT --display=json "$tmp/old.$ext" "$tmp/new.$ext" 2>/dev/null | toon
rm -rf "$tmp"

Real JSON schema (validated empirically against difft v0.66+):

{aligned_lines: [[old, new], ...],
 chunks: [[{lhs:{line_number, changes:[{start,end,content,highlight}]},
            rhs:{line_number, changes:[...]}}, ...]],
 language, path, status}

highlight values seen in the wild: normal, type, keyword, string, delimiter, comment (likely more — accept any string). line_number is 0-indexed; codanna's range.start_line is 1-indexed — add 1 when comparing.

Extract per file:

  • rhs changes (chunks[].rhs.changes[]) → list of (head_line_0indexed, char_start, char_end, highlight). These map to symbols at head.
  • lhs changes (chunks[].lhs.changes[]) → list of base-side change lines. Use these for removed symbol detection.

If toon parse fails or language: "Text" came back unexpectedly (file extension was preserved but difftastic still fell back) → drop to 5b for that file. If chunks is empty but lhs had changes (pure deletions, all on base), flag the file as lhs-only and process it via 5b for the head-side range mapping.

5b — Line hunks (fallback):

git diff -U0 --merge-base <base> <head> -- "$F"

Parse @@ -a,b +c,d @@ per hunk. Record (file, base_range, head_range). Mark these files as "line-fallback" — severity will report "unknown" for symbols inside.

6. Enumerate symbols per file

codanna retrieve search "fn" --kind function --limit 200 --json 2>/dev/null | toon

Then post-filter by file_path in the response (data[].symbol.file_path == <file>). Do not use --module <file> — codanna's --module filter expects a Rust-style module path (crate::core::src::vault), not the file path; passing a file path returns empty.

Normalize paths before comparing: codanna stores paths exactly as passed to codanna index <args>. If indexed via codanna index ., paths come back as ./core/src/foo.rs; if via codanna index core/ cli/, no leading ./. Strip leading ./ (or use os.path.normpath) on both sides before the equality check. Collect (name, symbol_id, range.start_line, range.end_line). Use a non-empty query ("fn", "struct", etc.) — empty queries return not_found.

For files codanna can't symbolize (returns empty for a non-trivial file, or language unsupported): route by extension to rg -nP --type <lang> '<function-decl-regex>' <file> and synthesize pseudo-symbols (file, line, name). Mark severity "unknown" for those.

7. Map change spans → symbols

A symbol is changed if its [range.start_line, range.end_line] intersects any change-span line number. Multiple spans inside one symbol count once. Build the set: {symbol → list_of_changes_inside_it}.

8. Classify each changed symbol

  • added — symbol present only at head. Cheap detection: git show <base>:<F> | grep -E "fn $name\b" returns nothing while git show <head>:<F> | grep returns a line. Tag as added and drop severity to low (callers necessarily target the new signature, there's nothing to "miss").
  • removed — symbol present only at base. Find by re-running step 6 against git show <base>:<F> content (lightweight: parse with zat if codanna can't index a temp file).
  • signature-changed — any change inside the symbol has head_line + 1 == symbol.range.start_line (the declaration line). Beware false positives: if both base and head signature lines are identical, the change must be on a neighbouring line that the codanna range happens to cover (e.g. a doc comment or attribute). Compare the two signature lines via grep; if identical, downgrade to modified.
  • modified — changes only on lines > range.start_line, none on the signature line.
  • modified-unknown-shape — symbol came from line-fallback mode (5b); can't distinguish signature from body.

9. Find callers

  • Always: codanna retrieve callers symbol_id:<N> --json (use symbol_id: to avoid same-name collisions like page_list in two files).
  • Only if classify is signature-changed or removed: also codanna mcp analyze_impact <name> for the blast radius.
  • Callers response shapedata[] items are wrapped: each item is {symbol: {id, name, file_path, range:{start_line, end_line}, ...}, file_path, relationships}. Read fields from data[].symbol, not flat. (The MCP find_callers variant may differ — verify per response.)

9b. Filter self-callers

Drop any caller where caller.symbol_id == changed_symbol.id (same symbol). Codanna's call graph commonly includes the symbol itself as a caller (recursion detection or self-reference artifact). These are pure noise in the report.

10. Validate each caller

Two booleans per caller:

  • G1: is caller.file_path in the diff's file list (step 3)?
  • G2: does caller.range intersect a change span (step 5a) in its file?
G1G2Status
no🔴 untouched
yesno🟡 sibling (file changed, this caller's body didn't)
yesyes🟢 adapted

If the caller's file is line-fallback (5b), G2 uses the line hunks instead and the caller is marked as <status>-fallback (e.g. 🟢-fallback).

11. Severity

  • signature-changed + 🔴 → high
  • removed + 🔴 → high
  • modified + 🔴 → medium
  • signature-changed + 🟡 → medium (caller's file moved but the signature use didn't)
  • anything + 🟢 → low (informational)
  • any *-unknown-shape or *-fallbackunknown

Sort the report high → medium → unknown → low.

12. Always-on regex sanity pass

For each changed symbol's name:

rg -nw --hidden -g '!.git' -g '!target' '<symbol_name>' .

-w matches whole words (so run doesn't hit runtime, running, etc.). If rg surfaces a file not in the codanna caller set, loop back to step 10 for it. This catches macros / dynamic dispatch / FFI / generated code / string-keyed dispatch.

Interpret with care: very common identifiers (run, new, main, get) produce hundreds of hits even with -w and are noise. Surface counts in the report but flag any count above ~50 as "common name — likely noise" rather than as findings. Rare/unique identifiers are where signal lives.

13. Report

Markdown, per changed function (severity-ordered):

### `add` (signature-changed, **high**)
- Old: `fn add(a: i32, b: i32) -> i32`
- New: `fn add(a: i32, b: i32, c: i32) -> i32`
- File: `src/math.rs:1`

| Caller | Status | Path |
|---|---|---|
| `main` | 🟢 adapted | `src/main.rs:5` |
| `compute_total` | 🔴 untouched | `src/totals.rs:42` |

**Untouched callers may need updating.**

Footer:

  • "N files dropped as cosmetic (formatter / whitespace / comment-only)" — names listed if N ≤ 10, else just count.
  • Regex sanity pass: any extra files surfaced.
  • difft version (difft --version | head -1) for reproducibility.

14. Offer markdown+mermaid report (only if mermaid-diagrams skill is available)

Ask the user. On yes, invoke mermaid-diagrams and emit:

  • Impact graph: changed function → callers, colored 🟢/🟡/🔴.
  • Optional before/after sequence diagram for high-severity findings.

Do not generate unprompted.

Output shape — difftastic JSON quick ref

Validated empirically against difft v0.66+:

{
  "aligned_lines": [[old_line|null, new_line|null], …],
  "chunks": [
    [
      {
        "lhs": {"line_number": <0-indexed>, "changes": [<change>, …]},
        "rhs": {"line_number": <0-indexed>, "changes": [<change>, …]}
      },
      …
    ]
  ],
  "language": "Rust",
  "path": "<file>",
  "status": "changed" | "unchanged"
}

<change> = {"start": <char>, "end": <char>, "content": "…", "highlight": <string>}. Highlights observed: normal, type, keyword, string, delimiter, comment. Accept any string; only comment matters for the tier-2 cosmetic filter.

No node_type field. Signature detection is range-based (does rhs.line_number + 1 equal codanna's symbol.range.start_line?). Comment-only filtering uses highlight: "comment".

status: "unchanged" means difftastic considers the files equivalent (whitespace-only); treat the file as cosmetic.

Common pitfalls

PitfallFix
--display=json errors with "unstable feature"Set DFT_UNSTABLE=yes (literal "yes").
difft reports language: "Text" for a .rs fileProcess substitutions <(…) produce extensionless FIFOs. Write to a tempdir with the real extension preserved (see step 5a).
--check-only --exit-code returns 1 for comment-only diffsExpected — --check-only only filters pure whitespace. Add a tier-2 filter: drop files where every changes[] has highlight: "comment".
difftastic JSON breaks toonSchema drift — degrade that file to line-fallback (step 5b), don't crash.
Empty chunks but file is in the diffAll changes were on lhs (pure deletions, no head-side adds) — process via line-fallback (5b) using git diff -U0.
search_symbols with --module <file> returns empty--module expects Rust module_path (crate::foo::bar), not file path. Drop --module and post-filter by file_path instead.
Empty search_symbols on a real fileLanguage not supported by codanna; route by file extension to a regex fallback (rg -nP --type <lang>).
symbol_id:N collisionsUse --json; field is data[].symbol.id for retrieve search and retrieve symbol.
retrieve callers returns None for rangesItems are wrapped: read data[].symbol.range.start_line, not data[].range.start_line.
Codanna's symbol ranges don't match diff linesCodanna index is at a different ref than <head>, OR incremental reindex left stale entries. Run codanna index <workspace_root> --force (full path, not subdir — --force on a subdir wipes the rest).
codanna index <subdir> --force halves the index--force wipes everything outside the indexed path. Always use workspace_root from .codanna/settings.toml.
Codanna returns paths with ./ prefixIndex was created via codanna index .. Normalize (strip leading ./) before comparing to git's file list.
Function is its own caller in codannaFilter caller.symbol_id == changed.symbol_id from the results — codanna emits self-references that are pure noise.
signature-changed but base/head sig lines identicalOff-by-one with attributes / doc comments next to the function. Compare the actual signature lines via grep "fn $name\b"; downgrade to modified if they match.
added symbol misclassified as signature-changedA function present only at head has no base signature. Detect via git show <base>:<F> | grep "fn $name\b" empty → classify as added, severity low.
rg sanity-pass count for common names is hugerun, new, main, get will hit hundreds of lines even with -w. Surface counts but treat anything > ~50 as noise.
Off-by-one between difft and codannadifft line_number is 0-indexed; codanna range.start_line is 1-indexed. Add 1 to difft lines before overlap checks.
analyze_impact for every symbol → slowOnly call it for signature-changed / removed. find_callers is enough for modified.
Renames within a file (not file-level)Known gap — caller of old name reads 🔴 untouched, which is the safe direction.
Cold nix run hangsOne-line warning before first invocation. Don't suggest nix profile install (can break home-manager).
analyze_impact doesn't show macro callersStatic analysis gap — the regex sanity pass (step 12) catches it.
PR refs not localgh pr checkout <id> or git fetch origin pull/<id>/head:pr/<id>.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.