An autonomous agent orchestrator that watches your GitHub project board and dispatches Claude Code agents to work on issues. It runs as a background daemon on macOS, polling every 3 minutes for work.
Three specialized agent types work together:
GitHub Issues
│
Assigned to bot
│
┌────────────┴────────────┐
│ │
No plan-approved Has plan-approved
│ │
┌──────┴──────┐ ┌─────┴─────┐
│ PLANNERS │ │ WORKER │
│ (up to N) │ │ (single) │
└──────┬──────┘ └─────┬─────┘
│ │
Read code, post plans Implements in
Revise on feedback git worktree
Detect approval Self-reviews
Add plan-approved label Creates PR
Create sub-issues │
│ ┌────┴────┐
│ │REVIEWERS│
│ │(up to M)│
│ └────┬────┘
│ │
│ Review PR
│ Push fixes
│ Approve/Request Changes
│ │
└───────────┬────────────┘
│
agent-waiting label
(ball in human's court)
Planners (up to MAX_PLANNERS concurrent, default 5) read issues and the codebase, write implementation plans, revise them based on feedback, and when the human approves, add the plan-approved label. They run in the repo root directory (read-only — never modify code). Multiple planners can work on different issues simultaneously.
Worker picks up issues with plan-approved, creates an isolated git worktree, implements the plan, self-reviews against the repo's CLAUDE.md standards, and creates a PR. It runs in the worktree so it never conflicts with your working copy.
Reviewers (up to MAX_REVIEWERS concurrent, default 3) are automatically triggered when a Worker creates a PR. They run two parallel review passes — semantic (plan alignment, scope) and engineering (CLAUDE.md standards) — push deterministic fixes, and submit a GitHub PR review. If the PR receives new commits after review, the reviewer re-reviews only the delta.
| Signal | Meaning |
|---|---|
| Assign issue to bot account | "Work on this" |
agent-waiting label |
Bot posted something, waiting for human |
plan-approved label |
Plan approved, worker should implement |
| Worker creates PR | Reviewer auto-triggered to review |
Human replies (while agent-waiting) |
Label auto-removed, agent re-engages immediately |
| Close issue | Done, agent ignores it |
- You create an issue and assign it to the bot account
- Planner reads the issue + codebase, posts a plan comment, adds
agent-waiting - You review the plan — approve or give feedback
- If feedback: Planner revises, re-posts, adds
agent-waitingagain - If approved: Planner adds
plan-approvedlabel - Worker creates a git worktree, implements, self-reviews, creates a PR, adds
agent-waiting - Reviewer automatically reviews the PR (semantic + engineering checks), pushes deterministic fixes, approves or requests changes
- You review the PR — merge or request changes
- If changes needed: Worker re-engages when you comment, Reviewer re-reviews the delta
For stories/epics, the Planner decomposes them into sub-issues (each auto-assigned to the bot with plan-approved), and the Worker implements them one at a time.
- macOS (uses launchd for background daemon)
- Claude Code CLI (
claude) installed and authenticated - GitHub CLI (
gh) installed and authenticated as the bot account - Python 3.10+ (for dashboard and stream parser)
- Git with worktree support (any modern version)
-
Bot account: Create a GitHub account for the agent (e.g.,
my-bot). Authenticateghas this account:gh auth login # Login as the bot account gh auth status # Verify: should show bot account
-
Project board: Create a GitHub Project (Projects v2) in your org. Add the bot as a member with write access to repos.
-
Token scopes: The bot's token needs:
repo,project,read:org,workflow. -
Labels: Create two labels in each repo the bot monitors:
agent-waiting— signals the bot posted and is waiting for human inputplan-approved— signals the plan is approved and the worker should implement
Workstrator expects a workspace directory containing your repo clones as siblings:
your-workspace/ # Parent directory
├── repo-a/ # Your repos (git clones)
├── repo-b/
├── repo-c/
└── workstrator/ # This directory
├── workstrator.sh # Main orchestrator daemon
├── config.sh # Your config (created from config.example.sh)
├── architecture.md # Your platform architecture (optional)
├── agent-prompt.md # Agent system prompt (state machine + conventions)
├── dashboard.py # Terminal UI
├── .stream-parser.py # Stream-json → text parser
├── install.sh # Install as launchd service
└── uninstall.sh # Remove service
cp config.example.sh config.shEdit config.sh with your GitHub org, bot account, repos, and project board IDs:
ORG="your-github-org"
PROJECT_NUMBER=1
BOT_LOGIN="your-bot-account"
REPOS="repo-a repo-b repo-c"
AGENT_MODEL="opus"
MAX_PLANNERS=5 # concurrent planner agents (default: 5)
MAX_REVIEWERS=3 # concurrent reviewer agents (default: 3)# Get project ID
gh project list --owner $ORG --format json | jq '.projects[] | {title, id, number}'
# Get field IDs
gh project field-list $PROJECT_NUMBER --owner $ORG --format json \
| jq '.fields[] | {name, id}'
# Get status option IDs (Todo, In progress, Done)
gh api graphql -f query='
query {
organization(login: "YOUR_ORG") {
projectV2(number: PROJECT_NUMBER) {
field(name: "Status") {
... on ProjectV2SingleSelectField {
options { id name }
}
}
}
}
}
'Add the IDs to config.sh:
PROJECT_ID="PVT_kwXXXXXX"
STATUS_FIELD_ID="PVTSSF_XXXXXXX"
STATUS_TODO="xxxxxxxx"
STATUS_IN_PROGRESS="xxxxxxxx"
STATUS_DONE="xxxxxxxx"If your platform has multiple services, create architecture.md from the example:
cp architecture.example.md architecture.mdEdit it with your service map, database collections, and integration points. This gets injected into every agent's system prompt so the Planner understands cross-repo relationships.
Each repo can have its own CLAUDE.md at its root with repo-specific coding conventions. The orchestrator automatically injects the repo's CLAUDE.md into the agent's system prompt alongside agent-prompt.md.
Example CLAUDE.md for a TypeScript repo:
# CLAUDE.md
## Stack
- Node 22, Express, TypeScript
- Tests: vitest
## Conventions
- No `any` types — use `unknown` and narrow
- async/await only — no `.then()` chains
- All function parameters and return types explicit
## Commands
- Build: `npm run build`
- Test: `npm test`
- Lint: `npm run lint`cd path/to/workstrator
bash install.shThis registers a macOS LaunchAgent that:
- Starts automatically on login
- Auto-restarts if it crashes (with 60s throttle)
- Captures PATH from your shell so it can find
claude,gh,git,python3
# Check service status
launchctl print gui/$(id -u)/com.workstrator
# Check logs
tail -f workstrator/logs/workstrator.logbash workstrator/uninstall.shbash uninstall.sh && bash install.shpython3 dashboard.pySplit-pane terminal UI:
- Header: Poll countdown, GraphQL remaining, service status
- Left pane: Running agents (Planners/Worker/Reviewers), issue queue, recent orchestrator log
- Right pane: Live agent output (auto-selects: worker > reviewer > most recent planner)
| Key | Action |
|---|---|
Up / Down |
Select issue in queue |
Tab |
Cycle right pane: Auto → Planner → Worker → Reviewer → Auto |
PgUp / PgDn |
Scroll agent log |
r |
Force refresh |
q |
Quit |
The dashboard reads all data from local files — it makes zero GitHub API calls. Board data comes from board-cache.json written by the workstrator each poll cycle.
- Create an issue in any monitored repo
- Assign it to your bot account
- The planner picks it up within 3 minutes
When the planner posts a plan, reply with one of:
- "approved", "lgtm", "looks good", "go ahead", "ship it"
- Or any reply that doesn't ask questions or suggest changes
To request revisions, just reply with your feedback. The planner will revise and re-post.
If you want the worker to implement immediately (e.g., a trivial fix), add the plan-approved label manually when creating the issue.
For large issues, the planner will propose a breakdown into sub-issues. When you approve, it creates the sub-issues (each auto-assigned to the bot with plan-approved) and the worker processes them sequentially.
GitHub allows 5,000 GraphQL calls/hour. Key costs:
| Operation | Approximate cost | Frequency |
|---|---|---|
| Board cache refresh | ~100 calls | Once per poll (3 min) |
| Repo scan (issue list) | ~1 per repo | Every poll |
| Issue detail fetch | ~1 per assigned issue | Every poll |
Agent gh commands |
Varies | During agent runs |
At 20 polls/hour with ~10 repos, the orchestrator uses ~2,400/hour, leaving headroom for agent work.
The dashboard reads from files only — it contributes zero API cost.
The orchestrator logs GraphQL usage every poll cycle:
Polling... (GraphQL remaining: 4685)
Poll complete. Checked 5 issues. Planners: 2 active. Worker: idle. Reviewers: 1 active. GraphQL: 4685→4664 (used 21)
The dashboard also shows a live poll countdown and GraphQL remaining in the header bar.
- Is the repo in REPOS? Check
REPOS=inconfig.sh. - Is the issue assigned to the bot? Check
gh issue view NUM --repo ORG/REPO --json assignees. - Does
agent-waitingneed removal? If the bot posted and is waiting, reply to trigger auto-removal. - Check the fingerprint:
cat state/planner-REPO-NUMorstate/worker-REPO-NUMorstate/reviewer-REPO-pr-NUM. Delete the state file to force reprocessing.
- Check if a stale worktree exists:
git worktree listin the repo directory - Clean up:
git worktree remove .worktrees/issue-NUM - Check if the branch already exists:
git branch | grep issue-NUM
- Check:
gh api rate_limit --jq '.resources.graphql' - Resets every hour. The orchestrator and dashboard will resume automatically.
- To reduce cost: increase
POLL_INTERVALinconfig.sh.
- Check launchd logs:
cat workstrator/logs/launchd-stderr.log - Check if another instance is running:
ls workstrator/.lock/ - Clean stale lock:
rm -rf workstrator/.lock - Verify
claudeis in PATH:which claude
- Add or update the repo's
CLAUDE.mdwith stricter conventions - The agent self-reviews against CLAUDE.md before creating PRs
- Review and reject PRs as you would with any contributor
The spec says review_comment_count in the fingerprint format, but get_pr_info() uses .reviews | length (review submissions, not inline comments). The implementation is more correct — a new review submission is a meaningful state change, while inline comment count is noisier. The spec should be updated to say review_count to match.
Every poll cycle calls get_pr_info() for each state/reviewer-* file. At 180s intervals this is fine for a handful of open PRs, but if state files accumulate (e.g., cleanup fails), it could add up. The MERGED/CLOSED cleanup path mitigates this — just worth monitoring. If you see GraphQL usage climbing, check ls state/reviewer-* for stale files.
| File | Purpose |
|---|---|
workstrator.sh |
Main daemon — polls, routes, spawns agents |
config.sh |
Your configuration (gitignored) |
config.example.sh |
Configuration template |
architecture.md |
Your platform architecture (optional, gitignored) |
architecture.example.md |
Architecture template |
agent-prompt.md |
Agent system prompt — state machine, conventions |
dashboard.py |
Terminal UI — reads local files, zero API cost |
.stream-parser.py |
Converts Claude's stream-json output to readable text |
install.sh |
Registers macOS LaunchAgent |
uninstall.sh |
Removes LaunchAgent |
logs/workstrator.log |
Orchestrator log (polls, agent lifecycle) |
logs/planner-*.log |
Per-run planner output |
logs/worker-*.log |
Per-run worker output |
logs/reviewer-*.log |
Per-run reviewer output |
state/planner-* |
Planner fingerprints (prevents re-processing unchanged issues) |
state/worker-* |
Worker fingerprints |
state/reviewer-* |
Reviewer fingerprints (3-line: fingerprint, last-reviewed SHA, issue num) |
running.json |
Currently running agents (read by dashboard) |
board-cache.json |
Project board snapshot (written by workstrator, read by dashboard) |
.lock/ |
Single-instance lock (mkdir-based, atomic) |
MIT