auto-drive-to-merge · v1.0.0 · 2026-10-18 · sha256 a398d0e874372c10
auto-drive-to-merge v1.0.0A
Immutable. This exact content never changes and is served at /api/v1/blob/a398d0e874372c10.
---
name: auto-drive-to-merge
description: |
Wraps default-workflow and drives the resulting PR all the way to merge:
build the PR, loop with the crusty-old-engineer skill as the maintainer's
proxy until zero concerns remain, then loop on the merge-ready criteria —
qa-team scenarios, CI, conflicts, review threads — until every one holds, and
merge behind an evidence gate. Both loops are terminated by agentic
loop-health evaluation, never by an iteration count.
Use when: "drive this to merge", "take this all the way", "build it and land it".
version: 1.0.0
allowed-tools: ["Read", "Grep", "Glob", "Bash", "Agent", "Edit", "Write"]
user-invocable: true
auto-activation:
priority: 4
keywords:
[
"auto drive to merge",
"drive to merge",
"drive it to merge",
"take it to merge",
"land this pr",
"all the way to merge",
"until crusty is happy",
"until it merges",
]
---
# Auto Drive To Merge
Build the thing, satisfy the maintainer's proxy, satisfy every merge-ready
criterion, then merge. Three phases, two loops, one gate.
```mermaid
flowchart TD
A[task] --> P0{criterion 1 needs gadugi?<br/>Node repo, or AUTODRIVE_QA_SCENARIO_DIR}
P0 -->|no: Rust CLI, Python| B[Phase 1: BUILD<br/>default-workflow, no_merge=true]
P0 -->|yes| P{gadugi-test on PATH?}
P -->|no| STOP0[stop: gadugi-test-not-installed]
P -->|yes| B
B --> C{PR exists?}
C -->|no| STOP1[stop: nothing to review]
C -->|yes| D[Phase 2: CRUSTY LOOP]
D --> D1[crusty-old-engineer reviews the PR]
D1 --> D2[structured verdict:<br/>CLEAN / CONCERNS]
D2 -->|CONCERNS| D3[address every concern, commit, push]
D3 --> D4[loop-health-evaluator]
D2 -->|CLEAN| D4
D4 -->|CONTINUE| D1
D4 -->|STUCK| STOP2[stop + escalate<br/>nothing merged]
D4 -->|DONE + verdict CLEAN| E[Phase 3: MERGE-READY LOOP]
E --> E0[code commits since the clean crusty round?<br/>re-run the crusty loop at phase 2's depth]
E0 --> E1[sync base, run the repository's tests<br/>and, where required, gadugi scenarios,<br/>wait for CI to a terminal state]
E1 --> E2[merge-ready skill criteria]
E2 --> E3[structured verdict:<br/>MERGE_READY / NOT_MERGE_READY]
E3 -->|NOT_MERGE_READY| E4[clear blockers, commit, push]
E4 --> E5[loop-health-evaluator]
E3 -->|MERGE_READY| E5
E5 -->|CONTINUE| E0
E5 -->|STUCK| STOP2
E5 -->|DONE + verdict MERGE_READY| F[MERGE GATE]
F --> F1{every criterion re-verified<br/>in THIS run, bound to ONE sha?}
F1 -->|no| STOP3[NOT_MERGED + evidence bundle]
F1 -->|yes| G[gh pr merge --squash --delete-branch<br/>--match-head-commit sha]
G --> H{platform confirms MERGED?}
H -->|no| STOP3
H -->|yes| I[MERGED]
```
## Prerequisites
| Tool | Why | Install |
| --- | --- | --- |
| `gadugi-test`, only where criterion 1 needs it | Merge-ready criterion 1 follows qa-team's repo-type table. A Node repository runs its qa-team scenarios with `gadugi-test`. So does any repository where the operator sets `AUTODRIVE_QA_SCENARIO_DIR`. A Rust CLI repository runs `cargo test`, a Python repository runs `pytest`, and neither needs `gadugi-test`. `autodrive_gadugi_required` in `autodrive_trust.sh` makes this decision for every step. Where `gadugi-test` is needed, `amplihack install` does not install it, and no agent can. The recipe's first step, `autodrive-prerequisites`, stops the run with `ERROR: gadugi-test-not-installed` before the build when it is missing, so the missing tool costs seconds rather than a build and a crusty loop. The install is pinned to the gadugi-agentic-test commit auto-drive was tested with, because auto-drive depends on how gadugi-test 1.0.x behaves: one `--scenario` per run (gadugi-agentic-test #207) and a scenario decided by its commands' exit codes alone. | `npm install -g github:rysweet/gadugi-agentic-test#6c120657798995b1b53399a5acf3693d418a2d8b` |
| `gh`, authenticated | Platform facts, CI, review threads and the merge itself. | [GitHub CLI](https://cli.github.com/) |
## Invoking it
```bash
# From a task description — builds the PR, then drives it.
amplihack recipe run auto-drive-to-merge \
-c task_description="Add rate limiting to the public API" \
-c repo_path=/home/user/src/myproject
# Against a PR that already exists — phase 1 becomes a no-op.
amplihack recipe run auto-drive-to-merge -c pr_number=1234 -c repo_path=.
# Verify everything, report what it WOULD merge, merge nothing.
amplihack recipe run auto-drive-to-merge -c pr_number=1234 -c autodrive_dry_run=true
```
| Context var | Default | Meaning |
| --- | --- | --- |
| `task_description` | `""` | What to build. Passed to `default-workflow`. |
| `requirements` | `""` | Extra requirements, passed through. |
| `repo_path` | `.` | Repository to work in. |
| `existing_branch` / `pr_number` | `""` | Point the workflow at work that already exists. Whether the PR came from `pr_number` or from phase 1, every evidence step runs in the worktree that has the PR branch checked out, and the run refuses if no worktree, or more than one, has it checked out, or that worktree is not at the PR head (see [PR head and worktree resolution](../../../docs/reference/auto-drive-to-merge.md#pr-head-and-worktree-resolution)). |
| `autodrive_state_dir` | derived | Durable state root for resumability. |
| `autodrive_dry_run` | `"false"` | Verify everything, merge nothing. |
## Why there is no iteration cap
Neither loop has a `max_rounds`, a backstop integer, or a wall-clock budget.
An integer cap fails in both directions: it cuts off the round that was about
to converge, and it lets a genuinely stuck loop burn the whole budget before
the counter fires. The signal worth acting on is **absence of progress**, not
number of attempts — a loop on round 2 that produced nothing twice is stuck; a
loop on round 12 that is still resolving findings is fine.
So each round ends with the `loop-health-evaluator` contract (issue #1337),
which stops and looks at the measured data — what the round actually produced,
which findings are new / recurring / resolved, whether test and CI signals
moved — and answers `CONTINUE`, `DONE`, or `STUCK`.
Host safety was never this workflow's job and still is not. It is enforced
structurally one layer down by **#1327** (sealed recursion ceiling) and
**#1332** (width cap + free-memory floor), both refusing with exit code `79`
before a child runs. Rounds here run **sequentially at constant session
depth** — each child inherits the same `AMPLIHACK_SESSION_DEPTH` — so a long
loop never walks toward that ceiling. No round recipe starts a loop: when
commits made after the clean crusty round need crusty again, the merge-ready
loop runs the crusty loop before its next round, from its own shell, at the
depth phase 2's crusty loop runs at. Started from inside a merge round, that
loop would be one recipe runner deeper and refused with exit `79`. `AMPLIHACK_MAX_DEPTH` is propagated
untouched and is **never** raised; the loop driver aborts if it changes.
## Structured verdicts only
No phase advances on a model's prose impression. Every gate reads a structured
field through the repo's canonical pipeline
(`extract-json --require-field … | extract-field --default …`, see
[Structured Verdict & Intent Parsing](../../../docs/reference/structured-verdict-parsing.md)),
matched against an exact-token allow-list:
| Signal | Field | Clean token | Fail-safe default |
| --- | --- | --- | --- |
| Crusty review | `crusty_verdict` | `CLEAN` | `CONCERNS` |
| Merge-ready assessment | `merge_ready_verdict` | `MERGE_READY` | `NOT_MERGE_READY` |
| Loop health | `loop_verdict` | `DONE` | `STUCK` |
**A missing or unparseable verdict is never the permissive one.** A verdict
that cannot be read means "we do not know", and "we do not know" does not
advance a phase and does not merge.
`--require-field` selects the **last** JSON object carrying the field rather
than the first parseable object of any shape. Without it a reviewer that quotes
its own output contract — a ` ```json ` example of a `CLEAN` verdict — hands
that example to the parser instead of its real verdict, and nothing downstream
re-measures crusty's judgement the way phase 3 re-measures CI.
A `MERGE_READY` verdict is additionally **downgraded by measurement**: if the
recorded `qa_status` is not `PASS`, `ci_status` is not `GREEN`, a conflict is
present, or review threads are unresolved, the verdict becomes
`NOT_MERGE_READY` regardless of what the assessment said.
## Two absolute prohibitions
1. **Never skip hooks on a commit.** No `--no-verify`, no `-n` shorthand, on
any commit this workflow makes. If a hook fails, fix what it is complaining
about.
2. **Never bypass branch protection.** No `--admin` on `gh pr merge`, and no
other bypass of required checks, required reviews, or the strict
up-to-date policy. If a check fails, fix the cause.
These are not advice. The merge gate builds its `gh` argv as a fixed literal
list and accepts **no** flags from any caller, so there is no argument through
which a bypass could be threaded. A guard test
(`forbidden_flags_never_appear_in_an_executable_position`) fails the build if
either string ever shows up in an executable position anywhere in this
workflow's recipes, tools, or skill.
## No silent merge
The merge gate does not trust the loop that preceded it. In the run that
merges, it re-verifies — and writes down, before merging — the head SHA, draft
state, mergeability and merge-state, review decision, unresolved review
threads, the full CI rollup, the `qa-team` evidence, and the merge-ready round
record. Everything is bound to **one** head SHA, and the merge itself passes
`--match-head-commit`, so GitHub refuses the merge if the head moved after the
evidence was captured.
**An unreadable criterion is a failure, never a pass.** Unreadable CI, zero
reported checks, unreadable review threads, missing `qa-team` evidence, an
absent round record — each is a blocker. After the merge, the platform must
confirm `MERGED`; a `gh` success that the platform does not confirm is
reported as not merged.
## Exit code 79 is terminal
Exit `79` (and `BLOCKED_TERMINAL`) is a final answer from a structural guard,
not an infrastructure hiccup. It is surfaced, it stops the run, and it is
**never** retried into — not at a deeper level, not with a raised ceiling. The
loop driver propagates `79` as its own exit code so a parent sees the refusal
rather than a generic failure.
## Resumability
A run that dies partway is re-runnable. Nothing merged is redone and no
resolved concern is reopened:
- The **platform** is the authority on whether the PR is merged. A merged PR
short-circuits every phase.
- An open PR for the branch makes phase 1 a no-op — it is never rebuilt.
- Completed phases are recorded in
`${AMPLIHACK_STATE_DIR:-~/.amplihack/state}/auto-drive/<key>/`, written only
by the host that ran them. There is deliberately **no pull-request-comment
ledger**: a PR comment is writable by anyone who can comment, and a forged
one could mark the crusty loop complete or seed the resolved-concern list,
skipping the review entirely on a fresh host. A fresh host redoes the phase
instead — that is the cheaper mistake.
- Concern ids that a previous run resolved **and** had confirmed clean by a
later round are handed back to crusty, which may still re-raise one — but
only with new evidence.
Local state is a cache, never a claim. The merge gate re-verifies everything
regardless of what any state file says.
## No short timeouts
No step in this workflow declares a `timeout` or `timeout_seconds`, and no
recipe declares a `default_step_timeout`. CI polling waits on a 60-second
interval until CI reaches a terminal state or becomes unreadable — bounded by
the build finishing, not by a stopwatch. Test suites, builds, and model calls
run to their natural end. A false timeout costs more than a slow run.
## What it composes
| Piece | Role |
| --- | --- |
| [`default-workflow`](../default-workflow/SKILL.md) | Phase 1: builds and publishes the PR. |
| [`crusty-old-engineer`](../crusty-old-engineer/SKILL.md) | Phase 2 reviewer, standing in for the maintainer. Emits the opt-in structured verdict. |
| [`merge-ready`](../merge-ready/SKILL.md) | Phase 3 criteria and guardrails. |
| [`qa-team`](../qa-team/SKILL.md) | Outside-in scenarios that must actually run and pass. |
| `loop-health-evaluator` | The agentic terminator for both loops (issue #1337, PR #1347). |
## Recipes and tools
| File | Role |
| --- | --- |
| `amplifier-bundle/recipes/auto-drive-to-merge.yaml` | Thin composer over the three phases. |
| `amplifier-bundle/recipes/autodrive-build.yaml` | Phase 1. |
| `amplifier-bundle/recipes/autodrive-crusty-round.yaml` | One crusty round. |
| `amplifier-bundle/recipes/autodrive-crusty-loop.yaml` | Phase 2 loop. |
| `amplifier-bundle/recipes/autodrive-merge-evidence.yaml` | Measured evidence for one merge round. |
| `amplifier-bundle/recipes/autodrive-merge-round.yaml` | One merge-ready round. |
| `amplifier-bundle/recipes/autodrive-merge-loop.yaml` | Phase 3 loop + merge gate. |
| `amplifier-bundle/tools/autodrive_loop.sh` | The uncapped, agentically-terminated loop driver. |
| `amplifier-bundle/tools/autodrive_merge_gate.sh` | The evidence gate and the fixed merge argv. |
| `amplifier-bundle/tools/autodrive_state.sh` | Resumable local state; platform truth for merged-ness. |
Full reference: [Auto Drive To Merge](../../../docs/reference/auto-drive-to-merge.md).