Dicklesworthstone avatar

ru

Repo Updater - Multi-repo synchronization with AI-assisted review orchestration. Parallel sync, agen

by Dicklesworthstone|Open Source
<p align="center"> <img src="https://img.shields.io/badge/version-1.2.0-blue?style=for-the-badge" alt="Version" /> <img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux-blueviolet?style=for-the-badge" alt="Platform" /> <img src="https://img.shields.io/badge/shell-Bash%204.0+-purple?style=for-the-badge" alt="Shell" /> <img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="License" /> </p> <h1 align="center">ru</h1> <h3 align="center">Repo Updater</h3> <div align="center"> <img src="ru_illustration.webp" alt="ru - Repo Updater: Keep all your GitHub repositories synchronized with a single command"> </div> <p align="center"> <strong>A beautiful, automation-friendly CLI for synchronizing GitHub repositories</strong> </p> <p align="center"> Keep dozens (or hundreds) of repos in sync with a single command.<br/> Clone missing repos, pull updates, detect conflicts, and get actionable resolution commands. </p> <p align="center"> <em>Pure Bash with no string parsing of git output. Uses git plumbing for reliable status detection.<br/> Meaningful exit codes for CI. JSON output for scripting. Non-interactive mode for automation.</em> </p>
<p align="center">
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/install.sh?ru_cb=$(date +%s)" | bash
# You can omit the `?ru_cb=...` once installed; it's just a cache-buster for the installer fetch.

Or via Homebrew (macOS/Linux):

brew install dicklesworthstone/tap/ru
</p>

๐Ÿค– Agent Quickstart (JSON/TOON)

Use structured output in agent contexts. stdout = data, stderr = diagnostics, exit 0 = success.

# Sync all repos (machine-readable)
ru sync --json

# Preview without changes
ru sync --dry-run --json

# Status only (no fetch)
ru status --no-fetch --json

# Machine-readable CLI documentation
ru robot-docs                   # All topics as JSON
ru robot-docs commands          # Command/flag reference
ru robot-docs quickstart        # Getting started guide
ru robot-docs examples          # Usage examples
ru robot-docs exit-codes        # Exit code reference
ru robot-docs formats           # Output format details
ru robot-docs schemas           # JSON schemas for command outputs
ru --schema                     # Shortcut for ru robot-docs schemas

๐Ÿค– Ready-made Blurb for AI Agents

[!IMPORTANT] Copy the blurb below to your project's AGENTS.md, CLAUDE.md, or .cursorrules file for AI agent integration with ru.

## ru Quick Reference for AI Agents

Syncs GitHub repos to local projects directory (clone missing, pull updates, detect conflicts).

```bash
ru sync                    # Sync all repos
ru sync --dry-run          # Preview only
ru sync -j4 --autostash    # Parallel + auto-stash
ru status --no-fetch       # Quick local status
ru list --paths            # Repo paths (stdout)
```

**Automation:** `--non-interactive --json` (structuredโ†’stdout, humanโ†’stderr).

**Exit:** 0=ok | 1=partial | 2=conflicts | 3=system | 4=bad args | 5=interrupted (`--resume`)

**Critical:**
- Never create worktrees/clones in projects dir โ†’ use `/tmp/`
- Never parse human output โ†’ use `--json`

๐ŸŽฏ The Primary Use Case: Keeping Your Projects Directory in Sync

The scenario: You work across multiple machines, contribute to dozens of repositories, and your local /data/projects directory needs to stay synchronized with GitHub. Manually running git pull in each directory is tedious and error-prone.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Your Repos     โ”‚     โ”‚  repos.d/       โ”‚     โ”‚       ru        โ”‚
โ”‚  on GitHub      โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚  public.txt     โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚     sync        โ”‚
โ”‚  (47 repos)     โ”‚     โ”‚  private.txt    โ”‚     โ”‚                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                                        โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
         โ–ผ                    โ–ผ                         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Clone Missing  โ”‚  โ”‚  Pull Updates   โ”‚     โ”‚  Report Status  โ”‚
โ”‚  (8 new repos)  โ”‚  โ”‚  (34 updated)   โ”‚     โ”‚  (2 conflicts)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                                        โ”‚
                                                        โ–ผ
                                             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                             โ”‚  Actionable     โ”‚
                                             โ”‚  Resolution     โ”‚
                                             โ”‚  Commands       โ”‚
                                             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The workflow:

  1. Configure once โ€” Add repos to ~/.config/ru/repos.d/public.txt
  2. Run ru sync โ€” Everything happens automatically
  3. Review conflicts โ€” Get copy-paste commands to resolve issues
# On any machine, sync all 47 of your repos
ru sync

# Output:
# โ†’ Processing 1/47: mcp_agent_mail
#   โ”œโ”€ Path: /data/projects/mcp_agent_mail
#   โ”œโ”€ Status: behind (0 ahead, 3 behind)
#   โ””โ”€ Result: โœ“ Updated (2s)
#
# โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
# โ”‚                    ๐Ÿ“Š Sync Summary                          โ”‚
# โ”‚  โœ… Cloned:     8 repos                                     โ”‚
# โ”‚  โœ… Updated:   34 repos                                     โ”‚
# โ”‚  โญ๏ธ  Current:    3 repos                                    โ”‚
# โ”‚  โš ๏ธ  Conflicts: 2 repos (need attention)                    โ”‚
# โ”‚  Total: 47 repos processed in 2m 34s                        โ”‚
# โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Comparison:

Without ruWith ru
cd into each of 47 directories and git pullOne command syncs everything
Forget which repos exist locally vs remotelyAutomatically clones missing repos
Wonder if your local branch divergedClear status: behind, ahead, diverged, conflict
Google the right git commands for conflictsCopy-paste resolution commands provided
Manual process breaks when network failsMeaningful exit codes for scripting

Table of Contents


๐Ÿ’ก Why ru Exists

Managing a large collection of GitHub repositories presents unique challenges:

ProblemWhy It's HardHow ru Solves It
Too many repos to update manually47 repos ร— cd + git pull = wasted timeOne command syncs everything
New repos on GitHub not cloned locallyNo way to detect missing repos automaticallyCompares list to local directory, clones missing
Diverged branches are confusing"Already up to date" vs actual divergenceGit plumbing detects ahead/behind/diverged states
Dirty working trees block pullsErrors with uncommitted changesClear warnings + resolution commands
Different clone strategies per repoSome need SSH, some HTTPS, some specific branchesFlexible repo spec syntax (repo@branch)
Need automation in CIInteractive prompts break scripts--non-interactive mode, JSON output, exit codes
Public vs private reposDifferent auth requirementsSeparate lists, automatic gh CLI integration

ru brings order to your projects directory. It's the tool you wish existed every time you've thought "I should really update all my repos."


โœจ Highlights

<table> <tr> <td width="50%">

Zero-Setup Installation

One-liner installer handles everything:

  • Checksum verification by default
  • Auto-installs to ~/.local/bin
  • Detects missing gh CLI and prompts
  • XDG-compliant configuration
</td> <td width="50%">

Automation-Grade Design

Built for scripting and CI from day one:

  • Meaningful exit codes (0-5)
  • --json mode for structured output
  • --non-interactive for unattended runs
  • --dry-run to preview changes
</td> </tr> <tr> <td width="50%">

Git Plumbing, Not String Parsing

Reliable status detection:

  • git rev-list --left-right for ahead/behind
  • git status --porcelain for dirty detection
  • Never parses "Already up to date" text
  • Locale-independent, version-safe
</td> <td width="50%">

Beautiful Terminal UI

Powered by gum with ANSI fallbacks:

  • Styled progress indicators
  • Boxed summary reports
  • Color-coded status (green/yellow/red)
  • Works without gum installed
</td> </tr> <tr> <td width="50%">

Subcommand Architecture

Clean CLI with focused commands:

  • sync โ€” Clone and pull repos
  • status โ€” Show status without changes
  • init โ€” Create configuration
  • add โ€” Add repo to list
  • doctor โ€” System diagnostics
</td> <td width="50%">

Conflict Resolution Help

Actionable commands for every issue:

  • Dirty working tree? Stash/commit/discard options
  • Diverged branches? Rebase/merge/push options
  • Auth failed? Token/login instructions
  • Copy-paste ready commands
</td> </tr> <tr> <td width="50%">

Parallel & Resumable Syncs

Efficient handling of large repo collections:

  • --parallel N for concurrent operations
  • Worker pool with portable coordination
  • --resume to continue interrupted syncs
  • State tracking for reliable restarts
</td> <td width="50%">

Orphan Repository Management

Keep your projects directory clean:

  • ru prune detects orphan repositories
  • --archive for non-destructive cleanup
  • Layout-aware directory scanning
  • Respects custom-named repos in config
</td> </tr> </table>

โšก Quickstart

Installation

Recommended: Homebrew (macOS/Linux)

brew install dicklesworthstone/tap/ru

This method provides:

  • Automatic updates via brew upgrade
  • Dependency management
  • Easy uninstall via brew uninstall

Windows: Scoop

scoop bucket add dicklesworthstone https://github.com/Dicklesworthstone/scoop-bucket
scoop install dicklesworthstone/ru

Alternative: Install Script

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/install.sh?ru_cb=$(date +%s)" | bash

If you suspect CDN caching (stale installer):

curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/install.sh?ru_cb=$(date +%s).$$" | bash

How cache-busting works:

GitHub's raw content is served through a CDN that caches aggressively. The ?ru_cb=... query parameter ensures fresh content:

TechniqueCache Bypass
$(date +%s)Unix timestamp (changes every second)
$(date +%s).$$Timestamp + PID (unique per invocation)
RU_CACHE_BUST_TOKEN=xyzCustom token override

Installer self-refresh mechanism:

When the installer runs via curl | bash, it performs automatic self-refresh to ensure you always get the latest version:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Installer Self-Refresh                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Detect piped   โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ stdin is not a TTY?
                    โ”‚  execution      โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚ yes
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Download fresh โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Fetch latest install.sh
                    โ”‚  installer      โ”‚         to temp file
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Validate       โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Check #!/usr/bin/env bash
                    โ”‚  shebang        โ”‚         header is present
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚ valid
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Re-exec with   โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ RU_INSTALLER_REFRESHED=1
                    โ”‚  fresh copy     โ”‚         prevents infinite loop
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

This ensures that even if CDN caching serves a stale version initially, the installer will fetch and execute the latest code. The RU_INSTALLER_REFRESHED=1 environment variable prevents recursion.

To disable self-refresh (for airgapped environments):

RU_INSTALLER_NO_SELF_REFRESH=1 bash install.sh
<details> <summary><strong>Manual installation</strong></summary>
# Download script
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/repo_updater/main/ru -o ~/.local/bin/ru
chmod +x ~/.local/bin/ru

# Ensure ~/.local/bin is in PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc  # or ~/.bashrc
source ~/.zshrc
</details> <details> <summary><strong>Installation options</strong></summary>
# Custom install directory
DEST=/opt/bin curl -fsSL .../install.sh | bash

# System-wide installation (requires sudo)
RU_SYSTEM=1 curl -fsSL .../install.sh | bash

# Install specific version
RU_VERSION=1.0.0 curl -fsSL .../install.sh | bash

# Install from main branch (not recommended for production)
RU_UNSAFE_MAIN=1 curl -fsSL .../install.sh | bash
</details>

First Run

# Initialize configuration
ru init

# Add some repos
ru add Dicklesworthstone/mcp_agent_mail
ru add Dicklesworthstone/beads_viewer

# Sync everything
ru sync

๐Ÿ› ๏ธ Commands

ru [command] [options]

Available Commands

CommandDescription
syncClone missing repos and pull updates (default)
statusShow repository status without making changes
initInitialize configuration directory and files
add <repo>Add a repository to your list
remove <repo>Remove a repository from your list
listShow configured repositories
doctorRun system diagnostics
self-updateUpdate ru to the latest version
configShow or set configuration values
pruneFind and manage orphan repositories
reviewAI-assisted code review orchestration
agent-sweepOrchestrate AI agents across dirty repos
ai-syncAuto-commit dirty repos using AI agents (via ntm)
dep-updateAI-powered dependency updates across repos
importImport repos from GitHub stars or org
robot-docsMachine-readable CLI documentation (JSON)

Global Options

FlagDescription
--help, -hShow help message
--version, -vShow version
--jsonOutput JSON to stdout (human output still goes to stderr)
--quiet, -qMinimal output (errors only)
--verboseDetailed output
--non-interactiveNever prompt (for CI/automation)

Command-Specific Options

ru sync

FlagDescription
--clone-onlyOnly clone missing repos, don't pull
--pull-onlyOnly pull existing repos, don't clone
--autostashStash changes before pull, pop after
--rebaseUse git pull --rebase instead of merge
--dry-runShow what would happen without making changes
--dir PATHOverride projects directory
--parallel N, -j NSync N repos concurrently (default: 1)
--timeout SECONDSNetwork timeout for slow operations (default: 30)
--resumeResume an interrupted sync from where it left off
--restartDiscard interrupted sync state and start fresh

Ad-hoc sync: You can also pass repo URLs directly without adding them to config:

ru sync owner/repo1 owner/repo2 https://github.com/owner/repo3

ru status

FlagDescription
--fetchFetch remotes first (default)
--no-fetchSkip fetch, use cached state

ru add

FlagDescription
--privateAdd to private repos list
--publicAdd to public repos list (default)
--from-cwdDetect repo from current directory's git remote

ru remove

FlagDescription
--privateRemove from private repos list only
--publicRemove from public repos list only
(none)Search and remove from all repo lists

ru list

FlagDescription
--publicShow only public repos
--privateShow only private repos
--pathsShow local paths instead of URLs

ru init

FlagDescription
--examplePopulate repos.txt with example repositories

ru self-update

FlagDescription
--checkCheck for updates without installing

ru config

FlagDescription
--printPrint all configuration values
--set KEY=VALUESet a configuration value

