name: toolr-command-authoring
description: |
Author toolr commands in a project's own tools/*.py files. Use when
adding, editing, or refactoring a toolr command, group, or context
hook; when introducing a new tools/ directory; when wiring
@command, @command_group, @arg, or @arg_section decorators;
when configuring a command's docstring-driven --help; or when
debugging "command not found" / "manifest stale" errors against
toolr. Triggers on phrases like "add a toolr command", "extend
toolr", "wire a new toolr group", "toolr tools/", @command_group,
ctx.run, toolr-manifest.json. Stays inert in projects that don't
use toolr and on requests to package commands as a distributable
plugin (covered by the toolr-command-packaging skill).
Authoring toolr commands
You are extending an existing toolr project — a repo that already has
(or will have) a tools/ directory at the root and a toolr binary
on PATH. Your job is to add or change tools/*.py files so the user
gets new subcommands under toolr ....
This skill teaches the shape of authoring toolr commands. For the
actual surface of decorators, Context methods, and docstring
conventions, consult the generated references in references/ —
they are rebuilt from toolr's own source on every release, so they
cannot drift.
Workflow
- Confirm the project is a toolr project. Look for
tools/at the repo root withtools/pyproject.toml. If it doesn't exist, ask the user to runtoolr project initfirst — that scaffolds everything correctly and you should not duplicate it. - Decide where the command goes. One file per subcommand is the easy default; group multiple commands in the same file when they share helpers or a parent group.
- Declare (or import) a group with
command_group(...). The group is whattoolr <group> <cmd>selects on. Reuse an existing group across files by passinggroup="..."to@commandrather than redeclaring it. - Write the function. Take
ctx: Contextas the first parameter, then your CLI arguments as ordinary Python parameters. Type hints drive argparse binding; defaults make arguments optional;Annotated[T, arg(...)]adds clap metadata (aliases,metavar,help_section,must_exist, etc.). - Document via Google-style docstring. The first line is the
short help (
toolr <group> --help). The rest is the long help (toolr <group> <cmd> --help).Args:populates per-argument help. - Try it.
toolr <group> <cmd> --helpbuilds the manifest on the fly if it's stale (the freshness work landed in 0.20.0); on older toolr fall back totoolr project manifest rebuild. If the command doesn't appear, the manifest builder rejected it — read the error.
What "looks right" looks like
A canonical single-file command:
"""Long-form group description, used as the group's `--help` long text."""
from toolr import Context, arg, command, command_group
command_group("greet", "Say hello in various ways", docstring=__doc__)
@command(group="greet")
def hello(ctx: Context, who: str = "world", *, loud: bool = False) -> None:
"""Print a greeting.
Args:
who: Who to greet. Defaults to ``"world"``.
loud: Shout instead of speak.
"""
msg = f"Hello, {who}!"
if loud:
msg = msg.upper()
ctx.info(msg)
Add this to tools/greet.py, then toolr greet hello --help works.
Common authoring moves
- Group across files. Pass
group="ci.build"to@commandfrom any file; only one file needs to callcommand_group("ci.build", ...). - Nested subgroups. A dotted name (
command_group("docker.image", ...)) attaches under the parent named before the last dot. - Help sections. Build an
ArgSectionat module scope viaarg_section("Logging", description="..."), then attach it viaAnnotated[bool, arg(help_section=LOGGING)]. SameArgSectionobject across all members or you'll silently create duplicate sections. - Calling subprocesses. Use
ctx.run(...); it inherits stderr for TTY-aware tools and propagates timeouts.
Runtime working directory
Commands run with the working directory set to the repo root,
regardless of where you invoke toolr from (the make/cargo
convention). Two consequences:
- Relative path arguments resolve from the repo root, not your
current directory.
toolr build ./out.txtrun fromtools/writes<repo-root>/out.txt, not<repo-root>/tools/out.txt. Pass an absolute path when you need a file relative to where you ran the command. toolr prints a one-line note on stderr if you pass a relative path argument from a subdirectory, so the surprise is visible. ctx.run(...)subprocesses inherit the repo root as their cwd unless you override it, so paths in the commands you spawn are also repo-root-relative by default.
Static-only discovery contract
toolr discovers commands only by static analysis of tools/*.py —
it never imports or executes your modules to build the manifest. Declare
command_group(...) at module top level and apply @command /
@group.command to module-level functions. Commands registered
dynamically — in a for loop, behind an if, or returned from a
factory called at import time — are not discovered and will not
appear in --help, completion, or dispatch. If a command is missing,
make its registration a top-level, statically-visible declaration.
Anti-patterns
- Don't register commands dynamically. A loop like
for name in names: group.command(...)or a factory that builds commands at import time produces nothing — the static parser can't see it. Declare each command at module level (see the static-only contract above). - Don't bypass the decorator surface. Defining a function and
then calling
register(...)directly skips the manifest builder. Always@commandor@<group>.command. - Don't reach into
toolr._*internals. Those modules are implementation detail; the public surface is exactly the names infrom toolr import (...), whichreferences/commands.mdlists. - Don't pass
description=anddocstring=to the samecommand_group(...). It raises. Pick one —docstring=__doc__is the canonical form when the module's docstring is the long description. - Don't write your own
argparsesubparser. toolr owns the parser; you describe shape via decorators and type hints.
References
references/commands.md— every name exposed byimport toolr. Signatures, defaults, annotations, and docstrings, regenerated fromtoolr.__all__on every release. Treat it as the source of truth for the decorator API.references/docstrings.md— exactly which Google-style section headers toolr's docstring parser recognises and how each is rendered. Generated from the sameKNOWN_SECTION_HEADERStable the parser reads at runtime.
Local feedback loop
toolr <group> --help— list commands in the group; if yours is missing, the manifest builder rejected it.toolr <group> <cmd> --help— full per-command help.toolr project manifest rebuild --force— bypass freshness detection and rebuild from scratch.- On a manifest error, the message points at the offending line in
tools/*.py. Fix the source; the manifest auto-rebuilds on the next dispatch.
Packaging is a different problem
If the user wants to ship an existing set of toolr commands as a
distributable Python plugin (so other projects can pip install and
get the commands), that is the
toolr-command-packaging
skill's job. This skill does not cover wheel-building, manifest
embedding, or PyPI publishing — invoke the packaging skill for that
work.
CI is a different problem
If the user wants to run these commands in GitHub Actions
(a caller workflow that installs toolr, sets up the venv, and
runs toolr <group> <cmd>), that is the
toolr-ci-setup
skill's job. This skill does not cover the s0undt3ch/ToolR
action, pinning policy, or CI cache shapes — invoke the
CI-setup skill for that work.