SKILL.md
Adding a New Tool to Lintro
Lintro is a unified CLI for code linting/formatting with a plugin architecture: tools are defined in lintro/tools/definitions/<tool>.py (via @registertool), parsed by lintro/parsers/<tool>/, tested in tests/unit/, with sample violation files in testsamples/tools/.
Related Skills
stand-py: Python coding standards (type hints, docstrings, trailing commas)test: pytest best practices (no classes, use fixtures, parametrize)commit: semantic commit format when committing changes
How to Implement: Copy a Reference, Don't Write From Scratch
Do NOT write plugin/parser/test code from a template. Pick the closest existing implementation, read all of its files (definition, issue class, parser, parser init.py, sample violation file, parser tests, plugin tests), and mirror that structure exactly for the new tool:
- Simple tool (no fix):
lintro/tools/definitions/actionlint.py,hadolint.py - Tool with fix support:
lintro/tools/definitions/ruff.py,black.py - Security scanner:
lintro/tools/definitions/bandit.py,semgrep.py - Shell tools:
lintro/tools/definitions/shellcheck.py,shfmt.py
The parser lives in lintro/parsers/<reference-tool>/ and its tests in tests/unit/parsers/ and tests/unit/tools/<reference-tool>/ — copy the pattern from the same reference tool so imports, mocking, and naming stay consistent.
Quick Reference
New files (paths)
lintro/parsers/<tool>/__init__.py
lintro/parsers/<tool>/<tool>_issue.py
lintro/parsers/<tool>/<tool>_parser.py
lintro/tools/definitions/<tool>.py
test_samples/tools/<category>/<tool>/<tool>_violations.<ext>
tests/unit/parsers/test_<tool>_parser.py
tests/unit/tools/<tool>/__init__.py
tests/unit/tools/<tool>/test_<tool>_plugin.py
Updated files (paths)
lintro/enums/tool_name.py # Add to ToolName enum (alphabetical)
lintro/tools/core/version_parsing.py # Add to TOOLS_WITH_SIMPLE_VERSION_PATTERN
lintro/tools/core/version_checking.py # Add install hints in get_install_hints()
lintro/_tool_versions.py # Add version (binary/cargo/rustup tools only)
lintro/tools/manifest.src.json # Add tool entry (NO version key; generator renders manifest.json)
lintro/cli_utils/commands/doctor.py # Add to TOOL_COMMANDS for health check
package.json # Pin the npm package (the version source for npm tools)
renovate.json # Add a custom manager for _tool_versions.py (binary tools only)
pyproject.toml # Add parser package + [tool.lintro.versions] entry
Dockerfile # Add to verification steps (root AND non-root blocks)
Dockerfile.tools # Add to verification step (tool --version)
scripts/utils/install-tools.sh # Add installation command (external tools)
scripts/ci/homebrew/templates/lintro.rb.template # Add depends_on + update caveats (if Homebrew-installable)
For every updated file, find an existing tool's entry in that file and add the new tool the same way (alphabetical order where the file is ordered).
Version Consistency (CRITICAL for external tools)
For external tools (not bundled Python packages), versions must be consistent across:
lintro/toolversions.py— source of truth for binary/cargo/rustup
tools (npm tools read package.json, bundled Python tools read pyproject.toml)
lintro/tools/manifest.src.json— the hand-authored entry carries
install metadata only, never a version key; the build-time generator renders lintro/tools/manifest.json (gitignored, not committed) with the version injected from the source above
- Plugin
min_version— in the tool definition, should match or be <=
the version source
renovate.json— a custom regex manager updatingtoolversions.py
(binary tools only; the rendered manifest follows via the generator)
There is no committed generated copy to drift (py-lintro #2176): the derived artifacts are generated at package build time. Run just generate in the py-lintro checkout after editing a version source to refresh the gitignored working-tree copies.
Homebrew Formula (for Homebrew-installable tools)
- Add
depends_on "<tool>"toscripts/ci/homebrew/templates/lintro.rb.template
and list the tool in the caveats under the appropriate category.
- Bundled Python tools (ruff, black, mypy, bandit, yamllint) are excluded from the
Homebrew venv via generateresources.py --exclude; they install as separate Homebrew formulae and are discovered via PATH (shutil.which), NOT python -m. PythonBundledBuilder in commandbuilders.py handles this automatically.
ToolType Options
ToolType.LINTER # Code quality checker
ToolType.FORMATTER # Code formatter
ToolType.TYPE_CHECKER # Type checking (mypy)
ToolType.DOCUMENTATION # Doc checker (darglint)
ToolType.SECURITY # Security scanner (bandit, semgrep, gitleaks)
ToolType.INFRASTRUCTURE # IaC linter (hadolint, actionlint)
ToolType.TEST_RUNNER # Test framework (pytest)
Can be combined: ToolType.LINTER | ToolType.FORMATTER
Common Gotchas
- Version command variations: some tools use
versioninstead of--version
(e.g., gitleaks version). Check the tool's CLI.
- ToolResult invariant for fix operations:
initialissuescount = fixedissuescount + remainingissuescount.
- Test mocking: mock the version check with
patch("lintro.plugins.executionpreparation.verifytoolversion", returnvalue=None) and subprocess calls with patch.object(plugin, "runsubprocess", ...) — copy the patterns from a reference tool's plugin tests.
- File discovery:
prepareexecution()handles filtering byfile_patterns;
use ctx.rel_files for the filtered list.
- Subprocess safety: always use list args, never
shell=True; add
# nosec B404 on the subprocess import.
- Return early: if
ctx.shouldskipis True, returnctx.earlyresult. - Parser function naming: must be
parse<tool>output(output: str | None) -> list[<Tool>Issue].
- Issue class: must inherit from
BaseIssue; useDISPLAYFIELDMAPfor
custom field name mappings.
Deprecated Patterns to Avoid
- Do NOT create tool-specific formatters (use the unified formatter)
- Do NOT modify
lintro/tools/tool_enum.py(deleted — registry is automatic) - Do NOT modify
lintro/tools/core/tool_base.py(deleted — use BaseToolPlugin)
Verification Checklist
-
uv run lintro toolsshows the new tool -
uv run lintro check --tools <tool> .runs without error — single-tool runs are
normally banned by the lint skill; this is the explicit sanctioned exception for verifying a tool under development. Still finish with a full uv run lintro chk.
-
uv run lintro doctorshows the tool with correct version (no "No cmd defined") - Tool detects violations in the sample file
- Parser unit tests pass:
pytest tests/unit/parsers/test<tool>parser.py -v - Plugin unit tests pass:
pytest tests/unit/tools/<tool>/ -v - Coverage >80% on new code
- No linting errors:
uv run lintro fmt && uv run lintro chk -
just generateruns cleanly (derived artifacts are gitignored, not committed) - Tool added to
Dockerfile(root and non-root blocks) andDockerfile.tools - Tool added to
install-tools.sh(external tools only) - Tool added to
lintro/tools/manifest.src.json(noversionkey) - Renovate manager added for
toolversions.py(binary tools only) - Homebrew template updated with
depends_on+ caveats (if Homebrew-installable) - Docker image builds:
docker build -t py-lintro:test .
Use the lintro-verify skill for the full post-implementation review.