ru prune

FlagDescription
(none)List orphan repos (dry run, default)
--archiveMove orphan repos to archive directory
--deletePermanently delete orphan repos (requires confirmation)

ru review

FlagDescription
--planDiscovery mode: find work items and plan review (default)
--applyApply mode: process approved changes from a previous plan
--dry-runShow what would be discovered without starting sessions
--statusShow current review state without running discovery
--analyticsDisplay analytics dashboard for past reviews
--basicUse basic TUI instead of terminal multiplexer
--mode=MODESession driver: auto, ntm, or local
--parallel N, -j NRun N review sessions concurrently (default: 4)
--repos=PATTERNFilter repos by pattern (glob or regex)
--priority=LEVELMinimum priority threshold: critical, high, normal, low, all
--skip-days=NSkip items reviewed within N days (default: 7)
--max-repos=NLimit number of repos to review (cost budget)
--max-runtime=NMaximum runtime in minutes (time budget)
--resumeResume an interrupted review session
--pushAllow pushing changes (with --apply)

ru import

FlagDescription
--starsImport from your GitHub stars
--org=NAMEImport from an organization
--user=NAMEImport from a user's repos
--limit=NMaximum repos to import
--privateAdd to private repos list

ru agent-sweep

FlagDescription
--dry-runPreview which repos would be processed
--parallel N, -jNProcess N repos concurrently (default: 1)
--repos=PATTERNFilter repos by glob pattern
--with-releaseInclude release step after commit
--resumeResume an interrupted sweep
--restartClear previous state and start fresh
--keep-sessionsKeep tmux sessions after completion
--keep-sessions-on-failKeep sessions only on failure
--attach-on-failAttach to session on failure
--execution-mode=MODEMode: plan, apply, or agent (default: agent)
--secret-scan=MODESecret scanning: none, warn, block (default: warn)
--phase1-timeout=NPhase 1 timeout in seconds (default: 300)
--phase2-timeout=NPhase 2 timeout in seconds (default: 600)
--phase3-timeout=NPhase 3 timeout in seconds (default: 300)
--jsonOutput results as JSON
--verbose, -vVerbose output
--debug, -dDebug output (implies verbose)

ru dep-update

FlagDescription
--dry-runShow what would be updated (no changes)
--manager=NAMEOnly update deps for specific manager (npm, pip, cargo, etc.)
--include=PATTERNOnly update deps matching regex pattern
--exclude=PATTERNSkip deps matching regex pattern
--majorInclude major version updates (default: skip major)
--test-cmd=CMDCustom test command (overrides auto-detection)
--max-fix-attempts=NMax fix iterations per dependency (default: 5)
--no-pushCommit changes but don't push to remote
--repo=PATHProcess single repo only (default: all repos)
--agent=TYPEAgent type: claude (default), codex, gemini
--timeout=SECPer-repo timeout in seconds (default: 900)

โš™๏ธ Configuration

ru uses XDG Base Directory Specification for configuration.

Directory Structure

~/.config/ru/
โ”œโ”€โ”€ config                    # Main configuration file
โ””โ”€โ”€ repos.d/
    โ”œโ”€โ”€ public.txt            # Public repositories
    โ””โ”€โ”€ private.txt           # Private repositories

~/.cache/ru/
โ””โ”€โ”€ (runtime cache)

~/.local/state/ru/
โ”œโ”€โ”€ logs/
โ”‚   โ”œโ”€โ”€ 2025-01-03/
โ”‚   โ”‚   โ”œโ”€โ”€ run.log           # Main run log
โ”‚   โ”‚   โ””โ”€โ”€ repos/
โ”‚   โ”‚       โ”œโ”€โ”€ mcp_agent_mail.log
โ”‚   โ”‚       โ””โ”€โ”€ beads_viewer.log
โ”‚   โ””โ”€โ”€ latest -> 2025-01-03  # Symlink to latest run
โ””โ”€โ”€ archived/                 # Orphan repos (from ru prune)

Configuration File

# ~/.config/ru/config

# Base directory for repositories
PROJECTS_DIR=/data/projects

# Directory layout: flat | owner-repo | full
#   flat:       $PROJECTS_DIR/repo
#   owner-repo: $PROJECTS_DIR/owner/repo
#   full:       $PROJECTS_DIR/github.com/owner/repo
LAYOUT=flat

# Update strategy: ff-only | rebase | merge
UPDATE_STRATEGY=ff-only

# Auto-stash local changes before pull
AUTOSTASH=false

# Parallel operations (1 = serial)
PARALLEL=1

# Network timeout in seconds (for slow connections)
TIMEOUT=30

# Check for ru updates on run
CHECK_UPDATES=false

Configuration Resolution

Priority (highest to lowest):

  1. Command-line arguments (--dir, --rebase, etc.)
  2. Environment variables (RU_PROJECTS_DIR, RU_LAYOUT, etc.)
  3. Config file (~/.config/ru/config)
  4. Built-in defaults

๐Ÿ“ Repo List Format

Basic Format

# ~/.config/ru/repos.d/public.txt
# Lines starting with # are comments
# Empty lines are ignored

# Full URL
https://github.com/owner/repo

# Shorthand (assumes github.com)
owner/repo

# Pin to specific branch or tag
owner/repo@develop
owner/repo@v2.0.1

# Custom local directory name
owner/repo as custom-name

# Combined: branch + custom name
owner/repo@develop as dev-version

# SSH URL format
git@github.com:owner/repo.git

# SSH with custom name
git@github.com:owner/repo.git as myrepo

Advanced Repo Spec Syntax

The repo spec parser supports flexible combinations:

<url_or_shorthand>[@<branch>] [as <local_name>]
SpecURLBranchLocal Name
owner/repoowner/repo(default)repo
owner/repo@developowner/repodeveloprepo
owner/repo as myrepoowner/repo(default)myrepo
owner/repo@v2 as stableowner/repov2stable
git@github.com:o/r.gitgit@github.com:o/r.git(default)r
git@github.com:o/r.git as xgit@github.com:o/r.git(default)x

Notes:

  • The @branch specifier must come before as name
  • Branch names cannot contain / (use v2 not feature/v2)
  • The SSH @ in git@github.com is not confused with branch syntax
  • Custom names are case-sensitive and become the directory name

Supported URL Formats

All of these are equivalent:

https://github.com/owner/repo
https://github.com/owner/repo.git
git@github.com:owner/repo.git
github.com/owner/repo
owner/repo

Path Layout Examples

LayoutInputLocal Path
flatDicklesworthstone/mcp_agent_mail/data/projects/mcp_agent_mail
owner-repoDicklesworthstone/mcp_agent_mail/data/projects/Dicklesworthstone/mcp_agent_mail
fullDicklesworthstone/mcp_agent_mail/data/projects/github.com/Dicklesworthstone/mcp_agent_mail

Note: flat layout is the default for backwards compatibility with existing /data/projects structures. Use owner-repo if you have repos with the same name from different owners.

Path Collision Detection

When using flat layout, different owners may have repositories with the same name. ru automatically detects these collisions:

โš ๏ธ  Path collision detected:
    user1/myapp -> /data/projects/myapp
    user2/myapp -> /data/projects/myapp

Only the first repository will be synced to this path.
Consider using: ru config --set LAYOUT=owner-repo

How collision detection works:

  1. Before syncing, ru resolves all repo URLs to local paths
  2. If multiple repos resolve to the same path, a warning is shown
  3. The first occurrence in your config wins (subsequent duplicates are skipped)
  4. Using owner-repo or full layout eliminates collisions

Resolution options:

  • Use owner-repo layout: ru config --set LAYOUT=owner-repo
  • Use custom names: user2/myapp as myapp-user2
  • Remove the duplicate from your config

๐Ÿ”„ Sync Workflow

What ru sync Does

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        ru sync                                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Load repo      โ”‚
                    โ”‚  lists          โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  For each repo: โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ              โ–ผ              โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚ Missing? โ”‚   โ”‚ Exists?  โ”‚   โ”‚ Mismatch?โ”‚
       โ”‚ โ†’ Clone  โ”‚   โ”‚ โ†’ Pull   โ”‚   โ”‚ โ†’ Warn   โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Check status   โ”‚
                    โ”‚  (plumbing)     โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                   โ–ผ                   โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚  current   โ”‚     โ”‚  behind    โ”‚     โ”‚  diverged  โ”‚
  โ”‚  โ†’ Skip    โ”‚     โ”‚  โ†’ Pull    โ”‚     โ”‚  โ†’ Report  โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Write result   โ”‚
                    โ”‚  to log         โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Print summary  โ”‚
                    โ”‚  + exit code    โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Per-Repo Processing

For each repository in your lists:

  1. Parse URL โ€” Extract host, owner, repo name
  2. Compute local path โ€” Based on layout configuration
  3. Check existence โ€” Does the directory exist?
  4. If missing โ†’ Clone with gh repo clone
  5. If exists โ†’ Verify remote URL matches, then check status
  6. Get status โ€” Using git plumbing (ahead/behind/dirty)
  7. Take action โ€” Pull if behind, skip if current, warn if diverged
  8. Log result โ€” Per-repo log file + NDJSON result

Parallel Sync

When syncing many repositories, parallel execution can significantly reduce total sync time:

# Sync 4 repos at a time
ru sync --parallel 4

# Or use the short form
ru sync -j 8

How it works:

  1. Worker pool โ€” ru spawns N worker processes (specified by --parallel or -j)
  2. Job queue โ€” Repositories are distributed among workers as they become available
  3. Portable coordination โ€” Directory-based locking prevents race conditions in shared resources
  4. Aggregated results โ€” All worker results are collected and reported in a unified summary
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     ru sync --parallel 4                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
               โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
               โ–ผ              โ–ผ              โ–ผ              โ–ผ
        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
        โ”‚ Worker 1 โ”‚   โ”‚ Worker 2 โ”‚   โ”‚ Worker 3 โ”‚   โ”‚ Worker 4 โ”‚
        โ”‚ repo A   โ”‚   โ”‚ repo B   โ”‚   โ”‚ repo C   โ”‚   โ”‚ repo D   โ”‚
        โ”‚ repo E   โ”‚   โ”‚ repo F   โ”‚   โ”‚ repo G   โ”‚   โ”‚ repo H   โ”‚
        โ”‚   ...    โ”‚   โ”‚   ...    โ”‚   โ”‚   ...    โ”‚   โ”‚   ...    โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ”‚              โ”‚              โ”‚              โ”‚
               โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Unified        โ”‚
                    โ”‚  Summary        โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Configuration:

# Set default parallelism in config
ru config --set PARALLEL=4

# Or use environment variable
export RU_PARALLEL=4

Requirements:

  • None (portable locking is built in)

Network Timeout Tuning

For slow or unreliable networks, ru provides timeout configuration to prevent hangs:

# Command-line override
ru sync --timeout 60

# Config file
# ~/.config/ru/config
TIMEOUT=60

# Environment variable
export RU_TIMEOUT=60

Advanced tuning via git environment:

VariableDefaultPurpose
GIT_TIMEOUT30Overall network timeout in seconds
GIT_LOW_SPEED_LIMIT1000Abort if transfer falls below this bytes/second
# For very slow connections
GIT_TIMEOUT=120 GIT_LOW_SPEED_LIMIT=100 ru sync

Timeout error detection:

ru automatically recognizes timeout-related errors:

  • "RPC failed"
  • "timed out"
  • "remote end hung up unexpectedly"
  • "transfer rate too slow"

When a timeout is detected, the conflict resolution output provides retry suggestions.

Resuming Interrupted Syncs

If a sync is interrupted (Ctrl+C, network failure, etc.), ru saves progress state and can resume where it left off:

# Start a large sync
ru sync
# ^C (interrupted)
# Exit code: 5

# Resume from where you left off
ru sync --resume

# Or discard state and start fresh
ru sync --restart

How state tracking works:

  1. State file โ€” Progress is saved to ~/.local/state/ru/sync_state.json
  2. Atomic updates โ€” State is updated after each repo completes
  3. Safe resume โ€” On --resume, already-completed repos are skipped
  4. Clean restart โ€” --restart clears state and processes all repos fresh

State file contents:

{
  "started_at": "2025-01-03T14:30:00Z",
  "repos_completed": ["repo1", "repo2", "repo3"],
  "repos_pending": ["repo4", "repo5", "..."],
  "last_repo": "repo3",
  "interrupted": true
}

Best practices:

  • Use --resume when you want to continue after an interruption
  • Use --restart when the repo list has changed significantly
  • In CI, prefer --restart to ensure consistent runs

๐Ÿ”ฌ Git Status Detection

ru uses git plumbing commands for reliable status detection, never parsing human-readable output.

Why Plumbing Matters

Fragile approach (what other tools do):

# This breaks with non-English locales!
if git pull 2>&1 | grep -q "Already up to date"; then
    echo "Current"
fi

Robust approach (what ru does):

# Works regardless of locale or git version
read -r ahead behind < <(git rev-list --left-right --count HEAD...@{u})
if [[ "$ahead" -eq 0 && "$behind" -eq 0 ]]; then
    echo "Current"
fi

Status States

StateAheadBehindMeaning
current00Fully synchronized
behind0>0Remote has new commits
ahead>00Local has unpushed commits
diverged>0>0Both have new commits
dirtyโ€”โ€”Uncommitted local changes
no_upstreamโ€”โ€”No tracking branch set

Dirty Detection

# Empty output = clean working tree
if [[ -n $(git -C "$repo_path" status --porcelain 2>/dev/null) ]]; then
    dirty="true"
fi

๐Ÿšจ Conflict Resolution

When ru encounters issues, it provides actionable resolution commands.

Example Output

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚  โš ๏ธ  Repositories Needing Attention                         โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

