Skip to content

Feature: Source Control Support for Sapling #127

@flora131

Description

@flora131

Summary

Add support for multiple source control systems in atomic, starting with Sapling as an alternative to the current GitHub-only implementation. The architecture should be extensible to support additional providers in the future.


Motivation

Currently, atomic is tightly coupled to GitHub/Git:

  • All commands in .claude/commands/ hardcode git and gh CLI commands
  • GitHub Actions workflows are bound to GitHub-specific events
  • No abstraction exists for source control operations

Many developers use alternative source control systems, particularly Sapling (developed by Meta), which offers:

  • Stack-based workflows with automatic rebasing
  • Visual smartlog for commit management
  • Native GitHub PR integration via sl pr submit

Supporting multiple providers will expand atomic's user base and provide a foundation for future integrations.


Proposed Solution

Architecture Overview

atomic/
├── .atomic/
│   └── config.yaml              # Project-level configuration (NEW)
├── src/
│   └── providers/               # Provider system (NEW)
│       ├── index.ts             # Provider registry & loader
│       ├── provider.ts          # SourceControlProvider interface
│       ├── github.ts            # GitHub provider implementation
│       └── sapling.ts           # Sapling provider implementation
└── .claude/
    └── commands/
        ├── commit.md            # Updated with provider variables
        └── create-pr.md         # Renamed from create-gh-pr.md

Technical Design

1. Provider Interface

// src/providers/provider.ts

export interface SourceControlProvider {
  /** Provider identifier */
  name: 'github' | 'sapling';

  /** Display name for UI */
  displayName: string;

  /** Primary CLI tool */
  cli: 'git' | 'sl';

  /** Command mappings for templates */
  commands: {
    // Status & info
    status: string;
    log: string;
    diff: string;
    branch: string;

    // Staging & committing
    add: string;
    commit: string;
    amend: string;

    // Remote operations
    push: string;
    pull: string;

    // PR/code review
    createPR: string;
    listPRs: string;
    viewPR: string;
  };

  /** Allowed tool patterns for YAML frontmatter */
  allowedTools: string[];

  /** Prerequisites check */
  checkPrerequisites(): Promise<PrerequisiteResult>;
}

export interface PrerequisiteResult {
  satisfied: boolean;
  missing: string[];
  installInstructions: Record<string, string>;
}

2. Provider Implementations

GitHub Provider

// src/providers/github.ts

export const GitHubProvider: SourceControlProvider = {
  name: 'github',
  displayName: 'GitHub (Git)',
  cli: 'git',

  commands: {
    status: 'git status --porcelain',
    log: 'git log --oneline',
    diff: 'git diff',
    branch: 'git branch --show-current',
    add: 'git add',
    commit: 'git commit',
    amend: 'git commit --amend',
    push: 'git push',
    pull: 'git pull',
    createPR: 'gh pr create',
    listPRs: 'gh pr list',
    viewPR: 'gh pr view',
  },

  allowedTools: [
    'Bash(git add:*)',
    'Bash(git status:*)',
    'Bash(git commit:*)',
    'Bash(git diff:*)',
    'Bash(git log:*)',
    'Bash(git push:*)',
    'Bash(git pull:*)',
    'Bash(gh pr:*)',
    'Bash(gh issue:*)',
  ],

  async checkPrerequisites() {
    const missing: string[] = [];

    if (!await commandExists('git')) missing.push('git');
    if (!await commandExists('gh')) missing.push('gh');

    return {
      satisfied: missing.length === 0,
      missing,
      installInstructions: {
        git: 'https://git-scm.com/downloads',
        gh: 'brew install gh  # or: https://cli.github.com/',
      },
    };
  },
};

Sapling Provider

// src/providers/sapling.ts

export interface SaplingProviderOptions {
  prWorkflow: 'stack' | 'branch';
}

export const SaplingProvider: SourceControlProvider = {
  name: 'sapling',
  displayName: 'Sapling',
  cli: 'sl',

  commands: {
    status: 'sl status',
    log: 'sl log --template "{node|short} {desc|firstline}\\n"',
    diff: 'sl diff',
    branch: 'sl log -r . --template "{bookmarks}"',
    add: 'sl add',
    commit: 'sl commit',
    amend: 'sl amend',
    push: 'sl push --to',  // or 'sl pr submit' based on prWorkflow
    pull: 'sl pull',
    createPR: 'sl pr submit',  // Creates stacked PRs
    listPRs: 'sl ssl',  // Smartlog with PR status
    viewPR: 'sl pr',
  },

  allowedTools: [
    'Bash(sl add:*)',
    'Bash(sl status:*)',
    'Bash(sl commit:*)',
    'Bash(sl diff:*)',
    'Bash(sl log:*)',
    'Bash(sl push:*)',
    'Bash(sl pull:*)',
    'Bash(sl pr:*)',
    'Bash(sl amend:*)',
    'Bash(sl goto:*)',
    'Bash(sl next:*)',
    'Bash(sl prev:*)',
    'Bash(gh:*)',  // Sapling uses gh for GitHub auth
  ],

  async checkPrerequisites() {
    const missing: string[] = [];

    if (!await commandExists('sl')) missing.push('sl');
    if (!await commandExists('gh')) missing.push('gh');  // Required for GitHub integration

    return {
      satisfied: missing.length === 0,
      missing,
      installInstructions: {
        sl: 'brew install sapling  # or: https://sapling-scm.com/docs/install',
        gh: 'brew install gh  # Required for GitHub PR integration',
      },
    };
  },
};

