Getting started
This page walks through installing ctrlrelay, writing a minimal orchestrator.yaml,
and running the dev pipeline against a real GitHub issue. Allow about 15 minutes.
Requirements
- Python 3.12+ — the package targets
requires-python = ">=3.12". claudeCLI — Claude Code installed and authenticated. ctrlrelay shells out toclaude -p .... See Claude Code installation.ghCLI — GitHub CLI installed and authenticated (gh auth login). ctrlrelay usesghfor all GitHub API calls.git2.20+ — forgit worktree add.- A unix-like shell (macOS or Linux). Windows is not supported.
Optional:
uvfor faster installs.- A Telegram bot if you want the human-in-the-loop bridge — see Telegram bridge.
Install
Clone the repo and install in editable mode:
git clone https://github.com/AInvirion/ctrlrelay.git
cd ctrlrelay
# With uv (recommended):
uv pip install -e .
# Or with pip:
pip install -e .
This installs the ctrlrelay console script. Verify:
ctrlrelay --version
Write your first config
ctrlrelay reads config/orchestrator.yaml by default. Start from the example:
cp config/orchestrator.yaml.example config/orchestrator.yaml
Open config/orchestrator.yaml and edit at least:
timezone— your local IANA timezone.repos[].name— theowner/reposlug of a repository you can push to.repos[].local_path— where the local clone lives (or will live) on disk. Or setpaths.repo_rootto a workspace root and let the path be derived as${repo_root}/${owner}/${repo}(recommended for >1 repo).
node_id is optional — when omitted it defaults to your machine’s
hostname (socket.gethostname()). Set it explicitly only if the
hostname is meaningless (CI runners, containers).
A minimal working config:
version: "1"
timezone: "America/New_York"
paths:
state_db: "~/.ctrlrelay/state.db"
worktrees: "~/.ctrlrelay/worktrees"
bare_repos: "~/.ctrlrelay/repos"
contexts: "~/.ctrlrelay/contexts"
skills: "~/.claude/skills"
claude:
binary: "claude"
default_timeout_seconds: 1800
output_format: "json"
transport:
type: "file_mock"
file_mock:
inbox: "~/.ctrlrelay/inbox.txt"
outbox: "~/.ctrlrelay/outbox.txt"
repos:
- name: "your-org/your-repo"
local_path: "~/Projects/your-repo"
Validate it:
ctrlrelay config validate
You should see something like:
✓ Config valid: config/orchestrator.yaml
Node ID: your-hostname.local
Timezone: America/New_York
Transport: file_mock
Repos: 1
For the full schema (every key, every default), see Configuration.
Your first dev-pipeline run
The dev pipeline takes a GitHub issue, spawns Claude Code in an isolated git worktree, and opens a PR. Pick a small issue assigned to you (or create one to test against), then:
ctrlrelay run dev --issue 42 --repo your-org/your-repo
What happens:
- ctrlrelay acquires a per-repo lock so only one session runs against the repo at a time.
- It clones the repo into
paths.bare_repos(if not already there) and creates a worktree underpaths.worktreeson a branch named fromrepos[].dev_branch_template(defaultfix/issue-{n}). - It spawns
claude -p ...inside the worktree with the issue title/body and a structured prompt that instructs Claude to TDD-implement the change, push the branch, and open a PR. - Claude writes a checkpoint JSON file (
DONE,BLOCKED_NEEDS_INPUT, orFAILED) when it finishes. ctrlrelay reads the checkpoint and reports the outcome. - On success, ctrlrelay removes the worktree (the branch stays — the open PR references it). On failure, it cleans up.
If Claude blocks asking a question, the answer mechanism only works when a
transport is connected. With file_mock, you’ll see the question on the console
but cannot resume. Switch to the Telegram transport to actually answer — see
Telegram bridge and Feedback loop.
Your first poller tick
The poller watches your configured repos for newly assigned issues and runs the dev pipeline against each one. To run it interactively:
ctrlrelay poller start --interval 60
On the very first run the poller seeds its “seen” set with whatever is currently assigned to you, so it does not replay the backlog. From that point onward, only issues assigned after the poller started will trigger a dev-pipeline run.
Hit Ctrl+C to stop. The poller’s seen-issue state is persisted to
{paths.state_db parent}/poller_state.json so a restart picks up where it left off.
To run the poller as a long-lived background service, see Operations.
Where to next
- Configuration — every key in
orchestrator.yaml. - Telegram bridge — how to set up the human-in-the-loop channel.
- Feedback loop — what
BLOCKED_NEEDS_INPUTactually does end-to-end. - CLI reference — every subcommand and flag.
- Operations — running ctrlrelay as a service.