Skip to main content

Overview

The SCM (Source Control Management) interface is the richest plugin interface, covering the full PR pipeline: PR detection, state tracking, CI checks, code reviews, and merge operations. Plugin Slot: scm
Default Plugin: github

Interface Definition

export interface SCM {
  readonly name: string;
  
  // PR Lifecycle
  detectPR(session: Session, project: ProjectConfig): Promise<PRInfo | null>;
  getPRState(pr: PRInfo): Promise<PRState>;
  getPRSummary?(pr: PRInfo): Promise<{
    state: PRState;
    title: string;
    additions: number;
    deletions: number;
  }>;
  mergePR(pr: PRInfo, method?: MergeMethod): Promise<void>;
  closePR(pr: PRInfo): Promise<void>;
  
  // CI Tracking
  getCIChecks(pr: PRInfo): Promise<CICheck[]>;
  getCISummary(pr: PRInfo): Promise<CIStatus>;
  
  // Review Tracking
  getReviews(pr: PRInfo): Promise<Review[]>;
  getReviewDecision(pr: PRInfo): Promise<ReviewDecision>;
  getPendingComments(pr: PRInfo): Promise<ReviewComment[]>;
  getAutomatedComments(pr: PRInfo): Promise<AutomatedComment[]>;
  
  // Merge Readiness
  getMergeability(pr: PRInfo): Promise<MergeReadiness>;
}

PR Lifecycle Methods

detectPR
(session: Session, project: ProjectConfig) => Promise<PRInfo | null>
required
Detect if a session has an open PR (by branch name).Parameters:
  • session - Session to check
  • project - Project configuration
Returns: PRInfo if PR exists, null otherwise
getPRState
(pr: PRInfo) => Promise<PRState>
required
Get current PR state.Parameters:
  • pr - PR info
Returns: "open", "merged", or "closed"
getPRSummary
(pr: PRInfo) => Promise<object>
Optional: Get PR summary with stats (state, title, additions, deletions).Parameters:
  • pr - PR info
Returns: Object with state, title, additions, deletions
mergePR
(pr: PRInfo, method?: MergeMethod) => Promise<void>
required
Merge a PR.Parameters:
  • pr - PR info
  • method - Merge method: "merge", "squash", or "rebase" (default: repo settings)
closePR
(pr: PRInfo) => Promise<void>
required
Close a PR without merging.Parameters:
  • pr - PR info

CI Tracking Methods

getCIChecks
(pr: PRInfo) => Promise<CICheck[]>
required
Get individual CI check statuses.Parameters:
  • pr - PR info
Returns: Array of CI checks with name, status, URL, timestamps
getCISummary
(pr: PRInfo) => Promise<CIStatus>
required
Get overall CI summary.Parameters:
  • pr - PR info
Returns: "pending", "passing", "failing", or "none"

Review Tracking Methods

getReviews
(pr: PRInfo) => Promise<Review[]>
required
Get all reviews on a PR.Parameters:
  • pr - PR info
Returns: Array of reviews with author, state, body, timestamp
getReviewDecision
(pr: PRInfo) => Promise<ReviewDecision>
required
Get the overall review decision.Parameters:
  • pr - PR info
Returns: "approved", "changes_requested", "pending", or "none"
getPendingComments
(pr: PRInfo) => Promise<ReviewComment[]>
required
Get pending (unresolved) review comments.Parameters:
  • pr - PR info
Returns: Array of unresolved comments
getAutomatedComments
(pr: PRInfo) => Promise<AutomatedComment[]>
required
Get automated review comments (bots, linters, security scanners).Parameters:
  • pr - PR info
Returns: Array of automated comments with severity

Merge Readiness Method

getMergeability
(pr: PRInfo) => Promise<MergeReadiness>
required
Check if PR is ready to merge.Parameters:
  • pr - PR info
Returns: MergeReadiness with mergeable flag, CI status, approval status, conflicts, and blockers

PRInfo

export interface PRInfo {
  number: number;
  url: string;
  title: string;
  owner: string;
  repo: string;
  branch: string;
  baseBranch: string;
  isDraft: boolean;
}
number
number
required
PR number
url
string
required
Full URL to PR
title
string
required
PR title
owner
string
required
Repository owner
repo
string
required
Repository name
branch
string
required
Source branch name
baseBranch
string
required
Target/base branch name
isDraft
boolean
required
Whether PR is in draft mode

PRState

export type PRState = "open" | "merged" | "closed";

CICheck

export interface CICheck {
  name: string;
  status: "pending" | "running" | "passed" | "failed" | "skipped";
  url?: string;
  conclusion?: string;
  startedAt?: Date;
  completedAt?: Date;
}

CIStatus

export type CIStatus = "pending" | "passing" | "failing" | "none";

Review

export interface Review {
  author: string;
  state: "approved" | "changes_requested" | "commented" | "dismissed" | "pending";
  body?: string;
  submittedAt: Date;
}

ReviewDecision

export type ReviewDecision = "approved" | "changes_requested" | "pending" | "none";

ReviewComment

export interface ReviewComment {
  id: string;
  author: string;
  body: string;
  path?: string;
  line?: number;
  isResolved: boolean;
  createdAt: Date;
  url: string;
}

AutomatedComment

export interface AutomatedComment {
  id: string;
  botName: string;
  body: string;
  path?: string;
  line?: number;
  severity: "error" | "warning" | "info";
  createdAt: Date;
  url: string;
}

MergeReadiness