3. Configuration Schema

File: .atomic/config.yaml

# yaml-language-server: $schema=https://atomic.dev/schema/config.json
version: 1

sourceControl:
  # Required: explicitly set by user during 'atomic init'
  provider: sapling  # 'github' | 'sapling'

  # Provider-specific options
  sapling:
    # How to create PRs
    # - 'stack': Uses 'sl pr submit --stack' (one PR per commit)
    # - 'branch': Uses 'sl push --to' (traditional branch-based PR)
    prWorkflow: stack

  github:
    # Future: GitHub-specific options (e.g., default base branch)

JSON Schema for Validation

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["version", "sourceControl"],
  "properties": {
    "version": {
      "type": "integer",
      "const": 1
    },
    "sourceControl": {
      "type": "object",
      "required": ["provider"],
      "properties": {
        "provider": {
          "type": "string",
          "enum": ["github", "sapling"],
          "description": "Source control provider to use"
        },
        "sapling": {
          "type": "object",
          "properties": {
            "prWorkflow": {
              "type": "string",
              "enum": ["stack", "branch"],
              "default": "stack",
              "description": "PR creation strategy"
            }
          }
        },
        "github": {
          "type": "object",
          "properties": {}
        }
      }
    }
  }
}

4. Template Variable System

Update Markdown command files to use provider-agnostic variables:

Syntax: ${{ provider.<path> }}

Example: .claude/commands/commit.md

---
allowed-tools:
  - ${{ provider.allowedTools }}
---

# Commit Changes

Execute the following steps to create a well-formatted commit.

## 1. Check Repository Status

