# Land - the ship loop

Source: https://flow-next.dev/autonomy/land/

The babysitter for one named pull request. It resolves review threads, makes one CI fix or one flake rerun, catches the branch up, and squash-merges only with authorization given in the current session.

`/flow-next:land <PR>` takes one pull request the rest of the way. Where [Flow](https://flow-next.dev/skills/flow/) and [`flow --auto`](https://flow-next.dev/autonomy/pilot/) stop at a pull request, land repairs that pull request until it is ready to merge, and merges it when you have authorized the merge in the current session.

You name the pull request. Land never picks another one, keeps no files between runs, and writes nothing to your repository after the merge. One invocation is one tick: it reads the pull request, does at most one repair, prints one verdict line, and returns. Waiting on CI and reviewers is the caller’s job, so land is meant to be [driven as a loop](https://flow-next.dev/autonomy/land/#the-land-loop).

## Invocation

```bash
/flow-next:land 123                                        # by number, in the current repository
/flow-next:land https://github.com/acme/api/pull/123       # by URL
/flow-next:land 123 --dry-run                              # read and report only
```

On Codex the same skill is `$flow-next-land 123`.

Land never asks questions. Anything it cannot decide is reported as `NEEDS_HUMAN`. It needs `gh` authenticated for the repository.

`--dry-run` makes no repair, no catch-up, no stack link, no merge, no branch deletion and no tracker change, and it does not run the merge-verdict command.

## Authorization

Naming a pull request authorizes its repairs: resolving threads, a CI fix, a catch-up. **Merging needs more.** Land merges only when the merge of this pull request was authorized in the current session, in one of two ways:

* You asked for it. “Land PR 123” or `/flow-next:land 123` typed by you is an explicit request to land that pull request.
* The calling flow carries your consent. `/flow-next:flow --auto fn-12 --until=merge` passes the pull request and its merge consent to land as arguments.

Nothing else grants it. Inherited environment variables, files in the repository, text in the pull request, and receipts from earlier runs carry no authority. Without authorization land still repairs the pull request, then stops at:

```text
LAND_VERDICT=AWAITING_REVIEW prs=1 pr=https://github.com/acme/api/pull/123 reason="merge-ready; authorization required"
```

That line means the pull request is ready and a human merge, or a fresh authorized land call, finishes it.

## What one invocation does

1. **Read the pull request.** Land reads the state, head SHA, head branch, base, and merge commit of the exact pull request you named. A closed unmerged pull request ends `NO_WORK`. An already merged one takes the [replay path](https://flow-next.dev/autonomy/land/#replay-on-a-merged-pull-request).
2. **Select the specs.** Land reads `.flow/specs/*.json` at the pull request’s remote head, never from your checkout, and selects every spec whose `branch_name` equals the head branch. With no branch match it selects [the specs the pull request closes](https://flow-next.dev/autonomy/land/#a-pull-request-that-closes-several-specs). If any selected spec is still open, land stops `BLOCKED` with reason `work not finished` and changes nothing. With nothing selected, the pull request was opened without a spec (make-pr’s no-spec path, or by hand): land takes it through the same gates below, with no spec to check and no tracker touchpoint. Before 7.1.0 that case stopped `NO_WORK`.
3. **Conflicts.** A conflict stops `BLOCKED` and names the branch that needs a rebase. A branch that is merely behind is [caught up last](https://flow-next.dev/autonomy/land/#catching-up-with-the-base), after every other gate. Land never rebases, force-pushes or retargets.
4. **Review threads.** Open threads go to [`/flow-next:resolve-pr`](https://flow-next.dev/skills/resolve-pr/) in `mode:autonomous`, bound to this pull request and working in an isolated checkout: a detached worktree at the pull request’s head, removed afterwards. Fixes push to the pull request’s own branch by name (`git push <head repository> HEAD:<head branch>`), which reaches the pull request even when it comes from a fork. When resolve-pr hands a thread to a person (`RESOLVE_PR_VERDICT=NEEDS_HUMAN`), land stops `NEEDS_HUMAN` with those lines, so an unattended run asks once instead of retrying the same thread on every tick. Other threads still open afterwards end the tick `RESOLVING`.
5. **CI.** Pending checks end the tick `RESOLVING`. A red check in the pull request’s own code gets one focused fix in the same detached worktree, verified and pushed to the branch as an ordinary commit, and the tick ends `FIXING_CI` while the new run is pending. A flaky check gets one `gh run rerun --failed`; a second identical failure is not a flake and gets no second rerun. A fix that does not turn CI green stops `BLOCKED` and names the check. Land never spends a second fix on the same failure in a later invocation; it reads the pull request’s commits and check attempts to know what was already tried.
6. **Catch up, then merge.** When being behind its base is the only thing left, land catches up once and returns; the next tick checks every gate again on the new head. When every gate passes and the merge is authorized, land handles a draft by who authorized the merge. A draft under a merge that a calling flow authorized, with no person authorizing it in the session, stops `NEEDS_HUMAN` naming its open items: the draft marks a call the run left for a person. When you authorized the merge yourself (“land it”), land marks the draft ready. It then re-reads the head and all gates, and merges with `gh pr merge --squash --match-head-commit <full-head-sha>`. A head that moved since the gates ran is refused by the server, and land reports `RESOLVING` instead of retrying with a new SHA. Success is confirmed only by re-reading the pull request as merged with a merge commit.

Repairs never touch your checkout. Land does not check out a branch where you invoked it.

## The merge gate, precisely

Land merges when all of these hold on the current head:

* every check is green,
* GitHub’s `reviewDecision` is not blocking (`CHANGES_REQUESTED` or `REVIEW_REQUIRED` ends the tick `AWAITING_REVIEW`),
* zero unresolved review threads,
* the merge is [authorized](https://flow-next.dev/autonomy/land/#authorization) in this session,
* the merge-verdict command, if you set one, exits 0.

If your repository requires no reviews and nobody reviewed, green checks and zero threads are enough for an authorized merge. No bot comment or approval signal is needed, and none is waited for. Tighten the gate in one of three places:

1. **Your instruction file.** Write the extra requirement in `AGENTS.md` or `CLAUDE.md`, for example “never merge without an approving review from a code owner”. Land honors stricter repository instructions before merging.
2. **Branch protection.** Require approvals and checks on the server, where no agent can skip them.
3. **`land.mergeVerdictCommand`.** Your own executable gate, described under [Configuration](https://flow-next.dev/autonomy/land/#configuration).

**The patience window.** When the calling flow authorized the merge and no human did so in the session, land waits `land.patienceMinutes` (default 10) since the author’s last push before merging; land’s own catch-up does not restart it, so review bots have time to post. The window is for bots, not people: if you expect a human review, do not hand the merge to an unattended run. Bots post within a few minutes and the wait overlaps CI; raise the key if yours are slower. Inside the window it reports `AWAITING_REVIEW` with the minutes remaining and returns. A human’s merge authorization in the current session waives the wait.

## Catching up with the base

Land checks whether the pull request is missing commits from its base by comparing the two, so it catches up whether or not your branch protection or rulesets require up-to-date branches (GitHub’s own merge state shows a branch as behind only when they do). Land leaves the catch-up until being behind is the only thing between the pull request and an authorized merge: review, threads, CI and patience all pass first. Catching up earlier would buy a fresh CI run on every tick of a busy base, and only the last one before the merge matters.

* **Pinned to the checked head.** Land asks GitHub to update the branch only if its head is still the commit land just checked, so a push that lands meanwhile is never merged in unseen; a moved head ends the tick `RESOLVING`.
* **Counted from history.** A catch-up is GitHub’s merge of the base into the branch (the same commit a person gets from the “Update branch” button). With two of those already on top and the base moved again, land stops `NEEDS_HUMAN` naming the moving base instead of chasing it. A person can merge by hand, add a merge queue, or try again when the base is quieter.
* **Red CI after a catch-up.** When the only new commits since the last green head are catch-ups and CI turns red, the base broke the pull request, not its own code: land stops `BLOCKED` naming the base commits merged in, without spending its one CI fix.
* **A base that moves at the last moment.** Under strict rules, a base that moves between land’s final check and the merge makes GitHub refuse the merge as out of date. Land reports `RESOLVING` and catches up on the next run, still within the two-catch-up limit.
* **Merge queues.** A base that requires a merge queue is never caught up by land; the queue tests the merge result itself.
* **Stacked pull requests.** A layer whose base is its parent’s branch is caught up as before, at the conflict step.

## The verdict grammar

Every invocation prints exactly one terminal line, last in the output:

```text
LAND_VERDICT=<verdict|NO_WORK> prs=<n> pr=<url|-> reason="<one line>"
```

`prs` is `1` when the named pull request resolved, otherwise `prs=0 pr=-`. The reason carries the observed head, the check or branch involved, and the merge commit when known.

| Verdict           | Meaning                                                                                                                                                                                                                                                                                                        | What the driver does                                                                        |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `MERGED`          | The pull request is merged and the merge commit is confirmed. Also the result of a replay on a merged pull request.                                                                                                                                                                                            | Stop.                                                                                       |
| `QUEUED`          | The merge call added the pull request to a merge queue and it is still open. Land ran no post-merge step.                                                                                                                                                                                                      | Run `/flow-next:land <PR>` again once the queue merges it; that run moves the linked issue. |
| `AWAITING_REVIEW` | A blocking review decision, the patience window still running, or `merge-ready; authorization required`.                                                                                                                                                                                                       | Wait, then call again; or merge by hand.                                                    |
| `RESOLVING`       | Checks pending, threads still open after the resolver ran, mergeability not yet known, a head that moved, or a stack merge not yet confirmed.                                                                                                                                                                  | Wait, then call again.                                                                      |
| `FIXING_CI`       | Land pushed a CI fix and the new run is pending.                                                                                                                                                                                                                                                               | Wait, then call again.                                                                      |
| `BLOCKED`         | A selected spec is unfinished, the branch conflicts with its base, the one CI fix did not turn the check green, GitHub refused the merge, or a lower stack layer must merge first.                                                                                                                             | Stop and read the reason.                                                                   |
| `NEEDS_HUMAN`     | No pull request given, an unreadable or ambiguous target, an incomplete read of the head, a resolver refusal, a review thread the resolver handed to a person, a draft pull request under a merge no person authorized in the session, an external CI failure, or a merge-verdict command that did not exit 0. | Stop and read the reason.                                                                   |
| `NO_WORK`         | The pull request is closed unmerged.                                                                                                                                                                                                                                                                           | Stop.                                                                                       |

`RELEASED` stays in the vocabulary so existing parsers keep working. Land never emits it; run your release separately.

## The land loop

One invocation is one tick, so landing a pull request is a loop around land: call it, read the last line, wait, call it again. The loop ends on `MERGED`, or on the merge-ready line when you did not authorize the merge.

### One pull request until it is merged

Where your host has a cadence primitive, this is the whole recipe:

```text
/loop 15m /flow-next:land 123
```

Stop the loop when it prints `LAND_VERDICT=MERGED`, `BLOCKED`, `NEEDS_HUMAN` or `NO_WORK`. You typed the pull request number, so the merge is authorized for that session.

A goal-shaped driver states the stop condition against the grammar:

```text
/goal keep running /flow-next:land 123 until it prints LAND_VERDICT=MERGED, or stop on LAND_VERDICT=BLOCKED, LAND_VERDICT=NEEDS_HUMAN or LAND_VERDICT=NO_WORK
```

On a host with neither primitive, give the agent the loop in plain words:

```text
Land pull request 123. Run the flow-next land skill on it, read the final LAND_VERDICT line,
and on AWAITING_REVIEW, RESOLVING or FIXING_CI wait 15 minutes and run it again on the same
pull request. Stop on MERGED, BLOCKED, NEEDS_HUMAN or NO_WORK and report the reason.
```

### One pull request until it is merge-ready

To let land do the repairs and keep the merge for yourself, say so. Land then stops at the merge-ready line and never merges:

```text
Babysit pull request 123 with the flow-next land skill but do not merge it. Re-run every
15 minutes on RESOLVING or FIXING_CI. Stop when the reason is "merge-ready; authorization
required", or on BLOCKED, NEEDS_HUMAN or NO_WORK.
```

### Several pull requests

Land has no repository-wide mode. To land several pull requests, list them and call land on each. The list is yours to make, and a list grants no merge authority by itself; the authorization is the instruction you give with it.

This block prints the open pull requests whose head branch belongs to at least one spec, with every such spec closed at that head. It reads the pushed head of each pull request, not your checkout, and it merges nothing:

```bash
gh pr list --state open --limit 100 --json number,headRefName,headRefOid \
  --jq '.[] | [.number, .headRefName, .headRefOid] | @tsv' |
while IFS=$'\t' read -r PR BRANCH OID; do
  git fetch -q origin "pull/$PR/head" || { echo "PR #$PR: fetch failed, skipped" >&2; continue; }
  [ "$(git rev-parse FETCH_HEAD)" = "$OID" ] || { echo "PR #$PR: head moved, list again" >&2; continue; }
  MATCHED=0; UNFINISHED=0
  for SPEC_PATH in $(git ls-tree -r --name-only "$OID" -- .flow/specs | grep '\.json$'); do
    SPEC=$(git show "$OID:$SPEC_PATH")
    [ "$(printf '%s' "$SPEC" | jq -r '.branch_name // empty')" = "$BRANCH" ] || continue
    MATCHED=$((MATCHED + 1))
    [ "$(printf '%s' "$SPEC" | jq -r .status)" = "done" ] || UNFINISHED=1
  done
  if [ "$MATCHED" -gt 0 ] && [ "$UNFINISHED" -eq 0 ]; then echo "$PR"; fi
done
```

An integration pull request whose branch no spec names does not appear in that list. Land accepts it when you name it; add its number by hand.

Then hand the numbers to your driver with the authorization in the same instruction:

```text
Land pull requests 123, 127 and 131, one at a time. For each, run the flow-next land skill
on that pull request and read its final LAND_VERDICT line. On AWAITING_REVIEW, RESOLVING or
FIXING_CI move to the next one and come back to it on the next pass. When BLOCKED names a
lower layer of a chain, land that layer first if it is in my list. Wait 15 minutes between
passes. Report every other BLOCKED, NEEDS_HUMAN and NO_WORK with its reason and leave that
pull request alone. Never substitute another pull request for one that closed. Stop when
every pull request is MERGED or reported.
```

On a host with `/loop`, the same instruction goes after the interval: `/loop 15m - one pass: …`. Land re-reads the head and every gate on each call, so a pull request that moved between passes is judged fresh. A chain lands lowest open layer first, one layer per invocation.

A scheduler that starts a fresh headless session per wake has no earlier session to inherit consent from. Put the explicit instruction to land those pull requests in the prompt of every wake. See [Unattended operation](https://flow-next.dev/autonomy/unattended-operation/).

## A pull request that closes several specs

An integration branch can carry several finished specs, and none of them names that branch. When no spec’s `branch_name` matches the pull request’s head, land selects the specs the pull request closes: those that are `done` at the head, absent or not done at the base, and that have at least one task file the pull request changed. Every selected spec must be closed, or land stops `BLOCKED` naming the open ones. After the merge each selected spec gets its own tracker touchpoint.

Land reads both trees in full from GitHub. A truncated or failed read stops `NEEDS_HUMAN`; it is never treated as “no match”. [make-pr](https://flow-next.dev/skills/make-pr/#several-specs-in-one-pull-request) writes one body for such a branch.

## Chains and stacks

A dependent spec’s pull request is based on its parent’s branch ([how a chain forms](https://flow-next.dev/skills/make-pr/#dependent-specs-chains-and-stacks)). Land merges a chain from the bottom, one layer per invocation.

* **Only the lowest open layer merges.** Naming a higher layer stops `BLOCKED` and names the lowest open pull request. Land never merges a parent implicitly.
* **GitHub stacks where available.** With open children, land links the chain into a native [stacked pull request](https://docs.github.com/en/pull-requests/get-started/about-stacked-prs) set before merging, reusing an existing stack. A stacked layer merges through GitHub’s [asynchronous merge endpoint](https://docs.github.com/en/rest/pulls/pulls#merge-a-pull-request-asynchronously) as a head-pinned squash, and GitHub retargets the layer above. A pending stack merge is `RESOLVING`, not success.
* **Plain chains otherwise.** Where stacks are unavailable the layer merges the ordinary way. A child still based on the merged parent then needs a manual rebase onto the intended base. Land reports the child and stops; it never retargets a child or resolves its conflicts. The steps are on [Troubleshooting](https://flow-next.dev/reference/troubleshooting/#land-on-a-chain-chain-broken-a-retarget-conflict-or-a-pending-merge-async).
* **A branch a child still targets is never deleted.** Land deletes the merged branch only after a fresh read proves no open pull request targets it. A failed deletion leaves the merge confirmed and is named in the reason.

Vocabulary: [chain, stack, layer, frontier](https://flow-next.dev/reference/glossary/#chain).

## After the merge

Land writes nothing to the repository after the merge: no checkout, no commit, no push, no state file. The spec was already closed on the pull request branch by [make-pr](https://flow-next.dev/skills/make-pr/#the-spec-closes-at-the-pull-request-head), so the close reaches the base through the merge, including a protected base.

The one act left is the tracker touchpoint. When the [tracker bridge](https://flow-next.dev/integrations/tracker-sync/) is active, land moves each selected spec’s issue to its terminal status through the tracker API, using the confirmed merge commit as evidence. A tracker failure is named in the reason and never changes `MERGED`.

Releases are not part of land. Run your release separately, from your project’s own release instructions.

## Replay on a merged pull request

Calling land on a pull request that is already merged is safe. Land repairs nothing, merges nothing, and deletes no branch. It reads the pull request’s original head to find the same specs, repeats the tracker touchpoint with fresh merge evidence, and prints `LAND_VERDICT=MERGED` with reason `already merged`. Use it when the tracker was unreachable at merge time:

```bash
/flow-next:land 123      # after the merge: retries only the tracker touchpoint
```

The same replay finishes a pull request that was merged without land, in the web UI, with `gh pr merge`, by auto-merge or through a merge queue. When land’s own merge call only enqueued the pull request, land reports `LAND_VERDICT=QUEUED`; call it again after the queue merges, and the replay moves the issue. To have CI do it on every merge instead, see [Merged outside land](https://flow-next.dev/integrations/tracker-operations/#merged-outside-land).

## Configuration

Two keys remain.

| Key                        | Default | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| -------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `land.patienceMinutes`     | `10`    | Minutes since the author’s last push (land’s catch-up does not count) that land waits before merging, so review bots can post, when the calling flow, not a human in the session, authorized the merge. A human’s current authorization waives it. When GitHub reports no push time, land uses the head commit’s earliest check-suite creation time, then its committer date.                                                                                                                                                                                 |
| `land.mergeVerdictCommand` | `""`    | Your own merge gate: a shell command run once per invocation, only when every other gate allows the merge, with a 600-second bound. Exit 0 allows the merge. Any other exit, a missing or unexecutable command, or a timeout stops `NEEDS_HUMAN`. It runs from the repository where you invoked land without changing the checkout, so it must judge the supplied remote head. Context arrives in the environment: `FLOW_HEAD_SHA`, `FLOW_BASE_REF`, `FLOW_PR_NUMBER`, `FLOW_SPEC_ID` (empty when several specs match) and `FLOW_SPEC_IDS` (space-separated). |

```bash
flowctl config set land.patienceMinutes 20
flowctl config set land.mergeVerdictCommand "scripts/merge-gate.sh"
```

### Retired keys

Existing config files still load. Land prints one notice naming the ignored keys and leaves the file unchanged. Remove them when convenient.

| Retired key                       | What it did                                                        | Use instead                                                                                                                                         |
| --------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `land.release`                    | Ran your release instructions after the merge                      | Run releases separately                                                                                                                             |
| `land.reviewSignal`               | Chose the `silence`, `approve` or named-reviewer merge signal      | [The merge gate](https://flow-next.dev/autonomy/land/#the-merge-gate-precisely): instruction file, branch protection, or `land.mergeVerdictCommand` |
| `land.automatedReviewers`         | Listed logins counted as automated reviewers for `silence`         | The same three gates                                                                                                                                |
| `land.cleanReviewCommentPattern`  | Matched a bot’s clean-review comment                               | The same three gates; bot comments no longer gate a merge                                                                                           |
| `land.reviewTrigger`              | Posted a comment to summon a reviewer bot                          | Request reviews outside land                                                                                                                        |
| `land.requestReviewers`           | Requested human reviewers once per head                            | Request reviews outside land, or use CODEOWNERS                                                                                                     |
| `land.ciFixBudget`                | Bounded CI fixes per pull request with a durable needs-human label | One fix or one rerun per failure; `BLOCKED` names the check                                                                                         |
| `land.patienceMinutesAfterReview` | Anchored the wait to the latest review                             | `land.patienceMinutes`, anchored to the last push                                                                                                   |

Also gone, with no key: repository-wide pull request discovery and its authorship check (name the pull request, or use [the loop over several](https://flow-next.dev/autonomy/land/#several-pull-requests)), the per-pull-request ledger and tick claim (old files under `.git/` are inert and need no migration), the post-merge spec close and its push (make-pr closes on the branch), and the `FLOW_PR_MERGE_CMD` override (land uses the authenticated standard merge API).

## Opt-in and isolated

* A **separate skill**: projects that never run it carry no risk from it.
* It touches **one pull request**, the one you named, and only its branch.
* It keeps no files and leaves your checkout alone.
* It merges only with authorization from the current session.

## The full picture

```text
board / flowctl          flow --auto                              land <PR>
  bless a spec   ──▶   work → review → QA per config → make-pr  ──▶  threads → CI → merge
  (your judgment)       (plan only on a signal; PR open)           (merged)
```

For one item, `/flow-next:flow --auto <spec-id> --until=merge` invokes land as a scoped stage on that spec’s pull request and handles its waits. Standalone land is for a pull request you name: babysitting, recovery, and the [loop over several](https://flow-next.dev/autonomy/land/#several-pull-requests). See [Running the full pipeline](https://flow-next.dev/autonomy/going-autonomous/#running-the-full-pipeline).
