Runs many single-hypothesis "arms" as do-not-merge PRs, measures each with paired, interleaved runs against a cost-matched control, keeps only the arms that clear a keep bar set in advance, records every negative, and composes the winners into a stack that is re-measured on real traffic after merge. It opens PRs and burns CI, so it runs only on `/lever arms` or an explicit ask to measure ideas ("which of these ideas actually helps?", "is this change worth it?", "try that tip I saw", "run arms on this") for a CI, perf, eval, benchmark or cost hill-climb where the system can be run on demand.
npx skills add https://github.com/github/lever --skill experiment-arms
The experiment arm method turns a pile of ideas into measured, landed improvements. Each hypothesis gets its own small, revertible PR (an arm) that exists only to be measured. Arms run in parallel against a shared control that does the same work, and each one is judged against a keep bar you wrote down before looking. Winners get composed into a stack and landed at whatever autonomy level the repo allows. Losers become closed [negative result] PRs plus a ledger row, so no one has to try them again. The number you report at the end comes from real traffic after merge, not from the estimate.
Start from a Loop Card (templates/loop-card.md). If you don't have one yet, run find-the-loop first.
Use it when:
Don't use it when:
| Role | Owns |
|---|---|
| Coordinator (you, or one session) | Loop Card, hypothesis table, ledger, budget, cutoff, status |
| One worker per arm | One change, its arm PR, its measurement, a KEEP/DROP report with numbers |
| Reviewer on a different model | Critique of the hypothesis table and the final stack (see cross-model-review) |
A coordinator that starts editing code stops coordinating. Keep it to orchestration.
baseline.md.budgets.max_parallel_arms allow, whichever is lower; queue the rest for the next batch. Each gets one worktree, one worker and one draft PR from a lever/arm-<name> branch, titled [measurement — do not merge] <arm> with a do-not-merge label. Push early so other work can stack on it. Don't serialize measurement while waiting for an earlier arm's result. Run everything now and decide the order afterwards.scripts/ci_arm_driver.py (see ci-adapter.md).10. Watch the base. If main (or the harness) breaks, void every round that ran on it. Merge the fix into every arm's measurement commit, since that's for measurement only, and re-run.
11. Decide keep or kill against the bar from step 5, using scripts/arm_stats.py (--min-pairs, default 6; it checks reliability before speed). Write every DROP to the ledger (templates/ledger.md) with its numbers, close its PR, and retitle it [negative result] <arm>. Re-test popular tips like any other arm: a widely shared one can lose on your hardware.
12. Compose the winners. Order them by dependency, then by measured value. Run one combined confirmation round (about 5 pairs) of the full stack against control, because effects don't always add up. If interactions are unclear, report a range ("between A and B depending on whether X and Y stack").
13. Land at the configured level. Open the real PRs, validate each one on its exact head SHA, and hand the set to landing-plan. Arms stay at L1. Winners land at the level resolved by docs/autonomy.md.
14. Re-measure after merge on real traffic at +24 h and +72 h. Publish the estimate next to the realized result, and say which is which. Paired arms are the *decision* instrument; post-merge traffic is the *reporting* instrument.
15. Clean up. Close every remaining arm and validation PR, file follow-ups, and feed the realized-vs-estimated error back into the next Loop Card.
Report status only when something changes, in the form metric: x → y (Δz%), with p50/p90, arms running/kept/dropped, budget used, and evidence level (templates/status-digest.md).
| Dial | Default | Options |
|---|---|---|
| Metric / statistic | felt metric, p50 + p90 | any metric with an instrument the agent can run |
| Keep bar | wins every round; median paired Δ ≥ bar and outside A/A noise; 95% CI excludes 0; equivalence passes | absolute (e.g. ≥ 1 min on the critical path), relative (≥ X%), cost or reliability win with no regression |
| A/A rounds before treatments | 3 | 0 (only if the noise floor is already known and recent) |
| Pairs per arm | pilot 3 → decide 6 → headline 10+ | 12+ for small effects |
| Rounds | 2 | 3 for borderline arms |
| Max concurrent arms | min(tier lane cap, budgets.max_parallel_arms), batched | hard cap 10, so shared runners aren't starved for real users |
| API rate floor | pause below 3,000 remaining core calls | any; set a shared budget across all agents |
| Poll interval | ≥ 4 min, query by head SHA only | longer for long jobs; never tight loops |
| Drivers per arm set | 1 | a duplicate driver's push cancels runs |
| Compute budget | the budget /lever hands down; billed minutes per arm, reported (unknown runner cost says unknown, not $0) | hard cap per round |
| Rerun policy | rerun failed jobs ≤ 2× as a person would; count reruns in the metric | 0 for pure-speed metrics on a stable fleet |
| Broken-base policy | void the round; merge the fix into every arm | pause until the base is green |
| Arm PRs | draft + do-not-merge label, closed at hand-off | local-only arms (no PR) for harness-only metrics |
| Negative results | closed [negative result] PR + ledger row | ledger row only |
| Merge authority for winners | repo ceiling via docs/autonomy.md | L2 hand-off · L3 on a recorded grant · L4 gated self-merge or enqueue |
| Cutoff | none | wall-clock time after which no new arms start |
| Post-merge re-measure | +24 h and +72 h | off only if the metric has no production signal |
baseline.md: historical p50/p90, critical path, noise floor.[negative result] PRs for the drops.x → y (Δz%), p50/p90, n, cost delta.Take github/experiment-arms from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.