How to Extend the SkillSpector Analyzer Pipeline with Custom Analyzers

To extend the SkillSpector analyzer pipeline, create a new Python module in src/skillspector/nodes/analyzers/ that implements a node(state: SkillspectorState) -> AnalyzerNodeResponse function, register it via an import in src/skillspector/nodes/__init__.py, and optionally adjust execution order in src/skillspector/graph.py.

SkillSpector is NVIDIA’s open-source LangGraph-based analysis framework for evaluating AI skill implementations. When you need to detect domain-specific patterns, integrate proprietary security checks, or add custom linting rules, you can extend the analyzer pipeline without modifying core engine code—provided you follow the strict state contract defined in the source.

Understanding the SkillSpector Analysis Architecture

SkillSpector orchestrates analysis through a LangGraph workflow where each analyzer operates as an independent node. The graph passes a shared state object through every node, and each analyzer contributes findings back via a standardized response payload.

Core State Components

Three types define the contract between your custom code and the execution engine:

  • SkillspectorState (defined in src/skillspector/state.py): A TypedDict containing all data flowing through the graph, including file_cache, manifest, findings, and model configuration.
  • AnalyzerNodeResponse (defined in src/skillspector/state.py): A TypedDict with a single field findings: list[Finding] that every analyzer must return.
  • Finding (defined in src/skillspector/models.py): A dataclass describing a single issue with fields for file_path, location, severity, title, description, and remediation.

The graph reducer automatically appends your returned findings to state["findings"] using the operator.add annotation specified in SkillspectorState.

How Built-in Analyzers Work

Analyzers are stateless functions that read from the shared state and return structured findings. Examine the implementation in src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py to see the pattern:

from skillspector.state import AnalyzerNodeResponse, SkillspectorState
from skillspector.models import AnalyzerFinding, Finding

def node(state: SkillspectorState) -> AnalyzerNodeResponse:
    # 1. Retrieve file contents from the state cache

    content = state["file_cache"][file_path]
    
    # 2. Run analysis logic to generate raw findings

    raw_findings = _run_tool_misuse_checks(content)
    
    # 3. Convert to standardized Finding objects

    findings = [f.to_finding() for f in raw_findings]
    
    # 4. Return strict response type (no side effects)

    return {"findings": findings}

The function performs no side effects—it only reads from state and returns the AnalyzerNodeResponse payload. The graph handles persistence and aggregation.

Step-by-Step Guide to Adding Custom Analyzers

Follow these steps to integrate your own analysis logic into the SkillSpector pipeline.

Step 1: Create the Analyzer Module

Create a new Python file in src/skillspector/nodes/analyzers/, for example my_custom_analyzer.py.

Step 2: Import Required Types

At the top of your module, import the state and model contracts:

from skillspector.state import AnalyzerNodeResponse, SkillspectorState
from skillspector.models import Finding, Severity, Location

Step 3: Implement the Node Function

Define a node function with the exact signature node(state: SkillspectorState) -> AnalyzerNodeResponse. Inside this function:

  • Pull data from state (commonly state["file_cache"] for source content or state["manifest"] for skill metadata).
  • Execute your custom analysis logic.
  • Construct Finding objects with all required fields:
Finding(
    file_path="relative/path/to/file.py",
    location=Location(line=start, col=start_col, end_line=end, end_col=end_col),
    severity=Severity.HIGH,
    title="Custom Issue Title",
    description="Detailed explanation of the problem.",
    remediation="Suggested fix or mitigation.",
)
  • Return {"findings": findings}.

Step 4: Register the Analyzer

Open src/skillspector/nodes/__init__.py and add an import for your new module:

from .analyzers import my_custom_analyzer  # noqa: F401

The graph builder uses pkgutil.iter_modules to auto-discover analyzers, so this import side-effect is sufficient for registration.

Step 5: Adjust Execution Order (Optional)

