Skip to content

Adding Capabilities

Capabilities define what tasks Iris can perform. Each capability is a TOML file containing a prompt and output schema. This guide shows you how to add a new capability to Git-Iris.

What is a Capability?

A capability combines three elements:

  1. Task Prompt — Instructions for Iris on how to approach the task
  2. Output Type — Structured format for the response (JSON schema)
  3. Tool Access — Which tools Iris can use for this task

Capability Structure

Basic TOML Format

toml
name = "my_capability"
description = "Brief description of what this does"
output_type = "MyOutputType"

task_prompt = """
Instructions for Iris go here.

## Tools Available
- `git_diff()` - Get code changes
- `file_read(path="...")` - Analyze specific files

## Output Requirements
Your requirements for the output format.

## JSON Output
Return a `MyOutputType` with: field1, field2, etc.
"""

Real Example: Commit Message Generation

From src/agents/capabilities/commit.toml:

toml
name = "commit"
description = "Generate commit messages from staged changes"
output_type = "GeneratedMessage"

task_prompt = """
Generate a commit message for the staged changes.

## Context Gathering
`project_docs(doc_type="context")` returns a compact snapshot of the README and agent instructions.
Start with `git_diff()` for change evidence, then call `project_docs` when repository conventions or product framing matter.

## Tools Available
- `project_docs(doc_type="context")` - Compact project conventions snapshot; use targeted doc types for full docs
- `git_diff()` - Get staged changes with relevance scores
- `git_log(count=5)` - Recent commits for style reference

## Workflow
1. Call `git_diff()` to see what changed
2. Call `project_docs(doc_type="context")` when repository conventions affect the wording
3. Generate the commit message following project conventions

## Output Requirements
- **Subject line**: Imperative mood, max 72 chars, no period
- **Body**: Explain WHY, not what. Wrap at 72 chars.
- **Plain text only**: No markdown, no code fences

## JSON Output
Return a `GeneratedMessage` with: `emoji` (string or null), `title` (subject), `message` (body)
"""

Step-by-Step: Adding a New Capability

Teaching Example

This section walks through creating a hypothetical "Feature Summary" capability. This capability does not exist in the current codebase — it's an example to illustrate the pattern. Follow along to learn how capabilities work.

Step 1: Create the TOML File

Create src/agents/capabilities/feature_summary.toml:

toml
name = "feature_summary"
description = "Generate a high-level summary of a feature branch"
output_type = "FeatureSummary"

task_prompt = """
You are Iris, an AI assistant analyzing a feature branch to create a high-level summary.

## Context Gathering
`project_docs(doc_type="context")` returns a compact project snapshot. Use it when repo terminology, conventions, or workflow rules affect the summary.

## Tools Available
- `project_docs(doc_type="context")` - Get a compact project context snapshot
- `git_diff(from="<default-branch>", to="HEAD")` - Get changes between branches
- `git_log(count=N)` - Get commit history
- `file_read(path="...")` - Analyze specific files in detail

## Workflow
1. Get the diff between the primary branch and the feature branch
2. Call `project_docs(doc_type="context")` when repository conventions or terminology affect the summary
3. Identify key files and patterns
4. Summarize the feature's purpose, implementation approach, and impact

## Output Requirements
- **Purpose**: 1-2 sentences on what this feature does
- **Approach**: Brief technical overview
- **Files Changed**: Count and categorization
- **Impact**: User-facing changes, API changes, internal refactors

## JSON Output
Return a `FeatureSummary` with:
- `purpose` (string)
- `approach` (string)
- `files_changed` (number)
- `impact` (string)
"""

Step 2: Define the Output Type

Create or update src/types/feature_summary.rs:

rust
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// Feature summary response
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct FeatureSummary {
    /// What this feature does (1-2 sentences)
    pub purpose: String,
    /// Technical approach overview
    pub approach: String,
    /// Number of files changed
    pub files_changed: usize,
    /// User-facing and technical impact
    pub impact: String,
}

impl FeatureSummary {
    /// Format as markdown for display
    pub fn format(&self) -> String {
        format!(
            "# Feature Summary\n\n\
            ## Purpose\n{}\n\n\
            ## Approach\n{}\n\n\
            ## Impact\n{}\n\n\
            Files changed: {}\n",
            self.purpose,
            self.approach,
            self.impact,
            self.files_changed
        )
    }
}

Add to src/types/mod.rs:

rust
pub mod feature_summary;
pub use feature_summary::FeatureSummary;

Step 3: Register in StructuredResponse

