Scheduled GitHub automation powered by the Claude CLI.
Yeti periodically scans GitHub repositories and uses Claude to:
- Plan issues — issues labelled
Needs Refinementget an AI-generated implementation plan posted as a comment - Work issues — issues labelled
Refinedare picked up, implemented in an isolated worktree, and submitted as a PR - Fix CI — open PRs with failing checks are analysed and patched automatically
Three jobs run on simple timers (5 min for issues, 10 min for CI). Each job:
- Queries GitHub via the
ghCLI for matching issues/PRs - Creates a git worktree for isolation
- Runs the
claudeCLI with a task-specific prompt - Pushes results (PR, comment, or commits) back to GitHub
- Cleans up the worktree
A serial queue ensures only one Claude process runs at a time. Labels (yeti-working, etc.) coordinate state and prevent duplicate work.
Yeti runs as a systemd service on a Linux server. An accompanying timer-based updater automatically pulls new GitHub releases, swaps the build artefacts, and restarts the service (with automatic rollback on health-check failure).
| Tool | Purpose |
|---|---|
| Node.js 22 | Runtime |
gh CLI |
GitHub API access (must be authenticated) |
claude CLI |
AI execution (default backend) |
copilot CLI |
AI execution (optional second backend for adversarial review) |
git |
Worktree management |
npm ci
npm run buildgh api repos/frostyard/yeti/contents/deploy/install.sh --jq .content | base64 -d | bashThis downloads the latest release to /opt/yeti, installs the systemd units (templated to the current user), and starts the service. Requires gh CLI to be installed and authenticated.
The service is managed by systemd:
sudo systemctl start yeti # start
sudo systemctl stop yeti # stop (sends SIGTERM, waits for in-flight jobs)
sudo systemctl status yeti # check status
journalctl -u yeti -f # tail logsThe process handles SIGTERM gracefully, so systemd can stop it cleanly.
The yeti-updater.timer checks for new GitHub releases every 60 seconds. When a new release is found, deploy/deploy.sh downloads the tarball, swaps the dist/ directory, restarts the service, and verifies health via http://localhost:9384/health. If the health check fails, it automatically rolls back to the previous version.
Starting from the release that introduces enabledJobs, you must explicitly list which jobs to run in ~/.yeti/config.json. Without this field, no jobs will start.
Before upgrading, add enabledJobs to your config:
{
"enabledJobs": [
"issue-worker",
"issue-refiner",
"ci-fixer",
"review-addresser",
"doc-maintainer",
"auto-merger",
"repo-standards",
"improvement-identifier",
"issue-auditor",
"triage-yeti-errors"
]
}The example above enables all jobs (matching previous default behavior). Remove any jobs you don't want to run.
If you upgrade without updating config, yeti will start but log a warning: "No jobs enabled — yeti is running but idle." You can then add enabledJobs to your config and yeti will pick it up via live reload — no restart needed.
Configuration is resolved per-field in this priority order:
- Environment variables (highest priority)
- Config file at
~/.yeti/config.json - Hardcoded defaults (where a sensible default exists)
| Config key | Env variable | Default | Description |
|---|---|---|---|
githubOwners |
YETI_GITHUB_OWNERS |
["frostyard","frostyard"] |
GitHub accounts to scan (env var is comma-separated) |
selfRepo |
YETI_SELF_REPO |
frostyard/yeti |
Repo used for self-referencing error issues |
port |
PORT |
9384 |
HTTP server port |
intervals.issueWorkerMs |
— | 300000 (5 min) |
Issue worker poll interval |
intervals.issueRefinerMs |
— | 300000 (5 min) |
Issue refiner poll interval |
intervals.ciFixerMs |
— | 600000 (10 min) |
CI fixer poll interval |
intervals.reviewAddresserMs |
— | 300000 (5 min) |
Review addresser poll interval |
allowedRepos |
YETI_ALLOWED_REPOS |
(absent — no filtering) | Repo short-name allow-list (env var is comma-separated). Self-repo always included. |
enabledJobs |
— | [] (no jobs) |
Which jobs to register with the scheduler (live-reloadable) |
By default, allowedRepos is absent from your config, which means all discovered repos are processed — no action is required on upgrade. To restrict which repos Yeti operates on:
- Add
allowedReposto~/.yeti/config.jsonwith the short names of repos you want Yeti to manage:
{
"allowedRepos": ["yeti", "my-app", "docs"]
}-
The self-repo (e.g.,
yeti) is always included implicitly — you don't need to list it, but it's fine if you do. -
An empty list (
[]) means only the self-repo gets jobs. To process no repos at all, pause all jobs instead. -
Repo names are case-insensitive and apply across all configured
githubOwners.
These tools must be installed and authenticated on the host — they are not configured through config.json or environment variables:
| Tool | How to authenticate |
|---|---|
gh CLI |
gh auth login — must have access to all repos in githubOwners |
claude CLI |
Follow Claude CLI setup |
By default, Yeti uses your personal gh CLI credentials. If you enable branch protection with required PR reviews, you'll need Yeti to operate as a separate identity so you can approve its PRs. Configure a GitHub App to give Yeti a [bot] identity. Set githubAppId, githubAppInstallationId, and githubAppPrivateKeyPath in config — requires restart.
Yeti runs 10 jobs on timers. Each job scans repos under the configured githubOwners, filtered by allowedRepos if set. Understanding what triggers each job is important — most jobs do not require labels and will discover work based on PR/issue state.
| Job | Trigger | What it does |
|---|---|---|
| issue-worker | Refined label on issue |
Implements the issue in an isolated worktree, submits a PR |
These only fire when Yeti has already created branches or issues:
| Job | Trigger | What it does |
|---|---|---|
| review-addresser | yeti/ branch PR with review comments |
Addresses reviewer feedback on Yeti-created PRs |
| triage-yeti-errors | Issue with [yeti-error] in title |
Investigates Yeti error issues |
| repo-standards | Periodic (daily) | Syncs label definitions — does not create PRs or issues |
These scan all open issues/PRs and will do work without any Yeti-specific labels:
| Job | Trigger | What it does |
|---|---|---|
| issue-refiner | Issues labelled Needs Refinement |
Generates an implementation plan comment |
| plan-reviewer | Issues labelled Needs Plan Review |
Adversarial review of plans using a different AI backend |
| ci-fixer | Any PR with failing checks or merge conflicts | Attempts to fix CI failures and conflicts |
| improvement-identifier | Periodic scan of codebase | Creates PRs for code improvement opportunities |
| issue-auditor | All open issues | Audits and classifies issue state, applies labels |
| doc-maintainer | Code changes since last doc update | Updates documentation (only on already-cloned repos) |
The auto-merger job will merge PRs without human approval in these cases:
| PR type | Human review required? | Conditions for auto-merge |
|---|---|---|
| Dependabot PRs | No | All checks passing |
Doc PRs (yeti/docs-*) |
No | Only .md or yeti/ files changed; checks pass or no checks configured |
Issue PRs (yeti/issue-*) |
Yes — requires LGTM | A valid LGTM comment must be posted after the latest commit |
All other PRs (non-Yeti, non-Dependabot) are ignored by auto-merger.
Issues move through labels to track state:
(new issue) → (refiner runs) → Plan comment posted → Ready
→ (reviewer runs) → Plan reviewed → Ready [if plan-reviewer enabled]
→ Human reviews plan + critique, then either:
• Adds "Refined" to approve implementation
• Posts feedback → refiner refines → reviewer re-reviews
Refined → (worker runs) → PR created → In Review
In Review + LGTM → (merger runs) → PR merged
The Ready label always means "waiting for a human decision." When plan-reviewer is enabled, the human sees both the plan and an adversarial critique before deciding whether to proceed. The review is for the human, not for automatic refinement — this prevents infinite back-and-forth between AIs.
The Priority label is used for queue ordering across all jobs but does not trigger any job on its own.
src/
├── main.ts Entry point — sets up jobs and signal handlers
├── config.ts Constants: owners, labels, intervals
├── scheduler.ts Interval-based job runner (skip-if-busy)
├── github.ts gh CLI wrapper
├── claude.ts Multi-backend AI dispatch (Claude + Copilot) + worktree helpers
├── log.ts Timestamped logging
├── db.ts SQLite for task tracking and job logs
├── server.ts HTTP dashboard
├── error-reporter.ts Deduplicating error reporter (Discord + GitHub issues)
├── discord.ts Discord bot for notifications and commands
├── notify.ts Notification dispatcher (Discord)
└── jobs/
├── issue-refiner.ts Refines issues into implementation plans
├── plan-reviewer.ts Adversarial plan review (configurable AI backend)
├── issue-worker.ts Implements issues as PRs
├── ci-fixer.ts Fixes failing CI on PRs
├── auto-merger.ts Auto-merges approved PRs
├── review-addresser.ts Addresses PR review comments
├── improvement-identifier.ts Identifies code improvements
├── issue-auditor.ts Audits and classifies issues
├── doc-maintainer.ts Keeps documentation up to date
├── repo-standards.ts Syncs label definitions
└── triage-yeti-errors.ts Investigates yeti error issues
Based on claws-snapshot from the blog post Building an AI-powered GitHub automation tool.