Skip to content
GhostbranchdocsGitHub

Merging and closing

Merge a published pull request, let sync tidy up after it, and close or park features yourself.

A published feature ends with a merge on GitHub and a ghostbranch sync that cleans up after it. This page covers that ending, and the two ways to end a feature that never merges: closing it, or parking it.

The pull request Ghostbranch opened is an ordinary one, and you merge it on GitHub as usual. Which merge method you pick decides what lands on main:

  • Create a merge commit keeps every published commit as it is and adds a merge commit.
  • Rebase and merge copies each published commit onto main, so they get new SHAs, but their messages and authors stay.
  • Squash and merge turns them into one commit with a message GitHub writes. That undoes a preserve publish: see History modes.

Here add-search (#24) was merged with a merge commit, then search-filters (#25), which had two commits, with a rebase. Public main afterwards:

Terminal
git log --oneline -3 origin/main
70a91b2 Filter search results by date
3d72d89 Add search filters
ad95a30 Merge pull request #24 from you/add-search

On the feature branch, the two search-filters commits were 51d90b1 and 3fe224a. The rebase gave them new SHAs on main, and Ghostbranch still counts the pull request as merged: it goes by the pull request, not by the SHAs.

On a native stack, a pull request’s merge button merges that pull request and every one below it, never the ones above. So merge a stack from its top pull request: one merge on its page merges the whole stack. A merge on a lower pull request lands only part of it. From a terminal, use merge-async on the top pull request, because gh pr merge refuses a stacked one. Stacked features has the details. On a stack, “Squash and merge” gives one commit per pull request, not one for the whole stack.

After the merge, run ghostbranch sync. It brings the merge into the shadow repo and closes the feature. Add --plan first to see exactly what it would delete:

Terminal
ghostbranch sync --plan
Dry run; nothing was changed. This would:
- move refs/heads/main in you/project-shadow from c63c081d62fb to ad95a30bdd2d
- delete refs/ghostbranch/features/add-search from you/project-shadow (at 75926c1120e9)
- delete refs/ghostbranch/plans/20261013T032248Z-ea7bcd from you/project-shadow (at 7cd4d7517f3f)
- delete refs/heads/ghostbranch-gate/add-search from you/project-shadow (at b7504ddb9e0e)
- delete refs/heads/add-search from you/project-shadow (at 11bb042250a0)
- close PR #14 in you/project-shadow
- remove /home/you/.local/state/ghostbranch/workspaces/default/plans/20261013T032248Z-ea7bcd.json

That’s the feature’s branch, its gate branch and draft pull request, its record, and its plan, in the shadow repo and on your machine. Then the real thing:

Terminal
ghostbranch sync
Shadow main fast-forwarded c63c081d62fb -> ad95a30bdd2d.
cancelled 1 push-triggered workflow run(s) in the shadow repo
closed merged feature add-search (PR #24)
progress lines, on stderr, and the Next lines left out

Its progress lines on stderr are left out. Both syncs also listed older features for a person to close, left out here. Sync explains the cancelled run.

Sync closes a feature on its own only when nothing would be lost: its pull request merged, and its branch hasn’t moved since the publish. A rebase merge counts, and so does a merge commit or a squash. Anything else waits for you, and status names it with the command to run.

A feature that apply archived closes the same way. When its pull request merges, sync closes it and also deletes its archive in the shadow repo, refs/ghostbranch/archive/<name>. It has no shadow branch or gate branch left to delete:

Terminal
ghostbranch sync --plan
Dry run; nothing was changed. This would:
- move refs/heads/main in you/project-shadow from ec0c2b31243a to b924a62f7732
the lines for two other merged features, then:
- delete refs/ghostbranch/archive/add-search from you/project-shadow (at 0312254b6ef3)
- delete refs/ghostbranch/features/add-search from you/project-shadow (at 95964513960f)
- delete refs/ghostbranch/plans/20261014T142810Z-e6a232 from you/project-shadow (at 148ad21aec65)
- remove /home/you/.local/state/ghostbranch/workspaces/default/plans/20261014T142810Z-e6a232.json

The sync that followed said closed merged feature add-search (PR #35), as for any other feature. It closes an archived feature only when the archive holds exactly what was published. If it doesn’t, the feature reads merged_moved, and a person closes it with ghostbranch feature close <name>.

ghostbranch feature close <name> deletes a feature from the shadow repo: its branch, gate branch, record and plan references, in one push. Then it closes the gate’s draft pull request and removes the feature’s plan files on your machine. Use it when:

  • you ran sync --no-close, or sync left a merged feature open;
  • the feature’s pull request was closed without merging (pr_closed);
  • the feature merged, but has commits since that were never published (merged_moved);
  • you’ve dropped the idea and the feature was never published.

Look first with --plan. Here search-filters had merged, and sync --no-close had left it open:

Terminal
ghostbranch feature close search-filters --plan
Dry run; nothing was changed. This would:
- delete refs/ghostbranch/features/search-filters from you/project-shadow (at e831af79d73d)
- delete refs/ghostbranch/plans/20261013T032638Z-1f4e67 from you/project-shadow (at faf0688f63a9)
- delete refs/heads/ghostbranch-gate/search-filters from you/project-shadow (at 64f5694ae38b)
- delete refs/heads/search-filters from you/project-shadow (at 3fe224a3cb3e)
- close PR #15 in you/project-shadow
- remove /home/you/.local/state/ghostbranch/workspaces/default/plans/20261013T032638Z-1f4e67.json
Next:
ghostbranch feature close search-filters --repo you/project
the preview lists what closing deletes; it is merged with no newer commits, so it closes without a console

A merged feature with no newer commits closes without asking:

Terminal
ghostbranch feature close search-filters
Closed search-filters (merged): deleted 4 ref(s) from the shadow repo; closed gate PR #15.
refs/ghostbranch/features/search-filters e831af79d73d
refs/ghostbranch/plans/20261013T032638Z-1f4e67 faf0688f63a9
refs/heads/ghostbranch-gate/search-filters 64f5694ae38b
refs/heads/search-filters 3fe224a3cb3e
The ledger keeps each SHA (ghostbranch log), so a deleted branch can be pushed back.

Anything else would lose work that was never published, so a person confirms the close with a one-time code at their own terminal. An agent grant doesn’t cover it. Earlier, while search-filters was still a draft, the preview said so:

Terminal
ghostbranch feature close search-filters --plan
Dry run; nothing was changed. This would:
- delete refs/ghostbranch/features/search-filters from you/project-shadow (at 38fac36b653d)
- delete refs/heads/search-filters from you/project-shadow (at ad95a30bdd2d)
Next:
ghostbranch feature close search-filters --repo you/project (a person must run this)
the preview lists what closing deletes; it is draft, so a person closes it at the console

If any of those refs moves while the close runs, it stops with FEATURE_MOVED and deletes nothing more. A feature with others stacked on it refuses with HAS_CHILDREN: deal with those first.

Every close writes each deleted ref and its SHA to the ledger, which ghostbranch log shows. To bring a branch back, push that SHA to it from a clone that still has the commit, for example git push shadow 3fe224a3cb3e:refs/heads/search-filters.

ghostbranch feature abandon <name> sets a feature aside without deleting anything. Its branch, gate and plans stay, and --undo brings it back:

Terminal
ghostbranch feature abandon search-filters
Abandoned search-filters (its branch is kept; undo with --undo).
ghostbranch feature list --all
NAME STATE DRIFT GATE PR
search-filters abandoned clean - -
ghostbranch feature abandon search-filters --undo
Restored search-filters; it is an active feature again.

The list above is cut down to the one feature. An abandoned feature drops out of feature list and sync’s count of active features, but feature list --all and status still show it. Its gate’s draft pull request in the shadow repo stays open.

Abandoning is for “not now”. To get rid of a feature for good, close it.

FEATURE_NOT_CLOSEDwarningSync didn't close a merged feature: the close failed, sync couldn't survey the features and so closed none, another feature's record couldn't be read, so sync left the merged features open, or its PR merged into the public branch below it, not main.
What to do

If details.deleted is set, the feature is gone: close its gate draft PR in the shadow repo by hand. With details.reason merged_into_parent_branch, its PR merged into public branch details.base, not main: a person checks that branch holds its changes, then closes it (ghostbranch feature close <name>); sync doesn't. With details.unreadable, the features named there have records that can't be read, and any of them may be stacked on this one: a person closes each at the console (ghostbranch feature close <name>) or fixes its record, and the next sync closes this feature. Otherwise run ghostbranch feature show <name>, then feature close <name>. With no details.feature, sync couldn't survey the features and closed none: run ghostbranch status to see why.

FEATURE_MOVEDexit 4A feature's branch or refs moved while it was being closed, so the close stopped; nothing more was deleted.
What to do

Run ghostbranch feature show <name> to see the new state, then close it again if that's still wanted.

HAS_CHILDRENexit 4The feature has children stacked on it (error.details.children), so it was not abandoned or closed: they would be left on a deleted parent.
What to do

Deal with each child in error.details.children first: fold it into this feature (ghostbranch feature fold <child>), abandon it (for abandon: ghostbranch feature abandon <child>) or close it (for close: ghostbranch feature close <child>), or move it onto main (ghostbranch feature restack <child> --onto main). An abandoned child blocks close, not abandon. Then run the command again.

APPROVAL_REQUIREDexit 5This needs a person: a code typed at a console. For promote and apply, an agent grant also works.
Causes
  • no_console: no terminal a person can type into
  • not_confirmed, timeout, too_many_attempts: the person did not confirm
  • grant_needs_yes: a grant exists, but --yes was not passed
What to do

Stop and give the human the command in next. It needs a person at a console; for promote and apply, an agent grant made with ghostbranch agents allow also works (feature close and remove ignore grants). Never fake a terminal, answer the code, edit grants.json or run ghostbranch agents allow yourself.

This needs a person.

LEDGER_NOT_WRITTENwarningThe command did what its result says, but its entry in the ledger (ghostbranch log) couldn't be written.
What to do

Nothing is undone and nothing needs running again. Keep details.refs when it is there: after a close it lists every deleted ref with its SHA, which the ledger would have kept for recovering a branch. If it repeats, check that the workspace's ledger file is writable and the disk has room.