Run status command to see changes:
\`\`\`bash
${{ provider.commands.status }}
\`\`\`

## 2. Review Changes

View the diff:
\`\`\`bash
${{ provider.commands.diff }}
\`\`\`

## 3. Stage and Commit

Stage specific files (prefer explicit file names over -A):
\`\`\`bash
${{ provider.commands.add }} <files>
\`\`\`

Create the commit:
\`\`\`bash
${{ provider.commands.commit }} -m "$(cat <<'EOF'
<commit message>
EOF
)"
\`\`\`

Resolution Logic

// src/template/resolver.ts

export async function resolveTemplate(
  content: string,
  config: AtomicConfig
): Promise<string> {
  const provider = getProvider(config.sourceControl.provider);

  // Replace ${{ provider.* }} patterns
  return content.replace(
    /\$\{\{\s*provider\.([a-zA-Z.]+)\s*\}\}/g,
    (match, path) => {
      const value = getNestedValue(provider, path);
      if (Array.isArray(value)) {
        // For allowedTools array, format as YAML list
        return value.map(t => `- ${t}`).join('\n  ');
      }
      return String(value);
    }
  );
}

User Onboarding Flow

atomic init Interactive Wizard

$ atomic init

┌─────────────────────────────────────────────────────────┐
│                                                         │
│   Welcome to Atomic Configuration                       │
│                                                         │
└─────────────────────────────────────────────────────────┘

? Select your source control provider:

  › GitHub (Git)
    Standard Git workflow with GitHub CLI integration.
    Uses: git, gh

    Sapling
    Stack-based workflow with smartlog visualization.
    Uses: sl, gh

[Use arrow keys to navigate, Enter to select]

If Sapling selected:

? Select your preferred PR workflow:

  › Stack-based (Recommended)
    Creates one PR per commit using 'sl pr submit --stack'.
    Best viewed with ReviewStack (reviewstack.dev).

    Branch-based
    Traditional workflow using 'sl push --to <branch>'.
    Creates single PR from branch.

[Use arrow keys to navigate, Enter to select]

Prerequisite Check:

Checking prerequisites...

  ✓ sl (Sapling CLI) found
  ✓ gh (GitHub CLI) found
  ✓ gh authenticated

Or if missing:

Checking prerequisites...

  ✗ sl (Sapling CLI) not found
  ✓ gh (GitHub CLI) found

Missing prerequisites must be installed:

  Sapling:
    macOS:   brew install sapling
    Linux:   https://sapling-scm.com/docs/install
    Windows: https://sapling-scm.com/docs/install

After installing, run 'atomic init' again.

Success Output:

✓ Created .atomic/config.yaml
✓ Provider set to: Sapling (stack-based workflow)

Configuration saved. Your agent commands will now use Sapling.

Next steps:
  1. Run 'atomic sync' to update agent configurations
  2. Verify with 'sl' to see your smartlog

Documentation:
  • Sapling basics: https://sapling-scm.com/docs/introduction/
  • Stacked PRs:    https://sapling-scm.com/docs/git/github/

Non-Interactive Mode

# For CI/automation
atomic init --provider=sapling --sapling-pr-workflow=stack

# Show current configuration
atomic config show

# Update specific setting
atomic config set sourceControl.provider github
atomic config set sourceControl.sapling.prWorkflow branch

Error Handling

No Configuration Found

Error: No source control provider configured

This project hasn't been set up with atomic yet.

Run 'atomic init' to configure your source control provider:

  $ atomic init

Or specify a provider directly:

  $ atomic init --provider=github
  $ atomic init --provider=sapling

Provider CLI Not Found

Error: Sapling CLI 'sl' not found in PATH

Your project is configured to use Sapling, but the CLI is not installed.

Install Sapling:
  macOS:   brew install sapling
  Linux:   See https://sapling-scm.com/docs/install
  Windows: See https://sapling-scm.com/docs/install

Or switch to GitHub provider:
  $ atomic config set sourceControl.provider github

Invalid Configuration

Error: Invalid configuration in .atomic/config.yaml

  Line 5: Unknown provider 'gitlab'

  Valid providers: github, sapling

Run 'atomic init' to reconfigure, or edit .atomic/config.yaml manually.

Migration Guide

For Existing Users

Users with existing atomic setups need to:

  1. Run atomic init to create .atomic/config.yaml
  2. Select their provider (GitHub for existing behavior)
  3. Run atomic sync to update command files

Backward Compatibility

  • If no .atomic/config.yaml exists and user runs a command, show helpful error directing to atomic init
  • The atomic sync command will regenerate command files with provider variables resolved

Implementation Phases

Phase 1: Configuration Foundation

  • Create .atomic/config.yaml schema and TypeScript types
  • Implement atomic init command with interactive wizard
  • Implement atomic config show and atomic config set commands
  • Add JSON Schema for editor autocomplete

Phase 2: Provider Abstraction

  • Define SourceControlProvider interface
  • Implement provider registry and loader
  • Implement GitHubProvider with current command mappings
  • Add prerequisite checking system

Phase 3: Sapling Provider

  • Implement SaplingProvider with command mappings
  • Add Sapling-specific options (prWorkflow)
  • Test with real Sapling repositories
  • Document Sapling-specific workflows

Phase 4: Template System

  • Implement ${{ provider.* }} variable resolution
  • Update .claude/commands/commit.md with variables
  • Update .claude/commands/create-gh-pr.mdcreate-pr.md
  • Update other command files as needed
  • Add atomic sync command to regenerate from templates

Phase 5: Polish & Documentation

  • Comprehensive error messages for all failure modes
  • Update README with multi-provider documentation
  • Add troubleshooting guide
  • Add provider comparison documentation

Acceptance Criteria

Functional Requirements

  • User can run atomic init and select from available providers via interactive prompt
  • User can run atomic init --provider=<name> for non-interactive setup
  • Configuration is persisted in .atomic/config.yaml
  • Commands in .claude/commands/ use provider-appropriate CLI commands
  • Sapling users can create stacked PRs with sl pr submit
  • GitHub users experience no change from current behavior
  • Missing prerequisites are detected with clear installation instructions

Non-Functional Requirements

  • Adding a new provider requires only implementing the SourceControlProvider interface
  • Provider selection is explicit (no auto-detection)
  • Configuration is project-level (per-repository)
  • Error messages are actionable and include next steps

Sapling Command Reference

For implementers, here's a mapping of key operations:

Operation Git/GitHub Sapling
Check status git status sl status
View log git log --oneline sl log or sl (smartlog)
Show diff git diff sl diff
Stage files git add <files> sl add <files>
Commit git commit -m "msg" sl commit -m "msg"
Amend commit git commit --amend sl amend
Push git push sl push --to <branch>
Pull git pull sl pull (fetch only, use sl goto to update)
Create PR gh pr create sl pr submit or sl pr submit --stack
View PRs gh pr list sl ssl (smartlog with PR status)
Navigate stack N/A sl next / sl prev
Interactive UI N/A sl web (launches ISL)

Open Questions

  1. Workflow files: Should .github/workflows/ also be provider-aware, or remain GitHub-specific? (Recommendation: Keep GitHub-specific, as they only run on GitHub)

  2. Agent config: Should src/config.ts AgentConfig interface be extended to include provider info?

  3. Scope of atomic sync: Should it also update .github/skills/ or just .claude/commands/?


References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions