Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ breaking changes may land in a minor release.

### Added

- **Cursor CLI (`cursor-agent`) profile** — a packaged `cursor` profile plus a new
`cursor-hooks-json` hook dialect, so the generic adapter drives Cursor with no Python.
Skills live in `.cursor/skills/`; the relay registers `sessionStart` and `stop` in a project
`.cursor/hooks.json`. That file is versioned and its entries are bare `{"command": …}`
objects — Cursor 3.x loads no hooks from a project file lacking a numeric top-level
`version`, so `merge_hooks` always writes one. Launches with `--force --trust`: an
interactive launch in an untrusted directory blocks on a workspace-trust dialog no
unattended session can answer, and `--force` alone does not clear it. Setting
`[adapter] extra_args` replaces the bypass flags, so it must keep `--trust`. Trust is
granted per launch, so `isolation = "worktree"` works. `usage_parser = "none"` pending a
transcript-schema probe, so no token usage is recorded. Supported and E2E-verified on
cursor-agent 2026.09.02: `probe-adapter cursor --probe` captured both events with 0
warnings, and a one-story dev loop completed with a real commit.
- Add a free-form `effort` key to `[adapter]` and every `[adapter.<stage>]` table,
inherited like `model`; `opencode-http` sends it as the per-prompt `variant` on
every turn, and `validate` warns (`policy.effort-unsupported`) when a tmux stage
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Plain Python drives the loop — **pick story → implement → adversarially re
![Status: early open beta](https://img.shields.io/badge/status-early%20open%20beta-orange)
[![CI](https://github.com/bmad-code-org/bmad-loop/actions/workflows/ci.yml/badge.svg)](https://github.com/bmad-code-org/bmad-loop/actions/workflows/ci.yml)
![Python](https://img.shields.io/badge/python-3.11%E2%80%933.14-blue)
![CLIs](https://img.shields.io/badge/agents-claude%20%C2%B7%20codex%20%C2%B7%20gemini%20%C2%B7%20copilot%20%C2%B7%20antigravity%20%C2%B7%20opencode-8a2be2)
![CLIs](https://img.shields.io/badge/agents-claude%20%C2%B7%20codex%20%C2%B7%20gemini%20%C2%B7%20copilot%20%C2%B7%20cursor%20%C2%B7%20antigravity%20%C2%B7%20opencode-8a2be2)
![No LLM in the loop](https://img.shields.io/badge/control%20loop-deterministic-success)
![License: MIT](https://img.shields.io/badge/license-MIT-green)

Expand Down Expand Up @@ -45,12 +45,12 @@ Inspired by the original [bmad-automator](https://github.com/bmad-code-org/bmad-
- 🔍 **Trust nothing, verify everything.** After each session the orchestrator checks artifacts on disk: spec frontmatter status, baseline-commit validity, non-empty diff, sprint-status sync, and _your_ test/lint commands before any commit. An exact recorded baseline passes; so does a uniquely resolved immutable descendant that is reachable from `HEAD`, but its proof is measured after that commit and must be tracked, staged, or committed — untracked-only residue does not count. Deferred-work bundles retain their older-ancestor exception. In the default shared checkout, later tracked changes prove work exists but cannot identify which session made it; `[scm] isolation = "worktree"` preserves that provenance.
- 📒 **One source of truth.** `sprint-status.yaml` is the workflow ledger: the loop's dev skill flips only its story spec's status, the orchestrator mirrors that onto the board through a single idempotent, never-regress writer, and verification re-checks the stage after every session.
- 🪟 **Fresh context per step.** Dev and review are separate sessions — review never inherits the implementer's context, so there's no anchoring bias.
- ♻️ **Resumable & multi-agent.** Every run is a resumable state machine on disk, and a generic tmux adapter drives `claude`, `codex`, `gemini`, `copilot`, or `antigravity` (mix per stage).
- ♻️ **Resumable & multi-agent.** Every run is a resumable state machine on disk, and a generic tmux adapter drives `claude`, `codex`, `gemini`, `copilot`, `cursor`, or `antigravity` (mix per stage).
- 🌿 **Optional worktree isolation.** Opt in (`[scm] isolation = "worktree"`) and each story runs in its own git worktree/branch and merges back locally — your main checkout stays free while a run is in flight.

## Requirements

- **Python 3.11+**, a **terminal multiplexer** (tmux is the bundled default; 3.2 is the supported minimum, not enforced at selection), **git 2.34 or newer** (the supported minimum, and this one _is_ enforced — `run`, `sweep` and `resume` refuse to start below it and `validate` reports it as a problem), and a supported coding CLI — `claude` by default; `codex`, `gemini`, `copilot`, and `antigravity` (`agy`) via [profiles](#other-coding-clis).
- **Python 3.11+**, a **terminal multiplexer** (tmux is the bundled default; 3.2 is the supported minimum, not enforced at selection), **git 2.34 or newer** (the supported minimum, and this one _is_ enforced — `run`, `sweep` and `resume` refuse to start below it and `validate` reports it as a problem), and a supported coding CLI — `claude` by default; `codex`, `gemini`, `copilot`, `cursor` (`cursor-agent`), and `antigravity` (`agy`) via [profiles](#other-coding-clis).
- **Linux or macOS** (or **Windows via WSL**, which _is_ Linux — it runs as-is). tmux is the bundled terminal-multiplexer backend (externals like the [herdr adapter](https://github.com/pbean/bmad-loop-adapter-herdr) co-install as packages and self-register — see [Terminal multiplexer backends](docs/multiplexer-backends.md)), and all of it sits behind a pluggable **registry** of OS seams (transport, process lifecycle, hook interpreter) with availability-aware selection — env var → persisted `[mux] backend` choice (`bmad-loop mux set <name>`) → platform default (`psmux` on Windows, `tmux` elsewhere) → first available platform match — so a native-Windows backend slots in as new files + a registration line each, with no engine edits — see [Porting bmad-loop to a new OS](docs/porting-to-a-new-os.md). Native Windows is not yet shipped.
- A **BMAD v6 project** (`_bmad/bmm/config.yaml`, a `sprint-status.yaml` from `bmad-sprint-planning`) on **BMAD-METHOD ≥ 6.10.0**, with three skill sets installed (standard BMAD skills stay untouched):
- the upstream dev primitive — `bmad-build-auto`, or a complete `bmad-dev-auto` on pre-rename releases. bmad-loop drives whichever is on disk under that name, so either era works with no config edit; the bare forwarding shim the rename leaves behind is refused as incomplete — it has neither `step-04-review.md` nor `customize.toml` — because a session dispatched into it stalls on an interactive migration gate.
Expand Down Expand Up @@ -459,7 +459,7 @@ skill = "bmad-dev-auto" # the only supported value — the generic upstream d
# No settings-schema entry: edit it here, not in the TUI editor.

[adapter]
name = "claude" # CLI profile: claude | codex | gemini | copilot | antigravity | opencode-http (alias: opencode) | custom
name = "claude" # CLI profile: claude | codex | gemini | copilot | cursor | antigravity | opencode-http (alias: opencode) | custom
model = "" # empty = CLI default (opencode-http wants "provider/model")
effort = "" # reasoning effort, free-form (e.g. "high", "max"); empty = provider default.
# Sent by opencode-http as the per-prompt `variant`; the tmux CLIs have no
Expand Down Expand Up @@ -678,6 +678,7 @@ One generic driver (`adapters/generic.py`) runs any coding CLI that fits the inj
| `codex` | supported, E2E-verified | Codex ≥ 0.139. No slash expansion in the initial prompt — the profile renders `$skill-name` mentions (plus a "use subagents as needed" nudge) instead. No SessionEnd hook; window-death fallback covers crashes. |
| `gemini` | supported, E2E-verified | Gemini CLI ≥ 0.46 (hooks on by default since then). Launches with `-i` to stay interactive; `AfterAgent` maps to canonical Stop. Usage parser validated against real chat logs. |
| `copilot` | supported, E2E-verified | GitHub Copilot **CLI** (the `copilot` binary, GA ≥ 2026-02) — _not_ the VS Code extension. Launches with `-i` to stay interactive; turn-end is `agentStop` (per response turn); `--allow-all-tools` for unattended runs. `copilot-events` usage parser reads token totals from the trailing `session.shutdown` line, so the profile waits a short grace (`usage_grace_s = 8`) before tallying. **Pin a capable model** (see below). |
| `cursor` | supported, E2E-verified | **Cursor CLI** (`cursor-agent`), verified against 2026.08.04 and 2026.09.02. Skills live in `.cursor/skills/`; hooks in a project `.cursor/hooks.json`, whose top-level `version` is required — without it Cursor 3.x loads no hooks at all. `stop` is the turn-end event, `sessionStart` marks liveness. **`--trust` is mandatory for unattended runs**: an interactive launch in an untrusted directory blocks on a workspace-trust dialog and `--force` alone does _not_ clear it, so the profile ships `--force --trust` in its bypass flags — if you set `[adapter] extra_args` (which _replaces_ them) you must keep `--trust`. Because the flag is per-launch, `isolation = "worktree"` works, unlike antigravity. `usage_parser = "none"` for now: the Stop payload names a transcript, but its token schema is unread, so no token usage is recorded yet. |
| `antigravity` | experimental — `isolation = "none"` only | Google **Antigravity CLI** (`agy` ≥ 1.1.3). Launches with `-i` to stay interactive; `Stop` is the turn-end event (agy has no SessionStart/SessionEnd hook). Skills and hooks live in `.agents/` (flat `Stop` handler in `.agents/hooks.json`, keyed by hook-group name). Hook payloads are protojson/camelCase. **Trust is exact-path**: `agy` blocks on a "trust this folder" dialog for any workspace not listed verbatim in `settings.json` `trustedWorkspaces`, and `--dangerously-skip-permissions` does not bypass it — so `isolation = "worktree"` hangs ([#169](https://github.com/bmad-code-org/bmad-loop/issues/169)). `usage_parser = "none"` is permanent, not pending: agy's transcript carries no usage data (tokens live only in an internal SQLite/protobuf store), so runs work but token columns stay empty. Verify against your build with `probe-adapter antigravity`. |
| `opencode` | supported, E2E-verified | **OpenCode** ≥ 1.18 (profile `opencode-http`), driven over HTTP/SSE — one headless `opencode serve` per session, **no tmux window**. Needs the extra: `pip install 'bmad-loop[opencode]'`. Auth once globally with `opencode auth login`; skills live in `.claude/skills/`; set `model` as `provider/model` (e.g. `anthropic/claude-haiku-4-5`). Watch sessions via `run_dir/logs/<task_id>.log` or the TUI Log tab; `resolve` is `--no-interactive` only; the Unity plugin's window guards are unsupported here. |

Expand All @@ -687,7 +688,7 @@ One generic driver (`adapters/generic.py`) runs any coding CLI that fits the inj

**Shared prerequisites:** the `bmad-loop-*` skills must be present in `.agents/skills/` (codex and gemini read it; Claude Code reads `.claude/skills/`), and each CLI must have been run once interactively in the project for auth/trust — `bmad-loop init --cli codex --cli gemini` installs the skills into `.agents/skills/`, registers the hook relay, and prints the per-CLI first-run steps.

**Adding a CLI without touching Python:** drop a TOML file in `<project>/.bmad-loop/profiles/<name>.toml` with at minimum a binary, `prompt_template`, bypass flags, and a `[hooks]` block picking one of the config dialects (`claude-settings-json` / `codex-hooks-json` / `gemini-settings-json` / `copilot-settings-json` / `antigravity-hooks-json`) plus a native→canonical event map. Every `CLIProfile` / `HookSpec` field and its default lives in the **[Profile field reference](docs/adapter-authoring-guide.md#profile-field-reference)**. The hook relay and orchestrator are CLI-agnostic — each registration passes the canonical event name as the script argument — so a CLI cloning an existing dialect needs nothing else; a genuinely different transport gets its own adapter class ([how](docs/adapter-authoring-guide.md#writing-a-new-adapter-class); worked example: `adapters/opencode_http.py`).
**Adding a CLI without touching Python:** drop a TOML file in `<project>/.bmad-loop/profiles/<name>.toml` with at minimum a binary, `prompt_template`, bypass flags, and a `[hooks]` block picking one of the config dialects (`claude-settings-json` / `codex-hooks-json` / `gemini-settings-json` / `copilot-settings-json` / `cursor-hooks-json` / `antigravity-hooks-json`) plus a native→canonical event map. Every `CLIProfile` / `HookSpec` field and its default lives in the **[Profile field reference](docs/adapter-authoring-guide.md#profile-field-reference)**. The hook relay and orchestrator are CLI-agnostic — each registration passes the canonical event name as the script argument — so a CLI cloning an existing dialect needs nothing else; a genuinely different transport gets its own adapter class ([how](docs/adapter-authoring-guide.md#writing-a-new-adapter-class); worked example: `adapters/opencode_http.py`).

**Finalizing a profile:** the facts a profile needs that live in no doc — the CLI's exact hook payload shape, its transcript location/format, and the token schema a `usage_parser` reads — are collected and sanitized by `bmad-loop probe-adapter <cli>` (a zero-launch scan by default, or `--probe` for a live capture). The [adapter authoring guide](docs/adapter-authoring-guide.md) walks through using it end to end.

Expand Down
Loading