If your analyzer must run before or after specific built-ins, edit src/skillspector/graph.py where the analyzer_nodes list is assembled. Insert your my_custom_analyzer.node reference at the desired position in the list.

Step 6: Test Your Implementation

Run the test suite to verify compatibility:

pytest -k my_custom_analyzer

Add dedicated unit tests under tests/nodes/analyzers/ that instantiate a synthetic SkillspectorState and assert the expected Finding objects are produced.

Complete Custom Analyzer Example

Below is a minimal working analyzer that flags Python files containing TODO markers:


# src/skillspector/nodes/analyzers/my_custom_analyzer.py

"""A tiny example that flags any Python file containing the literal string 'TODO'."""

from pathlib import Path
from skillspector.state import AnalyzerNodeResponse, SkillspectorState
from skillspector.models import Finding, Severity, Location


def _find_todos(content: str) -> list[Finding]:
    findings: list[Finding] = []
    for lineno, line in enumerate(content.splitlines(), start=1):
        if "TODO" in line:
            loc = Location(
                line=lineno, 
                col=line.find("TODO") + 1, 
                end_line=lineno, 
                end_col=line.find("TODO") + 5
            )
            findings.append(
                Finding(
                    file_path="",  # populated by caller

                    location=loc,
                    severity=Severity.MEDIUM,
                    title="TODO marker detected",
                    description="TODO comments may indicate unfinished code.",
                    remediation="Remove or resolve the TODO.",
                )
            )
    return findings


def node(state: SkillspectorState) -> AnalyzerNodeResponse:
    findings: list[Finding] = []
    # The graph guarantees state["file_cache"] contains all skill files

    for path, content in state["file_cache"].items():
        for f in _find_todos(content):
            f.file_path = Path(path).relative_to(state["skill_path"] or ".").as_posix()
            findings.append(f)
    
    return {"findings": findings}

After adding the import to src/skillspector/nodes/__init__.py, this analyzer automatically executes alongside built-in static and semantic checks.

Summary

  • SkillSpector uses a LangGraph workflow where analyzers receive SkillspectorState and return AnalyzerNodeResponse payloads.
  • Create custom analyzers in src/skillspector/nodes/analyzers/ by implementing a node(state) -> AnalyzerNodeResponse function that returns a list of Finding objects.
  • Register analyzers via import side-effects in src/skillspector/nodes/__init__.py.
  • Modify src/skillspector/graph.py to control execution order if dependencies exist between analyzers.
  • Store all custom logic in the analyzers directory to maintain compatibility with the existing execution engine and test infrastructure.

Frequently Asked Questions

What data is available in the SkillspectorState during analysis?

The SkillspectorState dictionary (defined in src/skillspector/state.py) contains the complete execution context, including file_cache (a mapping of absolute file paths to content strings), manifest (skill metadata), findings (accumulated results from previous nodes), and model configuration parameters. Analyzers should treat this state as read-only and return findings via the AnalyzerNodeResponse type rather than mutating the state directly.

Can I execute external tools or API calls from a custom analyzer?

Yes, provided the operation is synchronous and returns within the LangGraph node timeout. Implement external tool calls inside your node function (e.g., calling a linter, security scanner, or proprietary API), then transform the external output into Finding objects. Ensure you handle exceptions gracefully to prevent one analyzer from crashing the entire pipeline.

How do I control the severity levels for my custom findings?

Import the Severity enum from src/skillspector/models.py and assign one of the standard levels (LOW, MEDIUM, HIGH, CRITICAL) when constructing Finding objects. The severity determines how findings are prioritized in the final report and whether they trigger pipeline failures based on configured thresholds.

Where should I place unit tests for my custom analyzer?

Add test files under tests/nodes/analyzers/ following the pattern of existing tests. Construct a mock SkillspectorState dictionary with synthetic file_cache entries, pass it to your node function, and assert that the returned AnalyzerNodeResponse contains the expected Finding objects with correct file paths, locations, and descriptions.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →