An autonomous "loop engineering" harness for Coding Agents. You don't prompt Claude ; loops do. Each loop wakes up on a cadence, gives a Claude session a task-specific skill, lets it work in an isolated git worktree, has a second Claude session verify the work, and only then ships the output (PR, comments, Slack message).
Warning
This harness runs an LLM with shell access, unattended, against your repositories.
Write-capable loops can commit code, open PRs, and (with output: commit) push to branches. Read the safety model before pointing it at anything you care about, start with read-only loops (pr-reviewer, issue-groomer), smoke-test with run-once in the foreground, and never grant tools a loop doesn't need. You are responsible for what your loops ship. No warranty — see LICENSE.
scheduler tick ─▶ due loop ─▶ git worktree ─▶ primary agent (claude -p + skill)
│
▼
staged output (commits / outbox files)
│
▼
verifier agent (claude -p, skeptical) ── FAIL ─▶ log + retry next cycle
│ PASS
▼
ship: push + PR / post comments / Slack ─▶ state updated
Requirements
bash3.2+ (macOS default works),git,jq,curlgh— authenticated (gh auth login)claude— Claude Code CLI, logged in
Environment variables
| Variable | Required | Purpose |
|---|---|---|
GITHUB_TOKEN |
no* | GitHub auth for gh (*or use gh auth login) |
LOOP_SLACK_WEBHOOK |
no | Slack incoming-webhook URL for notifications. Unset = Slack silently skipped |
LOOP_WORKTREE_ROOT |
no | Where worktrees are created (default: $TMPDIR/loop-worktrees) |
Never hardcode tokens anywhere in this tree. The env var names are configurable in config.yaml (github_token_env, slack_webhook_env).
Quick start
# 1. Create your config and point it at your repos cp config.example.yaml config.yaml $EDITOR config.yaml # 2. Make scripts executable (once, after clone) chmod +x orchestrator.sh dashboard.sh connectors/*.sh # 3. Smoke-test a single loop in the foreground ./orchestrator.sh run-once triage-ci ~/projects/my-app # 4. Start everything ./orchestrator.sh start # 5. Watch ./dashboard.sh ./orchestrator.sh logs # orchestrator log ./orchestrator.sh logs triage-ci # latest run of one loop # 6. Graceful shutdown (waits for in-flight loops, up to 5 min) ./orchestrator.sh stop
./orchestrator.sh tick runs exactly one scheduler pass — useful if you'd rather drive the harness from cron/launchd instead of the built-in daemon.
Dashboard
./dashboard.sh gives a live status table across all configured loops:
Orchestrator: RUNNING (pid 48121)
LOOP CADENCE STATUS LAST RUN RUNS SUCCESS% ITEMS AVG DUR FAILURES
---- ------- ------ -------- ---- -------- ----- ------- --------
dependency-updater 0 6 * * * success 06:01 4 100% 6 312s 0
doc-sync 0 7 * * * success 07:02 4 100% 2 198s 0
issue-groomer every 1h success 14:30 9 100% 14 87s 0
pr-reviewer every 3m running 15:21 112 98% 31 64s 2
triage-ci every 10m success 15:14 38 95% 9 241s 2
The five starter loops
| Loop | Cadence | Writes code? | Ships |
|---|---|---|---|
triage-ci |
every 10m | yes (worktree) | PR with verified CI fix, or diagnosis |
issue-groomer |
every 1h | no | labels + P0 implementation plans |
pr-reviewer |
every 3m (polls for new PRs) | no | inline comments, summary, approval if clean |
dependency-updater |
daily 06:00 | yes (worktree) | PR with test-verified updates |
doc-sync |
daily 07:00 | yes (worktree) | PR patching doc drift |
How safety works
- Worktree isolation — write loops never touch your checkout; each run gets a fresh worktree on a
loop/<name>/<ts>branch. - Staged outputs — the primary agent cannot post or push. It stages commits, a
PR_BODY.md, or "outbox" action files (issue-comment-<n>.md,pr-approve-<n>.md, ...). - Verification gate — a second
claude -psession with read-only-ish tools inspects the diff and staged outputs, runs cheap checks, and must printVERDICT: PASS. Only then does the orchestrator push/post. - Scoped permissions — each loop's
allowed_toolsis passed toclaude --allowedTools. - Idempotence — every loop's state file dedups processed item IDs (CI run IDs, issue/PR numbers, package@version, commit SHAs). Re-running is always safe.
- Graceful degradation — a failing loop logs, records the failure in state, and the orchestrator moves on. The daemon never crashes because a loop did.
State
One JSON file per loop-instance at state/<loop>@<repo>.json:
{
"last_run": 1760000000, "last_status": "success",
"processed": {"12345": "2026-06-10T09:00:00+0000"},
"in_progress": {},
"failures": [{"item": "98", "error": "verification failed", "retries": 1, "ts": "..."}],
"metrics": {"runs": 42, "successes": 40, "items_processed": 61, "total_duration_s": 9000}
}Delete a state file to make a loop reprocess everything. state/.run/ holds pidfiles and locks — safe to delete when nothing is running.
Adding a new loop
-
Skill —
skills/my-loop.md: role, steps, success criteria, failure handling, and a finalRESULT:line format (RESULT: DONE items=<id,...>/NOTHING_TO_DO/BLOCKED reason=...). Item IDs initems=drive deduplication — make them stable. -
Definition —
definitions/my-loop.yaml:name: my-loop cadence: every 30m # or a 5-field cron expression: "0 9 * * 1-5" trigger: schedule skill: my-loop.md worktree: true # true for anything that writes code output: pr # pr | issue-comment | slack-message | commit | log-only state_file: state/my-loop.json verify: true timeout_minutes: 15 allowed_tools: "Bash,Read,Edit,Write,Glob,Grep" notify_slack: false
-
Wire it — add the loop name to a repo's
loops:list inconfig.yaml. -
Test —
./orchestrator.sh run-once my-loop, read the logs, tune the skill. Thenstart.
Output types
pr— agent commits in its worktree + writesPR_BODY.md(uncommitted); orchestrator pushes the branch and opens the PR after verification.issue-comment/slack-message— agent stages action files in$LOOP_OUTBOX; orchestrator posts them after verification. Formats:NN-issue-comment-<n>.md,NN-issue-label-<n>.txt,NN-pr-comment-<n>.md,NN-pr-inline-<n>.jsonl,NN-pr-approve-<n>.md,NN-slack.md.commit— push directly to the default branch (use sparingly).log-only— no external output.
Triggers
schedule is native. git-push / webhook / file-change are implemented as fast polling (see pr-reviewer: 3-minute cadence + state dedup ≈ "on new PR"). For true push triggers, call ./orchestrator.sh run-once <loop> from a webhook handler or git hook — it's idempotent, so duplicate triggers are harmless.
Config reference (config.yaml)
repos: # repo ▸ loops mapping (inline list required) - path: ~/projects/my-app loops: [triage-ci, pr-reviewer] concurrency: 5 # max parallel Claude sessions log_retention_days: 7 scheduler_tick_seconds: 30 slack_webhook_env: LOOP_SLACK_WEBHOOK github_token_env: GITHUB_TOKEN claude_bin: claude # override to pin a path/version defaults: # per-loop fallbacks worktree: true verify: true max_retries: 2 timeout_minutes: 15
The config parser is intentionally small (portable shell): keep the file flat, inline [a, b] lists for loops:, no anchors or multiline values.
Troubleshooting
- Loop never fires — check
./dashboard.sh(cadence parsed?), thenlogs. Cron cadences need the daemon running during the matching minute. - PRs not opening —
gh auth status; check the run's.logfile for push errors; verifier may be failing (see.verify.lognext to the run log). - Everything BLOCKED — usually
ghauth or rate limits; the RESULT line in.agent.logsays why. - Stuck lock — if a machine crash leaves
state/.run/<instance>.lockbehind, delete it.