Yoke a team of coding agents to work through a GitHub issue queue.
yoke is a TypeScript orchestrator that closes the loop on agentic software development. Write issues. Yoke handles the rest: it picks up each issue, asks Claude to implement it in a local checkout, opens a draft pull request, self-reviews the diff, fixes anything that needs fixing β review comments, merge conflicts, failing CI β writes a polished final description, and squash-merges. Then it moves on to the next issue.
It is for developers who want the creative part of software development β expressing intent and reviewing outcomes β without babysitting every agent handoff.
Modern coding agents are powerful, but they still need a conductor β someone to decide what starts next, avoid overloading the repo, notice blocked tasks, run a review pass, route fixes back for another round, preserve closing references, and merge the finished work.
yoke makes that conductor programmable and autonomous, with Claude as the worker behind every step.
Give your repository a prioritized issue backlog and run the loop. The project becomes a living assembly line:
issues β Claude implementation β PR β self-review β fixes β squash merge
Yoke is a force multiplier for a solo developer who wants to stay in the creative and strategic flow. You decide what matters β write the issues, set the acceptance criteria, shape the architecture. Yoke handles the mechanical work:
- Picks up the next unblocked issue automatically, so you never lose momentum between tasks.
- Implements, reviews, and merges in the background while you focus on design, testing, and product direction.
- Resolves merge conflicts quietly β when branches diverge, Yoke asks Claude to rebase and fix the conflicts before continuing the review cycle, without you ever touching a rebase command.
- Fixes failing CI without being asked β it reads the check logs and pushes a fix.
- Keeps the PR queue clean β only finished, merged work accumulates; no half-baked draft PRs or stale branches.
- Respects your control via the
manuallabel β apply it to any issue or PR you want to keep under your direct hand.
The result: you stay in your highest-value role while Yoke pulls weight in the background to maintain the project's forward momentum.
The diagram below shows the full lifecycle of an issue through Yoke, covering both Simple SDLC (fully automated, the default) and Project SDLC (human-in-the-loop via a GitHub Projects board).
flowchart TD
ISSUE(["π GitHub Issue (open)"])
ISSUE --> MANUAL{"Labeled\n'manual'?"}
MANUAL -->|Yes| SKIP_MANUAL["β Skipped β human-only"]
MANUAL -->|No| DEPS{"Open\nblockers?"}
DEPS -->|Yes| WAIT_DEP["βΈ Wait β blocked until\ndependency closes"]
DEPS -->|No| MODE{"Project\nmode?"}
MODE -->|"Simple SDLC\n(default)"| IMPLEMENT
MODE -->|"Project SDLC\n(github_project_number set)"| READY{"Issue status = Ready,\nor In Progress with\nno open PR?"}
READY -->|No| SKIP_READY["β Skipped β not Ready,\nand not In Progress\nwithout open PR"]
READY -->|Yes| IMPLEMENT
IMPLEMENT["β Claude: Implement issue\nin fresh local checkout\ncommit + push branch"]
IMPLEMENT --> OPEN_PR["π¬ Yoke opens draft Pull Request"]
OPEN_PR --> CONFLICTS{"Merge\nconflicts?"}
CONFLICTS -->|Yes| RESOLVE["π§ Claude: Rebase branch\nresolve conflicts + push"]
RESOLVE --> CONFLICTS
CONFLICTS -->|No| CI{"CI checks\nstatus"}
CI -->|"Failing or\nstuck > 10 min"| FIX_CI["π§ Claude: Read CI logs\nfix failures + push"]
FIX_CI --> CI
CI -->|Passing| REVIEW["π Claude: Self-review\nthe PR diff"]
REVIEW --> REVIEW_RESULT{"Changes\npushed?"}
REVIEW_RESULT -->|Yes β pushed fixes| CI
REVIEW_RESULT -->|"No β clean"| BRANCH{"Project\nmode?"}
BRANCH -->|Simple SDLC| REVIEW2["π Claude: Second\nself-review pass"]
REVIEW2 --> RESULT2{"Changes\npushed?"}
RESULT2 -->|Yes| CI
RESULT2 -->|"No β second\nclean pass"| MERGE["β
Yoke: Generate final PR body\n+ squash merge"]
MERGE --> CLOSED(["π Issue auto-closed by GitHub"])
BRANCH -->|Project SDLC| REQUEST["π€ Yoke: Mark PR ready-for-review\nrequest human review\nmove issue β In Review"]
REQUEST --> HUMAN{"Human\naction"}
HUMAN -->|"Converts PR to draft\nor adds comments\nor moves issue back to Ready"| REVIEW
HUMAN -->|"Approves and merges"| CLOSED
In Simple SDLC mode β the default for any project whose env.yaml entry has no github_project_number β Yoke runs fully autonomously from issue to merged PR:
- Any open, unblocked, non-
manualissue is eligible to start. - Issues are prioritized bugs-first, then by milestone, then by creation time.
- Claude implements, the PR opens as a draft, Claude self-reviews twice (CI gates apply after any code-changing pass; two consecutive clean passes trigger the merge), and Yoke squash-merges β no human action required.
- Merge conflicts and CI failures are handled quietly in the background.
This mode is ideal for personal projects, greenfield work, and any context where CI and branch protections serve as the safety net.
Enable Project SDLC per project by setting github_project_number in env.yaml to a GitHub Projects v2 board number. This mode adds a human review gate before any merge:
- Issues in Ready status are picked up, and issues already in In Progress are also picked up if they have no open PR linked and no active agent session already running.
- When work starts on a Ready issue, it moves to In Progress.
- After one clean self-review, Yoke marks the PR ready-for-review, requests human review, and moves the issue to In Review β it never auto-merges.
- Yoke resumes work automatically if:
- A human converts the PR back to a draft (wants more changes).
- A new review comment arrives on the PR.
- The issue is moved back to Ready on the project board.
List the GitHub logins to notify when a PR is ready under the project's reviewers key in env.yaml.
This mode suits teams where a human QA or architect approves each merge, while Yoke handles the full implementation-review-fix loop.
On every iteration, yoke:
- Loads open GitHub issues, open pull requests, pending workflow approvals, and local agent-session state.
- Builds a dependency-aware work plan from issue age plus relationships like
blocked by #123,depends on #123, andblocks #123. - Enforces a configurable concurrency limit so the repo does not get flooded with half-finished work.
- For each eligible issue, runs Claude locally in a fresh checkout to implement the change and open a draft pull request.
- Checks CI status on every open PR β waits for passing checks before advancing, fixes failing checks, and escalates checks stuck for more than 10 minutes.
- Detects merge conflicts and asks Claude to resolve them before continuing.
- Asks Claude to self-review the diff and push fixes if needed. Requires two consecutive clean self-reviews before advancing.
- Generates a polished final PR description with Claude, updates the PR body, preserves closing references, and squash-merges. Retries with
--adminif GitHub's branch policy requires it.
When the Claude CLI reports that the subscription's usage limit is exhausted, yoke reads the reset time out of the CLI's own message and treats it as a hard hold on the whole engine pool: nothing is planned, claimed, checked out or run until it passes, and work resumes by itself the moment it does. GitHub-only maintenance (workflow approvals, reconciliation, the activity feed) keeps running throughout.
The hold is said once, not once per engine per cycle:
- In the terminal, a single line updates in place for the duration β
waiting 2h 14m until Sep 21, 8:50 AM for Claude usage limitβ with no repeated checkout output and no repeated identical failures behind it. - On the Dashboard, a banner across the top of the page shows what is being waited on, how much is left, and the wall-clock time it ends. It is replayed to a browser that connects mid-hold, so a reload never makes a parked run look merely idle.
GitHub rate-limit holds get the same treatment.
yoke treats GitHub as the source of truth and Claude as the worker behind every action:
- Issues are intent. Write clear issues and dependencies; the loop decides when they are safe to start.
- Pull requests are work cells. Each PR moves through review, fix, re-review, and merge phases automatically.
- Local session state is memory. A small persisted session store prevents duplicate work and lets each loop cycle pick up where the last one stopped.
- Humans stay in control. You own the backlog, repository settings, branch protections, CI, and review standards.
See the deeper docs:
Install dependencies:
npm installLog in to Claude Code (once, if not already done):
claude loginCreate env.yaml from the template, then add a GitHub PAT under github_tokens and the repositories to run under projects:
cp env.example.yaml env.yaml
# edit env.yaml: set github_tokens[0].token and projects[].github_repositoryenv.yaml is the only place Yoke reads settings from (it is git-ignored). See Configuration for every key.
Run a safe one-shot preview:
npm start -- --dry-run --onceRun the real loop:
npm startYoke loads env.yaml from the current working directory and works every repository listed under projects; there are no positional CLI arguments.
yoke opens a single real-time Dashboard in your browser at http://localhost:3000 (change the port with dashboard_port) when the loop starts. One dashboard covers all configured projects: there is a single shared pool of max_concurrency engine cylinders, and each project's own max_concurrency caps how many of those cylinders may work it at once. When more than one project is configured, every cylinder, lifecycle pill, broadcast-feed card, and event-log line is labelled with the project (owner/repo) it belongs to; with a single project the name appears in the header only. The Dashboard shows:
-
Issue β PR Lifecycle panel: a row of two-halved pills, one per open issue. The left half shows the issue; the right half shows the linked pull request and transitions through states:
- (absent) β no PR yet
- dotted outline β implementation is planned for this iteration
- solid outline, draft β PR is open as a draft
- solid outline, ready β PR is open and ready for review
- completed (full fill) β PR is merged/closed
Each pill is colour-coded to its issue's own stable colour β derived from the issue number, so an issue keeps the same colour forever β and the broadcast feed uses that same colour for everything pertaining to the issue.
-
Implementation / Review / Broadcast Feed panels showing live orchestrator logs and GitHub activity.
-
Keep-awake lamp: the ON light in the top-right corner is not a CSS animation β it is a tiny looping, muted video, playing for as long as the Dashboard is open. An actively playing video is the one thing observed to stop a locked-down laptop sleeping or locking mid-run, so the Dashboard plays one itself instead of you parking a full-screen YouTube video next to it. There is nothing to switch on: it runs whenever the Dashboard is open, not only while cylinders are working, and the Screen Wake Lock API is requested alongside it wherever the browser allows. If the browser refuses to autoplay the loop, the lamp turns amber and reads CLICK β one click or keypress anywhere on the page starts it. The lamp is sized to the height of the header's own text, so it lights the corner without making the header bar taller.
To prevent the Dashboard from opening automatically, pass --no-browser:
npm start -- --no-browserThe Dashboard server still starts; the URL is printed to stdout so you can open it manually.
Graceful shutdown: Press Escape while the loop is running to let Yoke finish any in-flight actions before exiting. Press Ctrl+C to exit immediately.
- Node.js 18+
gitonPATH.- A GitHub PAT, listed under
github_tokensinenv.yaml. - The
claudeCLI (Claude Code) installed locally, onPATH, and logged in viaclaude login. Uses your Claude Code subscription β no API key required.
All configuration lives in env.yaml in the working directory; copy env.example.yaml to create it. No configuration is read from environment variables. Yoke uses the GitHub PAT from github_tokens directly for API calls and Git clone/fetch/push operations. Claude Code authentication is still handled by the claude CLI: ANTHROPIC_API_KEY, GITHUB_TOKEN, GH_TOKEN, and YOKE_GITHUB_TOKEN (plus the legacy VIBRATOR_GITHUB_TOKEN) are removed from the environment of every Claude subprocess, so the agent authenticates only through your Claude Code login and the configured PAT.
GitHub token permissions
Fine-grained PATs need access to the target repository with:
- Metadata: read
- Contents: read/write
- Pull requests: read/write
- Issues: read/write
- Actions: read/write
- Checks: read
- Commit statuses: read
- Projects: read/write, if using project mode
- Workflows: write, if the agent may push workflow file changes
Classic PATs may need repo, project when using project mode, and workflow when the agent may edit or push workflow files.
Global keys (top level of env.yaml)
| Key | Default | Purpose |
|---|---|---|
github_tokens |
β | Required. List of named GitHub PATs, each { name, token, default? }. Projects that set no github_token_name use the entry marked default: true, or the first entry. |
projects |
β | Required. List of repositories to run; see the per-project keys below. |
max_concurrency |
3 |
Total size of the shared engine-cylinder pool across all projects. |
cycle_minimum_seconds |
60 |
Minimum seconds between engine cycle starts. |
claude_code_initial_model |
claude-sonnet-4-6 |
Claude model used during initial implementation. |
claude_code_review_model |
claude-opus-4-8 |
Claude model used during self-review. |
claude_code_initial_effort |
high |
Reasoning effort for initial implementation. |
claude_code_review_effort |
high |
Reasoning effort for self-review. |
claude_describe_model |
claude-haiku-4-5-20251001 |
Claude model used to write the final PR description before merge. A faster model is appropriate here. |
claude_timeout_seconds |
10800 (3 h) |
How long one Claude run may take before it is killed. Size it to the scope of your issues β generation is server-side, so a large implementation takes the time it takes regardless of your machine. A run cut short is salvaged as an incomplete draft PR rather than discarded. |
dashboard_port |
3000 |
HTTP port for the single shared Dashboard server. |
dashboard_title |
Outright Mental |
Title displayed in the Dashboard header. |
github_api_base_url |
https://api.github.com |
GitHub REST API base URL, for GitHub Enterprise. |
github_api_version |
2022-11-28 |
Value of the X-GitHub-Api-Version request header. |
Per-project keys (each entry under projects)
Per-project values override the global ones for that project.
| Key | Default | Purpose |
|---|---|---|
github_repository |
β | Required. Repository slug in owner/repo form. |
github_token_name |
default token | Name of the github_tokens entry to use for this project. |
max_concurrency |
global max_concurrency |
Cap on how many of the shared cylinders may work this project at once. Never exceeds the global pool. |
github_project_number |
β | GitHub Projects v2 board number. Enables Project SDLC for this project. |
reviewers |
[] |
GitHub logins to request review from (Project SDLC only). |
focus_mode |
false |
When true, only issues labelled focus (and the PRs that advance them) are worked. |
claude_code_initial_model / claude_code_review_model |
global value | Per-project model overrides. |
claude_code_initial_effort / claude_code_review_effort |
global value | Per-project effort overrides. |
claude_describe_model |
global value | Per-project override for the final-description model. |
claude_timeout_seconds |
global value | Per-project override of the per-run timeout. |
cycle_minimum_seconds |
global value | Per-project override of the cycle minimum. |
session_store_path |
<cwd>/.yoke/<owner>-<repo>-sessions.json |
Path for persisted local agent-session state. |
CLI flags
These are the only command-line options; there are no positional arguments.
| Flag | Purpose |
|---|---|
--once |
Run a single iteration, then exit. |
--dry-run |
Print the plan without executing any Claude or GitHub actions. |
--no-browser |
Start the Dashboard server but do not auto-open a browser window. |
Focus mode is a per-project setting, not a flag: set focus_mode: true on a project in env.yaml and Yoke will pick up only issues labelled focus in that repository, plus the PRs that advance them.
Use normal GitHub issues, plus lightweight relationship phrases in the issue body:
blocked by #12
depends on #12
blocks #34yoke will not start an issue while any referenced blocker remains open. Older eligible issues start first (bugs first, then milestone order, then creation time), up to max_concurrency.
Apply the manual label to any issue or PR to remove it from automated work:
- Issues labeled
manualare never picked up by Yoke. - PRs labeled
manualreceive no automated actions (no self-review, no conflict resolution, no auto-merge) and do not count againstmax_concurrency.
Yoke creates the manual label in the repository on startup if it does not already exist.
Apply the review label to an issue to opt it into the
implement-then-wait-for-review workflow
(outrightmental.com#409):
Yoke implements the issue, self-reviews, and fixes failing checks exactly
as usual β but after the clean self-review it marks the PR ready and stops.
The final PR is never squash-merged; it waits for a human. This is the flag
that automated intake (e.g. Mailbot's note-to-issue pipeline) applies so a
one-line emailed note becomes a reviewable PR without ever landing on main
unattended.
- Works in every mode; in Project SDLC it is redundant (that mode already never auto-merges).
- The label is copied onto the PR when it is opened, so the no-merge gate survives even if the issue is closed or relabelled mid-flight.
- A parked PR does not count against
max_concurrency. - Converting the PR back to draft, or commenting on it, re-queues it: Yoke addresses the feedback, self-reviews, and requests review again.
- Removing the label from both the issue and the PR returns the PR to the normal auto-merge flow.
Yoke creates the review label in the repository on startup if it does not already exist.
Milestones act as a priority queue, not a gate. Issues in earlier milestones are preferred over later-milestone issues, but all eligible unblocked issues can start regardless of milestone. Bug-typed issues (GitHub's native Issue Type = "Bug") always jump ahead of every other type.
Priority order: Bug > earlier milestone > later milestone > no milestone, then by creation time within each tier.
GitHub sub-issues are understood natively. A parent issue is automatically blocked until all of its open sub-issues are resolved β no explicit dependency phrases needed.
npm test
npm run buildThe Dashboard's keep-awake loop (src/dashboard/assets/keep-awake.{webm,mp4}) is committed, so a normal build needs nothing extra. To change how the lamp looks, edit and re-run tools/generate-keep-awake-video.py β it needs Pillow and GStreamer, neither of which Yoke itself depends on β then commit the regenerated files.
This project is intentionally small and sharp: a local orchestrator, a GitHub client, a session store, a Claude agent client, and a planning engine. It is early infrastructure for people who want to run software projects as agentic systems instead of manually copying prompts between tabs.
