add-lang
Add tree-sitter language support to codegraph end-to-end — wire the grammar + extractor, write tests, then benchmark extraction quality and retrieval value on 3 popular real-world repos. Use when the user runs /add-lang <language> or asks to add/support a new language (e.g. Lua, Elixir, Zig, OCaml)
By colbymchenry · 1,133 installs
npx skills add colbymchenry/codegraph --skill add-lang
Source repository · Upstream listing
Add a language to CodeGraph
Wire a new tree sitter language into codegraph's extraction pipeline, prove it
extracts real symbols on popular repos, and prove it beats no codegraph for an
agent. Runs fully autonomously — pick repos, benchmark, update docs, then
report. Never commit, push, publish, or tag (house rule); leave all changes
for the user to review.
The argument is the language token used throughout the Language union, e.g.
lua , elixir , zig . If none was given, ask which language. Use the lowercase
single token form everywhere ( csharp , not c ).
Prerequisites
Run from the codegraph repo root. node , git , gh , and a logged in
claude CLI (the benchmark spawns real claude p runs).
The benchmark uses the local dev build — Step 8 builds + links it on PATH.
Workflow
Copy this checklist and work through it in order:
Step 1 — Resolve + short circuit
Check whether the language is already wired: look for the token in the
LANGUAGES const ( src/types.ts ) and the EXTRACTORS map
( src/extraction/languages/index.ts ). If it is already supported (e.g.
typescript , rust ), skip Steps 2–6 and go straight to benchmarking
(Steps 7–8) to validate/measure it — note in the report that no code changed.
Step 2 — Find a grammar, then health check it
Present → likely off the shelf; grammars.ts resolves it from
tree sitter wasms automatically. (Many languages: elixir, zig, ocaml,
solidity, toml, yaml, …)
Absent → vendor a .wasm into src/extraction/wasm/ (like pascal /
scala / lua ) and add the token to the vendored branch in Step 4.
Always health check before writing an extractor — a present grammar can
still be unusable:
It prints the grammar's ABI version and parses a valid sample many times in a
multi grammar runtime. If it FAILs (ERROR trees on valid code — an old ABI
corrupting the shared WASM heap, which silently drops nested calls/imports on
every file after the first; e.g. the tree sitter wasms Lua grammar is ABI 13
and fails), do NOT use that wasm. Vendor a newer (ABI 14/15) build instead:
then add the token to the vendored branch in Step 4 and re run check grammar on
the vendored path until it PASSes. If you cannot obtain a healthy wasm, STOP
and tell the user.
Step 3 — Discover AST node types
Get a representative source file (write a small sample covering functions,
classes/structs, imports, enums; or curl a raw file from a known repo), then:
The frequency table + field names ( name: , parameters: , body: ,
return type: ) tell you what to map. Open the existing extractor closest to the
language's paradigm as a model: rust.ts / scala.ts (functional, traits),
java.ts / csharp.ts (OO), python.ts / ruby.ts (scripting), go.ts
(top level methods + receivers).
Step 4 — Wire the language (4 files)
These are exact, fragile wiring — match the existing style precisely:
1. src/types.ts — TWO edits:
add '<lang ', to the LANGUAGES const (before 'unknown' );
add ' / .<ext ', to DEFAULT CONFIG.include . Don't skip this — it's
the file scan allowlist; without the glob, codegraph init finds 0
files even though detection/extraction are wired.
2. src/extraction/grammars.ts — three maps:
WASM GRAMMAR FILES : <lang : 'tree sitter <lang .wasm',
EXTENSION MAP : each file extension → '<lang ' (e.g. '.lua': 'lua', )
getLanguageDisplayName : <lang : '<Display Name ',
vendored only : add <lang to the
(lang === 'pascal' lang === 'scala' …) wasm path branch.
3. src/extraction/languages/<lang .ts — new file exporting
export const <lang Extractor: LanguageExtractor = { … } . Map the node types
from Step 3. Required fields: functionTypes , classTypes , methodTypes ,
interfaceTypes , structTypes , enumTypes , typeAliasTypes ,
importTypes , callTypes , variableTypes , nameField , bodyField ,
paramsField . Add hooks as the grammar needs them ( getSignature ,
getVisibility , isExported , extractImport , visitNode , getReceiverType ,
interfaceKind , enumMemberTypes , etc. — see
src/extraction/tree sitter types.ts ).
4. src/extraction/languages/index.ts — import { <lang Extractor } from
'./<lang '; and add <lang : <lang Extractor, to EXTRACTORS .
Sometimes a 5th, core touch in src/extraction/tree sitter.ts — variable
extraction has per language branches in extractVariable (the generic fallback
only finds direct identifier / variable declarator children). If the grammar
nests declared names (e.g. Lua's variable declaration → variable list ), add a
} else if (this.language === '<lang ') branch there, mirroring the existing
ts/python/go ones. Import forms that aren't a distinct node (Lua/Ruby require
is a call ) are handled in the extractor's visitNode hook instead.
Step 5 — Build + verify loop
Index a small sample repo and check extraction:
verify extraction.mjs fails (exit 1) if the language isn't detected or only
file / import nodes were produced — the classic symptom of wrong node type
names. On FAIL or a thin WARN: re run dump ast.mjs on a richer file, fix the
mappings in <lang .ts , npm run build , re index, re verify. Repeat until
PASS.
Step 6 — Tests
Add to tests /extraction.test.ts , modeled on the Rust Extraction block:
a detectLanguage assertion in describe('Language Detection')
a describe('<Lang Extraction') block asserting functions/classes/imports
are extracted from an inline source string.
Green before continuing.
Step 7 — Auto pick 3 repos + corpus
Pick without asking . Find candidates, then curate 3 that are genuinely
<lang dominant, one per size tier:
Tiers (match corpus.json ): Small <~150 files · Medium ~150–1500 ·
Large ~1500. Skip repos that are tagged <lang but mostly another
language. Write one cross file architecture question per repo (the kind that
needs tracing across files). Add a "<Language " block to
.claude/skills/agent eval/corpus.json (fields: name , repo , size ,
files , question ) so /agent eval can reuse them.
Step 8 — Benchmark all 3 (extraction + A/B)
Make the dev build the codegraph on PATH once , then loop:
bench.sh clones (shared /tmp/codegraph corpus ), wipes + indexes, runs
verify extraction.mjs , then the with/without retrieval A/B via
scripts/agent eval/run all.sh (skips the paid A/B if extraction is broken).
Read each parse run.mjs summary printed by run all.sh : tool calls, file
Read s, Grep/Bash, codegraph tool calls, duration, and cost — for both the
with and without arms. After the loop, restore the dev link if needed:
./scripts/local install.sh .
Step 9 — Docs + CHANGELOG
README.md : add <Lang to the "19+ Languages" feature bullet, and add a
row to the Supported Languages table:
<Lang \ .ext\ Full support (classes, methods, …) .
CHANGELOG.md : add an [Unreleased] section at the top (above the
latest version) with Added → a user perspective bullet, e.g.
"CodeGraph now indexes <Lang ( .ext ) — functions, classes, imports, and
call edges." If [Unreleased] already exists, append under it. (It's
folded into the next versioned block at release time.)
Step 10 — Report (do NOT commit)
Summarize for review:
Files changed : the 4 wiring edits + new extractor + tests + README +
CHANGELOG + corpus.json (+ any vendored .wasm ).
Extraction per repo: files / nodes / edges / verify extraction result.
A/B per repo: with vs without (tool calls, file Reads, cost) and a
one line verdict — did codegraph reduce effort, and did both arms reach a
correct answer?
Gaps / follow ups (node types not yet mapped, resolution edges missing,
framework routes, etc.).
Hand the changes to the user. Do not run git commit / push or publish —
releases go through the GitHub Actions Release workflow.
Notes
The A/B spawns real paid claude p runs (opus, max budget usd ),
2 arms × 3 repos. The corpus dir /tmp/codegraph corpus is shared with
/agent eval , so clones are reused across runs.
Any new .wasm must live in src/extraction/wasm/ — copy assets (run by
npm run build ) ships it; otherwise it won't be in dist/ .
An index must be served by the same binary that built it. Step 8 builds +
links the dev build first, so this holds.
If a grammar can't be obtained, or extraction can't reach PASS, STOP and
report — don't ship a half wired language.