# Landing and cleanup (/docs/scruff/landing)



`landed` is the predicate the whole safety story rests on: it decides whether a
branch **dies**, and getting it wrong in the permissive direction destroys
work.

## Landed, across every merge shape [#landed-across-every-merge-shape]

The question is "did this branch's work reach the default branch", not "was
there a merge commit". Six shapes get there, and each is caught by something
different:

| How the work got there                         | Caught by                                                                                    |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
| fast-forward, or a merge commit                | ancestry: the tip is already in the default branch                                           |
| **rebase-and-merge** on the forge (new SHAs)   | the PR's `headRefOid` still equals your local tip                                            |
| **squash-and-merge** (one new commit)          | the same                                                                                     |
| **cherry-picked** commit by commit             | patch equivalence: `git cherry` marks every commit as already upstream                       |
| a local squash and a direct push, no PR at all | an empty merge tree: the branch adds nothing to the default branch                           |
| merged, then the lane **kept committing**      | the PR is merged and `headRefOid` is behind the tip. See [below](#when-a-lane-outran-its-pr) |

The default branch is resolved from `refs/remotes/origin/HEAD`, never from
whatever the main checkout happens to have checked out. A branch merged into a
side branch is not a branch that landed.

## The four verdicts [#the-four-verdicts]

`landed.verdict` is a closed set, and each carries a `via` (which signal
answered) and a `confidence`.

|             |                                                                                                            |
| ----------- | ---------------------------------------------------------------------------------------------------------- |
| `yes`       | it reached the default branch. Reapable                                                                    |
| `no`        | it did not, or nothing could prove it did. **Kept**                                                        |
| `fresh`     | the branch has never carried a commit of its own, so there is nothing here to have landed                  |
| `contained` | the branch adds nothing to the default branch, by an empty merge tree. A heuristic, and not a reap trigger |

<Callout type="warn" title="fresh is not yes">
  A lane cut from the default branch five seconds ago is trivially *contained in*
  the default branch, because it **is** the default branch. Render it as *nothing
  yet*, never as merged.

  A never-committed branch has nothing on it to lose, so `fresh` stays reapable.
  What is worth protecting is the checkout, so the sweep spares a lane for its
  first hour and names the window it is inside. After that hour, the thing that
  holds a lane is a commit.
</Callout>

`contained` is deliberately not enough to delete on. An empty merge tree is true
of a squash merge, of a manual re-implementation, and of an empty branch alike,
so `--json` says `contained`, `reap` keeps the lane, and a person decides.

## Uncertainty resolves to keep [#uncertainty-resolves-to-keep]

No `gh`, no network, a rate limit, an archived repo: every one means scruff
**cannot prove** the branch landed, and every one keeps the branch. The run exits
**3**, says so in `warnings`, and shows a marker in the listing rather than
degrading in silence.

## Working in a fork [#working-in-a-fork]

`origin` your fork, `upstream&#x60; the project, pull requests opened from one to the
other. **`origin` wins**, so scruff knows this repo as your fork, and your fork
is where it looks for your pull requests. It will not find them there, because
they live in the project.

Nothing breaks, but one rung of the ladder goes quiet. What is left is what git
alone can prove, which still covers a merge commit or a rebase once you have
pulled the project's branch back down. A **squash** merge leaves nothing to
prove, so the lane reads `no` and `reap` keeps it. Right way round, and it does
mean a fork is a repo where lanes pile up.

`scruff doctor` names it rather than leaving you to work it out:

```
findings
  fork remotes     you/kilo
                   this repo's remotes disagree about who it is: identity is
                   you/kilo, and upstream is antirez/kilo. scruff keys on
                   origin, so every PR query asks you/kilo …
```

Two ways to act on it. `scruff drop <name>` takes one lane by hand, which is
always allowed because a person typed the name. Or write a
[`landed` seam](/docs/scruff/seams) that asks the project instead of your fork,
and the sweep works again:

```toml
# ~/.config/scruff/config.toml
[hooks]
landed = "~/.local/bin/scruff-landed-upstream"
```

scruff will not pick the second remote for you. `upstream` is a habit, not a
declaration: it is `up` on somebody else's machine and a vendor mirror nobody
sends a PR to on a third. Choosing one of them to believe would be a guess, and
this is the one thing scruff does that deletes a branch.

## Sweeping [#sweeping]

```sh
scruff reap
```

Most cleanup already happened: closing a pane reaps a merged branch, and every
listing sweeps parked branches whose PR has since landed. `reap` is the
idempotent backstop for the endings that skip the hook, a reboot, a crash, or a
`scruff child` checkout that no pane owns.

It is deliberately conservative and it tells you why, lane by lane. It keeps
anything **dirty**, anything **not provably landed**, any checkout somebody
is **standing in**, including the one you are standing in right now, and any
lane **made in the last hour** that has nothing committed on it yet. Two things
no seam and no flag can override: the checkout scruff is being run **from** is
never swept, and a **stray** (a directory git has disowned, from an interrupted
`git worktree remove`) is only ever reported.

```
reaped nebelung/sparkle
kept pounce/smart-ranking — something is standing in the checkout: node (pid 46864)
kept haus/quiet-bar — worktree-quiet-bar is not in main yet, and scruff only sweeps
  what has landed. Nothing to do while it is still in flight; scruff drop haus/quiet-bar
  retires it if it never will.
nothing reaped — see above for what held each lane back.
```

That second kind is the commonest and the one worth reading twice: it is what
scruff **measured**, never what the forge said. On a repo whose pull requests
scruff cannot see at all (GitLab, sourcehut, a bare remote, an unauthenticated
`gh`, or [a fork](#working-in-a-fork) whose PRs live on `upstream`) every lane
gets that line, forever, and none of them is a lane scruff forgot.

### Getting one back [#getting-one-back]

Every branch deletion writes a line to a ledger **before** the delete: when,
repo, name, branch, SHA, why, and the PR.

```sh
scruff reaped
```

A branch deletion destroys its own evidence: `git branch -D` takes the branch's
reflog with it, so a lane that vanished between two listings leaves nothing
behind to say what it was. The ledger outlives that reflog entry, which makes
every reap **reversible** rather than merely attributable, and `scruff reaped`
prints the `git branch` line that brings one back.

### Work that will never land [#work-that-will-never-land]

Two lanes can never land, and neither of them is "not landed yet": a branch whose
PR was **closed unmerged**, and a branch in a repo that has been **archived**.
Both read exactly like an in-flight branch.

`reap&#x60; names them and refuses to take them, on purpose. The asymmetry is the
design: &#x2A;*`reap` is automatic, so it may only ever take landed work; `drop` is a
human typing one lane's name, so it may take anything.**

```sh
scruff drop that-rejected-idea
```

`drop` waives the landed requirement and nothing else. It still refuses a lane
somebody is standing in, and it refuses a dirty tree with no PR to fall back on
rather than deciding for you: park it first. And it writes the same ledger line,
so a drop is as reversible as a reap.

When several have piled up, `--dead-ends` is the same thing said once:

```sh
scruff reap --dead-ends
```

It sweeps the landed lanes as usual, then retires every dead end it named,
through the same code `drop` runs: the same refusals, a line in the ledger for
each, the undo printed as it goes. A lane it can't take (somebody is standing in
it, the tree is dirty) is kept and named, and the run carries on to the next.

