Land - the ship loop
/flow-next:land <PR> takes one pull request the rest of the way. Where Flow and flow --auto 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.
Invocation
Section titled “Invocation”/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 onlyOn 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
Section titled “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 123typed by you is an explicit request to land that pull request. - The calling flow carries your consent.
/flow-next:flow --auto fn-12 --until=mergepasses 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:
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
Section titled “What one invocation does”- 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. - Select the specs. Land reads
.flow/specs/*.jsonat the pull request’s remote head, never from your checkout, and selects every spec whosebranch_nameequals the head branch. With no branch match it selects the specs the pull request closes. If any selected spec is still open, land stopsBLOCKEDwith reasonwork not finishedand 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 stoppedNO_WORK. - Conflicts. A conflict stops
BLOCKEDand names the branch that needs a rebase. A branch that is merely behind is caught up last, after every other gate. Land never rebases, force-pushes or retargets. - Review threads. Open threads go to
/flow-next:resolve-prinmode: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 stopsNEEDS_HUMANwith those lines, so an unattended run asks once instead of retrying the same thread on every tick. Other threads still open afterwards end the tickRESOLVING. - 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 endsFIXING_CIwhile the new run is pending. A flaky check gets onegh run rerun --failed; a second identical failure is not a flake and gets no second rerun. A fix that does not turn CI green stopsBLOCKEDand 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. - 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_HUMANnaming 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 withgh pr merge --squash --match-head-commit <full-head-sha>. A head that moved since the gates ran is refused by the server, and land reportsRESOLVINGinstead 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
Section titled “The merge gate, precisely”Land merges when all of these hold on the current head:
- every check is green,
- GitHub’s
reviewDecisionis not blocking (CHANGES_REQUESTEDorREVIEW_REQUIREDends the tickAWAITING_REVIEW), - zero unresolved review threads,
- the merge is authorized 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:
- Your instruction file. Write the extra requirement in
AGENTS.mdorCLAUDE.md, for example “never merge without an approving review from a code owner”. Land honors stricter repository instructions before merging. - Branch protection. Require approvals and checks on the server, where no agent can skip them.
land.mergeVerdictCommand. Your own executable gate, described under 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
Section titled “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_HUMANnaming 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
BLOCKEDnaming 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
RESOLVINGand 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
Section titled “The verdict grammar”Every invocation prints exactly one terminal line, last in the output:
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
Section titled “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
Section titled “One pull request until it is merged”Where your host has a cadence primitive, this is the whole recipe:
/loop 15m /flow-next:land 123Stop 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:
/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_WORKOn a host with neither primitive, give the agent the loop in plain words:
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 samepull request. Stop on MERGED, BLOCKED, NEEDS_HUMAN or NO_WORK and report the reason.One pull request until it is merge-ready
Section titled “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:
Babysit pull request 123 with the flow-next land skill but do not merge it. Re-run every15 minutes on RESOLVING or FIXING_CI. Stop when the reason is "merge-ready; authorizationrequired", or on BLOCKED, NEEDS_HUMAN or NO_WORK.Several pull requests
Section titled “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:
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"; fidoneAn 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:
Land pull requests 123, 127 and 131, one at a time. For each, run the flow-next land skillon that pull request and read its final LAND_VERDICT line. On AWAITING_REVIEW, RESOLVING orFIXING_CI move to the next one and come back to it on the next pass. When BLOCKED names alower layer of a chain, land that layer first if it is in my list. Wait 15 minutes betweenpasses. Report every other BLOCKED, NEEDS_HUMAN and NO_WORK with its reason and leave thatpull request alone. Never substitute another pull request for one that closed. Stop whenevery 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.
A pull request that closes several specs
Section titled “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 writes one body for such a branch.
Chains and stacks
Section titled “Chains and stacks”A dependent spec’s pull request is based on its parent’s branch (how a chain forms). Land merges a chain from the bottom, one layer per invocation.
- Only the lowest open layer merges. Naming a higher layer stops
BLOCKEDand 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 set before merging, reusing an existing stack. A stacked layer merges through GitHub’s asynchronous merge endpoint 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.
- 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.
After the merge
Section titled “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, 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 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
Section titled “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:
/flow-next:land 123 # after the merge: retries only the tracker touchpointThe 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.
Configuration
Section titled “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). |
flowctl config set land.patienceMinutes 20flowctl config set land.mergeVerdictCommand "scripts/merge-gate.sh"Retired keys
Section titled “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: 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), 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
Section titled “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
Section titled “The full picture”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. See Running the full pipeline.