export interface MergeReadiness {
  mergeable: boolean;
  ciPassing: boolean;
  approved: boolean;
  noConflicts: boolean;
  blockers: string[];
}
mergeable
boolean
required
Overall merge readiness (all checks must pass)
ciPassing
boolean
required
Whether CI is passing
approved
boolean
required
Whether PR is approved
noConflicts
boolean
required
Whether PR has no merge conflicts
blockers
string[]
required
Array of blocker descriptions (e.g. "CI failing", "Changes requested")

MergeMethod

export type MergeMethod = "merge" | "squash" | "rebase";

Usage Examples

Implementing an SCM Plugin

import type { SCM, PRInfo, PRState, CIStatus, ReviewDecision, MergeReadiness } from "@composio/ao-core";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export function create(): SCM {
  return {
    name: "github",
    
    async detectPR(session: Session, project: ProjectConfig): Promise<PRInfo | null> {
      if (!session.branch) return null;
      
      const { stdout } = await execFileAsync("gh", [
        "pr",
        "view",
        session.branch,
        "--repo", project.repo,
        "--json", "number,url,title,headRefName,baseRefName,isDraft"
      ], { timeout: 30_000 }).catch(() => ({ stdout: "" }));
      
      if (!stdout) return null;
      
      const data = JSON.parse(stdout);
      const [owner, repo] = project.repo.split("/");
      
      return {
        number: data.number,
        url: data.url,
        title: data.title,
        owner,
        repo,
        branch: data.headRefName,
        baseBranch: data.baseRefName,
        isDraft: data.isDraft
      };
    },
    
    async getPRState(pr: PRInfo): Promise<PRState> {
      const { stdout } = await execFileAsync("gh", [
        "pr",
        "view",
        pr.number.toString(),
        "--repo", `${pr.owner}/${pr.repo}`,
        "--json", "state"
      ], { timeout: 10_000 });
      
      const data = JSON.parse(stdout);
      return data.state.toLowerCase() as PRState;
    },
    
    async getCISummary(pr: PRInfo): Promise<CIStatus> {
      const checks = await this.getCIChecks(pr);
      
      if (checks.length === 0) return "none";
      
      const anyFailed = checks.some(c => c.status === "failed");
      const anyPending = checks.some(c => c.status === "pending" || c.status === "running");
      
      if (anyFailed) return "failing";
      if (anyPending) return "pending";
      return "passing";
    },
    
    async getReviewDecision(pr: PRInfo): Promise<ReviewDecision> {
      const { stdout } = await execFileAsync("gh", [
        "pr",
        "view",
        pr.number.toString(),
        "--repo", `${pr.owner}/${pr.repo}`,
        "--json", "reviewDecision"
      ], { timeout: 10_000 });
      
      const data = JSON.parse(stdout);
      
      if (!data.reviewDecision) return "none";
      
      const decision = data.reviewDecision.toLowerCase();
      if (decision === "approved") return "approved";
      if (decision === "changes_requested") return "changes_requested";
      if (decision === "review_required") return "pending";
      return "none";
    },
    
    async getMergeability(pr: PRInfo): Promise<MergeReadiness> {
      const [ciStatus, reviewDecision, state] = await Promise.all([
        this.getCISummary(pr),
        this.getReviewDecision(pr),
        this.getPRState(pr)
      ]);
      
      const ciPassing = ciStatus === "passing" || ciStatus === "none";
      const approved = reviewDecision === "approved";
      const noConflicts = true; // Would need to check mergeable state
      
      const blockers: string[] = [];
      if (!ciPassing) blockers.push("CI failing");
      if (!approved) blockers.push("Not approved");
      if (!noConflicts) blockers.push("Merge conflicts");
      
      return {
        mergeable: blockers.length === 0 && state === "open",
        ciPassing,
        approved,
        noConflicts,
        blockers
      };
    },
    
    async mergePR(pr: PRInfo, method = "squash"): Promise<void> {
      await execFileAsync("gh", [
        "pr",
        "merge",
        pr.number.toString(),
        "--repo", `${pr.owner}/${pr.repo}`,
        method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash",
        "--delete-branch"
      ], { timeout: 30_000 });
    }
  };
}

Using SCM in Lifecycle Manager

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

const scm: SCM = registry.get("scm", "github");

// Detect PR
const pr = await scm.detectPR(session, project);
if (pr) {
  // Check CI
  const ciStatus = await scm.getCISummary(pr);
  console.log(`CI status: ${ciStatus}`);
  
  // Check reviews
  const decision = await scm.getReviewDecision(pr);
  console.log(`Review decision: ${decision}`);
  
  // Check merge readiness
  const readiness = await scm.getMergeability(pr);
  if (readiness.mergeable) {
    await scm.mergePR(pr, "squash");
  } else {
    console.log(`Blockers: ${readiness.blockers.join(", ")}`);
  }
}

Implementation Notes

Authentication

SCM plugins typically use CLI tools or API tokens:
  • GitHub: gh CLI (uses ~/.config/gh/hosts.yml)
  • GitLab: glab CLI or GITLAB_TOKEN

Rate Limiting

Be mindful of API rate limits:
  • Cache PR info in session metadata
  • Batch requests when possible
  • Use GraphQL for complex queries (GitHub)

Review Comment Filtering

Distinguish between:
  • Human reviews (priority)
  • Bot comments (linters, security scanners)
  • Resolved vs unresolved comments

Built-in Plugins

  • github - GitHub (default, uses gh CLI)
Future plugins could support GitLab, Bitbucket, Gitea, etc.

See Also

Build docs developers (and LLMs) love