A closed PR is not forever, which is why this is a flag and not the default. You
can reopen one on GitHub as long as the branch is still there, and scruff treats
an **open** PR on the branch as the last word: close #12, open #13 on the same
branch, and the lane is in review, not rejected. `reap` asks the forge fresh on
every run, so a PR you reopened a minute ago is safe from the sweep you start
now.

## When a lane outran its PR [#when-a-lane-outran-its-pr]

You merge the PR, keep talking to the agent, and it makes a few more commits.
Those commits are now in an odd place: the PR that covered them is closed, and
the forge deleted the branch's remote counterpart when it merged, so they are
committed, unpushed, unreviewed, on a branch nothing is watching.

scruff never reaps that, and it says so everywhere. The listing marks the row
&#x2A;*`live+3`**, `reap` names it instead of sweeping it, and one command clears it:

```sh
scruff reship            # push this branch, open the follow-up PR
scruff reship sparkle    # or name one, from anywhere
```

It re-creates the remote the forge deleted and opens a PR against the default
branch. If a PR is already open at that tip, the marker was never there in the
first place: an open PR standing at this exact tip covers everything since the
merge, so the lane is simply in flight, and the push lands on it.

A branch with **no PR at all** has outrun nothing, so `reship` refuses it with
exit 2 before it pushes rather than letting the remote answer. That branch just
needs to go up the ordinary way:

```sh
git push -u origin worktree-sparkle && gh pr create
```

<Callout type="warn" title="`~3` is the lookalike, and it wants the opposite">
  `~3` means the branch has **diverged**: its tip does not build on what merged,
  so the checkout is stale or sideways (a second local copy of the branch, an
  amend after the merge) rather than ahead. `reship` refuses it, because pushing
  would send content the merge already superseded. Remove that lane instead.

  A rebase is **not** this case, whatever it does to your SHAs. The question is
  settled by asking whether `origin/<default>` is an ancestor of the tip first,
  which is what stops "squash-merge, rebase onto main, keep working" (the lane
  that did exactly the right thing) from being told its work is stale.
</Callout>

## Who is standing here [#who-is-standing-here]

`reap` needs to know a checkout is in use. On a human's machine `lsof` answers
that. Anything else, a container, a CI runner, an orchestrator holding one
session per lane, says so itself:

```sh
scruff heartbeat            # this checkout is in use while the calling process lives
scruff heartbeat --release  # done with it
```

A lease naming a live pid **self-expires the moment that process dies**, so a
crash cannot leave a lane pinned forever. Where there is no local process to
watch, use `--pid 0` and refresh inside the 90 second TTL. A lease can only ever
*save* a lane from the sweep, never condemn one.

<Callout type="info" title="occupied is not the same as a pane">
  What scruff observes is a process with its cwd in the checkout, and a dev server,
  a file watcher or a daemon orphaned to pid 1 is not a pane. So `occupied` means
  "something is standing here", and `occupied_by` names it.
</Callout>