1. mcp_agent_mail
   Path:   /data/projects/mcp_agent_mail
   Branch: main
   Issue:  Dirty working tree (3 files modified)
   Log:    ~/.local/state/ru/logs/2025-01-03/repos/mcp_agent_mail.log

   Resolution options:
     a) Stash and pull:
        cd /data/projects/mcp_agent_mail && git stash && git pull && git stash pop

     b) Commit your changes:
        cd /data/projects/mcp_agent_mail && git add . && git commit -m "WIP"

     c) Discard local changes (DESTRUCTIVE):
        cd /data/projects/mcp_agent_mail && git checkout . && git clean -fd

2. beads_viewer
   Path:   /data/projects/beads_viewer
   Branch: main
   Issue:  Diverged (2 ahead, 5 behind)

   Resolution options:
     a) Rebase your changes:
        cd /data/projects/beads_viewer && git pull --rebase

     b) Merge (creates merge commit):
        cd /data/projects/beads_viewer && git pull --no-ff

     c) Push your changes first (if intentional):
        cd /data/projects/beads_viewer && git push

Common Issues and Fixes

IssueCauseResolution
Dirty working treeUncommitted changesStash, commit, or discard
DivergedLocal and remote both have commitsRebase, merge, or push
No upstreamBranch doesn't track remotegit branch --set-upstream-to=origin/main
Remote mismatchDifferent repo at same pathRemove directory or update list
Auth failedgh not authenticatedgh auth login or set GH_TOKEN

๐Ÿงน Managing Orphan Repositories

Over time, your projects directory may accumulate "orphan" repositoriesโ€”directories that exist locally but aren't in your configuration. The ru prune command helps identify and manage these.

What is an Orphan?

