smithery.ai

develop-pants-plugin

Develop and modify existing Pants plugins. Use when adding targets, rules, goals to existing plugins, debugging plugin issues, writing tests, or upgrading plugins for new Pants versions.

First seen Apr 16, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Allowed toolsWrite, Edit, Read, Bash, Glob, Grep

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 9,287 B
  • docs SUMMARY.md 214 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Pants Plugin Development Assistant

Help developers work with existing Pants plugins - add features, debug issues, write tests, and upgrade for compatibility.

When to Use This Skill

  • Adding new targets, fields, rules, or goals to an existing plugin
  • Debugging plugin issues (rules not firing, caching problems, type errors)
  • Writing or improving tests with RuleRunner
  • Upgrading a plugin for a new Pants version
  • Understanding how an existing plugin works
  • Performance optimization and caching improvements

Workflow

Phase 1: Understand the Existing Plugin

First, explore the plugin structure to understand what exists:

  1. Find the entry point: Look for register.py - this shows all registered rules and targets
  2. Map the components:

- targets.py - What targets and fields exist? - rules.py - What rules process those targets? - goals.py - What user commands are available? - subsystem.py - What configuration options exist?

  1. Check tests: Look at tests/ to understand expected behavior
  2. Review dependencies: Check pyproject.toml for Pants version compatibility

Phase 2: Common Development Tasks

Adding a New Field to an Existing Target

# In targets.py - add the field class
class NewField(StringField):
    alias = "new_field"
    help = "Description of what this field does"
    default = "default_value"

# Update the target's core_fields
class ExistingTarget(Target):
    core_fields = (
        *COMMON_TARGET_FIELDS,
        ExistingField,
        NewField,  # Add here
    )

Adding a New Target Type

# In targets.py
class NewTarget(Target):
    alias = "new_target"
    help = "What this target represents"
    core_fields = (
        *COMMON_TARGET_FIELDS,
        SourcesField,
        # Add your fields
    )

# In register.py - add to target_types()
def target_types():
    return [ExistingTarget, NewTarget]

Adding a New Rule

# In rules.py
from dataclasses import dataclass

@dataclass(frozen=True)
class NewOutput:
    """Always use frozen dataclass for rule outputs."""
    result: str
    exit_code: int

@rule
async def new_rule(
    target: WrappedTarget,
    subsystem: PluginSubsystem,
) -> NewOutput:
    """
    Rules must:
    - Be async
    - Have complete type hints
    - Return frozen dataclass
    - Have no side effects
    """
    # Implementation
    return NewOutput(result="done", exit_code=0)

# Make sure collect_rules() picks it up
def rules():
    return collect_rules()

Adding a New Goal

# In goals.py
class NewGoalSubsystem(GoalSubsystem):
    name = "new-goal"
    help = "What this goal does"

class NewGoal(Goal):
    subsystem_cls = NewGoalSubsystem

@goal_rule
async def run_new_goal(
    console: Console,
    targets: Targets,
) -> NewGoal:
    # Implementation
    console.print_stdout("Processing...")
    return NewGoal(exit_code=0)

Adding Configuration Options

# In subsystem.py
class PluginSubsystem(Subsystem):
    options_scope = "my-plugin"

    # Add new options
    new_option = StrOption(
        default="value",
        help="What this option controls",
    )

    new_list_option = StrListOption(
        help="List of values",
    )

    new_bool = BoolOption(
        default=False,
        help="Enable/disable something",
    )

Phase 3: Debugging Common Issues

Rule Not Being Called

  1. Check registration: Is the rule in collect_rules()?
  2. Check types: Do input/output types match what's expected?
  3. Check target filtering: Is the target type correct?
# Debug: Add logging
import logging
logger = logging.getLogger(__name__)

@rule
async def my_rule(target: WrappedTarget) -> Output:
    logger.debug(f"Processing target: {target.target.address}")
    # ...

Run with debug logging:

pants --debug my-goal ::

Caching Issues

Rules must be deterministic. Check for:

  • Timestamps or random values in outputs
  • Reading files outside the sandbox
  • Environment variables not declared
# BAD: Non-deterministic
@rule
async def bad_rule() -> Output:
    return Output(timestamp=time.time())  # Changes every run!

# GOOD: Deterministic
@rule
async def good_rule() -> Output:
    return Output(value="constant")

Type Errors at Startup

