verify-and-land · git:20261026.4cbceda · 2026-10-26 · sha256 c7c46a5abedd4d42
verify-and-land git:20261026.4cbcedaA
Immutable. This exact content never changes and is served at /api/v1/blob/c7c46a5abedd4d42.
---
name: verify-and-land
description: |
Build the checked-out branch, launch that exact app, let the user confirm the
fix is really there, then push and land it by the project's landing mode.
---
# Verify, then land
The gap this closes: a fix is committed, the user opens "the app", the fix is not
there — so they conclude it did not work. Usually it did. They were looking at a
**different build**: the copy in `/Applications` that Spotlight, the Dock and
Launchpad all open, or a `DerivedData` product from another branch. Nothing rebuilds
when the branch changes, because nothing ever does. Measured on one real project: the
installed copy was a month older than the branch build, and four other stale build
products were registered besides.
So this skill never asks the user to open the app. It **builds the branch they are
standing on, launches that exact bundle, proves on screen which one is running**, and
only then asks whether the fix is there. A yes lands the branch the way the project
has decided to land (Phase 7), without a second question unless the branch itself
changes how the project lands; a no keeps the branch open and says what to look at next.
Invoke with: `/superpowers-gstack:verify-and-land`
## Phase 0 — refuse early, and commit first
| Condition | Do this |
|---|---|
| Not a git repo | Refuse: "Not a git repository — there is no branch to verify or land." |
| No runnable app (Phase 2 finds none) | Refuse: "No app target found. This skill verifies something you can look at; for a library or CLI, run the tests instead." |
| Detached HEAD | Refuse landing later, so say now: give the work a branch first (`git switch -c <name>`), then re-run. |
| On the default branch | Build and launch normally, then skip Phase 7 — there is nothing to land. Say so once. |
**Uncommitted changes must be committed before the build.** Not a style preference:
the build compiles the working tree, but `git push` and every landing skill move
*commits*. Verify a dirty tree and land it and the user has approved something that
is not what shipped. Offer to commit — with a real message, per the repo's
convention — and only then build. If they decline, build anyway so they can look, but
**Phase 7 is blocked**: say plainly that landing would ship something other than what
they just saw.
## Phase 1 — name the change, so the user knows what to look at
```bash
DEFAULT_REF=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/||')
[ -z "$DEFAULT_REF" ] && for c in origin/main origin/master main master; do
git rev-parse -q --verify "$c" >/dev/null && DEFAULT_REF=$c && break
done
BASE=$(git merge-base HEAD "$DEFAULT_REF")
git log --oneline "$BASE..HEAD"; git diff --stat "$BASE..HEAD"
```
Keep the *ref* that verified, not a stripped name — in a fresh clone `origin/main`
exists and `main` may not, and `merge-base` against a missing ref fails.
Turn the diff into **one sentence per observable change**, not files: "the sidebar
should keep its width when you resize the window" beats "3 commits touching
`SidebarView.swift`". A branch often carries more than one change — name each,
because the gate in Phase 6 asks about all of them: one yes must never quietly ship
three changes of which the user looked at one. If a change has no user-visible
effect — a refactor, a test — say so: looking is the wrong check for it; the tests
cover it and the gate covers the rest.
**Name the views the diff touches.** A diff that changes SwiftUI views also needs the
view gate (`/superpowers-gstack:swiftui-view-gate`), because "it builds" and "the linters
are clean" are not a look:
```bash
SKILL_DIR='<the base directory the Skill tool printed when this skill loaded>'
VG="$SKILL_DIR/../../scripts/view-gate.py"; WS="$SKILL_DIR/../../scripts/window-shot.sh"
[ -f "$VG" ] || VG=$(ls ~/.claude/plugins/cache/*/superpowers-gstack/*/scripts/view-gate.py 2>/dev/null | sort -V | tail -1)
[ -f "$WS" ] || WS=$(ls ~/.claude/plugins/cache/*/superpowers-gstack/*/scripts/window-shot.sh 2>/dev/null | sort -V | tail -1)
VIEW_FILES=$(python3 "$VG" files --base "$DEFAULT_REF") \
|| { echo "the view gate could not be checked (view-gate.py missing or the base ref does not resolve)" >&2; VIEW_FILES="?"; }
```
Keep the list. Empty means the gate does not apply; say nothing about it. A failed check
is not empty: `?` counts as views touched, and the failure is reported in one line.
## Phase 2 — resolve the container, the scheme, and the destination
**Generate the project first if it is generated.** A `project.yml` is itself an
Apple-project signal: many repos commit only the spec and gitignore the
`.xcodeproj`, so listing schemes before generating finds nothing at all. If
`project.yml` exists and either no `.xcodeproj` is present or the spec (or anything
it includes) is newer, run `xcodegen generate` before anything else.
**Carry the container through every command.** A repo holding both a `.xcworkspace`
and a `.xcodeproj` makes bare `xcodebuild` ambiguous, and it will pick the wrong one
or fail:
```bash
if ls *.xcworkspace >/dev/null 2>&1; then XC=(-workspace "$(ls -d *.xcworkspace | head -1)")
elif ls *.xcodeproj >/dev/null 2>&1; then XC=(-project "$(ls -d *.xcodeproj | head -1)")
else XC=(); fi
xcodebuild "${XC[@]}" -list -json
```
Every later `xcodebuild` call takes `"${XC[@]}"`. One scheme is the answer; several,
ask once.
Split macOS from iOS by the scheme's own settings, never by guessing:
```bash
xcodebuild "${XC[@]}" -showBuildSettings -scheme "$SCHEME" -configuration Debug 2>/dev/null \
| grep -E '^ +SUPPORTED_PLATFORMS '
```
`macosx` → macOS. `iphonesimulator` → iOS. Both → ask which one the fix is in.
**Pin the destination, then re-read the settings through it.** `BUILT_PRODUCTS_DIR`
is platform-specific (`Debug` vs `Debug-iphonesimulator`); read without a destination
it answers for the scheme's *default* platform, so the launch would look in a
directory nothing was written to — and the proof step would validate the wrong place.
For iOS, pin one simulator by UDID: `booted` is ambiguous with several up, and a
`name=` destination need not be the one that is booted.
```bash
# iOS only — choose exactly one device and use it for build, install and launch
UDID=$(xcrun simctl list devices booted -j | python3 -c 'import json,sys;d=json.load(sys.stdin)["devices"];print(next((x["udid"] for v in d.values() for x in v),""))')
[ -z "$UDID" ] && { UDID=$(xcrun simctl list devices available -j | python3 -c 'import json,sys;d=json.load(sys.stdin)["devices"];print(next((x["udid"] for v in d.values() for x in v if "iPhone" in x["name"]),""))'); xcrun simctl boot "$UDID"; }
DEST="platform=iOS Simulator,id=$UDID" # macOS: DEST='platform=macOS'
eval "$(xcodebuild "${XC[@]}" -showBuildSettings -scheme "$SCHEME" -configuration Debug -destination "$DEST" 2>/dev/null \
| awk -F' = ' '/^ +(BUILT_PRODUCTS_DIR|FULL_PRODUCT_NAME|PRODUCT_BUNDLE_IDENTIFIER|EXECUTABLE_NAME) /{gsub(/^ +/,"",$1); print $1"=\""$2"\""}')"
```
All four values are used below, and **the launch phase refuses to run without them**.
### Non-Xcode projects
- **SwiftPM with a GUI app bundle** — build with `swift build`, then treat the
produced `.app` exactly as below. A plain **command-line** executable has no bundle,
no `PRODUCT_BUNDLE_IDENTIFIER` and nothing to look at: do not force it through this
flow. Run it once, show its output, and say that tests are the real check.
- **Web** — see Phase 6b.
## Phase 3 — the stale-app census (do not skip)
This is the phase that addresses the actual complaint. Before building, find every
bundle the user could plausibly be looking at:
```bash
# 1. What Spotlight, the Dock and `open -a` would actually start. Ask LaunchServices
# BY BUNDLE ID — `path to application "<name>"` matches the *display* name, which
# need not equal the executable name, and a silent miss defeats the census. Do not
# probe with `open -Ra` either: it pops a Finder window as a side effect.
osascript -e "POSIX path of (path to application id \"$PRODUCT_BUNDLE_IDENTIFIER\")" 2>/dev/null
# 2. Build products lying around. Spotlight does not index DerivedData, so `mdfind`
# finds none of these — glob the directory instead.
ls -d ~/Library/Developer/Xcode/DerivedData/*/Build/Products/*/"$FULL_PRODUCT_NAME" 2>/dev/null
# 3. Anything of this app already running, wherever it came from.
pgrep -fl "/Contents/MacOS/${EXECUTABLE_NAME}$" 2>/dev/null
```
Expect several — a project rebuilt under different DerivedData hashes accumulates
them. Report each with its build time (`date -r "<app>/Contents/MacOS/<exec>"`), then
say plainly: *"the one I am about to build and open is `<BUILT_PRODUCTS_DIR>/<name>`;
the copy in /Applications is from `<date>` and does not have this fix."* One sentence
— this is the misunderstanding that sends people hunting a bug that is not there. If
no other copy exists, say nothing; do not narrate an ambiguity that is absent.
## Phase 4 — build the branch that is checked out
Record the commit you are about to build: `VERIFIED_HEAD=$(git rev-parse HEAD)`. Phase 5
names it as the commit on screen, and a yes in Phase 6 is about this commit only.
```bash
xcodebuild "${XC[@]}" -scheme "$SCHEME" -configuration Debug -destination "$DEST" build
```
For iOS, `mcp__XcodeBuildMCP__build_run_sim` does build, install and launch in one
step — check availability with `ToolSearch` first, since these tools load on demand.
The CLI equivalent installs and launches **on the same UDID** that was built for:
`xcrun simctl install "$UDID" "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME"` then
`xcrun simctl launch "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER"`.
A failing build ends the skill here. Report the first real error, fix it if it is
within the current task, and never proceed to a launch that would show stale code.
A build can also exit 0 without leaving the product where the settings said it would
be. Check before launching — `open` on a missing path raises a macOS dialog that
explains nothing:
```bash
[ -d "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME" ] || { echo "build succeeded but $FULL_PRODUCT_NAME is not in $BUILT_PRODUCTS_DIR"; exit 1; }
```
## Phase 5 — launch that exact bundle, and prove it
**Refuse to run this phase with an unset variable.** The first line is not
decoration: an empty `$EXECUTABLE_NAME` would turn the fallback below into
`pkill -f "/Contents/MacOS/"`, which matches every bundled process on the machine —
`loginwindow` included — and would log the user out. Measured, not theorised.
```bash
: "${EXECUTABLE_NAME:?resolve build settings in Phase 2 first}" \
"${PRODUCT_BUNDLE_IDENTIFIER:?resolve build settings in Phase 2 first}" \
"${BUILT_PRODUCTS_DIR:?resolve build settings in Phase 2 first}"
```
Quit any running instance — otherwise macOS activates the copy already up, very often
the wrong one — and **wait for it to actually exit**, because both quit paths return
before the process does:
```bash
if pgrep -f "/Contents/MacOS/${EXECUTABLE_NAME}$" >/dev/null; then
osascript -e "tell application id \"$PRODUCT_BUNDLE_IDENTIFIER\" to quit" 2>/dev/null \
|| pkill -f "/Contents/MacOS/${EXECUTABLE_NAME}$"
for _ in $(seq 1 20); do
pgrep -f "/Contents/MacOS/${EXECUTABLE_NAME}$" >/dev/null || break; sleep 0.25
done
fi
open "$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME"
```
Then **prove** what came up rather than assuming — and wait for it, since a cold start
with signature validation takes well over a second. Match on the **full executable
path**, not a process name, so a helper or another copy cannot answer for it:
```bash
WANT="$BUILT_PRODUCTS_DIR/$FULL_PRODUCT_NAME/Contents/MacOS/$EXECUTABLE_NAME"
PID=""
for _ in $(seq 1 40); do
PID=$(pgrep -n "$EXECUTABLE_NAME" 2>/dev/null || true)
[ -n "$PID" ] && [ "$(ps -o comm= -p "$PID" 2>/dev/null)" = "$WANT" ] && break
PID=""; sleep 0.25
done
[ -n "$PID" ] && echo "on screen: $(ps -o comm= -p "$PID")" || echo "the branch build did not come up within 10s"
```
Match by **string equality on the full path**, not by a pattern: `pgrep -f` treats
its argument as a regular expression, and a bundle path is not one — a `+` or `(`
anywhere in it would quietly stop the proof from ever matching.
If nothing appeared, that is a launch failure — **not** a verdict on the fix. Report
it and stop. If a different copy is running instead, quit it, wait, and relaunch.
**iOS needs its own proof**, for the same reason: the simulator may still hold an
older install of the same bundle id, and `launch` will happily start that one.
```bash
xcrun simctl get_app_container "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER" app
stat -f '%Sm' "$(xcrun simctl get_app_container "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER" app)"
```
The timestamp must be from this run. If it is older the install did not take:
`xcrun simctl uninstall "$UDID" "$PRODUCT_BUNDLE_IDENTIFIER"`, install again.
Report one line: which branch, which bundle path, and which commit is on screen.
Record the launched bundle's literal absolute path from that line (the expanded
`<BUILT_PRODUCTS_DIR>/<FULL_PRODUCT_NAME>`, for example
`/Users/me/Library/Developer/Xcode/DerivedData/App-abc/Build/Products/Debug/App.app`),
the way `VERIFIED_HEAD` is recorded: Phase 7 hands it to `install` as `--quit-path`, and
shell variables do not survive from one Bash call to the next, so `$BUILT_PRODUCTS_DIR`
would be empty by then.
## Phase 5b — look yourself first
Only when `VIEW_FILES` is not empty, `$PID` came out of the Phase 5 proof, **and**
`.gstack/visual-verify` reads exactly `host`:
the project owner has said, once, in the repo, that the agent may look at the app it
launched. The user is the sensor of last resort, so look before asking.
```bash
WINDOW_SHOT_EXPECT="$WANT" "$WS" "$PID" "${TMPDIR:-/tmp}/verify-$(git rev-parse --short HEAD).png"
```
Shell variables do not survive from one Bash call to the next: run this in the same call
as the Phase 5 proof and the Phase 1 lookup of `$WS`, or write the literal PID, executable
path and script path from those phases in their place.
It captures one window by id, never the display. Read the PNG and list what is on screen,
including defects nobody asked about (a truncated label, a missing chevron). A defect you
can fix is fixed first: back to Phase 4, with a new `VERIFIED_HEAD`. A non-zero exit (3 not
macOS, 4 no window, 5 blank capture, 6 wrong process, 7 the Swift helper cannot be built) or
a `BLOCKED` pin is one line in the report and no more: go on to the gate and let the user
look. Never fall back to a whole-display capture. The result belongs in the `View gate:` line.
## Phase 6 — the human gate
The user is the instrument; a person looking is the entire point. Ask with
`AskUserQuestion`, naming the behaviour from Phase 1.
When the diff changes SwiftUI views, name each changed view file in the question and
say, per file, whether you *rendered and saw* it (`live-swiftui`, or a screenshot of the
launched bundle) or only *built* it, appearance unverified. A project adapted since 3.12.0
carries the five steps (look up, lint, affordance check, see, report) in the "SwiftUI view
changes" section of its CLAUDE.md; the statement above applies with or without it. A view you did not see is listed as unverified, and the user's yes then covers
what they looked at, not the whole diff.
**View gate.** When `VIEW_FILES` is not empty, the session must hold a `View gate:` line
from `/superpowers-gstack:swiftui-view-gate` for these files. Without one, run the gate
first. If the user waives it, the line reads `View gate: waived by user`. A
`built, appearance unverified` line is allowed through but named in the yes option, so the
user knows the branch is landing on their eyes alone. Do not present "ready to land" for
views with no line at all.
The options:
- **Yes, it works** — asked per observable change when there are several; only when
all of them hold → Phase 7. Phase 7 then lands without asking again, so settle now
what a yes does, and say it in this option:
- Name `VERIFIED_HEAD`, recorded in Phase 4 before the build: the commit the user is
looking at. Do not re-read `HEAD` here; a commit made since the build is not on screen.
- Read the landing mode (the rule in Phase 7, step 2) and name what follows: `solo` —
push, then land on the default branch through `/superpowers-gstack:land`; `pr` —
push, then open a pull request through `/ship`; no valid line — push, then `land`
asks once how this project lands. With no remote configured, say that nothing is
pushed and the landing stays on this machine. If `/review` has not run on this
branch in this session, say that a yes lands code no review has seen (`land`
assumes one has).
- When the default branch's `CLAUDE.md` has `Installable: macos-app <Scheme>` (read
like the landing mode, outside code blocks) and the landing mode is `solo`, the
option says: yes, make this the current version and install it in /Applications.
`land` runs `/superpowers-gstack:work-state` on its exit 0, and that starts the
install. The yes also covers quitting the branch build Phase 5 opened, so Phase 7
can hand it to `install` as `--quit-path`. With `pr` the install waits for the pull
request; say so instead. With no valid line, the option does not promise the
install: `land` first asks how this project lands, and only a `solo` answer lands
and installs in this run.
- When Phase 0 skipped or blocked Phase 7 (the default branch, a detached HEAD, or
uncommitted changes the user declined to commit), say instead that a yes lands
nothing.
- **No, it still behaves the old way** → stay on the branch, land nothing. Say what
you would check next and offer `/investigate`.
- **Something else broke** → same: land nothing, investigate.
**End your message at this question.** Do not pre-run the landing steps and do not
describe them as though they had happened.
### Phase 6b — the web track, briefly
Record `VERIFIED_HEAD=$(git rev-parse HEAD)` first, as Phase 4 does. Then start the
project's dev script **and keep its PID**. A ready port does not prove the
server is yours: an older one from another branch or worktree may already own it, in
which case the new process exits and the readiness probe succeeds against stale code
— the same bug as the stale bundle, wearing a different hat. Check that the process
you started is still alive before trusting the port, and if the port was taken, say
so and use a free one. Then the same gate, unchanged. Skip Phase 3 — a dev server
serves the working tree, so no stale-bundle ambiguity exists.
## Phase 7 — land it
Landing has two halves and they are not the same: **pushing is backup, landing is
completion.** Do both, in that order. The yes in Phase 6 is the one human review; Phase 7
adds no question of its own when the project has already decided how it lands.
0. **Check that the yes still applies.** A yes covers the build the user saw and the
landing they were told about, nothing else. If `git rev-parse HEAD` is no longer
`VERIFIED_HEAD`, or the landing mode now reads differently from what the Phase 6
option said, or `VIEW_FILES` is not empty and the session holds no `View gate:` line,
land nothing and say what changed. A new commit means a new build: go
back to Phase 4. A changed mode only invalidates the announcement: go back to Phase 6
and ask again with the mode as it now reads.
1. **Push first**, so the verified work exists in more than one place whatever is
decided next. Pass the branch as a quoted argument — refs may legally contain
`$( )`, `;` and `&`:
```bash
branch=$(git symbolic-ref --short HEAD)
git rev-parse -q --verify "@{upstream}" >/dev/null \
&& git push \
|| git push -u origin -- "$branch"
```
If the push fails, land nothing: report the error and stop, so landing never runs
without the backup the Phase 6 option promised. With no remote configured there is
nothing to push: say plainly that the work stays on this machine, then go on.
2. **Then land it by the project's landing mode.** Read the `Landing mode:` line the way
`land` does: the exact line `Landing mode: solo` or `Landing mode: pr` in the
`CLAUDE.md` at the root of this worktree (`git rev-parse --show-toplevel`), a real line
outside fenced code blocks. Compare it with the same line on the default branch
(`git show "$DEFAULT_REF":CLAUDE.md`, the ref from Phase 1). When `CLAUDE.md` is a
symlink, that prints only the link's target path: read the target the same way
(`git show "$DEFAULT_REF":AGENTS.md` for a link to `AGENTS.md`), as `land` does.
- **The branch changes the landing mode** (the two differ) → land nothing
automatically: a branch must not set the policy it is landed under. Ask once with
`AskUserQuestion` which mode holds, naming both values, and end your message at it.
Act on the answer as `land`'s exit-14 row says: it is the same stop, made by `land`
itself when it is invoked on such a branch.
- **`solo`** → invoke `/superpowers-gstack:land` for this worktree, from its root, with
no further question.
- **`pr`** → invoke `/ship` inside this worktree, with no further question from this
skill. `/ship` may ask its own questions about the pull request; those are not a
second ask of the landing. When `/ship` has opened the pull request, run
`/superpowers-gstack:work-state` and say that the work waits for a pull request
before it is landed or installed; end the message with its line.
- **No valid line** (missing, only inside a code block, or both values) → invoke
`/superpowers-gstack:land` anyway. Its exit-2 row owns this question: it asks the
user once (solo or pull request? or, with both values, which one holds) and records
the answer. Do not ask it here as well.
When `land` stops with an exit code, follow `land`'s own exit-code table: it is the
authority on the next step. Add no menu of your own on top of it, and never offer
`/superpowers:finishing-a-development-branch` from this skill: it merges without the
project's pre-merge gate, which is why `land` stops at exit 8 when `wt` is not
installed instead of falling back to it.
**If the branch is checked out in a worktree**, run `/ship` and `/superpowers-gstack:land`
from that worktree's root: a branch cannot be checked out twice, and `git checkout` fails
outright elsewhere (`git worktree list` gives the path). The landing skills move the
default branch themselves; do not hand-roll a merge.
After `land` exits 0, follow its `remaining` field: it prints the cleanup commands in the
order git needs them (the worktree first, then the branch). `land` then runs
`/superpowers-gstack:work-state`. When its `install` entry starts and the Phase 6 yes named
the install, add `--quit-path <the literal bundle path recorded in Phase 5>` to that
entry, written out in full (never a shell variable: `install` compares the path as exact
text) and as ONE argv element, without quote characters, so the commonest path does not stop at code 4 because the branch build is still
running. Without that yes in this session, pass no quit flag: code 4 is
then a question for the user, as the work-state skill says.
## What this skill is not
- Not a test run. Tests answer "did I break something else"; this answers "is the
thing I fixed actually fixed". `/ship` runs the tests on the way to a PR.
- Not UI automation. `/superpowers-gstack:e2e-route` owns automated E2E; this is the
one case where a human eye is the point.
- Not a release. It builds Debug and launches locally; shipping to users is
`/land-and-deploy`.