Skip to main content

Overview

The Tracker interface defines how the orchestrator integrates with issue tracking systems. Tracker plugins fetch issue details, generate branch names, and create prompts for agents to work on specific issues. Plugin Slot: tracker
Default Plugin: github

Interface Definition

export interface Tracker {
  readonly name: string;
  
  getIssue(identifier: string, project: ProjectConfig): Promise<Issue>;
  isCompleted(identifier: string, project: ProjectConfig): Promise<boolean>;
  issueUrl(identifier: string, project: ProjectConfig): string;
  issueLabel?(url: string, project: ProjectConfig): string;
  branchName(identifier: string, project: ProjectConfig): string;
  generatePrompt(identifier: string, project: ProjectConfig): Promise<string>;
  listIssues?(filters: IssueFilters, project: ProjectConfig): Promise<Issue[]>;
  updateIssue?(identifier: string, update: IssueUpdate, project: ProjectConfig): Promise<void>;
  createIssue?(input: CreateIssueInput, project: ProjectConfig): Promise<Issue>;
}

Methods

name
string
required
Plugin name identifier (e.g. "github", "linear", "jira").
getIssue
(identifier: string, project: ProjectConfig) => Promise<Issue>
required
Fetch issue details from the tracker.Parameters:
  • identifier - Issue identifier (e.g. "42", "INT-1327", GitHub URL)
  • project - Project configuration
Returns: Issue object with id, title, description, state, etc.
isCompleted
(identifier: string, project: ProjectConfig) => Promise<boolean>
required
Check if issue is completed/closed.Parameters:
  • identifier - Issue identifier
  • project - Project configuration
Returns: true if issue is closed/done
issueUrl
(identifier: string, project: ProjectConfig) => string
required
Generate a URL for the issue.Parameters:
  • identifier - Issue identifier
  • project - Project configuration
Returns: Full URL to issue
issueLabel
(url: string, project: ProjectConfig) => string
Optional: Extract a human-readable label from an issue URL (e.g. "INT-1327", "#42").Parameters:
  • url - Issue URL
  • project - Project configuration
Returns: Short label for display
branchName
(identifier: string, project: ProjectConfig) => string
required
Generate a git branch name for the issue.Parameters:
  • identifier - Issue identifier
  • project - Project configuration
Returns: Branch name (e.g. "fix-auth-bug-42", "int-1327-improve-performance")
generatePrompt
(identifier: string, project: ProjectConfig) => Promise<string>
required
Generate a prompt for the agent to work on this issue.Parameters:
  • identifier - Issue identifier
  • project - Project configuration
Returns: Formatted prompt string with issue details
listIssues
(filters: IssueFilters, project: ProjectConfig) => Promise<Issue[]>
Optional: List issues with filters.Parameters:
  • filters - Filter criteria (state, labels, assignee, limit)
  • project - Project configuration
Returns: Array of issues
updateIssue
(identifier: string, update: IssueUpdate, project: ProjectConfig) => Promise<void>
Optional: Update issue state.Parameters:
  • identifier - Issue identifier
  • update - Fields to update (state, labels, assignee, comment)
  • project - Project configuration
createIssue
(input: CreateIssueInput, project: ProjectConfig) => Promise<Issue>
Optional: Create a new issue.Parameters:
  • input - Issue fields (title, description, labels, assignee, priority)
  • project - Project configuration
Returns: Created issue

Issue

export interface Issue {
  id: string;
  title: string;
  description: string;
  url: string;
  state: "open" | "in_progress" | "closed" | "cancelled";
  labels: string[];
  assignee?: string;
  priority?: number;
}
id
string
required
Unique issue identifier
title
string
required
Issue title
description
string
required
Full issue description (may be markdown)
url
string
required
URL to view issue
state
string
required
Issue state: "open", "in_progress", "closed", or "cancelled"
labels
string[]
required
Array of label strings
assignee
string
Username of assignee
priority
number
Priority level (higher = more urgent)

IssueFilters

export interface IssueFilters {
  state?: "open" | "closed" | "all";
  labels?: string[];
  assignee?: string;
  limit?: number;
}

IssueUpdate

export interface IssueUpdate {
  state?: "open" | "in_progress" | "closed";
  labels?: string[];
  assignee?: string;
  comment?: string;
}

CreateIssueInput

export interface CreateIssueInput {
  title: string;
  description: string;
  labels?: string[];
  assignee?: string;
  priority?: number;
}

Usage Examples

Implementing a Tracker Plugin