Pants validates the rule graph at startup. Common issues:

# ERROR: Missing type hint
@rule
async def bad(target):  # Missing type hints!
    pass

# ERROR: Wrong return type
@rule
async def bad(target: Target) -> str:  # Must return dataclass
    return "result"

# CORRECT
@rule
async def good(target: WrappedTarget) -> Output:
    return Output(...)

Process Failures

@rule
async def run_tool(request: ToolRequest) -> ToolResult:
    process = Process(
        argv=["tool", "--arg"],
        input_digest=request.digest,
        description="Running tool",
    )
    result = await Get(ProcessResult, Process, process)

    # Check for failures
    if result.exit_code != 0:
        raise ProcessExecutionFailure(
            result.exit_code,
            result.stdout.decode(),
            result.stderr.decode(),
        )

    return ToolResult(output=result.stdout.decode())

Phase 4: Writing Tests

Basic Rule Test

from pants.testutil.rule_runner import RuleRunner, QueryRule

@pytest.fixture
def rule_runner():
    return RuleRunner(
        rules=[
            *my_plugin_rules(),
            QueryRule(Output, [Input]),
        ],
        target_types=[MyTarget],
    )

def test_my_rule(rule_runner):
    rule_runner.write_files({
        "BUILD": 'my_target(name="test", sources=["*.txt"])',
        "file.txt": "content",
    })

    result = rule_runner.request(Output, [Input(...)])
    assert result.exit_code == 0

Testing Goals

def test_goal(rule_runner):
    rule_runner.write_files({
        "BUILD": 'my_target(name="test")',
    })

    result = rule_runner.run_goal(MyGoal, [":test"])
    assert result.exit_code == 0
    assert "expected output" in result.stdout

Testing with Subsystem Options

def test_with_options(rule_runner):
    rule_runner.set_options(["--my-plugin-option=value"])
    rule_runner.write_files({"BUILD": "..."})
    # Run test

Phase 5: Upgrading for New Pants Versions

Check the Plugin Upgrade Guide

# Check current Pants version compatibility
grep -r "pants" pyproject.toml

Handle API Changes

from pants.version import PANTS_SEMVER
from packaging.version import Version

if PANTS_SEMVER >= Version("2.18.0"):
    from pants.engine.new_module import NewClass
else:
    from pants.engine.old_module import OldClass as NewClass

Common Breaking Changes

  • Import path changes: Module reorganization
  • Type signature changes: New required parameters
  • Deprecated removals: Check deprecation warnings
  • New required fields: FieldSet or Target changes

Phase 6: Performance Optimization

Batch Processing

# BAD: N+1 rule calls
@rule
async def process_one(target: Target) -> Result:
    # Called once per target

# GOOD: Batch processing
@rule
async def process_batch(targets: Targets) -> BatchResult:
    results = await MultiGet(
        Get(SingleResult, Target, t) for t in targets
    )
    return BatchResult(results)

Minimize Process Calls

# BAD: Multiple process calls
for file in files:
    await Get(ProcessResult, Process, Process(argv=["tool", file]))

# GOOD: Single process with multiple files
await Get(ProcessResult, Process, Process(argv=["tool", *files]))

Critical Gotchas (v2.30+)

Before developing, be aware of these common pitfalls:

  1. Namespace subsystem scopes - Don't use ruff, use myplugin-ruff to avoid conflicts
  2. One goal definition per scope - Define goals only in goals/, not in rule files
  3. Use 3-arg Get() syntax - Dict syntax is broken; accept deprecation warnings
  4. No from future import annotations - Breaks Pants type inference
  5. Subsystems don't have .rules() - They auto-register when used as parameters
  6. Bump version for each test - Pants caches wheels by filename

See reference/gotchas.md for detailed explanations and fixes.

Reference Documentation

For detailed API information, see:

  • reference/gotchas.md - Critical v2.30+ gotchas and fixes
  • reference/debugging.md - Comprehensive debugging guide
  • reference/testing.md - Testing patterns and fixtures
  • reference/upgrading.md - Version migration guide
  • reference/performance.md - Optimization techniques

Quick Commands

# Run specific tests
hatch run pytest tests/unit/test_specific.py -v

# Debug rule execution
pants --debug my-goal ::

# View target metadata
pants peek src:target-name

# Check what rules exist
pants backends | grep my_plugin

# Validate plugin loads
pants help my-goal