An autonomous coding loop. It picks up work from a backlog, implements it, runs the tests, opens a PR, responds to review feedback, and follows the PR through to merge — repeating until there is nothing left to do.
The loop is the product; the agent is swappable. bot.agent selects Claude
Code (default), Codex, or Cursor.
One clone targets one project, configured by config.json. To drive several
repositories, clone once per project.
Each iteration picks a single story and acts on its status:
| Status | Action | Next |
|---|---|---|
pending |
implement, test, commit | committed |
committed |
push branch, open PR | pushed |
pushed |
merge if ready, otherwise address review feedback | merged / pushed |
merged |
nothing — terminal | terminal |
Stories are chosen reviewer-first, so a PR with a waiting reviewer is always
handled before new development starts. Every pushed PR is re-checked for
merge readiness on every iteration, so an approved PR can never get stuck.
See docs/workflow-state-machine.md for the full state machine and selection tiers.
- Claude Code CLI (or
codex/cursor-agent/bravebot, perbot.agent) - GitHub CLI (
gh), authenticated - Git, Python 3
- jq —
brew install jq/apt install jq - flock (optional) — used for run locking when present; without it a directory-based lock is used instead
- Access to the target GitHub repositories
git clone [email protected]:brave-experiments/brave-dev-loop.git my-project-loop
cd my-project-loop
make setupmake setup is interactive and idempotent — safe to re-run at any time. It
creates config.json and the data files, configures the bot's git identity and
commit signing, installs the pre-commit hooks, caches org membership, and
repairs configuration left over from older versions.
For an existing brave-core deployment, cp config.brave-core.json config.json
first to skip the wizard, then run make setup.
run.sh works from any directory; it resolves paths from its own location.
./run.sh # 10 iterations (default)
./run.sh 20 # 20 iterations
./run.sh 10 tui # interactive terminal UI
./run.sh --agent codex --model gpt-5
./run.sh --agent cursor
./run.sh --agent bravebot
./run.sh --agent bravebot --agent-bin ~/bravebot/target/release/bravebot
./run.sh --model opus
./run.sh 1 --agent bravebot --comparison-run # do the story twice and critique the first
./run.sh --status # what is running in this directoryAgent precedence is --agent flag > BOT_AGENT env var > bot.agent in
config. --model overrides the model for whichever agent is selected, and
--agent-bin overrides the binary it runs from — which is how bravebot is
pointed at a local build, since it has no config keys of its own.
tail -f data/progress.txt # watch progress
make schedules # install/update this project's cron jobs
make view-schedules # show the current schedule
make unmount-worktrees # remove idle story worktrees (ALL=1 for all)
./scripts/reset-run-state.sh # reset run state between runs
./scripts/reset-run.sh --stale # clear slots left behind by a killed runStory worktrees are collected by every run, so make unmount-worktrees is for
reclaiming the disk now — the ../<repo>-<issue> checkouts and their build
directories, a few gigabytes each. It removes the ones idle for over 24 hours;
ALL=1 removes every one whatever it holds, and DRY_RUN=1 only reports. See
Worktrees between runs for what
that can lose (uncommitted changes, and nothing else) and what is kept regardless.
To look at what a pull request actually did, open a shell in the worktree that holds its branch:
make worktree PR=https://github.com/brave/bravebot/pull/351
make worktree PR=351 # the repo comes from config.json
make worktree # asks which pull requestAn existing worktree is found by branch rather than by directory name, since a
worktree is named after its story's issue and not its branch. When none has the
branch, one is created from the pull request's head — in <target repo>-<issue>,
the directory that story's own worktree would use. Either way the main
checkout's .envrc is copied in and allowed, because untracked files are not
shared between worktrees and a fresh one would otherwise have no environment.
Exit the shell to come back. No model is involved, and nothing is committed or
pushed.
Scheduled jobs are per-project: make schedules installs one crontab block for
the project in config.json and leaves any other deployment's block alone, and
what goes in it comes from projects/<profile>/schedules.sh — see
Project profiles.
Several runs can share one bot directory — set bot.maxConcurrentRuns and see
Concurrent runs. It is off by default.
--comparison-run redoes each story with a second tool in a throwaway worktree
and files issues for what the first tool did worse. Also off by default, and it
triples the cost of an iteration: see Comparison runs.
A run titles its terminal tab #<issue> PR #<pr> <story title>, so tabs are
told apart by the numbers you search GitHub by; the PR number joins the title
as soon as the story has one, mid-iteration. Stories with neither number show
their story id instead.
Stop with Ctrl+C; the bot returns the target repo to its default branch on
the way out.
Skills are run interactively from the bot directory:
claude
> /review-prs 1d open| Configuration | config.json keys, PRD format, run state |
| Concurrent runs | Run slots, claims, --status, clearing a killed run |
| Project profiles | Per-project validations, test targets, docs |
| Comparison runs | --comparison-run: redo a story with another tool and critique the first |
| Skills | The slash commands this repo provides |
| Development | Working on the loop itself: tests, lint, the security scan |
| Bot identity | Signing, SSH keys, gh isolation, hooks |
| State machine | Task selection and status transitions |
| Testing requirements | What the bot must run before claiming success |
| PR descriptions | The body shape reviewers get, and the checker that enforces it |
| Signal notifications | Optional real-time alerts |
| Troubleshooting | Common failures |
| Security | Prompt-injection handling for GitHub data |
Per-status workflow docs live in docs/workflow-*.md, and the agent's own
instructions in .claude/CLAUDE.md.
make test # pytest
make lint # ruff check + format --check
make format # ruff check --fix + format
make check-reviewdog # the brave/security-action scan, on this branch's changes
make check # all three: the before-you-push pass./tests/test-suite.sh validates an installation end to end.
The security scan is the only check CI runs on pull requests here, and
make check-reviewdog is the same scan locally — see
Development.