import type { Tracker, Issue, ProjectConfig } from "@composio/ao-core";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export function create(): Tracker {
  return {
    name: "github",
    
    async getIssue(identifier: string, project: ProjectConfig): Promise<Issue> {
      // Normalize identifier (handle URLs, #42, or just "42")
      const issueNumber = identifier.replace(/^#/, "").split("/").pop()!;
      
      // Use gh CLI
      const { stdout } = await execFileAsync("gh", [
        "issue",
        "view",
        issueNumber,
        "--repo", project.repo,
        "--json", "number,title,body,url,state,labels,assignees"
      ], { timeout: 30_000 });
      
      const data = JSON.parse(stdout);
      
      return {
        id: data.number.toString(),
        title: data.title,
        description: data.body || "",
        url: data.url,
        state: data.state === "OPEN" ? "open" : "closed",
        labels: data.labels.map((l: any) => l.name),
        assignee: data.assignees[0]?.login
      };
    },
    
    async isCompleted(identifier: string, project: ProjectConfig): Promise<boolean> {
      const issue = await this.getIssue(identifier, project);
      return issue.state === "closed";
    },
    
    issueUrl(identifier: string, project: ProjectConfig): string {
      const issueNumber = identifier.replace(/^#/, "");
      return `https://github.com/${project.repo}/issues/${issueNumber}`;
    },
    
    issueLabel(url: string, project: ProjectConfig): string {
      const match = url.match(/\/(\d+)$/);
      return match ? `#${match[1]}` : url;
    },
    
    branchName(identifier: string, project: ProjectConfig): string {
      const issueNumber = identifier.replace(/^#/, "");
      return `issue-${issueNumber}`;
    },
    
    async generatePrompt(identifier: string, project: ProjectConfig): Promise<string> {
      const issue = await this.getIssue(identifier, project);
      
      return [
        `# ${issue.title}`,
        ``,
        issue.description,
        ``,
        `Issue: ${issue.url}`,
        issue.labels.length > 0 ? `Labels: ${issue.labels.join(", ")}` : ""
      ].filter(Boolean).join("\n");
    },
    
    async listIssues(filters: IssueFilters, project: ProjectConfig): Promise<Issue[]> {
      const args = ["issue", "list", "--repo", project.repo];
      
      if (filters.state && filters.state !== "all") {
        args.push("--state", filters.state);
      }
      
      if (filters.assignee) {
        args.push("--assignee", filters.assignee);
      }
      
      if (filters.labels) {
        for (const label of filters.labels) {
          args.push("--label", label);
        }
      }
      
      if (filters.limit) {
        args.push("--limit", filters.limit.toString());
      }
      
      args.push("--json", "number,title,body,url,state,labels,assignees");
      
      const { stdout } = await execFileAsync("gh", args, { timeout: 30_000 });
      const items = JSON.parse(stdout);
      
      return items.map((data: any) => ({
        id: data.number.toString(),
        title: data.title,
        description: data.body || "",
        url: data.url,
        state: data.state === "OPEN" ? "open" : "closed",
        labels: data.labels.map((l: any) => l.name),
        assignee: data.assignees[0]?.login
      }));
    }
  };
}

Using Tracker in Session Manager

import type { Tracker } from "@composio/ao-core";

const tracker: Tracker = registry.get("tracker", "github");

// Fetch issue details
const issue = await tracker.getIssue("42", projectConfig);
console.log(`Issue: ${issue.title}`);

// Generate branch name
const branch = tracker.branchName("42", projectConfig);
console.log(`Branch: ${branch}`);

// Generate prompt for agent
const prompt = await tracker.generatePrompt("42", projectConfig);

// Spawn session with issue
await sessionManager.spawn({
  projectId: "my-app",
  issueId: "42",
  prompt
});

Implementation Notes

Issue Identifier Normalization

Trackers should accept flexible identifier formats:
  • GitHub: "42", "#42", "https://github.com/owner/repo/issues/42"
  • Linear: "INT-1327", "https://linear.app/team/issue/INT-1327"
  • Jira: "PROJ-123", "https://company.atlassian.net/browse/PROJ-123"

Prompt Generation

The generatePrompt() method should:
  1. Include issue title and description
  2. Add issue URL for reference
  3. Include relevant metadata (labels, priority)
  4. Format for agent consumption (markdown preferred)

Authentication

Trackers typically rely on environment variables or CLI tools:
  • GitHub: gh CLI (uses ~/.config/gh/hosts.yml)
  • Linear: LINEAR_API_KEY environment variable
  • Jira: JIRA_TOKEN + JIRA_URL

Built-in Plugins

  • github - GitHub Issues (default, uses gh CLI)
  • linear - Linear (uses GraphQL API)
Future plugins could support Jira, Asana, ClickUp, etc.

See Also

  • Session - Session interface
  • SCM - Source control and PR lifecycle interface

Build docs developers (and LLMs) love