Edit src/agents/iris.rs. The shipped variants today are CommitMessage, PullRequest, Changelog, ReleaseNotes, Review, SemanticBlame, and PlainText — note that the review arm is named Review, not MarkdownReview, and wraps crate::types::Review (a structured type, not a markdown wrapper):

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StructuredResponse {
    CommitMessage(crate::types::GeneratedMessage),
    PullRequest(crate::types::MarkdownPullRequest),
    Changelog(crate::types::MarkdownChangelog),
    ReleaseNotes(crate::types::MarkdownReleaseNotes),
    /// Structured code review with parseable findings
    Review(crate::types::Review),
    /// Semantic blame explanation (plain text)
    SemanticBlame(String),
    PlainText(String),
    // Add your new type:
    FeatureSummary(crate::types::FeatureSummary),
}

impl fmt::Display for StructuredResponse {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            // ... existing arms ...
            StructuredResponse::FeatureSummary(summary) => {
                write!(f, "{}", summary.format())
            }
        }
    }
}

Also extend serialize_artifact_for_critic if the critic should be able to inspect your new variant — see Critic Verification below.

Step 4: Load the Capability

Add the embedded TOML constant in src/agents/iris.rs alongside the existing ones:

rust
const CAPABILITY_COMMIT: &str = include_str!("capabilities/commit.toml");
const CAPABILITY_PR: &str = include_str!("capabilities/pr.toml");
// ... existing capabilities ...
const CAPABILITY_FEATURE_SUMMARY: &str = include_str!("capabilities/feature_summary.toml");

Then register the new capability inside load_capability_config(). The loader uses a plain match against the capability name, not a HashMap insertion:

rust
let content = match capability {
    "commit" => CAPABILITY_COMMIT,
    "pr" => CAPABILITY_PR,
    "review" => CAPABILITY_REVIEW,
    "changelog" => CAPABILITY_CHANGELOG,
    "release_notes" => CAPABILITY_RELEASE_NOTES,
    "chat" => CAPABILITY_CHAT,
    "semantic_blame" => CAPABILITY_SEMANTIC_BLAME,
    "feature_summary" => CAPABILITY_FEATURE_SUMMARY,  // Add your arm
    _ => {
        // Unknown capabilities fall back to a generic prompt + PlainText output type
        return Ok((
            format!(
                "You are helping with a {capability} task. Use the available Git tools to assist the user."
            ),
            "PlainText".to_string(),
        ));
    }
};

Step 5: Add Execution Logic

Iris dispatches on the capability's declared output_type (the string in the TOML), not on the capability name. Find execute_output_type(&self, output_type: &str, system_prompt: &str, user_prompt: &str) in src/agents/iris.rs and add a match arm. It delegates the schema-driven call to the internal execute_with_agent::<T>() helper:

rust
async fn execute_output_type(
    &self,
    output_type: &str,
    system_prompt: &str,
    user_prompt: &str,
) -> Result<StructuredResponse> {
    match output_type {
        "GeneratedMessage" => {
            let response = self
                .execute_with_agent::<crate::types::GeneratedMessage>(
                    system_prompt,
                    user_prompt,
                )
                .await?;
            Ok(StructuredResponse::CommitMessage(response))
        }
        // ... existing arms (MarkdownPullRequest, MarkdownChangelog, MarkdownReleaseNotes, Review, SemanticBlame) ...
        "FeatureSummary" => {
            let response = self
                .execute_with_agent::<crate::types::FeatureSummary>(
                    system_prompt,
                    user_prompt,
                )
                .await?;
            Ok(StructuredResponse::FeatureSummary(response))
        }
        _ => {
            let agent = self.build_agent()?;
            let full_prompt = format!("{system_prompt}\n\n{user_prompt}");
            let response = agent.prompt_multi_turn(&full_prompt, 50).await?;
            Ok(StructuredResponse::PlainText(response))
        }
    }
}

Step 6: Test Your Capability

bash
# Build
just build

# Test in CLI (you may need to add a CLI command for your capability)
just run -- feature-summary main..feature-branch

# Or test in Studio (if you add a mode for it)
just studio

Best Practices

Prompt Engineering

Be specific about workflow:

toml
## Workflow
1. Call `project_docs(doc_type="context")` first
2. Get the diff with `git_diff()`
3. For files over 500 lines, use `file_read(path="...", start_line=1, num_lines=200)` for targeted analysis
4. Synthesize findings into output format

Provide clear output requirements:

toml
## Output Requirements
- **Title**: Max 100 chars, action-oriented
- **Summary**: 2-3 paragraphs, focus on impact
- **Be precise**: State verified facts clearly and call out inferences when evidence is incomplete

Give examples:

toml
Example output:
{
  "title": "Add user authentication with JWT",
  "summary": "Implements JWT-based authentication...",
  "impact": "Breaking: All API endpoints now require auth headers"
}

Context Strategy

Guide Iris on how to handle different changeset sizes:

toml
## Context Strategy by Size
- **Small** (≤3 files, <100 lines): Consider all changes equally
- **Medium** (≤10 files, <500 lines): Focus on files with >60% relevance
- **Large** (>10 files or >500 lines): Use top 5-7 highest-relevance files
- **Very Large** (>20 files): Use `parallel_analyze` to distribute work

Tool Selection

Only list tools relevant to the task:

toml
## Tools Available
- `git_diff()` - Get changes (use detail="summary" first)
- `git_log(count=5)` - Recent commits for context
- `file_read(path="...")` - Deep analysis of specific files
# Don't list every possible tool—keep it focused

Certainty Standards

Enforce definitive language:

toml
## Writing Standards
- Be precise about confidence. If evidence is incomplete, gather more context and call out what is verified versus inferred.
- If unsure, use tools to gather more context

Output Type Patterns

Simple JSON Schema

For structured data:

rust
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct SimpleOutput {
    pub field1: String,
    pub field2: Vec<String>,
    pub field3: Option<usize>,
}

Markdown Wrapper

For LLM-driven formatting:

rust
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct MarkdownOutput {
    /// Markdown content (LLM controls structure)
    pub content: String,
}

impl MarkdownOutput {
    pub fn raw_content(&self) -> &str {
        &self.content
    }
}

Use markdown wrappers when you want the LLM to control the exact structure while still having parseable output.

Common Patterns

Multi-Stage Analysis

toml
## Workflow
1. Initial scan: `git_diff(detail="summary")` for overview
2. Identify key areas from relevance scores
3. Deep dive: `file_read(path="...", start_line=1, num_lines=200)` on top 5 files
4. Synthesize into structured output

Parallel Processing

For large changesets:

toml
## Very Large Changesets (>20 files)
Use `parallel_analyze` to distribute work:
parallel_analyze({
  "tasks": [
    "Analyze API changes in src/api/",
    "Review database schema changes",
    "Check frontend component updates"
  ]
})
Each subagent analyzes independently, then you synthesize.

Style Adaptation

Allow preset-based customization:

toml
## Style Adaptation
If STYLE INSTRUCTIONS are provided, prioritize that style in your output.
The structural requirements still apply, but adapt tone and word choice.

Critic Verification (Internal Output Types)

Not every output_type corresponds to a user-facing StructuredResponse variant. The shipped verify capability (src/agents/capabilities/verify.toml) returns a Critique value that is consumed entirely inside iris.rs:

  • should_run_critic(capability, output_type) decides whether the critic should run for (commit, GeneratedMessage), (review, Review), (pr, MarkdownPullRequest), (changelog, MarkdownChangelog), or (release_notes, MarkdownReleaseNotes).
  • verify_response_if_enabled() loads the verify capability, runs execute_with_agent::<Critique>(), and either accepts the response or re-runs execute_output_type() with a revision prompt.
  • Critique is a private internal type — it is never serialized into StructuredResponse.

Use this pattern when you want a capability that audits or post-processes another capability's artifact without surfacing its raw output to the user. Add the capability TOML, wire it into load_capability_config(), but call it explicitly from another method rather than threading it through execute_output_type().

Chat and Content Updates

The chat capability (src/agents/capabilities/chat.toml) is the conversational entry point users open with / in Studio. It does not return a StructuredResponse variant — instead, Iris mutates the active mode's artifact through dedicated tools defined in src/agents/tools/content_update.rs:

  • update_commit(emoji, title, message) — rewrites the commit message
  • update_pr(content) — rewrites the PR description
  • update_review(content) — rewrites the review

Each tool ships a ContentUpdate value over an mpsc::Sender<ContentUpdate> that the Studio app drains on its event loop, applying the change to the relevant *State struct. When you want chat to manipulate a new artifact type, add a corresponding Update<X>Tool next to the existing trio, extend the ContentUpdate enum with the new variant, and handle the new variant in the Studio receiver. The chat capability's TOML lists these tools so the LLM knows when to invoke them.

Integration with Studio

If you add a Studio mode for your capability:

  1. Add mode variant to Mode enum in src/studio/state/mod.rs
  2. Create state struct in src/studio/state/modes.rs
  3. Implement handler and renderer (see Adding Studio Modes)

Troubleshooting

Iris doesn't use the right tools

Make the tool list more explicit and add workflow steps that require specific tools.

Output parsing fails

Ensure your JSON schema matches exactly. Test with --debug to see raw responses.

Responses are too vague

Add certainty standards and specific output requirements. Show examples.

Context is incomplete

Guide Iris on when to gather more context. Add size-based strategies.

Examples in Codebase

Study these real capabilities:

  • commit.toml — Simple structured output, style adaptation
  • review.toml — Markdown wrapper, parallel analysis, size strategies
  • pr.toml — Multi-stage workflow, context gathering
  • changelog.toml — Version comparison, structured formatting

Next Steps

Released under the Apache 2.0 License.