Imported from bossanova-dev/bossanova (
plugins/bossd-plugin-claude/skilldata/skills/boss-finalize/SKILL.md). Install upstream withnpx skills add bossanova-dev/bossanova --skill boss-finalize. Copyright stays with the author.
Land the Plane: Session Completion Workflow
"Landing the plane" is the mandatory end-of-session process ensuring all work is committed and pushed to remote. Work is NOT complete until git push succeeds.
⛔ BLOCKING REQUIREMENTS - READ FIRST ⛔
All eight must hold before this workflow is complete.
| # | Requirement | How to Verify |
|---|---|---|
| 1 | All quality gates pass | Discover and run the repo's quality gates. Prefer a single project-declared aggregate command when it covers build/lint/test; otherwise run the minimal non-duplicative command set. ALL must pass. Fix failures — do NOT dismiss them as "pre-existing" without verifying on the PR base branch. |
| 2 | PR base is current | Fetch the PR base and verify git merge-base --is-ancestor "origin/$BASE_BRANCH" HEAD before rewriting commits, squashing, or pushing. If it fails, rebase onto origin/$BASE_BRANCH first — always rebase, never merge the base branch in. |
| 3 | PR number in ALL commits | Every commit on this branch (compared to the PR base branch) MUST have [#PR-NUM] in the message. Check with git log origin/$BASE_BRANCH..HEAD --oneline. If ANY commit is missing it, you MUST run the fix script. |
| 4 | Commits squashed and tidied | You MUST squash commits into logical groups and force-push. Do NOT ask for permission — just do it. |
| 5 | GitHub checks not failing | After pushing, gather gh pr checks --json name,state,bucket and the SHA-keyed check-runs payload without dumping raw logs, then decide the verdict with $BOSS_FINALIZE_TOOLBOX/pr-check-state.mjs (toolbox/pr-check-state.mjs) — this table states no green-or-red rule of its own. Only its failing state MUST be investigated and fixed; pending and unknown are not failures and not passes. See Step 6b. |
| 6 | PR marked Ready for Review | After all checks pass or are non-blocking, run gh pr ready "$PR_URL" and verify gh pr view "$PR_URL" --json isDraft -q .isDraft returns false. Do NOT leave the PR as a draft. |
| 7 | No merge conflicts | Check GitHub for merge conflicts with gh pr view --json mergeable -q .mergeable. If CONFLICTING, rebase onto the PR base branch and resolve conflicts before completing. |
| 8 | History stays linear | git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD MUST be 0 before the push in Step 6. A merge commit on the branch structurally breaks a rebase-merge repo, so GitHub refuses the PR however green the checks are. Linearize before pushing. |
Workflow Steps
Step 0: Dispatch the finalize workflow to an isolated subagent
The orchestrator does not run Steps 1–8 inline on its own context. Instead it dispatches the
entire finalize workflow (Steps 1–8 below) to one fresh subagent (Agent/Task tool,
subagent_type: general-purpose, model: "sonnet") and awaits it — never run_in_background;
use $BOSS_FINALIZE_TOOLBOX/bs-dispatch-await.mjs (toolbox/bs-dispatch-await.mjs) for completion classification.
Pass the model: "sonnet" alias (not a pinned date-suffixed id) so it follows the current Sonnet target
selected by the agent runtime. This intentionally accepts alias drift; when the alias target changes,
rerun the tiered-vs-Opus artifact diff before relying on prior proof.
Keep judgment off Sonnet (stop-and-report). The tiered subagent runs only the mechanical happy
path. If it hits a genuine merge conflict requiring 3-way resolution (BLOCKING REQUIREMENT 7) or a
failing quality gate that needs a code edit to fix (BLOCKING REQUIREMENT 1), it must not resolve
it on Sonnet — it stops immediately and returns NEEDS_OPUS: <one-line reason> instead of a terminal
result. Detecting when to stop is itself mechanical, not judgment: a conflict announces itself with a
non-zero rebase exit and <<<<<<< markers, and a failing gate with a non-zero exit — Sonnet only has
to notice the signal, never to resolve it. Plain mechanical remediation stays on Sonnet: a clean
fast-forward/rebase with no conflict, or re-running a gate that flaked. Conflict resolution and code
edits are judgment and belong on Opus.
The dispatch brief passes:
- The branch, the PR URL and number, the base branch.
- The discovered quality-gate command(s).
- The full text of the ⛔ BLOCKING REQUIREMENTS - READ FIRST ⛔ table above, verbatim — these are the contract the subagent must satisfy in full. They stay in the subagent's brief unchanged.
The subagent runs Steps 1–8 in full, satisfying all 8 BLOCKING REQUIREMENTS above. It keeps ALL bulk
output inside its own context — git log, git diff, gh pr checks, squash/rebase output — and
returns only a short structured result to the orchestrator: the final PR state
(isDraft/mergeable), checks status, and what was squashed/pushed. Do not paste raw diffs or logs back
to the orchestrator.
Bulk-output discipline (no raw dumps). Never paste full diffs, CI logs, gh run view output, or
review threads into the main thread — that bulk is re-charged on every later turn. Read them inside a
subagent and return a summary, or filter to the few relevant lines: scan checks with
gh pr checks --json name,state,bucket (or gh pr view --json statusCheckRollup) and pull failure
logs with gh run view <run-id> --log-failed | tail, not the full log. This rule holds whether the
finalize workflow runs in the Step 0 subagent or falls back inline.
After the subagent returns, the orchestrator re-verifies the terminal invariant cheaply, with ONE
call — gh pr view --json isDraft,mergeable,statusCheckRollup — instead of re-reading the workflow.
The completion contract is unchanged: work is NOT complete until git push succeeds and the PR is
Ready for Review (isDraft=false).
If the subagent dispatch itself fails (a tool error, not a workflow failure), or returns
NEEDS_OPUS (it hit a conflict/gate escape hatch per the stop-and-report rule above), the
orchestrator falls back to running Steps 1–8 inline on its own model (Opus). The dispatch is awaited
and its failure is non-fatal — fall back inline rather than abandoning the session. This keeps the
mechanical happy path on Sonnet while any genuine judgment (conflict resolution, code edits) finalizes
at full capability.
The steps below (1–8) are what the dispatched subagent runs.
Step 1: Assess Current State
Run these commands to understand what needs to be done:
git status # Uncommitted changes?
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || true)
if [ -z "$BASE_BRANCH" ]; then
CURRENT_BRANCH=$(git branch --show-current)
UPSTREAM_BRANCH=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null | sed 's#^origin/##' || true)
# `head -1 | cut` and not an `awk` field reference: a slash-command invocation rewrites every
# positional parameter in this body — a dollar sign followed by one digit — before any shell
# runs it, replacing each with nothing when the command was invoked without arguments. An awk
# program that selects a field then arrives with an empty print list, prints the whole line,
# and hands back a wrong branch at exit 0. (This comment spells no positional itself, or the
# substitution would eat the example too.) The `test -n` guard below covers the unrelated
# case: no candidate branch at all. One behavioural difference the rewrite does introduce:
# `head -1` exits as soon as it has the first line where awk consumed all of the input. This
# block sets no `pipefail`, so the substitution's status is unchanged today — but under a future
# `set -o pipefail` a SIGPIPE'd `sort` would fail it.
BASE_BRANCH=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin | sed 's#^origin/##' | grep -Fvx HEAD | grep -Fvx "$CURRENT_BRANCH" | { if [ -n "$UPSTREAM_BRANCH" ]; then grep -Fvx "$UPSTREAM_BRANCH"; else cat; fi; } | while read -r branch; do base=$(git merge-base HEAD "origin/$branch" 2>/dev/null) || continue; git merge-base --is-ancestor HEAD "origin/$branch" 2>/dev/null && continue; printf '%s %s\n' "$(git show -s --format=%ct "$base")" "$branch"; done | sort -nr | head -1 | cut -d' ' -f2)
if [ -n "$BASE_BRANCH" ]; then echo "Using inferred git base branch: $BASE_BRANCH"; fi
fi
test -n "$BASE_BRANCH" || { echo "Could not determine PR base branch"; exit 1; }
git fetch origin "$BASE_BRANCH"
git log "origin/$BASE_BRANCH"..HEAD --oneline # ALL commits on this branch (vs PR base)
gh pr view --json number -q .number # Get PR number
IMPORTANT: Always compare to origin/$BASE_BRANCH, not the feature branch or default branch. If GitHub metadata is unavailable, the git fallback infers the most likely base from fetched origin/* branches; verify the printed branch before continuing. This shows ALL commits on your branch that aren't in the PR base branch, regardless of whether they're "pushed" to the feature branch.
Base freshness is mandatory before any commit rewrite:
BASE_TIP=$(git rev-parse "origin/$BASE_BRANCH")
MERGE_BASE=$(git merge-base HEAD "origin/$BASE_BRANCH")
BRANCH_OWNED_FILES=$(mktemp)
git diff --name-only "$MERGE_BASE"..HEAD > "$BRANCH_OWNED_FILES"
if ! git merge-base --is-ancestor "origin/$BASE_BRANCH" HEAD; then
echo "PR base is not included in HEAD. Rebase before squashing or pushing."
git rebase "origin/$BASE_BRANCH"
MERGE_BASE=$(git merge-base HEAD "origin/$BASE_BRANCH")
git diff --name-only "$MERGE_BASE"..HEAD > "$BRANCH_OWNED_FILES"
fi
BASE_REVERTS=$(
git diff --name-only "$MERGE_BASE".."origin/$BASE_BRANCH" | while IFS= read -r file; do
base_blob=$(git rev-parse "$MERGE_BASE:$file" 2>/dev/null || true)
head_blob=$(git rev-parse "HEAD:$file" 2>/dev/null || true)
base_tip_blob=$(git rev-parse "origin/$BASE_BRANCH:$file" 2>/dev/null || true)
if [ -n "$base_blob" ] && [ "$head_blob" = "$base_blob" ] && [ "$base_tip_blob" != "$base_blob" ]; then
echo "$file"
fi
done
)
test -z "$BASE_REVERTS" || { echo "HEAD reverts files changed on origin/$BASE_BRANCH:"; echo "$BASE_REVERTS"; exit 1; }
test "$BASE_TIP" = "$(git rev-parse origin/$BASE_BRANCH)" || { echo "origin/$BASE_BRANCH moved during finalize; restart Step 1"; exit 1; }
Sync with the base by rebasing only. Merging the base ref into the branch — or any git pull that
records a merge — leaves a merge commit that structurally breaks a rebase-merge repo, so GitHub
refuses the PR no matter how green the checks are. Use git pull --rebase when a pull is
unavoidable, and keep git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD at 0.
Do NOT use git reset --soft origin/$BASE_BRANCH unless origin/$BASE_BRANCH is already an ancestor of HEAD. Soft-resetting stale branch history onto a newer base stages reverse diffs for base-only changes and can commit other people's work as reverts.
Determine your situation:
- Uncommitted changes exist? → Go to Step 2
- Commits exist on branch? → Go to Step 4 (MUST check PR numbers!)
- No commits on branch vs PR base? → Skip to Step 7
Step 2: Run Quality Gates
Blocking requirement 1: the repo's quality gates must pass.
Step 2a: Discover the Gate Commands
Find the commands this repo expects contributors to run. Check these sources in order:
- User/project instructions (
AGENTS.md,CLAUDE.md,README,CONTRIBUTING, package docs) - CI workflows (
.github/workflows, Buildkite, CircleCI, GitLab CI, etc.) - Project command files (
Makefile,justfile,Taskfile.yml,package.json,go.mod,Cargo.toml,pyproject.toml, etc.)
Choose the smallest command set that covers the repo's required generate/build/lint/test checks without running the same check twice.
Step 2b: Run Project Gates
Prefer an explicit aggregate gate when present and complete:
make # Only if Makefile exists and default target is the project gate
make lint # Common lint gate
make test # Common test gate
just check # If justfile declares the project gate
task check # If Taskfile declares the project gate
If the aggregate gate does not cover everything, run only the missing targets that exist. Examples:
make build
make lint
make test
For repos without a Makefile, use the native project commands. Examples:
pnpm lint && pnpm test
npm run lint && npm test
go test ./...
cargo test
pytest
Do not assume make exists. Do not blindly run make, then make lint, then make test. Inspect the repo first. Some make targets already include lint and test; some repos have no Makefile.
If gates fail due to missing dependencies (e.g., node_modules missing, missing codegen tool), install the repo's documented dependencies first, then re-run the same gate commands.
If format changed files: Stage the formatting fixes and include them in your commit.
If any gate fails: Fix the issues, stage the fixes, and re-run until all pass. Do NOT skip a failing gate. Do NOT proceed to commit until all gates are green.
⛔ "Pre-existing" failures — verify before dismissing
Do NOT assume a failure is pre-existing. A failure is only pre-existing if it also fails on the PR base branch. Before dismissing any failure:
- Check if it's a missing prerequisite (generated code, dependencies) — if so, fix it
- If you believe it's truly pre-existing, verify by checking CI on the PR base branch or running the same command on the PR base branch
- Only after verification can you note it and proceed — and you MUST inform the user explicitly
Step 3: Commit Changes
Use conventional-commit format (see the git-committing skill). Always include the PR number.
Step 4: Fix ALL Commits Missing PR Numbers
Blocking requirement 3: fix the commits, don't just report on them.
# Get the PR number
PR_NUM=$(gh pr view --json number -q .number 2>/dev/null || echo "UNKNOWN")
echo "PR number: $PR_NUM"
# Show ALL commits on this branch (compared to PR base)
git log origin/$BASE_BRANCH..HEAD --oneline
Check every non-empty commit message for [#PR-NUM]:
- ✅ Good:
feat(mobile): [#2137] add feature X - ✅ Good:
chore: [skip ci] create pull requestwhen the commit is empty - ❌ Bad:
feat(mobile): add feature Xon a non-empty commit
⛔ If ANY non-empty commit is missing the PR number, you MUST run the fix script:
# Run from repo root - automatically detects PR number
~/.claude/skills/boss-finalize/add-pr-numbers.sh
DO NOT skip this step. Even if the branch is "up to date with origin", the commits still need PR numbers. The script compares against the PR base branch, not the feature branch.
The script now verifies this post-condition itself: after the rebase it re-checks every commit, skips empty commits, and exits non-zero only for non-empty commits it could not tag — typically the ones whose amended message a repo hook rejected. Treat a non-zero exit as "the branch still has at least one untagged non-empty commit" and fix those commits before pushing. The manual verification below stays as the belt-and-braces check.
After the script completes, force-push to update the branch:
git push --force-with-lease
If the rebase fails: Reset with git rebase --abort or git reset --hard origin/<branch-name> and try again.
Verify all non-empty commits now have PR numbers before proceeding:
git log origin/$BASE_BRANCH..HEAD --format='%H%x09%s' |
while IFS=$'\t' read -r sha subject; do
tree=$(git show -s --format=%T "$sha") || exit 1
parent=$(git rev-parse --verify "$sha^" 2>/dev/null || true)
if [ -n "$parent" ]; then
if ! parent_tree=$(git show -s --format=%T "$parent"); then exit 1; fi
else
if ! parent_tree=$(git hash-object -t tree /dev/null); then exit 1; fi
fi
[ "$tree" = "$parent_tree" ] && continue
case "$subject" in *"[#$PR_NUM]"*) ;; *) printf '%s %s\n' "${sha:0:12}" "$subject";; esac
done
# Expected output is empty; any listed non-empty commit still needs fixing
Step 5: Squash and Tidy Commits
Blocking requirement 4: squash into logical groups before pushing.
git log origin/$BASE_BRANCH..HEAD --oneline
MERGE_BASE=$(git merge-base HEAD "origin/$BASE_BRANCH")
if [ -z "${BRANCH_OWNED_FILES:-}" ]; then BRANCH_OWNED_FILES=$(mktemp); fi
git diff --name-only "$MERGE_BASE"..HEAD > "$BRANCH_OWNED_FILES"
Squashing rules:
- Drop empty "create pull request" commits — these are scaffolding commits (e.g.,
chore: [skip ci] create pull request) with no code changes. Usedropingit rebase -ito remove them entirely. - Group commits by logical unit of work (e.g., one commit per service/feature area)
- Squash fix-up commits, lint fixes, and review feedback into their parent commits
- Combine related changes (feature + tests + fixes = one commit)
- Keep genuinely unrelated work in separate commits
- Each final commit should represent a coherent, self-contained change
- Use
git rebase -iwithfixupto squash, andrewordto clean up messages - Always use
--force-with-leasewhen force-pushing after rebase
Determine the logical grouping, then squash and force-push immediately. Do NOT ask for permission — just do it.
After squashing, verify:
git log origin/$BASE_BRANCH..HEAD --oneline # Clean, logical commits
git log origin/$BASE_BRANCH..HEAD --format='%H%x09%s' |
while IFS=$'\t' read -r sha subject; do
tree=$(git show -s --format=%T "$sha") || exit 1
parent=$(git rev-parse --verify "$sha^" 2>/dev/null || true)
if [ -n "$parent" ]; then
if ! parent_tree=$(git show -s --format=%T "$parent"); then exit 1; fi
else
if ! parent_tree=$(git hash-object -t tree /dev/null); then exit 1; fi
fi
[ "$tree" = "$parent_tree" ] && continue
case "$subject" in *"[#$PR_NUM]"*) ;; *) printf '%s %s\n' "${sha:0:12}" "$subject";; esac
done # All non-empty commits have PR numbers
test -n "${BRANCH_OWNED_FILES:-}" || { echo "Missing BRANCH_OWNED_FILES; rerun the Step 5 file capture"; exit 1; }
git diff --name-only origin/$BASE_BRANCH..HEAD # Only branch-owned/finalize-intended files
comm -13 <(sort "$BRANCH_OWNED_FILES") <(git diff --name-only origin/$BASE_BRANCH..HEAD | sort)
# Should return NOTHING - if it returns files, stop before pushing and rebuild from origin/$BASE_BRANCH
Step 6: Push to Remote
git fetch origin "$BASE_BRANCH"
git merge-base --is-ancestor "origin/$BASE_BRANCH" HEAD || { echo "origin/$BASE_BRANCH is not in HEAD; rebase before push"; exit 1; }
MERGE_COUNT=$(git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD) || exit 1
test "$MERGE_COUNT" = 0 || { echo "Merge commit(s) on this branch; linearize before pushing"; exit 1; }
git push --force-with-lease
git status # Verify "up to date with origin"
Capture the count and compare it as a string — test "$(…)" -eq 0 fails open, because an
unresolvable origin/$BASE_BRANCH yields an empty operand that zsh compares equal to 0.
The merge-commit assertion gates this push in Step 6 and therefore the un-draft in Step 6c: a nonzero count means one or more merge commits (most often a base merge) poisoned the branch and a rebase-merge repo will refuse the PR.
Only if that assertion fails (nonzero count), linearize, re-assert 0, then push. Flattening
discards anything recorded only in a merge commit (manual conflict resolutions, files added in
the merge), so list the merges first with
git rev-list --merges --oneline "origin/$BASE_BRANCH..HEAD" and lift any such edit out into a
normal commit before running this — boss-repair's Linear-History Invariant carries the full recipe:
git fetch origin "$BASE_BRANCH"
git rebase --onto "origin/$BASE_BRANCH" "$(git merge-base "origin/$BASE_BRANCH" HEAD)"
# Resolve any conflicts until the rebase COMPLETES, then re-assert before pushing:
git merge-base --is-ancestor "origin/$BASE_BRANCH" HEAD || { echo "rebase did not land on the base"; exit 1; }
MERGE_COUNT=$(git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD) || exit 1
test "$MERGE_COUNT" = 0 || { echo "Branch still has merge commits; resolve by hand"; exit 1; }
git push --force-with-lease
If push fails, resolve and retry until success.
Step 6b: Verify GitHub Checks
Blocking requirement 5: verify no check is failing.
After pushing, wait a moment for checks to register, then gather the payloads with a --json filter
(keeps the raw check table out of the main thread — see the bulk-output discipline in Step 0) and
hand them to the shared classifier. The verdict is the helper's, not this step's — a bucket
payload on its own cannot separate a gate that ran and passed from one that never attached:
BOSS_FINALIZE_TOOLBOX="${BOSS_SKILLS_HOME:-$HOME/.claude/skills}/boss-finalize/toolbox"
if [ ! -d "$BOSS_FINALIZE_TOOLBOX" ]; then BOSS_FINALIZE_TOOLBOX="$HOME/.codex/skills/boss-finalize/toolbox"; fi
CHECK_DIR="$(mktemp -d)"
HEAD_SHA="$(gh pr view --json headRefOid -q .headRefOid)"
gh pr checks --json name,state,bucket > "$CHECK_DIR/checks.json"
gh api "repos/OWNER/REPO/commits/$HEAD_SHA/check-runs?per_page=100" --paginate --slurp > "$CHECK_DIR/runs.json"
node "$BOSS_FINALIZE_TOOLBOX/pr-check-state.mjs" classify \
--head-sha "$HEAD_SHA" --observed-sha "$HEAD_SHA" \
--checks "$CHECK_DIR/checks.json" --check-runs "$CHECK_DIR/runs.json"
How to route each verdict state:
- ✅
green— the session can complete.provesGreen: trueadditionally means a gate actually ran and the check set was compared against the prior head; without it the set may be too small to prove anything. - ⏳
pending— not a failure and not a pass. Keep waiting, except when thereasonisabsent-gate: a named gate the prior head carried is missing from this one, so waiting never resolves — report the missing gates instead of blocking on them. - ❓
unknown— an unreadable read, an unclassifiable state, a stale SHA, or a set in which nothing ran. Never green and never red; report it as unobserved rather than completing on it. - ❌
failing— the only blocking state. You MUST investigate and fix it.
If the verdict is failing:
- Read
gh pr checks --json name,state,bucketto identify which check(s) failed - Read the failure logs inside a subagent (or
gh run view <run-id> --log-failed | tail) and return only the relevant lines — do not paste the full log into the main thread - Investigate the root cause and fix it locally
- Commit the fix, push again, and re-check
- Repeat until no checks are red/failing
Do NOT leave the session with failing checks. If a check failure is unrelated to your changes (e.g., a flaky test or pre-existing CI issue), you MUST inform the user and get their explicit acknowledgment before proceeding.
Step 6c: Mark PR as Ready for Review
Blocking requirement 6: mark the PR ready for review.
After checks are passing (or pending/in_progress), mark the PR as ready and verify GitHub actually recorded the state change:
PR_URL=$(gh pr view --json url -q .url)
gh pr ready "$PR_URL"
IS_DRAFT=""
for attempt in 1 2 3 4 5 6; do
IS_DRAFT=$(gh pr view "$PR_URL" --json isDraft -q .isDraft)
if [ "$IS_DRAFT" = "false" ]; then break; fi
sleep 5
done
test "$IS_DRAFT" = "false" || {
echo "PR is still draft after gh pr ready: $PR_URL"
exit 1
}
This converts the PR from draft to ready-for-review status. Do NOT leave the PR as a draft when landing the plane.
If the PR is already ready for review, this command is a no-op and safe to run.
Do NOT treat a successful gh pr ready exit alone as sufficient. The postcondition is isDraft == false for the exact PR URL. If verification fails, stop and report the failure instead of completing.
Step 6d: Check for Merge Conflicts
Blocking requirement 7: verify there are no merge conflicts.
gh pr view --json mergeable -q .mergeable
Expected result: MERGEABLE — the PR can be merged cleanly.
If the result is CONFLICTING:
- Fetch the PR base branch:
git fetch origin $BASE_BRANCH - Rebase onto the PR base branch:
git rebase origin/$BASE_BRANCH— resolving drift by merging the base in is forbidden; it leaves a merge commit that deadlocks a rebase-merge repo - Resolve any conflicts during the rebase
- Re-run the repo's quality gates to ensure nothing broke
- Assert linear history — guarded, so a nonzero count stops the push:
MERGE_COUNT=$(git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD) || exit 1thentest "$MERGE_COUNT" = 0 || { echo "linearize before pushing"; exit 1; } - Force-push:
git push --force-with-lease - Wait and re-check:
gh pr view --json mergeable -q .mergeable - Repeat until
MERGEABLE
If the result is UNKNOWN: GitHub is still computing mergeability. Wait a few seconds and re-check.
Do NOT leave the session with merge conflicts. A PR with conflicts cannot be merged and blocks the review process.
Step 7: Clean Up and Verify
git stash list # Note any stashes (don't auto-clear without asking)
git remote prune origin
git status # Confirm clean state
Step 8: Provide Handoff
Provide a summary:
Session Complete
Completed: [summary of work] Quality gates: [pass/fail status] Push status: All commits pushed to origin
Next steps:
- [follow-up item 1]
- [follow-up item 2]
Recommended prompt: "Continue work on [task]: [context]"
Checklist
Before saying "done", verify ALL items:
- Repo quality gates discovered from project instructions, CI, or command files
- Minimal non-duplicative gate command set passed
-
origin/$BASE_BRANCHis an ancestor ofHEAD -
git rev-list --merges --count "origin/$BASE_BRANCH"..HEADis0(base synced by rebase) - No base-only files appear in
git diff origin/$BASE_BRANCH..HEAD - All non-empty commits have
[#PR-NUM]in message - Commits squashed into logical groups (force-pushed)
- Empty "create pull request" commits dropped
-
git pushsucceeded - GitHub checks are not failing (idle/queued/in_progress/passing are OK)
- PR marked as ready for review and verified with
gh pr view "$PR_URL" --json isDraft -q .isDraft→false - No merge conflicts (
gh pr view --json mergeable -q .mergeable→MERGEABLE) - Provided handoff with next steps
Common Failures
| Failure | Why It's Wrong | What You Should Have Done |
|---|---|---|
| Skipped project gate discovery | Wrong commands run | Inspect project instructions, CI, and command files before choosing gates |
| Ran duplicate gate commands | Slow and noisy | Prefer one aggregate command when it covers build/lint/test; otherwise run only missing targets |
| Skipped quality gates | CI will fail | Run the repo's required build/lint/test gates |
| Dismissed failure as "pre-existing" | Failure was fixable | Verify on the PR base branch before dismissing. Missing generated code or dependencies are NOT pre-existing |
| Missing dependencies in worktree | Generate/format fails | Install the repo's documented dependencies, then re-run the same gate commands |
| Stopped to ask permission to push | Blocked automation | Just push — do NOT ask for permission. Force-push is expected and authorized. |
| Squashed stale branch onto new base | PR reverts base changes | Rebase onto origin/$BASE_BRANCH first; never soft-reset stale history onto the new base |
Non-empty commit missing [#PR-NUM] |
PR not linked | Run ~/.claude/skills/boss-finalize/add-pr-numbers.sh to fix all non-empty commits |
| Reported issue but didn't fix | Commits still broken | You MUST run the script, not just report that commits need fixing |
| Compared against feature branch | Wrong comparison | Always compare to origin/$BASE_BRANCH to find all branch commits |
| Branch "up to date" so skipped | Commits still need PR# | Even pushed non-empty commits need PR numbers - compare to the PR base branch, not feature branch |
| Didn't squash commits | Messy history | ALWAYS squash into logical groups — this is mandatory, not optional |
| Said "ready when you are" | Work stranded | YOU push immediately — do not wait for user to do it or ask permission |
| Left session with failing checks | CI is red | Run gh pr checks, investigate failures with gh run view --log-failed, fix and re-push |
| Ignored failing check as "unrelated" | CI still red | Even if unrelated, inform user and get explicit acknowledgment |
| Left empty "create PR" commit | Messy history | Use drop in rebase to remove empty scaffolding commits like chore: [skip ci] create pull request |
| Left PR as draft | Not reviewable | Run gh pr ready to mark the PR as ready for review before completing |
Ran gh pr ready but did not verify |
Silent no-op possible | Verify isDraft == false for the exact PR URL and fail finalize if GitHub still reports draft |
| Left PR with merge conflicts | PR can't be merged | Run gh pr view --json mergeable -q .mergeable, rebase onto the PR base branch if CONFLICTING |
| Merged the base branch in | Rebase-merge deadlocks | Rebase onto the base; assert git rev-list --merges --count "origin/$BASE_BRANCH"..HEAD is 0 before push |
Related Skills
| Skill | Relationship |
|---|---|
/boss-verify |
Run verification before finalizing |
/boss-repair |
Repair PR conflicts / failing checks |
/git-committing |
Conventional commit format reference |