From tweag
Universal code formatter using Tree-sitter queries for languages lacking dedicated formatters like JSON, TOML, Bash, Nickel, CSS. Aids writing .scm query files and configuring via languages.ncl.
npx claudepluginhub vinnie357/claude-skills --plugin tweagThis skill uses the workspace's default tool permissions.
Topiary is a universal code formatter that uses Tree-sitter grammars and queries to define formatting rules. Rather than implementing language-specific formatting logic, Topiary uses declarative Tree-sitter query files (.scm) to specify how code should be formatted.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Topiary is a universal code formatter that uses Tree-sitter grammars and queries to define formatting rules. Rather than implementing language-specific formatting logic, Topiary uses declarative Tree-sitter query files (.scm) to specify how code should be formatted.
Activate this skill when:
Tree-sitter is a parser generator that creates language parsers from grammar definitions. Topiary uses Tree-sitter queries to analyze code structures and apply formatting rules. Queries match patterns in the syntax tree using S-expression syntax and decorate matched nodes with capture names (prefixed with @) that describe formatting actions.
Language formatting is defined in .scm (Scheme) query files. Each capture name instructs Topiary how to format the matched node:
@prepend_hardline, @append_hardline (always insert newline), @prepend_empty_softline, @append_empty_softline (newline or nothing), @prepend_spaced_softline, @append_spaced_softline (newline or space)@prepend_indent_start, @append_indent_start, @prepend_indent_end, @append_indent_end (mark indentation scopes)@prepend_space, @append_space (add single space)@delete (remove matched node), @do_nothing (suppress default behavior), @leaf (prevent formatting within node)@prepend_input_softline, @append_input_softline (preserve original line break presence)Topiary formatting is idempotent—running the formatter repeatedly on already-formatted code produces identical output. This guarantee allows safe integration into version control hooks and CI pipelines.
Topiary respects the structural intent of code by preserving language-specific semantics. It focuses on whitespace, indentation, and line breaks rather than restructuring code. This approach works especially well with languages whose semantics are simple and whitespace-independent.
Add Topiary to your mise.toml:
[tools]
topiary = "latest"
[tasks]
fmt = "topiary fmt"
fmt:check = "topiary fmt --check"
Install directly from crates.io:
cargo install topiary-cli
Check the installed version and available languages:
topiary --version
topiary config show-sources
Format files in place by detected file extension:
topiary fmt src/main.ml
topiary fmt config.json
topiary fmt *.toml
Add --check to verify formatting without modifications:
topiary fmt --check src/
Format piped input by specifying language and optional query file:
echo '{"name":"example"}' | topiary format --language json
cat script.sh | topiary format --language bash
Use visualise to debug queries by inspecting the syntax tree:
topiary visualise script.json
topiary visualise --language bash script.sh
This outputs the Tree-sitter parse tree, useful for understanding node structure when writing .scm queries.
Override the default query file for a language:
topiary fmt --language nickel --query custom.scm script.ncl
Display the active configuration including registered languages and indentation settings:
topiary config show-sources
Create a .topiary/languages.ncl file in your project root to customize language registration, indentation, and grammar sources. This file uses Nickel syntax:
{
languages = {
json = {
extensions = ["json"],
indent = " ",
},
bash = {
extensions = ["sh", "bash"],
indent = " ",
}
}
}
Topiary searches for configuration in this order (first found wins):
--configuration CLI argument.topiary/languages.ncl in current directory or parent directories~/.config/topiary/languages.ncl (Linux), ~/Library/Application Support/topiary/languages.ncl (macOS), %APPDATA%\topiary\languages.ncl (Windows)Register custom Tree-sitter grammars by specifying Git source or local path:
{
languages = {
my-lang = {
extensions = ["ml"],
grammar.source.git = {
git = "https://github.com/example/tree-sitter-my-lang",
rev = "abc123def456"
}
}
}
}
Or reference a pre-compiled grammar on disk:
{
languages = {
my-lang = {
extensions = ["ml"],
grammar.source.path = "./grammars/my-lang.so"
}
}
}
Add formatting support for new languages by following these steps:
Create .topiary/languages.ncl in your project with the grammar source:
{
languages = {
my-lang = {
extensions = ["ml"],
indent = " ",
grammar.source.git = {
git = "https://github.com/my-org/tree-sitter-my-lang",
rev = "main"
}
}
}
}
Verify the grammar source URL points to a valid Tree-sitter grammar repository.
Create .topiary/queries/my-lang.scm with formatting rules. Start minimal:
; Format binary operators with surrounding spaces
((binary_op) @op
(#match? @op "^(+|-|=)$"))
@prepend_space
@append_space
; Hard line after statements
((statement) @stmt)
@append_hardline
Use topiary visualise to inspect the parse tree and understand node names and structure.
Test the formatter on sample files:
topiary fmt --language my-lang test-file.ml
topiary visualise --language my-lang test-file.ml
Iterate on the query file until formatting behaves as expected. Run topiary fmt on actual code to verify formatting is idempotent (run twice, should produce identical output).
Tree-sitter queries use S-expression syntax. A basic query matches nodes and captures them:
; Match a function definition and capture its name
(function_def
name: (identifier) @func-name)
The @func-name is a capture; function_def, name, and identifier are node types. Field names like name: link to specific children.
Format operators with spaces on both sides:
((binary_op operator: _ @op))
@prepend_space
@append_space
Indent function bodies:
(function_def
body: (block) @body)
@append_indent_start
@prepend_indent_end
Insert line breaks between statements:
((statement) @stmt)
@append_hardline
Preserve input line breaks (single line or multi-line lists):
(list
"[" @open
_ @item
"]" @close)
@append_spaced_softline
Use predicates to refine matches:
; Match only specific operators
((binary_op operator: _ @op)
(#match? @op "^(=|==|!=)$"))
@prepend_space
@append_space
; Match nodes that are NOT in comments
((statement) @stmt
(#not-eq? @stmt comment))
Available predicates: #match? (regex), #eq? (equality), #not-eq? (inequality).
Topiary actively maintains formatting for:
External maintainers provide formatters for:
Topiary works best with languages where formatting is independent of semantics. Whitespace-sensitive languages (Python, Haskell) present challenges and are not officially supported, as formatting changes could alter code meaning.
Define formatting tasks in mise.toml:
[tasks.fmt]
description = "Format all code"
run = """
topiary fmt src/
"""
[tasks.fmt:check]
description = "Check formatting without modifying"
run = """
topiary fmt --check src/
"""
[tasks.fmt:debug]
description = "Visualize parse tree for debugging"
run = """
topiary visualise {file}
"""
Run tasks with:
mise run fmt
mise run fmt:check
mise run fmt:debug -- script.ncl
Before claiming that Topiary formats a language or query correctly, verify behavior by running topiary fmt on actual code files and inspecting the output. Do not assume query behavior without testing; use topiary visualise to inspect the parse tree and confirm node names and structure match the query. Test idempotency by running the formatter twice and confirming output is identical.