An orphan is a git repository in your projects directory that:

  • Exists as a valid git repository (has .git directory)
  • Is NOT listed in any of your repos.d/*.txt configuration files
  • May have been manually cloned, removed from config, or leftover from experiments

Detection

# List orphan repositories (dry run)
ru prune

# Output:
# Found 3 orphan repositories:
#   /data/projects/old-experiment
#   /data/projects/manually-cloned
#   /data/projects/removed-from-config
#
# Use --archive to move to archive, or --delete to remove

Archive Mode

Move orphans to a timestamped archive directory instead of deleting:

ru prune --archive

# Orphans moved to:
# ~/.local/state/ru/archived/old-experiment-2025-01-03-143022/
# ~/.local/state/ru/archived/manually-cloned-2025-01-03-143022/

Benefits of archiving:

  • Non-destructiveโ€”repos can be recovered
  • Timestamped for audit trail
  • Clears your projects directory without losing work

Delete Mode

Permanently remove orphan repositories:

# Interactive (asks for confirmation)
ru prune --delete

# Non-interactive (CI-safe, no prompts)
ru --non-interactive prune --delete

Safety measures:

  • Interactive mode requires explicit confirmation
  • --archive and --delete are mutually exclusive
  • Only git repositories are considered (plain directories ignored)

Layout Awareness

Prune respects your configured layout mode:

LayoutScan DepthExample Orphan Path
flat1 level/data/projects/orphan
owner-repo2 levels/data/projects/owner/orphan
full3 levels/data/projects/github.com/owner/orphan

Custom Names

Prune correctly handles custom-named repositories:

# In repos.d/public.txt:
# owner/long-repository-name as shortname

# The directory 'shortname' is NOT an orphan
# because it matches the custom name in config

๐Ÿ“ค Output Modes

Default: Human-Readable (stderr)

Progress and results go to stderr, paths to stdout:

ru sync
# stderr: โ†’ Processing 1/47: mcp_agent_mail...
# stderr: โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
# stdout: /data/projects/mcp_agent_mail
# stdout: /data/projects/beads_viewer

JSON Mode: --json

Structured output on stdout for scripting. All JSON commands share a normalized envelope:

{
  "generated_at": "2026-02-09T18:06:35Z",
  "version": "1.2.1",
  "output_format": "json",
  "command": "<command-name>",
  "data": { ... },
  "_meta": { ... }
}
Envelope fieldTypeDescription
generated_atstringISO-8601 UTC timestamp
versionstringru version
output_formatstring"json" or "toon"
commandstringSubcommand name (sync, status, list)
dataobjectCommand-specific payload (see below)
_metaobjectOptional. Timing, exit code, diagnostics

sync โ€” data contains config, summary, and repos; _meta includes duration_seconds and exit_code:

ru sync --json 2>/dev/null
{
  "generated_at": "2026-02-09T18:08:23Z",
  "version": "1.2.1",
  "output_format": "json",
  "command": "sync",
  "data": {
    "config": {
      "projects_dir": "/data/projects",
      "layout": "flat",
      "update_strategy": "ff-only"
    },
    "summary": {
      "total": 47,
      "cloned": 8,
      "updated": 34,
      "current": 3,
      "conflicts": 2,
      "failed": 0
    },
    "repos": [
      {
        "name": "mcp_agent_mail",
        "path": "/data/projects/mcp_agent_mail",
        "action": "pull",
        "status": "updated",
        "duration": 2
      }
    ]
  },
  "_meta": {
    "duration_seconds": 154,
    "exit_code": 2
  }
}

status โ€” data contains total and repos:

ru status --json 2>/dev/null
{
  "generated_at": "2026-02-09T18:07:09Z",
  "version": "1.2.1",
  "output_format": "json",
  "command": "status",
  "data": {
    "total": 47,
    "repos": [
      {
        "repo": "owner/repo",
        "path": "/data/projects/repo",
        "status": "current",
        "branch": "main",
        "ahead": 0,
        "behind": 0,
        "dirty": false,
        "mismatch": false
      }
    ]
  }
}

list โ€” data contains total and repos:

ru list --json 2>/dev/null
{
  "generated_at": "2026-02-09T18:06:35Z",
  "version": "1.2.1",
  "output_format": "json",
  "command": "list",
  "data": {
    "total": 47,
    "repos": [
      {
        "repo": "owner/repo",
        "url": "owner/repo",
        "path": "/data/projects/repo"
      }
    ]
  }
}

Parse with jq:

# Get paths of all cloned repos
ru sync --json 2>/dev/null | jq -r '.data.repos[] | select(.action=="clone") | .path'

# Get count of failures
ru sync --json 2>/dev/null | jq '.data.summary.failed'

# Get all dirty repos from status
ru status --json 2>/dev/null | jq -r '.data.repos[] | select(.dirty) | .repo'

Quiet Mode: --quiet

Only errors to stderr, still outputs paths to stdout:

ru sync --quiet
# Only shows errors, no progress

๐Ÿค– AI-Assisted Code Review

ru includes a powerful review orchestration system for managing AI-assisted code review across your repositories. It discovers open issues and pull requests, prioritizes them intelligently, and coordinates review sessions using Claude Code or other AI assistants.

The Review Workflow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                       ru review --plan                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ               โ–ผ               โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚  GraphQL โ”‚    โ”‚  Parse   โ”‚    โ”‚  Cache   โ”‚
       โ”‚  Batch   โ”‚    โ”‚  Work    โ”‚    โ”‚  Digest  โ”‚
       โ”‚  Query   โ”‚    โ”‚  Items   โ”‚    โ”‚  Check   โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚               โ”‚               โ”‚
              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Priority       โ”‚
                    โ”‚  Scoring        โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ              โ–ผ              โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚ CRITICAL โ”‚   โ”‚   HIGH   โ”‚   โ”‚  NORMAL  โ”‚
       โ”‚ security โ”‚   โ”‚   bugs   โ”‚   โ”‚ features โ”‚
       โ”‚ bugs >60dโ”‚   โ”‚ bugs >30dโ”‚   โ”‚          โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Session Driver โ”‚
                    โ”‚  (tmux/ntm)     โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                   โ–ผ                   โ–ผ
  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
  โ”‚ Session 1  โ”‚     โ”‚ Session 2  โ”‚     โ”‚ Session 3  โ”‚
  โ”‚ (worktree) โ”‚     โ”‚ (worktree) โ”‚     โ”‚ (worktree) โ”‚
  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Two-Phase Review: Plan then Apply

Phase 1: Discovery (--plan)

  • Queries GitHub for open issues and PRs across all repos
  • Scores items by priority using label analysis and age
  • Creates isolated git worktrees for safe review
  • Spawns Claude Code sessions in terminal multiplexer
  • Checkpoints progress for interruption recovery

Phase 2: Application (--apply)

  • Reviews proposed changes from discovery phase
  • Runs quality gates (ShellCheck, tests, lint)
  • Optionally pushes approved changes (--push)
  • Archives completed work items
# Discover and plan reviews
ru review --plan

# After reviewing AI suggestions, apply approved changes
ru review --apply --push

Priority Scoring Algorithm

ru uses a multi-factor scoring system to prioritize work items intelligently:

FactorPointsLogic
Type0-20PRs: +20, Issues: +10, Draft PRs: -15
Labels0-50security/critical: +50, bug/urgent: +30, enhancement: +10
Age (bugs)0-50>60 days: +50, >30 days: +30, >14 days: +15
Age (features)-10 to 0Very old (>180 days): -10 (likely stale)
Recency0-15Updated <3 days: +15, <7 days: +10
Staleness-20Recently reviewed: -20

Priority levels:

ScoreLevelMeaning
โ‰ฅ150CRITICALSecurity issues, long-standing bugs
โ‰ฅ100HIGHBugs, urgent items
โ‰ฅ50NORMALRegular features and issues
<50LOWBacklog items

Session Drivers

ru supports multiple backends for managing review sessions:

DriverDescriptionBest For
autoAutomatically detect best availableDefault choice
ntmNamed Tmux Manager integrationMulti-agent workflows
localDirect tmux sessionsSimple setups
# Use specific driver
ru review --mode=ntm --plan

# Parallel sessions
ru review -j 4 --plan

ntm (Named Tmux Manager) Integration

ntm is a tmux session orchestration tool that provides a robot mode API for automated session management. When available, ru uses ntm for enhanced capabilities:

Robot Mode API Functions:

FunctionPurpose
ntm --robot-spawnCreate a Claude Code session in a new tmux pane
ntm --robot-sendSend prompts with chunking for long messages
ntm --robot-waitBlock until session completes with timeout
ntm --robot-activityQuery real-time session state (idle/typing/thinking)
ntm --robot-statusGet status of all managed sessions
ntm --robot-interruptSend Ctrl+C to interrupt long operations

Session Lifecycle:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    ntm Session Lifecycle                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚
        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ntm_spawn_    โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Creates tmux session with Claude Code
โ”‚ session()     โ”‚         running in isolated worktree
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚
        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ntm_send_     โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Delivers prompt (auto-chunks if >4KB)
โ”‚ prompt()      โ”‚         Handles message delivery confirmation
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚
        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ntm_wait_     โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Polls for completion with timeout
โ”‚ completion()  โ”‚         Returns JSON with status/duration
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
        โ”‚
        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ntm_kill_     โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Cleans up session when done
โ”‚ session()     โ”‚         Idempotentโ€”safe to call on gone sessions
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Activity State Mapping:

ntm reports granular activity states that ru maps to unified states:

ntm Stateru Unified StateMeaning
IDLEidleSession waiting for input
TYPINGactiveUser/agent is typing
THINKINGactiveAI processing
TOOL_USEactiveExecuting tool calls
COMPLETEdoneSession finished
ERRORerrorSomething went wrong

Fallback Behavior:

When ntm is not available, ru falls back to the local driver which uses raw tmux commands. The local driver provides the same core functionality but without:

  • Message chunking for long prompts
  • Activity state detection
  • Delivery confirmation

Claude Code Integration

ru orchestrates AI review sessions using Claude Code with stream-json output parsing:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Claude Code Session                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  stream-json    โ”‚
                    โ”‚  NDJSON output  โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                   โ–ผ                   โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  system  โ”‚       โ”‚ assistantโ”‚       โ”‚  result  โ”‚
   โ”‚  events  โ”‚       โ”‚  events  โ”‚       โ”‚  events  โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Event types parsed:

EventData Extracted
system.initSession ID, available tools, working directory
assistantTool uses, text responses, thinking
resultStatus, duration, cost (USD)

Interactive question detection:

When Claude Code uses AskUserQuestion, ru detects and surfaces these for human input:

# Session asks: "Should I refactor this function?"
# ru detects and shows:
#   [Session owner/repo] Question pending: "Should I refactor this function?"
#   Options: 1) Yes  2) No  3) Skip

Cost tracking:

Each session reports cost, enabling budget enforcement:

{"type":"result","status":"success","duration_ms":45000,"cost_usd":0.0234}

Git Worktree Isolation

Each review session operates in an isolated git worktree, ensuring:

  • Main working directory stays untouched
  • Multiple reviews can run in parallel
  • Changes can be discarded without affecting HEAD
  • Clean state for each AI session
~/.local/state/ru/worktrees/
โ””โ”€โ”€ 20250105-143022-1234/
    โ”œโ”€โ”€ owner_repo1/          # Isolated worktree
    โ”œโ”€โ”€ owner_repo2/          # Isolated worktree
    โ””โ”€โ”€ mapping.json          # Worktree registry

Digest Caching

To avoid redundant API calls and repeated reviews, ru caches repository digests:

~/.local/state/ru/review/
โ”œโ”€โ”€ digests/                  # Cached repo digests
โ”‚   โ”œโ”€โ”€ owner_repo.json       # Issues/PRs snapshot
โ”‚   โ””โ”€โ”€ ...
โ”œโ”€โ”€ state.json                # Current review state
โ”œโ”€โ”€ checkpoint.json           # Resumable checkpoint
โ””โ”€โ”€ results/                  # Review outcomes
    โ””โ”€โ”€ 20250105-143022/
        โ””โ”€โ”€ results.ndjson    # Per-item results

Cache invalidation:

# Invalidate specific repos
ru review --invalidate-cache=owner/repo1,owner/repo2

# Invalidate all caches
ru review --invalidate-cache=all

Cost Budgets

Control resource usage with budget constraints:

# Limit to 10 repos
ru review --max-repos=10 --plan

# Maximum 30 minutes runtime
ru review --max-runtime=30 --plan

# Skip recently reviewed items
ru review --skip-days=14 --plan

Review Analytics

View statistics on past reviews:

ru review --analytics

Metrics tracked:

~/.local/state/ru/metrics/
โ”œโ”€โ”€ 2025-01.json              # Monthly aggregates
โ”œโ”€โ”€ 2025-02.json
โ””โ”€โ”€ decisions.jsonl           # Per-decision audit log

Monthly metrics:

MetricDescription
reviews.totalTotal review sessions started
reviews.repos_reviewedUnique repositories reviewed
reviews.issues_processedIssues examined by AI
reviews.issues_resolvedIssues closed or fixed
reviews.questions_askedAI questions to human
reviews.questions_answeredHuman responses
timing.total_duration_minutesCumulative review time
timing.avg_session_minutesAverage session length
outcomes.changes_appliedCommits made
outcomes.changes_rejectedChanges discarded

Decision logging:

Every significant action is logged to decisions.jsonl for auditability:

{"timestamp":"2025-01-05T14:30:00Z","repo":"owner/repo","issue":42,"action":"close","reason":"Fixed by commit abc123","actor":"ai"}
{"timestamp":"2025-01-05T14:31:00Z","repo":"owner/repo","issue":43,"action":"label","label":"wontfix","actor":"human"}

Analytics dashboard:

ru review --analytics

Shows:

  • Total reviews completed
  • Average resolution time
  • Most active repositories
  • Issue type distribution
  • Review velocity trends
  • Success/rejection ratios

GitHub Actions Execution

After review sessions complete, ru can execute planned GitHub actions from the review plan:

# Actions are proposed during --plan, executed during --apply
ru review --apply

Supported actions:

ActionCommandDescription
commentgh issue commentAdd comment to issue/PR
closegh issue closeClose an issue
labelgh issue edit --add-labelAdd labels to issue

Idempotent execution:

Actions are logged and deduplicated to prevent repeated execution:

~/.local/state/ru/review/gh_actions.jsonl
{"timestamp":"2025-01-05T14:30:00Z","repo":"owner/repo","target":"issue/42","action":"close","status":"success"}
{"timestamp":"2025-01-05T14:30:01Z","repo":"owner/repo","target":"issue/43","action":"comment","status":"success"}

If a session is restarted, already-executed actions are skipped automatically.

Action parsing:

Actions are extracted from review plan files with canonicalization:

owner/repo#42    โ†’ repo: owner/repo, type: issue, number: 42
owner/repo/pull/5 โ†’ repo: owner/repo, type: pr, number: 5

Review Policies

Configure review behavior with policy files:

# ~/.config/ru/review/policy.json
{
  "REVIEW_LINT_REQUIRED": true,
  "REVIEW_SECRET_SCAN": true,
  "REVIEW_ALLOW_PUSH": false,
  "REVIEW_REQUIRE_APPROVAL": true,
  "REVIEW_SKIP_PRS": false,
  "REVIEW_DEEP_MODE": false
}
PolicyDefaultDescription
REVIEW_LINT_REQUIREDtrueRun linters before applying
REVIEW_SECRET_SCANtrueScan for secrets in changes
REVIEW_ALLOW_PUSHfalseAllow pushing without --push
REVIEW_REQUIRE_APPROVALtrueRequire explicit approval
REVIEW_SKIP_PRSfalseSkip PRs (issues only)
REVIEW_DEEP_MODEfalseEnable deep analysis

GraphQL Batch Querying

ru uses efficient GraphQL alias batching to minimize API calls when discovering work items across many repositories:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   GraphQL Batch Query                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  repo0:  โ”‚         โ”‚  repo1:  โ”‚         โ”‚  repo24: โ”‚
   โ”‚  issues  โ”‚   ...   โ”‚  issues  โ”‚   ...   โ”‚  issues  โ”‚
   โ”‚  + PRs   โ”‚         โ”‚  + PRs   โ”‚         โ”‚  + PRs   โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                    โ”‚                    โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Single API     โ”‚
                    โ”‚  Response       โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

How it works:

  1. Alias batching โ€” Up to 25 repositories per GraphQL query using named aliases (repo0, repo1, ...)
  2. Parallel field fetching โ€” Issues, PRs, and labels retrieved in one request per batch
  3. Automatic chunking โ€” Large repo lists split into optimal batch sizes
  4. Retry on failure โ€” Failed batches retried with exponential backoff

Efficiency comparison:

Approach100 ReposAPI Calls
Individual REST100 repos ร— 2 (issues + PRs)200 calls
GraphQL batched100 repos รท 25 per batch4 calls

Rate-Limit Governor

ru includes an adaptive parallelism governor that adjusts concurrency based on GitHub API rate limits and error patterns:

# The governor tracks:
#   - GitHub API remaining quota
#   - Model rate limit (429) responses
#   - Error frequency within sliding window
#   - Circuit breaker state

Adaptive behavior:

ConditionAction
GitHub remaining < 100Reduce parallelism to 1
GitHub remaining < 500Reduce parallelism by 50%
Model 429 detectedPause new sessions for 60s
Error rate > 50% (5min window)Open circuit breaker
Circuit open > 2 minutes + no errorsClose circuit breaker

Circuit breaker pattern:

Normal Operation โ†’ Errors Spike โ†’ Circuit OPEN โ†’ Cool-down โ†’ Half-Open โ†’ Test โ†’ CLOSED
                       โ†‘                              โ”‚
                       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ More Errors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The governor runs as a background monitor during review sessions, checking rate limits every 30 seconds and adjusting REVIEW_PARALLEL dynamically.

Global Backoff Coordination

When running parallel agent-sweep operations, ru uses a shared backoff mechanism to coordinate pause signals across all worker processes:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Global Backoff Coordination                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Worker detects โ”‚
                    โ”‚  rate limit 429 โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚ agent_sweep_    โ”‚โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Acquire lock
                    โ”‚ backoff_trigger โ”‚         Write pause_until + reason
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         Release lock
                             โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ–ผ                        โ–ผ                        โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚Worker 1 โ”‚            โ”‚Worker 2 โ”‚            โ”‚Worker 3 โ”‚
โ”‚ check   โ”‚            โ”‚ check   โ”‚            โ”‚ check   โ”‚
โ”‚backoff.โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€state   โ”‚
โ”‚state    โ”‚            โ”‚    โ”‚    โ”‚            โ”‚         โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜            โ””โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”˜            โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”˜
     โ”‚                      โ”‚                      โ”‚
     โ–ผ                      โ–ผ                      โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚          All workers sleep until pause_until expires            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Backoff behavior:

TriggerInitial DelayBehavior
First rate limit30 secondsWait, then resume
Repeated rate limitPrevious ร— 2Exponential backoff
Maximum delay10 minutesCapped to prevent infinite wait
Jitterยฑ25%Prevents thundering herd

State file format (backoff.state):

{
  "reason": "rate_limited",
  "pause_until": 1704307200,
  "delay": 60
}

Workers check this file before starting work on each repo. If pause_until is in the future, they sleep until it expires. This ensures all workers respect a global cooldown without requiring inter-process communication.

Quality Gates

Before applying changes, ru runs automated quality gates to catch issues early:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Quality Gates                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚   Test   โ”‚         โ”‚   Lint   โ”‚         โ”‚  Secret  โ”‚
   โ”‚   Gate   โ”‚         โ”‚   Gate   โ”‚         โ”‚   Scan   โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                    โ”‚                    โ”‚
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  Auto-   โ”‚         โ”‚  Auto-   โ”‚         โ”‚  Pattern โ”‚
   โ”‚  detect  โ”‚         โ”‚  detect  โ”‚         โ”‚  Match   โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Auto-detection by project type:

Project TypeTest CommandLint Command
npm/yarnnpm test / yarn testnpm run lint / eslint
Cargo (Rust)cargo testcargo clippy
Gogo test ./...golangci-lint run
Pythonpytest / python -m pytestruff check / flake8
Makefilemake testmake lint
Shell scripts(none)shellcheck *.sh

Secret scanning patterns:

The secret scanner checks for accidentally committed credentials:

  • API keys and tokens (sk-, ghp_, xox, AKIA)
  • Private keys (BEGIN RSA PRIVATE KEY, BEGIN OPENSSH PRIVATE KEY)
  • Connection strings and passwords
  • Cloud provider credentials
# Run quality gates on a specific directory
ru review --apply  # Gates run automatically before pushing

# Skip gates (not recommended)
REVIEW_LINT_REQUIRED=false REVIEW_SECRET_SCAN=false ru review --apply

Session Health Monitoring

ru monitors review sessions for external prompts and blocking conditions:

Detected wait reasons:

PatternRisk LevelDescription
Password:HighCredential prompt
Enter passphraseHighSSH key passphrase
Enter OTPHighTwo-factor authentication
CONFLICT.*MergeMediumGit merge conflict
(yes/no)MediumHost key verification
Please enter.*commitLowGit commit message prompt

Risk classification:

  • High risk โ€” Credentials, authentication, permissions (requires human intervention)
  • Medium risk โ€” Merge conflicts, host verification (may need review)
  • Low risk โ€” Informational prompts (often auto-resolvable)

When a blocking prompt is detected, the session is marked as waiting and the reason is logged for review:

ru review --status
# Shows: Session owner/repo waiting on "Password:" (high risk)

๐Ÿค– Agent-Driven Sweep

The ru agent-sweep command orchestrates AI coding agents (via Claude Code) to automatically process repositories with uncommitted changes. It spawns isolated tmux sessions for each repository, monitors agent progress, and aggregates results.

The Agent Sweep Workflow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                       ru agent-sweep                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Load dirty     โ”‚
                    โ”‚  repositories   โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ              โ–ผ              โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚ Preflightโ”‚   โ”‚ Preflightโ”‚   โ”‚ Preflightโ”‚
       โ”‚  Checks  โ”‚   โ”‚  Checks  โ”‚   โ”‚  Checks  โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚              โ”‚              โ”‚
              โ–ผ              โ–ผ              โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚  Spawn   โ”‚   โ”‚  Spawn   โ”‚   โ”‚  Spawn   โ”‚
       โ”‚  Agent   โ”‚   โ”‚  Agent   โ”‚   โ”‚  Agent   โ”‚
       โ”‚ (tmux)   โ”‚   โ”‚ (tmux)   โ”‚   โ”‚ (tmux)   โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚              โ”‚              โ”‚
              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Monitor &      โ”‚
                    โ”‚  Aggregate      โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                             โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ              โ–ผ              โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚  Secret  โ”‚   โ”‚  Quality โ”‚   โ”‚   Push   โ”‚
       โ”‚   Scan   โ”‚   โ”‚  Gates   โ”‚   โ”‚  (opt)   โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Agent Sweep Prerequisites

Before using agent-sweep, ensure you have:

RequirementDescription
ntmNamed Tmux Manager for session orchestration
tmuxTerminal multiplexer for isolated sessions
Claude CodeAI coding agent (claude command)

Verify installation:

# Check ntm
ntm --version

# Check tmux
tmux -V

# Check Claude Code
claude --version

Basic Usage

# Process all repos with uncommitted changes
ru agent-sweep

# Dry run - preview what would be processed
ru agent-sweep --dry-run

# Process 4 repos in parallel
ru agent-sweep -j4

# Filter to specific repos
ru agent-sweep --repos="myproject*"

# Include release step after commit
ru agent-sweep --with-release

# Resume an interrupted sweep
ru agent-sweep --resume

# Start fresh (clear previous state)
ru agent-sweep --restart

# Verbose output with file logging
ru agent-sweep --verbose

# Debug mode for troubleshooting
ru agent-sweep --debug

Three-Phase Agent Workflow

Each repository goes through a structured three-phase workflow that separates planning from execution:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Three-Phase Workflow                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ               โ–ผ               โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚  PHASE 1 โ”‚    โ”‚  PHASE 2 โ”‚    โ”‚  PHASE 3 โ”‚
       โ”‚ Planning โ”‚โ”€โ”€โ”€โ–ถโ”‚  Commit  โ”‚โ”€โ”€โ”€โ–ถโ”‚ Release  โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚               โ”‚               โ”‚
              โ–ผ               โ–ผ               โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚  Analyze โ”‚    โ”‚  Execute โ”‚    โ”‚  Create  โ”‚
       โ”‚  changes โ”‚    โ”‚   plan   โ”‚    โ”‚   tag/   โ”‚
       โ”‚  + plan  โ”‚    โ”‚ + commit โ”‚    โ”‚  release โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Phase 1: Planning (--phase1-timeout, default 300s)

  • Claude Code analyzes the uncommitted changes in the repository
  • Determines which files should be staged (respecting denylist)
  • Generates a structured commit message following conventional commit format
  • Outputs a JSON plan with files, message, and rationale

Phase 2: Commit (--phase2-timeout, default 600s)

  • Validates the plan from Phase 1 (file existence, denylist compliance)
  • Stages approved files with git add
  • Creates the commit with the generated message
  • Runs quality gates (linting, secret scan)
  • Optionally pushes to remote

Phase 3: Release (--phase3-timeout, default 300s, requires --with-release)

  • Analyzes commit history since last tag
  • Determines appropriate version bump (patch/minor/major)
  • Creates git tag and optionally GitHub release
  • Respects per-repo release strategy configuration

Execution modes:

ModeBehavior
--execution-mode=agentFull AI-driven workflow (default)
--execution-mode=planPhase 1 only: generate plan, stop
--execution-mode=applyPhase 2+3: execute existing plan

Why separate phases?

  • Auditability: Each phase produces inspectable output
  • Recovery: Can resume from any phase after interruption
  • Control: Can run planning across all repos, then review before applying
  • Safety: Validation happens between phases, catching issues early

Preflight Checks

Before spawning an agent, each repository undergoes preflight validation:

CheckReasonSkip Reason
Is git repositoryBasic requirementnot_a_git_repo
Git email configuredFor commitsgit_email_not_configured
Git name configuredFor commitsgit_name_not_configured
Not a shallow cloneSome ops may failshallow_clone
No dirty submodulesAvoid conflictsdirty_submodules
No rebase in progressUnsafe staterebase_in_progress
No merge in progressUnsafe statemerge_in_progress
No cherry-pick in progressUnsafe statecherry_pick_in_progress
Not detached HEADNeed branch refdetached_HEAD
Has upstream branchFor pushno_upstream_branch
Not divergedWould need rebasediverged_from_upstream
No unmerged pathsMerge conflicts existunmerged_paths
git diff --check cleanWhitespace/conflict markersdiff_check_failed
Untracked files < 1000Too many untrackedtoo_many_untracked_files

Handling preflight failures:

# View why a repo was skipped (JSON output)
ru agent-sweep --json 2>/dev/null | jq '.data.repos[] | select(.status == "skipped")'

# Fix common issues
cd /data/projects/problematic-repo
git config user.email "you@example.com"
git config user.name "Your Name"
git rebase --abort  # If rebase in progress

Security Guardrails

Agent-sweep includes multiple security layers to prevent accidents:

1. File Denylist

Certain files are never committed, regardless of agent output:

CategoryPatterns
Secrets.env, .env.*, *.pem, *.key, id_rsa, credentials.json, secrets.json
Build artifactsnode_modules, __pycache__, dist, build, .next, target, vendor
Logs/temp*.log, *.tmp, *.swp, .DS_Store
IDE files.idea, .vscode, *.iml

Extend the denylist:

# Via environment variable
export AGENT_SWEEP_DENYLIST_EXTRA="*.bak *.orig internal/*"

# Via per-repo config (.ru/agent-sweep.conf)
echo 'DENYLIST_EXTRA="*.backup proprietary/*"' >> .ru/agent-sweep.conf

2. Secret Scanning

Before any push, files are scanned for secrets:

# Modes
--secret-scan=none   # Disable scanning
--secret-scan=warn   # Warn but continue (default)
--secret-scan=block  # Block push on detection

Patterns detected:

  • API keys (sk-, ghp_, xox, AKIA)
  • Private keys (BEGIN RSA PRIVATE KEY, BEGIN OPENSSH PRIVATE KEY)
  • Connection strings and passwords
  • Cloud provider credentials

3. File Size Limits

Large or binary files are blocked from commits to prevent repository bloat. The default limit is 1MB, configurable via AGENT_SWEEP_MAX_FILE_MB or per-repo config.

4. Plan Validation

Before executing any commit or release plan, ru validates the AI-generated JSON output:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Plan Validation Pipeline                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚  JSON    โ”‚         โ”‚  Schema  โ”‚         โ”‚  File    โ”‚
   โ”‚  Parse   โ”‚         โ”‚  Verify  โ”‚         โ”‚  Verify  โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                    โ”‚                    โ”‚
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ Extract  โ”‚         โ”‚ Required โ”‚         โ”‚  Files   โ”‚
   โ”‚ between  โ”‚         โ”‚  fields  โ”‚         โ”‚  exist?  โ”‚
   โ”‚ markers  โ”‚         โ”‚ present? โ”‚         โ”‚ Allowed? โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Validation checks for commit plans:

CheckValidationFailure Behavior
JSON structureValid JSON between ---PLAN-START--- and ---PLAN-END--- markersReject plan
Required fieldsfiles array, commit_message stringReject plan
File existenceEach file in files[] exists in working treeWarn, filter out missing
Denylist complianceNo file matches denylist patternsBlock file, continue with rest
Commit message formatNon-empty, reasonable lengthReject if empty

Validation checks for release plans:

CheckValidationFailure Behavior
Version formatMatches semver (X.Y.Z)Reject plan
Tag availabilityTag doesn't already existReject plan
Release strategyMatches configured strategyAdjust to match
Changelog presenceRelease notes providedWarn, use default

Why structured output with markers?

AI output often includes explanatory text before/after the JSON. The marker-based extraction (---PLAN-START--- / ---PLAN-END---) reliably isolates the machine-readable portion without complex parsing.

Per-Repository Configuration

Customize agent-sweep behavior per repository:

Option 1: YAML config (.ru-agent.yml) โ€” Recommended:

# ~/.../your-repo/.ru-agent.yml
agent_sweep:
  enabled: true
  max_file_size: 5242880  # 5MB
  extra_context: "This is a Python project using FastAPI"
  pre_hook: "make lint"
  post_hook: "make test"
  skip_phases: []
  denylist_extra:
    - "*.backup"
    - "internal/*"

Option 2: JSON config (.ru-agent.json):

{
  "agent_sweep": {
    "enabled": true,
    "max_file_size": 5242880,
    "denylist_extra": ["*.backup", "internal/*"]
  }
}

Option 3: Shell config for release strategy (.ru/agent-sweep.conf):

# ~/.../your-repo/.ru/agent-sweep.conf
# Only release strategy is configurable via shell config
AGENT_SWEEP_RELEASE_STRATEGY=auto  # never|auto|tag-only|gh-release

Option 4: User config (~/.config/ru/agent-sweep.d/<repo>.conf):

# ~/.config/ru/agent-sweep.d/my-repo.conf
# Per-repo overrides without modifying the repo itself
AGENT_SWEEP_RELEASE_STRATEGY=never

State Management

Agent-sweep tracks progress for resume capability:

~/.local/state/ru/
โ”œโ”€โ”€ agent-sweep/
โ”‚   โ”œโ”€โ”€ state.json            # Current sweep state
โ”‚   โ”œโ”€โ”€ results.ndjson        # Per-repo results
โ”‚   โ”œโ”€โ”€ completed_repos.txt   # Successfully completed repos
โ”‚   โ”œโ”€โ”€ instance.lock/        # Concurrent execution lock
โ”‚   โ”‚   โ””โ”€โ”€ pid               # Lock holder PID
โ”‚   โ”œโ”€โ”€ locks/                # Coordination locks
โ”‚   โ””โ”€โ”€ runs/<run-id>/        # Per-run artifacts
โ””โ”€โ”€ logs/
    โ””โ”€โ”€ YYYY-MM-DD/
        โ”œโ”€โ”€ agent_sweep.log   # Main log
        โ””โ”€โ”€ repos/
            โ””โ”€โ”€ <repo>.log    # Per-repo logs

State operations:

# View current state
cat ~/.local/state/ru/agent-sweep/state.json | jq .

# Resume interrupted sweep
ru agent-sweep --resume

# Clear state and start fresh
ru agent-sweep --restart

# View per-repo results
cat ~/.local/state/ru/agent-sweep/results.ndjson | jq -s .

Troubleshooting Agent Sweep

<details> <summary><strong>"ntm is not available"</strong></summary>

Cause: Named Tmux Manager not installed or not in PATH.

Fix:

# Install ntm (if available via package manager)
# Or add to PATH if installed elsewhere
export PATH="$HOME/.local/bin:$PATH"

# Verify
ntm --version
</details> <details> <summary><strong>"Another agent-sweep is already running"</strong></summary>

Cause: Lock file exists from previous run.

Fix:

# Check if actually running
ps aux | grep "agent-sweep"

# If stale, remove lock
rm -rf ~/.local/state/ru/agent-sweep/instance.lock
</details> <details> <summary><strong>Agent session hangs</strong></summary>

Cause: Agent waiting for input or slow network.

Fixes:

  1. Increase timeouts: --phase2-timeout=1200
  2. Attach to session: ru agent-sweep --attach-on-fail
  3. Keep sessions for debugging: --keep-sessions-on-fail
# List active sessions
tmux list-sessions

# Attach to specific session
tmux attach -t ru-sweep-myrepo
</details> <details> <summary><strong>"diverged_from_upstream" skip reason</strong></summary>

Cause: Local and remote branches have diverged.

Fix:

cd /data/projects/affected-repo
git fetch origin
git rebase origin/main  # Or merge
</details> <details> <summary><strong>Secret detected, push blocked</strong></summary>

Cause: Agent committed a file matching secret patterns.

Fixes:

  1. Remove the secret: git reset HEAD~1 && git checkout -- file
  2. If false positive, adjust denylist
  3. Use --secret-scan=warn to continue with warning
</details>

Exit Codes for Agent Sweep

CodeMeaning
0All repos processed successfully
1Some repos failed (agent error, timeout)
2Quality gate failures (secrets, tests)
3System error (ntm, tmux missing)
4Invalid arguments
5Interrupted (use --resume)

๐Ÿค– AI Sync

The ai-sync subcommand automatically commits uncommitted changes across repositories using AI agents. It scans for dirty repos, launches an AI agent per repo (via ntm), and the agent reads project context, reviews changes, creates logical commits, and pushes.

Prerequisites

  • ntm (Named Tmux Manager) - for session orchestration
  • claude-code, codex, or gemini CLI - for AI agent execution

Usage

ru ai-sync                        # Process all dirty repos
ru ai-sync --dry-run              # Show what would be processed
ru ai-sync --include=my-project   # Only process matching repos
ru ai-sync --exclude=test-*       # Skip matching repos
ru ai-sync --timeout=1200         # Allow 20 minutes per repo
ru ai-sync --no-push              # Commit but don't push
ru ai-sync --agent=codex          # Use a different AI agent
ru ai-sync --no-untracked         # Ignore untracked files

How It Works

  1. Scans all configured repos for uncommitted changes
  2. For each dirty repo, launches an AI agent session via ntm
  3. The agent reads AGENTS.md and README.md to understand the project
  4. Reviews all changes and groups them into logical commits
  5. Creates detailed commit messages explaining the changes
  6. Pushes to the remote (unless --no-push is specified)

๐Ÿ“ฆ Dependency Updates

The dep-update subcommand uses AI agents to automatically update dependencies across your repositories. It analyzes changelogs for breaking changes, creates migration plans, updates dependencies one at a time, runs tests, and commits successful updates.

Prerequisites

  • ntm (Named Tmux Manager) - for session orchestration
  • claude-code, codex, or gemini CLI - for AI agent execution
  • jq - for JSON processing

Supported Package Managers

ru dep-update automatically detects and supports these package managers:

ManagerDetection FilesUpdate Command
npm/yarn/pnpmpackage.json, package-lock.json, yarn.lock, pnpm-lock.yamlnpm update / yarn upgrade
pippyproject.toml, requirements.txt, setup.py, Pipfilepip install --upgrade
cargoCargo.tomlcargo update
gogo.modgo get -u
composercomposer.jsoncomposer update
bundlerGemfilebundle update
mavenpom.xmlmvn versions:use-latest-releases
gradlebuild.gradle, build.gradle.kts./gradlew dependencies --write-locks

Update Workflow

The AI agent follows this workflow for each repository:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. ANALYZE                                                          โ”‚
โ”‚     โ”œโ”€ Detect package manager(s)                                     โ”‚
โ”‚     โ”œโ”€ List outdated dependencies                                    โ”‚
โ”‚     โ”œโ”€ Fetch changelogs for breaking changes                         โ”‚
โ”‚     โ””โ”€ Create risk-assessed migration plan                           โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  2. UPDATE (for each dependency)                                     โ”‚
โ”‚     โ”œโ”€ Update single dependency                                      โ”‚
โ”‚     โ”œโ”€ Run test suite                                                โ”‚
โ”‚     โ”œโ”€ If tests fail: attempt automated fix (up to N iterations)     โ”‚
โ”‚     โ”œโ”€ If fix succeeds: commit with descriptive message              โ”‚
โ”‚     โ””โ”€ If fix fails: roll back and report                           โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  3. REPORT                                                           โ”‚
โ”‚     โ”œโ”€ Summary of successful updates                                 โ”‚
โ”‚     โ”œโ”€ List of dependencies that couldn't be updated                 โ”‚
โ”‚     โ””โ”€ Recommendations for manual intervention                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Basic Usage

# Update all dependencies in all configured repos
ru dep-update

# Preview what would be updated (no changes)
ru dep-update --dry-run

# Update a single repository
ru dep-update --repo=./my-project

# Use a specific AI agent
ru dep-update --agent=codex

Filtering Dependencies

You can control which dependencies get updated using filters:

# Only update npm packages
ru dep-update --manager=npm

# Only update packages matching a pattern
ru dep-update --include='react|vue|angular'

# Exclude specific packages from updates
ru dep-update --exclude='typescript|webpack'

# Include major version updates (risky, disabled by default)
ru dep-update --major

# Combine filters
ru dep-update --manager=pip --exclude='django' --major

Controlling the Update Process

# Custom test command (overrides auto-detection)
ru dep-update --test-cmd="npm run test:ci"

# Increase fix attempts for complex updates
ru dep-update --max-fix-attempts=10

# Commit changes but don't push to remote
ru dep-update --no-push

# Increase timeout for large repos
ru dep-update --timeout=1800

Exit Codes

CodeMeaning
0All updates successful
1Some dependencies failed to update
3Missing dependencies (ntm, claude-code)
4Invalid arguments

๐Ÿ”ข Exit Codes

ru uses meaningful exit codes for automation:

CodeMeaningWhen
0SuccessAll repos synced or already current
1Partial failureSome repos failed (network/auth/remote error)
2Conflicts existSome repos have unresolved conflicts
3Dependency errorgh CLI missing, auth failed, etc.
4Invalid argumentsBad CLI options, missing config files
5InterruptedSync interrupted by user (Ctrl+C); use --resume to continue

Using in Scripts

#!/bin/bash
ru sync --non-interactive
exit_code=$?

case $exit_code in
    0) echo "All repos synchronized successfully" ;;
    1) echo "Some repos failed - check logs" ;;
    2) echo "Conflicts detected - manual resolution required" ;;
    3) echo "Missing dependencies - run 'ru doctor'" ;;
    4) echo "Invalid configuration" ;;
    5) echo "Sync interrupted - run 'ru sync --resume' to continue" ;;
esac

CI Usage

# GitHub Actions example
- name: Sync repositories
  run: |
    ru sync --non-interactive --json > sync-results.json
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  continue-on-error: true

- name: Check sync status
  run: |
    if [ $? -eq 2 ]; then
      echo "::warning::Some repos have conflicts"
    fi

๐Ÿ—๏ธ Architecture

Component Overview

ru is organized into logical layers, with each layer building on the ones below. The full implementation is approximately 17,700 lines of pure Bash, with no external dependencies beyond git, curl, and optionally gh CLI and gum for enhanced UX.

ru (bash, ~17,700 LOC)
โ”‚
โ”œโ”€โ”€ Core Foundation Layer
โ”‚   โ”œโ”€โ”€ Utilities & Guards
โ”‚   โ”‚   โ”œโ”€โ”€ is_interactive()      # TTY detection for prompts
โ”‚   โ”‚   โ”œโ”€โ”€ can_prompt()          # Interactive + non-CI guard
โ”‚   โ”‚   โ”œโ”€โ”€ ensure_dir()          # Create directory if missing
โ”‚   โ”‚   โ””โ”€โ”€ json_escape()         # Safe JSON string escaping
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Logging (stderr=humans, stdout=data)
โ”‚   โ”‚   โ”œโ”€โ”€ log_info/warn/error() # Human messages to stderr
โ”‚   โ”‚   โ”œโ”€โ”€ log_step/success()    # Progress indicators
โ”‚   โ”‚   โ””โ”€โ”€ write_result()        # NDJSON structured logging
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Configuration
โ”‚   โ”‚   โ”œโ”€โ”€ get_config_value()    # Read from ini-style file
โ”‚   โ”‚   โ”œโ”€โ”€ set_config_value()    # Write to file atomically
โ”‚   โ”‚   โ””โ”€โ”€ resolve_config()      # CLI > env > file > default
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ Path Security
โ”‚       โ”œโ”€โ”€ validate_path()       # Reject traversal attacks
โ”‚       โ”œโ”€โ”€ safe_path_concat()    # Prevent /a + ../b escapes
โ”‚       โ””โ”€โ”€ canonicalize_path()   # Resolve symlinks safely
โ”‚
โ”œโ”€โ”€ Infrastructure Layer
โ”‚   โ”œโ”€โ”€ Portable Locking
โ”‚   โ”‚   โ”œโ”€โ”€ dir_lock_try_acquire() # Atomic mkdir-based locks
โ”‚   โ”‚   โ”œโ”€โ”€ dir_lock_wait()        # Blocking lock with timeout
โ”‚   โ”‚   โ””โ”€โ”€ dir_lock_release()     # rmdir-based release
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ URL & Path Parsing
โ”‚   โ”‚   โ”œโ”€โ”€ parse_repo_url()      # Extract host/owner/repo
โ”‚   โ”‚   โ”œโ”€โ”€ normalize_url()       # Canonical URL form
โ”‚   โ”‚   โ”œโ”€โ”€ parse_repo_spec()     # owner/repo@branch as name
โ”‚   โ”‚   โ””โ”€โ”€ url_to_local_path()   # Layout-aware path resolution
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Dependency Detection
โ”‚   โ”‚   โ”œโ”€โ”€ detect_os()           # macOS/Linux detection
โ”‚   โ”‚   โ”œโ”€โ”€ check_gh_*()          # gh CLI status checks
โ”‚   โ”‚   โ””โ”€โ”€ ensure_dependencies() # Full dependency flow
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ Gum Integration
โ”‚       โ”œโ”€โ”€ check_gum()           # Feature availability
โ”‚       โ”œโ”€โ”€ gum_confirm()         # Y/N with fallback
โ”‚       โ””โ”€โ”€ print_banner()        # Styled box output
โ”‚
โ”œโ”€โ”€ Git Operations Layer (no cd, plumbing-based)
โ”‚   โ”œโ”€โ”€ Status Detection
โ”‚   โ”‚   โ”œโ”€โ”€ get_repo_status()     # Ahead/behind via rev-list
โ”‚   โ”‚   โ”œโ”€โ”€ is_dirty()            # status --porcelain
โ”‚   โ”‚   โ””โ”€โ”€ is_shallow()          # rev-parse --is-shallow
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Actions
โ”‚   โ”‚   โ”œโ”€โ”€ do_clone()            # gh repo clone with retry
โ”‚   โ”‚   โ”œโ”€โ”€ do_pull()             # Strategy-aware pull
โ”‚   โ”‚   โ””โ”€โ”€ do_fetch()            # Fetch with timeout handling
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ Worktree Management
โ”‚       โ”œโ”€โ”€ create_worktree()     # Isolated checkout for review
โ”‚       โ”œโ”€โ”€ cleanup_worktree()    # Safe removal
โ”‚       โ””โ”€โ”€ get_worktree_list()   # Active worktree tracking
โ”‚
โ”œโ”€โ”€ Sync Orchestration Layer
โ”‚   โ”œโ”€โ”€ Repo List Management
โ”‚   โ”‚   โ”œโ”€โ”€ load_repo_list()      # Parse list files
โ”‚   โ”‚   โ”œโ”€โ”€ detect_collisions()   # Path collision warnings
โ”‚   โ”‚   โ””โ”€โ”€ enumerate_repos()     # Combine public + private
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Work-Stealing Queue
โ”‚   โ”‚   โ”œโ”€โ”€ queue_init()          # Write repos to temp file
โ”‚   โ”‚   โ”œโ”€โ”€ queue_dequeue()       # Atomic pop with lock
โ”‚   โ”‚   โ””โ”€โ”€ worker_loop()         # Process until empty
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Sync State Management
โ”‚   โ”‚   โ”œโ”€โ”€ sync_state_init()     # Create run directory
โ”‚   โ”‚   โ”œโ”€โ”€ sync_state_save()     # Persist resume state
โ”‚   โ”‚   โ””โ”€โ”€ sync_state_load()     # Resume interrupted sync
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ Result Aggregation
โ”‚       โ”œโ”€โ”€ tally_results()       # Count by status
โ”‚       โ”œโ”€โ”€ format_summary()      # Human-readable report
โ”‚       โ””โ”€โ”€ format_json()         # Machine-readable output
โ”‚
โ”œโ”€โ”€ Review & Agent Coordination Layer
โ”‚   โ”œโ”€โ”€ Discovery & Prioritization
โ”‚   โ”‚   โ”œโ”€โ”€ discover_items()      # Find issues/PRs to review
โ”‚   โ”‚   โ”œโ”€โ”€ score_priority()      # Multi-factor ranking
โ”‚   โ”‚   โ””โ”€โ”€ filter_by_policy()    # Apply review policies
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Session Management
โ”‚   โ”‚   โ”œโ”€โ”€ create_review_session() # Initialize session
โ”‚   โ”‚   โ”œโ”€โ”€ monitor_session()     # Health tracking
โ”‚   โ”‚   โ””โ”€โ”€ cleanup_session()     # Resource cleanup
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Quality Gates
โ”‚   โ”‚   โ”œโ”€โ”€ validate_commit_plan() # Security & size checks
โ”‚   โ”‚   โ”œโ”€โ”€ check_file_allowlist() # Denylist enforcement
โ”‚   โ”‚   โ””โ”€โ”€ verify_tests_passed()  # Test requirement gate
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ GitHub Actions Execution
โ”‚       โ”œโ”€โ”€ execute_gh_action()   # Comment, close, merge, etc.
โ”‚       โ”œโ”€โ”€ verify_action_safe()  # Security validation
โ”‚       โ””โ”€โ”€ batch_actions()       # Rate-limit aware batching
โ”‚
โ”œโ”€โ”€ Agent Sweep Layer
โ”‚   โ”œโ”€โ”€ Preflight & Safety
โ”‚   โ”‚   โ”œโ”€โ”€ preflight_checks()    # Environment validation
โ”‚   โ”‚   โ”œโ”€โ”€ check_repo_config()   # Per-repo review settings
โ”‚   โ”‚   โ””โ”€โ”€ abort_conditions()    # Safety circuit breakers
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ Orchestration
โ”‚   โ”‚   โ”œโ”€โ”€ agent_sweep_loop()    # Main iteration logic
โ”‚   โ”‚   โ”œโ”€โ”€ spawn_review()        # ntm/Claude Code session
โ”‚   โ”‚   โ””โ”€โ”€ wait_for_completion() # Timeout handling
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ State & Recovery
โ”‚       โ”œโ”€โ”€ sweep_state_save()    # Progress checkpointing
โ”‚       โ”œโ”€โ”€ sweep_state_load()    # Resume after interrupt
โ”‚       โ””โ”€โ”€ cleanup_stale()       # Remove orphaned worktrees
โ”‚
โ””โ”€โ”€ Command Layer
    โ”œโ”€โ”€ Core Commands
    โ”‚   โ”œโ”€โ”€ cmd_sync()            # Clone and pull repos
    โ”‚   โ”œโ”€โ”€ cmd_status()          # Read-only status check
    โ”‚   โ”œโ”€โ”€ cmd_init()            # Create configuration
    โ”‚   โ”œโ”€โ”€ cmd_add()             # Add repo to list
    โ”‚   โ””โ”€โ”€ cmd_remove()          # Remove from list
    โ”‚
    โ”œโ”€โ”€ Maintenance Commands
    โ”‚   โ”œโ”€โ”€ cmd_prune()           # Detect orphan repos
    โ”‚   โ”œโ”€โ”€ cmd_doctor()          # System diagnostics
    โ”‚   โ””โ”€โ”€ cmd_self_update()     # Version checking
    โ”‚
    โ”œโ”€โ”€ Review Commands
    โ”‚   โ”œโ”€โ”€ cmd_review()          # Single-repo review mode
    โ”‚   โ””โ”€โ”€ cmd_agent_sweep()     # Multi-repo agent sweep
    โ”‚
    โ””โ”€โ”€ CLI Infrastructure
        โ”œโ”€โ”€ show_help()           # Usage message
        โ”œโ”€โ”€ dispatch_command()    # Route to handler
        โ””โ”€โ”€ on_exit()             # Cleanup trap

Data Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Config    โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚  Repo List  โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚  Processing โ”‚
โ”‚   Files     โ”‚     โ”‚   Parser    โ”‚     โ”‚    Loop     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ”‚
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    โ–ผ                    โ–ผ                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Clone  โ”‚       โ”‚   Status    โ”‚       โ”‚   Pull   โ”‚
โ”‚ (gh)    โ”‚       โ”‚ (plumbing)  โ”‚       โ”‚  (git)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    โ”‚                    โ”‚                     โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
                                               โ–ผ
                                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                                        โ”‚   Results   โ”‚
                                        โ”‚  (NDJSON)   โ”‚
                                        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                               โ”‚
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
                    โ–ผ                          โ–ผ
             โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
             โ”‚   Summary   โ”‚           โ”‚    JSON     โ”‚
             โ”‚  (stderr)   โ”‚           โ”‚  (stdout)   โ”‚
             โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

NDJSON Results Logging

ru tracks per-repo results in Newline-Delimited JSON (NDJSON) format for easy parsing and CI integration:

{"repo":"mcp_agent_mail","path":"/data/projects/mcp_agent_mail","action":"pull","status":"updated","duration":2,"message":"","timestamp":"2025-01-03T14:30:00Z"}
{"repo":"beads_viewer","path":"/data/projects/beads_viewer","action":"clone","status":"cloned","duration":5,"message":"","timestamp":"2025-01-03T14:30:05Z"}
{"repo":"repo_updater","path":"/data/projects/repo_updater","action":"skip","status":"current","duration":0,"message":"Already up to date","timestamp":"2025-01-03T14:30:05Z"}

Fields:

FieldDescription
repoRepository name
pathLocal filesystem path
actionWhat was attempted: clone, pull, skip, fail
statusResult: cloned, updated, current, conflict, failed
durationSeconds taken
messageError message if failed, empty otherwise
timestampISO-8601 timestamp

Use with jq:

# Count by status
cat ~/.local/state/ru/logs/latest/results.ndjson | jq -s 'group_by(.status) | map({status: .[0].status, count: length})'

# Find failures
cat ~/.local/state/ru/logs/latest/results.ndjson | jq -r 'select(.status == "failed") | "\(.repo): \(.message)"'

Portable Locking

ru uses directory-based locking for coordination across parallel workers and concurrent processes. This approach works on all POSIX systems without requiring flock (which isn't available on all platforms):

# Lock acquisition via atomic mkdir
dir_lock_try_acquire() {
    local lock_dir="$1"
    if mkdir "$lock_dir" 2>/dev/null; then
        # Write lock info for debugging
        echo "$$" > "$lock_dir/pid"
        return 0
    fi
    return 1
}

How it works:

  • mkdir is atomic on POSIX filesystemsโ€”only one process can create a directory
  • Lock holder's PID is written to a file inside the lock directory
  • Timeout-based acquisition retries with configurable duration
  • Automatic cleanup on process exit via trap handlers

Use cases:

  • Parallel sync workers accessing shared state
  • Worktree mapping updates during concurrent reviews
  • Sync state file coordination

Work-Stealing Queue

For parallel sync operations (--parallel N), ru uses a work-stealing queue pattern where multiple worker processes compete for work items atomically:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                      Work Queue (temp file)                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”           โ”‚
โ”‚  โ”‚ repo1   โ”‚ repo2   โ”‚ repo3   โ”‚ repo4   โ”‚ repo5   โ”‚ ...       โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
         โ–ผ                    โ–ผ                    โ–ผ
   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
   โ”‚ Worker 1 โ”‚         โ”‚ Worker 2 โ”‚         โ”‚ Worker 3 โ”‚
   โ”‚ (subshell)โ”‚        โ”‚ (subshell)โ”‚        โ”‚ (subshell)โ”‚
   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                    โ”‚                    โ”‚
         โ”‚     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
         โ”‚     โ–ผ                             โ–ผ     โ”‚
         โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
         โ””โ”€โ–ถโ”‚       Queue Lock (mkdir)           โ”‚โ—€โ”˜
            โ”‚   - Atomic dequeue via head/tail   โ”‚
            โ”‚   - Spin-wait with timeout         โ”‚
            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Algorithm:

  1. Queue initialization: Write all repo specs to a temporary file, one per line
  2. Worker spawning: Fork N worker subshells that compete for work
  3. Atomic dequeue:
    • Acquire directory lock via mkdir (atomic on POSIX)
    • Read first line from queue file (head -1)
    • Remove first line (tail -n +2 > tmp && mv tmp queue)
    • Release lock via rmdir
  4. Work completion: Worker processes repo, appends result to shared results file (also lock-protected)
  5. Progress tracking: Atomic counter updated under separate lock

Why work-stealing?

  • Load balancing: Fast repos don't block slow onesโ€”workers grab more work when free
  • No coordinator bottleneck: Workers self-organize without a central scheduler
  • Graceful degradation: If a worker dies, remaining workers continue

Lock contention handling:

  • Spin-wait with 100ms sleep between attempts
  • Configurable timeout (default 60s)
  • Workers exit cleanly if they can't acquire lock

Path Security Validation

ru includes two security functions that guard against path traversal attacks, especially important when paths come from state files or user input:

_is_safe_path_segment(segment) โ€” Validates individual path components:

CheckBlocksReason
Empty string""Invalid path component
Dot-only., ..Path traversal attack
Leading dash-rfGit option confusion
Path separatorsfoo/barUnexpected subdirectory
Control characters\x1b[31mTerminal escape injection

_is_path_under_base(path, base) โ€” Verifies a path is safely within a base directory:

# These pass:
_is_path_under_base "/data/projects/repo" "/data/projects"     # โœ“ Direct child
_is_path_under_base "/data/projects/a/b/c" "/data/projects"    # โœ“ Nested child

# These fail:
_is_path_under_base "../etc/passwd" "/data/projects"           # โœ— Traversal
_is_path_under_base "/data/projects/../etc" "/data/projects"   # โœ— Dot segment
_is_path_under_base "relative/path" "/data/projects"           # โœ— Not absolute
_is_path_under_base "/data/projects" "/"                       # โœ— Base is root

Key design decisions:

  • Lexical checks only: Does NOT resolve symlinks. This is intentionalโ€”rm -rf on a symlink removes the link, not the target, so lexical containment is the correct safety check.
  • Rejects dot segments: Any /. or /.. in path or base causes rejection, preventing path/../../../etc/passwd attacks.
  • Requires absolute paths: Relative paths are rejected to prevent confusion about the current directory.

These functions are used throughout ru to protect rm -rf operations on worktrees and state directories.

Retry with Exponential Backoff

Network operations and API calls use intelligent retry logic with exponential backoff and jitter:

retry_with_backoff MAX_ATTEMPTS BASE_DELAY_SECONDS -- command args...

Algorithm:

  1. Attempt the operation
  2. On failure, wait BASE_DELAY * 2^(attempt-1) seconds
  3. Add random jitter (ยฑ25%) to prevent thundering herd
  4. Retry until MAX_ATTEMPTS exhausted
  5. Return last exit code on final failure

Example:

# Retry GitHub API call up to 5 times with 2-second base delay
retry_with_backoff 5 2 -- gh api repos/owner/repo
# Delays: 2s, 4s, 8s, 16s (with jitter)

Capture modes:

  • --capture=all โ€” Capture both stdout and stderr (default)
  • --capture=stdout โ€” Capture only stdout, let stderr pass through

This is used throughout ru for:

  • GitHub GraphQL batch queries
  • Clone operations on rate-limited repos
  • Network-dependent status checks

JSON Utilities with Fallback Chain

ru provides portable JSON handling that works across environments with varying tool availability:

json_get_field(json, field) โ€” Extract a field from JSON with graceful degradation:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   JSON Field Extraction                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
              โ–ผ               โ–ผ               โ–ผ               โ–ผ
       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
       โ”‚    jq    โ”‚    โ”‚ python3  โ”‚    โ”‚   perl   โ”‚    โ”‚   sed    โ”‚
       โ”‚  (best)  โ”‚    โ”‚ (good)   โ”‚    โ”‚ (ok)     โ”‚    โ”‚ (basic)  โ”‚
       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
              โ”‚               โ”‚               โ”‚               โ”‚
              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
                              โ–ผ
                    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                    โ”‚  Field value    โ”‚
                    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
ToolHandlesLimitations
jqAll JSON types, nested objects, arraysNone (preferred)
python3All JSON typesRequires Python 3
perlAll JSON typesRequires JSON::PP module
sedSimple string fields onlyNo nested objects, arrays, escapes

json_escape(string) โ€” Safe string escaping for JSON embedding:

# Handles all JSON-sensitive characters
json_escape 'Hello "World"'     # โ†’ Hello \"World\"
json_escape $'Line1\nLine2'     # โ†’ Line1\nLine2
json_escape 'Path: C:\Users'    # โ†’ Path: C:\\Users

Characters escaped: \ โ†’ \\, " โ†’ \", newline โ†’ \n, tab โ†’ \t, carriage return โ†’ \r, backspace โ†’ \b, form feed โ†’ \f

json_validate(json) โ€” Check if a string is valid JSON:

if json_validate "$response"; then
    # Safe to parse
    value=$(json_get_field "$response" "status")
fi

The validate function uses the same fallback chain, returning success (0) if any available tool can parse the JSON.


๐Ÿงญ Design Principles

1. No Global cd

ru never changes the working directory. All git operations use git -C:

# DO: Use git -C
git -C "$repo_path" status --porcelain
git -C "$repo_path" pull --ff-only

# DON'T: cd into directories
cd "$repo_path"     # Can fail, leaves state
git status          # Which directory are we in?
cd -                # Error-prone

2. Explicit Error Handling (No set -e)

ru uses set -uo pipefail but not set -e. This allows:

  • Continuing after individual repo failures
  • Capturing exit codes correctly
  • Aggregating results for summary
# With set -e, this would exit before capturing exit_code:
output=$(failing_command); exit_code=$?

# ru's approach:
if output=$(git pull --ff-only 2>&1); then
    log_success "Pulled"
else
    exit_code=$?
    log_error "Failed: $output"
    # Continue to next repo
fi

3. Stream Separation

Human-readable output goes to stderr; data to stdout:

# Human messages โ†’ stderr
log_info "Syncing repos..." >&2
log_success "Done!" >&2

# Data โ†’ stdout (can be piped)
echo "$repo_path"        # For scripts
output_json "$data"      # For --json mode

# This works correctly:
ru sync --json | jq '.data.summary'
# Progress shows in terminal, JSON pipes to jq

4. Git Plumbing for Status

Never parse human-readable git output:

# WRONG: Locale-dependent, version-fragile
git pull 2>&1 | grep "Already up to date"

# RIGHT: Machine-readable plumbing
git rev-list --left-right --count HEAD...@{u}
git status --porcelain
git rev-parse HEAD

5. Prompted, Not Automatic

ru never auto-installs without asking:

# Interactive mode: ask first
if gum_confirm "GitHub CLI (gh) not found. Install now?"; then
    install_gh
fi

# Non-interactive mode: fail clearly
log_error "gh not installed. Run with --install-deps or install manually."
exit 3

6. Progress Reporting

ru provides real-time progress feedback through a unified progress API:

# Initialize progress tracking
progress_init "$total_repos"

# Report start of each repo
progress_start_repo "$repo_name" "$current_index"

# Report completion with result
progress_complete_repo "$repo_name" "$status" "$duration"

# Final summary
progress_summary

Output adapts to context:

ContextBehavior
Interactive terminalLive-updating progress line with spinner
Non-interactive/CISimple line-by-line output
--quiet modeErrors only
--json modeNDJSON events to stdout, human summary to stderr

7. CI Environment Detection

ru automatically detects CI environments and adjusts behavior accordingly:

Detected CI environments:

  • GitHub Actions (GITHUB_ACTIONS)
  • GitLab CI (GITLAB_CI)
  • Jenkins (JENKINS_URL)
  • Travis CI (TRAVIS)
  • CircleCI (CIRCLECI)
  • Azure Pipelines (TF_BUILD)
  • Generic CI (CI=true)

Behavior changes in CI:

  • Forces --non-interactive mode (no prompts)
  • Disables gum (uses ANSI fallbacks)
  • Suppresses spinners and live progress
  • Uses line-buffered output for proper log streaming
  • Enables stricter error handling
# Manual CI mode override
CI=true ru sync

# Explicit non-interactive
ru sync --non-interactive

๐Ÿ›ก๏ธ File Denylist System

When scanning repositories or processing file changes, ru applies a comprehensive denylist to prevent accidental exposure of secrets, processing of large build artifacts, or noise from IDE/editor files.

Default Denylist Patterns

The built-in denylist covers four categories:

CategoryPatternsExamples
Secrets & Credentials.env, .env.*, *.pem, *.key, id_rsa*, credentials.json, secrets.json, .netrc, .npmrc, .pypircAPI keys, TLS certificates, SSH keys
Build Artifactsnode_modules, __pycache__, dist, build, .next, target, vendor, *.pycDependencies, compiled output
Logs & Temp Files*.log, *.tmp, *.temp, *.swp, *.swo, *~, .DS_Store, Thumbs.dbDebug logs, swap files, OS metadata
IDE/Editor.idea, .vscode, *.imlJetBrains, VS Code settings

Pattern Matching Algorithm

The is_file_denied() function uses a three-tier matching approach:

is_file_denied "frontend/node_modules/lodash/index.js"  # โœ— Blocked
is_file_denied "src/.env.local"                          # โœ— Blocked
is_file_denied "src/components/Button.tsx"               # โœ“ Allowed

Matching order:

  1. Full path match: Compare entire path against pattern (e.g., dist/* matches dist/bundle.js)
  2. Basename match: Compare just the filename (e.g., .env matches config/.env)
  3. Directory containment: Check if file is inside a denied directory at any nesting level (e.g., node_modules blocks frontend/node_modules/pkg/index.js)

Extending the Denylist

Via environment variable (space-separated):

export AGENT_SWEEP_DENYLIST_EXTRA="*.custom internal_docs/*"
ru agent-sweep

Via config file (YAML):

# ~/.config/ru/config.yml
agent_sweep:
  denylist_extra:
    - "*.custom"
    - "internal_docs/*"
    - "vendor/private/*"

Via config file (JSON):

{
  "agent_sweep": {
    "denylist_extra": ["*.custom", "internal_docs/*"]
  }
}

Programmatic Access

# List all active denylist patterns
ru agent-sweep --show-denylist

# From scripts, use the exported functions:
source /path/to/ru
if is_file_denied "path/to/file"; then
    echo "File is blocked by denylist"
fi

# Filter a list of files
echo -e "main.py\n.env\nREADME.md" | filter_files_denylist
# Output: main.py
#         README.md

๐Ÿงช Testing

ru includes a comprehensive test suite with 70+ test files covering unit tests, integration tests, and end-to-end workflows. The testing infrastructure is designed around a key principle: tests must work offline and deterministically.

Why Offline Testing Matters

Git operations are notoriously difficult to test reliably:

  • Network dependencies make tests flaky and slow
  • Real repositories can change unexpectedly
  • CI environments may have restricted network access
  • Authentication tokens expire or get revoked

ru solves this with a local git harness that creates temporary repositories with precisely controlled statesโ€”ahead, behind, diverged, dirty, shallow, detachedโ€”all without touching the network.

Test Architecture

scripts/
โ”œโ”€โ”€ test_framework.sh              # Core assertion library and utilities
โ”œโ”€โ”€ test_git_harness.sh            # Offline git repository factory
โ”œโ”€โ”€ test_e2e_framework.sh          # E2E test isolation and helpers
โ”‚
โ”œโ”€โ”€ Unit Tests (test_unit_*.sh) โ€” 36 files
โ”‚   โ”œโ”€โ”€ test_unit_config.sh        # Configuration loading and resolution
โ”‚   โ”œโ”€โ”€ test_unit_core_utils.sh    # Core utility functions
โ”‚   โ”œโ”€โ”€ test_unit_parsing_functions.sh # URL/spec parsing (40 tests)
โ”‚   โ”œโ”€โ”€ test_unit_git_harness.sh   # Git harness self-tests (24 tests)
โ”‚   โ”œโ”€โ”€ test_unit_dependencies.sh  # Dependency checking logic
โ”‚   โ”œโ”€โ”€ test_unit_quality_gates.sh # Commit plan validation
โ”‚   โ”œโ”€โ”€ test_unit_command_validation.sh # Argument validation
โ”‚   โ””โ”€โ”€ ... (29 more unit test files)
โ”‚
โ”œโ”€โ”€ Integration Tests
โ”‚   โ”œโ”€โ”€ test_parsing.sh            # URL parsing (76 tests, 156 assertions)
โ”‚   โ”œโ”€โ”€ test_local_git.sh          # Local git operations with harness
โ”‚   โ””โ”€โ”€ test_sync_state.sh         # Sync state management
โ”‚
โ””โ”€โ”€ End-to-End Tests (test_e2e_*.sh) โ€” 20 files
    โ”œโ”€โ”€ test_e2e_sync_clone.sh     # Clone operations
    โ”œโ”€โ”€ test_e2e_sync_pull.sh      # Pull operations
    โ”œโ”€โ”€ test_e2e_sync_edge_cases.sh # Edge case handling
    โ”œโ”€โ”€ test_e2e_add_remove.sh     # Repo list management
    โ”œโ”€โ”€ test_e2e_status.sh         # Status command
    โ””โ”€โ”€ ... (15 more E2E test files)

Git Harness: Deterministic Repository States

The git harness (test_git_harness.sh) creates temporary git repositories with specific states for testing. It uses bare repositories as "remotes" in /tmp, enabling complete offline testing of sync operations.

# Create a repo that's 2 commits ahead of remote
git_harness_setup
repo=$(git_harness_create_repo "myrepo" --ahead=2)
# $repo now contains a working directory with 2 unpushed commits

# Create a diverged state (local and remote have different commits)
repo=$(git_harness_create_repo "diverged" --ahead=1 --behind=3)

# Create various states
repo=$(git_harness_create_repo "dirty" --dirty)           # Uncommitted changes
repo=$(git_harness_create_repo "shallow" --shallow=5)     # Shallow clone
repo=$(git_harness_create_repo "detached" --detached)     # Detached HEAD
repo=$(git_harness_create_repo "nobranch" --no-remote)    # No tracking branch

git_harness_cleanup  # Removes all temp directories

Available state options:

OptionEffect
--ahead=NCreate N local commits not on remote
--behind=NCreate N remote commits not in local
--divergedShortcut for --ahead=1 --behind=1
--dirtyAdd uncommitted changes to working tree
--shallow=NCreate shallow clone with depth N
--detachedCheckout detached HEAD state
--no-remoteCreate repo without remote tracking
--branch=NAMEUse NAME instead of 'main'

Manipulation helpers:

git_harness_add_commit "$repo" "feat: new feature"     # Add local commit
git_harness_add_commit_and_push "$repo" "fix: bug"     # Add and push
git_harness_make_dirty "$repo"                          # Add uncommitted changes
git_harness_make_staged "$repo"                         # Add staged changes
git_harness_add_untracked "$repo" "newfile.txt"        # Add untracked file
git_harness_simulate_rebase "$repo"                     # Create rebase state
git_harness_simulate_merge "$repo"                      # Create merge conflict

Query helpers:

status=$(git_harness_get_status "$repo")   # Returns: current|ahead|behind|diverged
git_harness_is_dirty "$repo" && echo "Has uncommitted changes"
git_harness_is_shallow "$repo" && echo "Is shallow clone"
git_harness_is_detached "$repo" && echo "HEAD is detached"

Why this design? The harness uses git plumbing commands exclusively (per AGENTS.md guidelines), ensuring reliable detection regardless of locale or git version. Each test runs against known states, making failures reproducible and debuggable.

Running Tests

# Run all tests
./scripts/run_all_tests.sh

# Run specific test file
./scripts/test_parsing.sh

# Run with verbose output
VERBOSE=1 ./scripts/test_unit_review.sh

# Run only unit tests
for f in scripts/test_unit_*.sh; do "$f"; done

# Run E2E tests
for f in scripts/test_e2e_*.sh; do "$f"; done

# Check test coverage summary
./scripts/test_coverage.sh

Test Tiers and Requirements

Unit tests use extracted ru functions and avoid external network dependencies.
Integration tests use local git repos under /tmp (no network).
E2E tests run full CLI flows in isolated XDG roots and capture detailed logs.

Required tools:

  • bash 4.0+, git
  • jq for JSON-oriented tests (skips when missing)
  • gh for tests that exercise GitHub auth behavior (skips when missing or unauthenticated)
  • ntm/tmux only for agent-sweep tests (mocked by default in E2E)

Logging and Artifacts

The test frameworks support structured logs and artifact capture:

  • TF_LOG_LEVEL=debug|info|warn|error|none controls verbosity.
  • TF_LOG_FILE=/path/to/test.log writes human-readable logs.
  • TF_JSON_LOG_FILE=/path/to/test.jsonl writes machine-readable JSON lines.
  • E2E runs create $E2E_TEMP_DIR/test_logs/ with per-test stdout/stderr captures.
  • Failed-test artifacts can be preserved under TF_FAILED_ARTIFACTS_DIR (default: /tmp/ru-test-failures).

Gated Tests (gh/ntm)

Some tests require external tooling and will skip gracefully:

  • GitHub auth: use require_gh_auth (from scripts/test_framework.sh) in tests that need authenticated gh.
    • Force-skip with TF_SKIP_GH_AUTH=1|true|yes.
  • ntm/tmux: E2E tests use mocks via e2e_create_mock_ntm; you can control behavior with NTM_MOCK_SCENARIO (see scripts/test_bin/ntm).

Test Categories

Unit Tests โ€” Test individual functions in isolation:

  • URL parsing (parse_repo_url, normalize_url)
  • Repo spec parsing (parse_repo_spec with @branch as name)
  • Configuration resolution
  • Gum wrapper fallback behavior

E2E Tests โ€” Test complete workflows with real file operations:

  • Full init โ†’ add โ†’ sync โ†’ status cycle
  • Prune detection and archive/delete modes
  • Self-update version checking
  • Error handling and edge cases

Test Framework: Assertion Library

The test framework (test_framework.sh) provides a comprehensive assertion library designed for Bash testing. It tracks pass/fail counts, supports structured logging, and handles cleanup automatically.

Core assertions:

# Value comparisons
assert_equals "expected" "$actual" "Values should match"
assert_not_equals "bad" "$actual" "Should differ"
assert_contains "$haystack" "needle" "Should contain substring"
assert_not_contains "$output" "error" "Should not have errors"

# Boolean assertions (execute expressions)
assert_true "[[ -f '$file' ]]" "File should exist"
assert_false "[[ -d '$dir' ]]" "Directory should not exist"

# Exit code testing
assert_exit_code 0 some_command arg1 arg2 "Command should succeed"
assert_exit_code 1 failing_command "Command should fail"

# File system assertions
assert_file_exists "/path/to/file" "Config file created"
assert_dir_exists "/path/to/dir" "Directory created"
assert_file_contains "/path/to/file" "pattern" "File has expected content"

Test lifecycle:

# Run a test with automatic timing and result tracking
run_test test_my_feature

# Skip tests conditionally
skip_test "Requires network access"

# Print final results with exit code
print_results
exit "$(get_exit_code)"

Structured logging:

The framework supports both human-readable and machine-readable output:

# Control log level
TF_LOG_LEVEL=debug ./scripts/test_unit_config.sh

# Write human logs to file
TF_LOG_FILE=/tmp/test.log ./scripts/test_unit_config.sh

# Write NDJSON for CI integration
TF_JSON_LOG_FILE=/tmp/test.jsonl ./scripts/test_unit_config.sh

Function extraction for unit testing:

Unit tests can source individual functions from ru without executing the whole script:

source_function "parse_repo_url"
source_function "normalize_url"

# Now test the isolated functions
local host owner repo
parse_repo_url "https://github.com/owner/repo" host owner repo
assert_equals "github.com" "$host" "Host extracted"

Writing Tests

#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/test_framework.sh"

test_parse_url_https_basic() {
    log_test_start "parse_repo_url handles HTTPS URLs"

    local host owner repo
    parse_repo_url "https://github.com/owner/repo" host owner repo

    assert_equals "github.com" "$host" "Host extracted"
    assert_equals "owner" "$owner" "Owner extracted"
    assert_equals "repo" "$repo" "Repo extracted"

    log_test_pass "parse_repo_url handles HTTPS URLs"
}

test_sync_with_dirty_repo() {
    log_test_start "sync skips dirty repos"

    # Create a dirty repo using the git harness
    git_harness_setup
    local repo=$(git_harness_create_repo "dirtytest" --dirty)

    # Test sync behavior
    local output
    output=$("$RU_SCRIPT" sync --dir="$(dirname "$repo")" 2>&1)

    assert_contains "$output" "dirty" "Reports dirty state"
    assert_exit_code 0 git -C "$repo" status --porcelain "Still has changes"

    git_harness_cleanup
    log_test_pass "sync skips dirty repos"
}

# Register and run tests
setup_cleanup_trap
run_test test_parse_url_https_basic
run_test test_sync_with_dirty_repo
print_results
exit "$(get_exit_code)"

Test Best Practices

The test suite follows these principles:

  1. No network dependencies โ€” Use the git harness for repository states
  2. Isolated environments โ€” Each test gets fresh XDG directories
  3. Deterministic results โ€” Same input always produces same output
  4. Fast execution โ€” Unit tests complete in milliseconds
  5. Clear failures โ€” Assertion messages explain what went wrong
  6. Bash 4.0 compatible โ€” Uses ${arr[@]+"${arr[@]}"} pattern for empty arrays

๐Ÿงญ Troubleshooting

Common Issues

<details> <summary><strong>"gh: command not found"</strong></summary>

Cause: GitHub CLI not installed.

Fix: Install gh and authenticate:

# macOS
brew install gh

# Ubuntu/Debian
sudo apt install gh

# Then authenticate
gh auth login
</details> <details> <summary><strong>"gh: auth required"</strong></summary>

Cause: gh CLI installed but not authenticated.

Fixes:

  1. Interactive: gh auth login
  2. Non-interactive: Set GH_TOKEN environment variable
export GH_TOKEN=ghp_xxxxxxxxxxxx
ru sync --non-interactive
</details> <details> <summary><strong>"Cannot fast-forward"</strong></summary>

Cause: Local and remote have diverged.

Fixes:

  1. Rebase: git pull --rebase
  2. Merge: git pull --no-ff
  3. Use --rebase flag: ru sync --rebase
  4. Push first if your changes are intentional
</details> <details> <summary><strong>"dirty working tree"</strong></summary>

Cause: Uncommitted local changes.

Fixes:

  1. Stash: git stash && git pull && git stash pop
  2. Commit: git add . && git commit -m "WIP"
  3. Use --autostash: ru sync --autostash
  4. Discard (careful!): git checkout . && git clean -fd
</details> <details> <summary><strong>Config directory doesn't exist</strong></summary>

Cause: First run without ru init.

Fix:

ru init
# Creates ~/.config/ru/ with default files
</details> <details> <summary><strong>Wrong repository cloned to path</strong></summary>

Cause: Path collision from different owners with same repo name.

Fixes:

  1. Use owner-repo layout: ru config --set LAYOUT=owner-repo
  2. Use custom name: owner/repo as different-name
  3. Remove conflicting directory and re-sync
</details>

Debug Mode

Check per-repo logs for detailed output:

# View latest run log
cat ~/.local/state/ru/logs/latest/run.log

# View specific repo log
cat ~/.local/state/ru/logs/latest/repos/mcp_agent_mail.log

System Check

Run diagnostics:

ru doctor

Checks performed:

CheckWhat It Verifies
GitInstallation and version
GitHub CLI (gh)Installation, version, and authentication status
gh authShows logged-in GitHub username
Config directoryExistence of ~/.config/ru/
Repo countNumber of repositories configured
Projects directoryExistence and write permissions
gum (optional)Availability for prettier terminal UI

Example output:

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚                    ๐Ÿ” ru doctor                              โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

โœ“ git: 2.43.0
โœ“ gh: 2.40.1 (authenticated as yourname)
โœ“ Config: ~/.config/ru/ (47 repos configured)
โœ“ Projects: /data/projects (writable)
โœ“ gum: 0.13.0 (optional)

All checks passed!

Exit code: Returns 3 if critical issues found, 0 otherwise


๐ŸŒ Environment Variables

Runtime Variables

VariableDescriptionDefault
RU_PROJECTS_DIRBase directory for repos/data/projects
RU_LAYOUTPath layout (flat/owner-repo/full)flat
RU_PARALLELNumber of parallel workers1
RU_TIMEOUTNetwork timeout in seconds30
RU_AUTOSTASHAuto-stash before pullfalse
RU_UPDATE_STRATEGYPull strategy (ff-only/rebase/merge)ff-only
RU_CONFIG_DIRConfiguration directory~/.config/ru
RU_LOG_DIRLog directory~/.local/state/ru/logs
GH_TOKENGitHub token for authentication(from gh CLI)
CIDetected CI environmentunset

XDG Variables

VariableDescriptionDefault
XDG_CONFIG_HOMEBase config directory~/.config
XDG_STATE_HOMEBase state directory~/.local/state
XDG_CACHE_HOMEBase cache directory~/.cache

Installer Variables

VariableDescriptionDefault
DESTInstall directory~/.local/bin
RU_SYSTEMInstall to /usr/local/binunset
RU_VERSIONSpecific version to installlatest
RU_UNSAFE_MAINInstall from main branchunset
RU_INSTALLER_NO_SELF_REFRESHDisable installer self-refresh when pipedunset

๐Ÿ“ฆ Dependencies

Required

DependencyVersionPurpose
Bash4.0+Script runtime
git2.0+Repository operations
gh2.0+GitHub CLI for cloning
curlanyInstallation and updates

Optional

DependencyPurpose
gumBeautiful terminal UI
jqJSON processing (for scripts)

System Requirements

PlatformRequirements
macOSmacOS 10.15+ (Catalina or later)
Linuxglibc 2.17+ (Ubuntu 18.04+, Debian 10+)

๐Ÿ›ก๏ธ Security & Privacy

Security Features

  • Checksum verification: Installer verifies SHA256 before installation
  • Release downloads: Default installation from GitHub Releases, not main
  • No credential storage: Uses gh CLI's secure credential storage
  • Prompted installation: Never auto-installs without user confirmation

Privacy

  • Local execution: All processing happens on your machine
  • No telemetry: No data sent anywhere except to GitHub (via gh)
  • No logging to remote: All logs are local only
  • Config is local: No cloud sync of configuration

Audit

The entire codebase is a single bash script:

less ~/.local/bin/ru

๐Ÿ”ง Uninstallation

# Remove script
rm ~/.local/bin/ru

# Remove configuration
rm -rf ~/.config/ru

# Remove logs and state
rm -rf ~/.local/state/ru

# Remove cache
rm -rf ~/.cache/ru

๐Ÿค Contributing

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.


๐Ÿ“„ License

MIT License. See LICENSE for details.


<div align="center">

Report Bug ยท Request Feature


<sub>Built with Bash, git plumbing, and a desire to never manually cd into 47 directories again.</sub>

</div>