{"slug": "loop-harness-is-here", "title": "Loop Harness Is Here", "summary": "Loop Harness, an autonomous loop engineering tool for coding agents, is now available, enabling scheduled Claude sessions to perform tasks like code review and PR creation in isolated git worktrees with a second verification step before shipping output. The system runs LLMs with shell access unattended against user repositories, capable of committing code and opening pull requests, with a safety model that requires users to start with read-only loops and never grant unnecessary tools.", "body_md": "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).\n\nWarning\n\n**This harness runs an LLM with shell access, unattended, against your repositories.**\nWrite-capable loops can commit code, open PRs, and (with `output: commit`\n\n) push to branches. Read the [safety model](#how-safety-works) before pointing it at anything you care about, start with read-only loops (`pr-reviewer`\n\n, `issue-groomer`\n\n), smoke-test with `run-once`\n\nin the foreground, and never grant tools a loop doesn't need. You are responsible for what your loops ship. No warranty — see [LICENSE](/lSAAGl/loop-harness/blob/main/LICENSE).\n\n```\nscheduler tick ─▶ due loop ─▶ git worktree ─▶ primary agent (claude -p + skill)\n                                  │\n                                  ▼\n                     staged output (commits / outbox files)\n                                  │\n                                  ▼\n                  verifier agent (claude -p, skeptical) ── FAIL ─▶ log + retry next cycle\n                                  │ PASS\n                                  ▼\n                  ship: push + PR / post comments / Slack ─▶ state updated\n```\n\n`bash`\n\n3.2+ (macOS default works),`git`\n\n,`jq`\n\n,`curl`\n\n— authenticated (`gh`\n\n`gh auth login`\n\n)— Claude Code CLI, logged in`claude`\n\n| Variable | Required | Purpose |\n|---|---|---|\n`GITHUB_TOKEN` |\nno* | GitHub auth for `gh` (*or use `gh auth login` ) |\n`LOOP_SLACK_WEBHOOK` |\nno | Slack incoming-webhook URL for notifications. Unset = Slack silently skipped |\n`LOOP_WORKTREE_ROOT` |\nno | Where worktrees are created (default: `$TMPDIR/loop-worktrees` ) |\n\nNever hardcode tokens anywhere in this tree. The env var *names* are configurable in `config.yaml`\n\n(`github_token_env`\n\n, `slack_webhook_env`\n\n).\n\n```\n# 1. Create your config and point it at your repos\ncp config.example.yaml config.yaml\n$EDITOR config.yaml\n\n# 2. Make scripts executable (once, after clone)\nchmod +x orchestrator.sh dashboard.sh connectors/*.sh\n\n# 3. Smoke-test a single loop in the foreground\n./orchestrator.sh run-once triage-ci ~/projects/my-app\n\n# 4. Start everything\n./orchestrator.sh start\n\n# 5. Watch\n./dashboard.sh\n./orchestrator.sh logs            # orchestrator log\n./orchestrator.sh logs triage-ci  # latest run of one loop\n\n# 6. Graceful shutdown (waits for in-flight loops, up to 5 min)\n./orchestrator.sh stop\n```\n\n`./orchestrator.sh tick`\n\nruns exactly one scheduler pass — useful if you'd rather drive the harness from cron/launchd instead of the built-in daemon.\n\n`./dashboard.sh`\n\ngives a live status table across all configured loops:\n\n```\nOrchestrator: RUNNING (pid 48121)\n\nLOOP                   CADENCE      STATUS     LAST RUN   RUNS   SUCCESS%  ITEMS    AVG DUR   FAILURES\n----                   -------      ------     --------   ----   --------  -----    -------   --------\ndependency-updater     0 6 * * *    success    06:01      4      100%      6        312s      0\ndoc-sync               0 7 * * *    success    07:02      4      100%      2        198s      0\nissue-groomer          every 1h     success    14:30      9      100%      14       87s       0\npr-reviewer            every 3m     running    15:21      112    98%       31       64s       2\ntriage-ci              every 10m    success    15:14      38     95%       9        241s      2\n```\n\n| Loop | Cadence | Writes code? | Ships |\n|---|---|---|---|\n`triage-ci` |\nevery 10m | yes (worktree) | PR with verified CI fix, or diagnosis |\n`issue-groomer` |\nevery 1h | no | labels + P0 implementation plans |\n`pr-reviewer` |\nevery 3m (polls for new PRs) | no | inline comments, summary, approval if clean |\n`dependency-updater` |\ndaily 06:00 | yes (worktree) | PR with test-verified updates |\n`doc-sync` |\ndaily 07:00 | yes (worktree) | PR patching doc drift |\n\n**Worktree isolation**— write loops never touch your checkout; each run gets a fresh worktree on a`loop/<name>/<ts>`\n\nbranch.**Staged outputs**— the primary agent cannot post or push. It stages commits, a`PR_BODY.md`\n\n, or \"outbox\" action files (`issue-comment-<n>.md`\n\n,`pr-approve-<n>.md`\n\n, ...).**Verification gate**— a second`claude -p`\n\nsession with read-only-ish tools inspects the diff and staged outputs, runs cheap checks, and must print`VERDICT: PASS`\n\n. Only then does the orchestrator push/post.**Scoped permissions**— each loop's`allowed_tools`\n\nis passed to`claude --allowedTools`\n\n.**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.\n\nOne JSON file per loop-instance at `state/<loop>@<repo>.json`\n\n:\n\n```\n{\n  \"last_run\": 1760000000, \"last_status\": \"success\",\n  \"processed\": {\"12345\": \"2026-06-10T09:00:00+0000\"},\n  \"in_progress\": {},\n  \"failures\": [{\"item\": \"98\", \"error\": \"verification failed\", \"retries\": 1, \"ts\": \"...\"}],\n  \"metrics\": {\"runs\": 42, \"successes\": 40, \"items_processed\": 61, \"total_duration_s\": 9000}\n}\n```\n\nDelete a state file to make a loop reprocess everything. `state/.run/`\n\nholds pidfiles and locks — safe to delete when nothing is running.\n\n-\n**Skill**—`skills/my-loop.md`\n\n: role, steps, success criteria, failure handling, and a final`RESULT:`\n\nline format (`RESULT: DONE items=<id,...>`\n\n/`NOTHING_TO_DO`\n\n/`BLOCKED reason=...`\n\n). Item IDs in`items=`\n\ndrive deduplication — make them stable. -\n**Definition**—`definitions/my-loop.yaml`\n\n:\n\n```\nname: my-loop\ncadence: every 30m        # or a 5-field cron expression: \"0 9 * * 1-5\"\ntrigger: schedule\nskill: my-loop.md\nworktree: true            # true for anything that writes code\noutput: pr                # pr | issue-comment | slack-message | commit | log-only\nstate_file: state/my-loop.json\nverify: true\ntimeout_minutes: 15\nallowed_tools: \"Bash,Read,Edit,Write,Glob,Grep\"\nnotify_slack: false\n```\n\n-\n**Wire it**— add the loop name to a repo's`loops:`\n\nlist in`config.yaml`\n\n. -\n**Test**—`./orchestrator.sh run-once my-loop`\n\n, read the logs, tune the skill. Then`start`\n\n.\n\n`pr`\n\n— agent commits in its worktree + writes`PR_BODY.md`\n\n(uncommitted); orchestrator pushes the branch and opens the PR after verification.`issue-comment`\n\n/`slack-message`\n\n— agent stages action files in`$LOOP_OUTBOX`\n\n; orchestrator posts them after verification. Formats:`NN-issue-comment-<n>.md`\n\n,`NN-issue-label-<n>.txt`\n\n,`NN-pr-comment-<n>.md`\n\n,`NN-pr-inline-<n>.jsonl`\n\n,`NN-pr-approve-<n>.md`\n\n,`NN-slack.md`\n\n.`commit`\n\n— push directly to the default branch (use sparingly).`log-only`\n\n— no external output.\n\n`schedule`\n\nis native. `git-push`\n\n/ `webhook`\n\n/ `file-change`\n\nare implemented as fast polling (see `pr-reviewer`\n\n: 3-minute cadence + state dedup ≈ \"on new PR\"). For true push triggers, call `./orchestrator.sh run-once <loop>`\n\nfrom a webhook handler or git hook — it's idempotent, so duplicate triggers are harmless.\n\n```\nrepos:                      # repo ▸ loops mapping (inline list required)\n  - path: ~/projects/my-app\n    loops: [triage-ci, pr-reviewer]\nconcurrency: 5              # max parallel Claude sessions\nlog_retention_days: 7\nscheduler_tick_seconds: 30\nslack_webhook_env: LOOP_SLACK_WEBHOOK\ngithub_token_env: GITHUB_TOKEN\nclaude_bin: claude          # override to pin a path/version\ndefaults:                   # per-loop fallbacks\n  worktree: true\n  verify: true\n  max_retries: 2\n  timeout_minutes: 15\n```\n\nThe config parser is intentionally small (portable shell): keep the file flat, inline `[a, b]`\n\nlists for `loops:`\n\n, no anchors or multiline values.\n\n**Loop never fires**— check`./dashboard.sh`\n\n(cadence parsed?), then`logs`\n\n. Cron cadences need the daemon running during the matching minute.**PRs not opening**—`gh auth status`\n\n; check the run's`.log`\n\nfile for push errors; verifier may be failing (see`.verify.log`\n\nnext to the run log).**Everything BLOCKED**— usually`gh`\n\nauth or rate limits; the RESULT line in`.agent.log`\n\nsays why.**Stuck lock**— if a machine crash leaves`state/.run/<instance>.lock`\n\nbehind, delete it.", "url": "https://wpnews.pro/news/loop-harness-is-here", "canonical_source": "https://github.com/lSAAGl/loop-harness", "published_at": "2026-06-11 23:31:07+00:00", "updated_at": "2026-06-11 23:49:06.861938+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "ai-infrastructure", "large-language-models"], "entities": ["Claude", "Loop Harness", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/loop-harness-is-here", "markdown": "https://wpnews.pro/news/loop-harness-is-here.md", "text": "https://wpnews.pro/news/loop-harness-is-here.txt", "jsonld": "https://wpnews.pro/news/loop-harness-is-here.jsonld"}}