Skip to content
Back to skills

Land

ASecurity

Squash-merges an approved plan branch to main with a conventional subject that closes its issues, then deletes the branch.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
developmentgobashgit

Security analysis

A100/100

Scanned October 7, 2026

npx -y skills add Zalaras/muster --skill land --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Land?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Land
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zalaras-land/badge)](https://www.skillsdirectory.com/skills/zalaras-land)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: land
description: "Squash-merges an approved plan branch to main with a conventional subject that closes its issues, then deletes the branch."
argument-hint: "<plan-name>"
allowed-tools: Read, Grep, Glob, Bash, Edit, AskUserQuestion
---

> Maintainer note: a skill in the main session because it is the one step that commits on `main`
> and pushes, so it must be able to stop and ask (kb:adr/process-land-decides-proposed-backlog).

You land an approved plan branch on `main`. `/orchestrate` deliberately never merges or pushes
(`.claude/skills/orchestrate/SKILL.md` Completion step 8) — this is that missing step.

Plan name: **$ARGUMENTS**

## Why the close happens here

GitHub closes an issue when a commit whose message contains `closes #N` lands on the default
branch. That is the correct moment: the fix is on `main`, accepted, and the close event links to
the commit. Nothing earlier qualifies — a review verdict of `approved` is the reviewer's opinion
of the work, not the developer's acceptance of it, and a plan branch can still be rejected or
reworked. So `/orchestrate` records the issue numbers and **you** put them in the subject.

## 1. Preflight — refuse, don't warn

Check all of these before touching anything. If any fails, stop and say exactly which:

1. `plans/<plan>/review.md` exists and contains `**Verdict**: approved` — **read it from disk**,
   never rely on the conversation (orchestrate's state script refuses `completed` on the same test).
2. `plans/<plan>/orchestration-state.json` has `"status": "completed"`.
3. `git status --short` is clean, apart from untracked strays that are not part of the plan
   (e.g. a stray screenshot). Leave those strays where they are: stage nothing with `git add -A`,
   and never stash.
4. `git rev-parse --verify plan/<plan>` succeeds.
5. The branch has something to land. **Do not use `git log main..plan/<plan>` for this** —
   a squash-merge never empties that range.
   Use the tree test instead:

   ```bash
   test "$(git merge-tree --write-tree main plan/<plan> | head -1)" = "$(git rev-parse main^{tree})"
   ```

   Equal means merging would change nothing — already landed, so say "nothing to land" and stop.
   A squash of an empty range produces an empty commit, the worst outcome available here.
6. The plan's worktree is there and idle: `make worktrees` lists `../muster-<plan>` on
   `plan/<plan>` (kb:adr/process-pipeline-runs-in-sibling-worktree), and no `claude` has it as
   its cwd (`lsof -a -d cwd -c claude -Fn` prints no `n<that path>` line) — end that session
   first; this command runs from the primary checkout, on `main`. A plan from before worktrees,
   or a `/fix` plan (a `**Shape**` header) that `make worktrees` does not list, has no tree: then
   step 2 and preflight 7 use `git checkout plan/<plan>` here instead, which needs preflight 3
   strictly clean, and step 6 skips the worktree removal.
7. For each name in the plan's `**Features**` and `**Touches**`, `go run ./tools/kb ls --feature <f> --status
   proposed` lists no record with `refs: plan:<plan>`, and `make -C ../muster-<plan> check-kb`
   (or `make check-kb` on the checked-out branch when there is no tree) exits 0. A `proposed` ADR
   here means orchestrate's Completion step 4 or `/fix` step 8 was skipped — send it back rather
   than flipping it yourself. The only files this command ever edits are `TODO.md` and
   `proposed-backlog.md` files, in step 2, on the developer's answers.

A plan that never went through `/orchestrate` or `/fix` (no state file, no review) is not landable
by this command. Say so and let the developer commit it themselves.

## 2. Decide the proposed follow-ups

A run files nothing into `TODO.md` beyond what the approved plan's `## Out of scope` names, copied
verbatim; everything else it proposes in `plans/<plan>/proposed-backlog.md`
(kb:adr/process-backlog-entries-are-the-users-to-file). This is where the developer decides, so the
decisions ride the same squash as the fix (kb:adr/process-land-decides-proposed-backlog).

**Scope.** This plan's file, plus every other `plans/*/proposed-backlog.md` **as it is on
`main`** (`git show main:<path>`) that has no `## Decisions` section, or a Decisions line marked
`deferred` — the sweep catches plans committed without `/land`. A swept file is edited on the
plan branch only when `git diff --quiet main plan/<plan> -- <path>` holds; otherwise leave it for
the next `/land` and say so. Nothing open → "no proposals", one line, and go to step 3.

**Verify before showing.** For each proposal, check it still holds: grep the symbol or line it
names, `git log -S` for a fix. Mark one fixed since as **already done** with the commit or line
as evidence, and one that repeats another proposal or an open `TODO.md` entry as **duplicate of
#N**, each with that evidence.

**Print** a numbered list grouped by plan, one short line each — the block's **Summary** line
where it has one — tagging **Asked** where **Change requested** is yes, then a tally: distinct,
already done, duplicates. **Ask in prose, not an `AskUserQuestion` menu**: the developer answers per
number — yes, not doing, defer, or a changed wording ("add as an investigation"). Asked to
explain one, explain it plainly and wait.

**Apply**, on `plan/<plan>` in its worktree — edit `../muster-<plan>/TODO.md` and the
`proposed-backlog.md` files there and commit with `git -C ../muster-<plan>` (the branch is
checked out in that tree, so a `git checkout plan/<plan>` here is refused). With no tree:
`git checkout plan/<plan>` here, edit, commit, `git checkout main`, and the two checks below run
from this checkout:

- File only what the developer chose. Each yes → `TODO.md`, in the section they name, else at the end of **Pre-v1**, under a
  `Filed <date> by the developer from the plans' proposed-backlog.md files` lead line: `- [ ] **Title** — body … From \`plans/<plan>/\`.` An
  entry that would reverse an accepted ADR names it.
- Every decision → one line in a `## Decisions (the developer, <date>)` section appended to its
  file: `filed: TODO.md § <section>, "<title>"`, `not doing`, `already done: <evidence>. Not
  filed.`, or `deferred`.
- Commit the files by name, `docs(<plan>): file follow-ups from proposed-backlog`, then
  `make -C ../muster-<plan> check-kb` and `../muster-<plan>/.claude/skills/orchestrate/scripts/dead-refs.py --all`
  (it works in its own checkout) exit 0, and `git merge-tree --write-tree main plan/<plan>`
  exits 0 (1 means the edit now conflicts with `main` — stop and ask).

## 3. Compose the subject

Format, per `docs/conventions.md` § Commits — one sentence, no body:

```
type(scope): imperative summary (closes #N, closes #M)
```

- **Type** decides the release, so choose it deliberately: `feat` (minor) for new behaviour;
  `fix`, `perf` or `refactor` (patch) when that is what shipped; the remaining types
  (`docs`/`test`/`chore`/`ci`/`build`/`style`/`revert`) release nothing. Read the plan's
  description and the impl logs rather than guessing from the plan name. A `**Shape**` header
  names the type outright; `fix`/`perf`/`refactor` still need a staged file that changes the
  shipped artifact (`.githooks/commit-msg` refuses otherwise) — a branch whose diff is all tests,
  tooling or docs lands as the hook allows, and you say so.
- **`!` needs an explicit go-ahead from the developer** — the commit-msg hook rejects it unless a
  human sets `MUSTER_BREAKING=1`, and this skill never sets it on its own. On 0.x it is safe
  when sanctioned: `release.yml` runs `svu next --v0`, so a breaking marker bumps minor, never
  1.0.0 (`docs/conventions.md` § Commits). Mark a breaking change with `!` only: the hook rejects
  the breaking-footer phrase anywhere in a message, bodies included.
- **Summary** describes what shipped, not what the plan was called, and is **at most 72
  characters** before the `(closes …)` tail.

  **This subject is published verbatim as the release note.** `.goreleaser.yaml` sets
  `changelog.use: github` with `include: ^(feat|fix|perf|refactor)`, so every subject of those
  four types — and only those — becomes one bullet in the GitHub Release. Write it for that
  reader, taking the shape from `docs/conventions.md` § Commits' worked example — not from this
  repo's older, overlong subjects, which are the mistake this rule exists to stop.

### The issue references

Read `closes_issues` from `plans/<plan>/orchestration-state.json` — orchestrate writes it at
completion for every issue the plan **fully** resolves, and `[]` when it closes none. If the key is
absent (an older plan, or a run that skipped the step),
grep the plan's ticked items in `docs/history/todo-done.md` — and, for a `/fix` plan, its
`## Symptom` — for issue links and ask the developer to confirm.

Append one reference per issue, lowercase: `... (closes #2, closes #4)`.

**The subject carries no `(plan <name>)` marker.** It is bookkeeping in a user-facing release
note, and the plan is always recoverable from the commit itself — the squash includes
`plans/<name>/`, so `git show --stat <sha> | grep plans/` names it. If a plan closes no
issues, the subject simply has no tail.

**Only fully-resolved issues.** Ticking a TODO item and closing an issue are different claims —
a plan can advance an issue without finishing it. If orchestrate flagged an issue as partially
addressed, name it in the report as deliberately not closing.

## 4. Show before doing

Print, and get confirmation:

- the exact subject line;
- the issues it will close;
- `git log --oneline main..plan/<plan>` — what is being squashed;
- `git diff --stat main...plan/<plan>` — the size of what lands;
- the backlog items the branch adds — `git diff main...plan/<plan> -- TODO.md | grep '^+- \[ \]'`
  — and, for each, whether step 2 filed it or the plan's `## Out of scope` names it. Anything else is a run filing
  work you did not approve (kb:adr/process-backlog-entries-are-the-users-to-file): say so here
  rather than after the merge;
- the **predicted release** — landing chooses the version bump, so make it visible:
  `feat`→minor; `fix`/`perf`/`refactor`→patch; everything else→none.
  `.github/workflows/release.yml` computes the actual version on push (`svu next --v0`, plus its
  perf/refactor patch shim).

## 5. Land

```bash
git checkout main
git merge --squash plan/<plan>
git commit -m "<subject>"
git push
```

`git push` is not in `.claude/settings.json`'s allowlist, so the point of no return prompts on
its own — leave it that way. Push plainly: no force-push, and no amending a commit already on
`main`. The push triggers `release.yml`, which tags and publishes darwin
archives, and GitHub closes the referenced issues.

## 6. Delete the branch

A squash-merge leaves git considering the branch unmerged, so `git branch --merged` is useless
here and `-d` will refuse. `-D` is therefore required — which means the verification has to be
real. **`git diff main plan/<plan>` is not it**: it also reports everything `main` gained after
the branch landed, so a correctly-landed branch shows differences.

Preferred check — the same tree test as preflight 5:

```bash
test "$(git merge-tree --write-tree main plan/<plan> | head -1)" = "$(git rev-parse main^{tree})"
```

A no-op merge is proof the branch holds nothing `main` lacks. If it is **not** a no-op, that is
usually staleness rather than lost work — `merge-tree` writes conflict markers into the tree when
an old branch collides with later work on `main`, which changes the tree hash without any content
being unlanded. Don't delete on that alone; establish all three:

1. the plan's squash commit exists on `main` — find it by the issues it closed
   (`git log --oneline --grep "closes #<N>"`) or by the plan directory it carries
   (`git log --oneline -- plans/<plan>/`); subjects no longer carry a `(plan <name>)` marker,
2. the branch has **zero commits after** that squash landed (compare `git log -1 --format=%ci`
   on both) — this is the case that would actually lose work,
3. `git diff plan/<plan> <squash-sha>` shows only lines `main` later superseded, never lines the
   branch has and `main` lacks.

If any of the three is unclear, keep the branch and ask. A branch costs nothing; lost work does.

Then, in order: `git worktree remove ../muster-<plan>` without `--force` when `make worktrees`
lists the tree — a refusal means it is dirty or still in use, so stop and ask — and only then
`git branch -D plan/<plan>`, which git refuses while the branch is checked out in a tree or here
(`git branch --show-current` must print `main`). `git worktree remove` is deliberately not in
`.claude/settings.json`'s allowlist: like `git push`, the point of no return prompts on its own.

## 7. Report

- the squash SHA on `main` and the subject that landed;
- which issues will close, and any deliberately left open with the reason;
- the proposals decided in step 2 — filed (and where), not doing, already done, deferred;
- the release workflow triggered and the predicted bump;
- the branch deleted, with the step 6 verification (the no-op tree test, or the three checks)
  stated as evidence.

Then remind the developer that `/triage --audit` will show any issue whose close silently failed.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.