| name | boss-finalize |
| description | End-of-session workflow ensuring all work is committed and pushed. Use when ending a work session or when asked to "land the plane". |
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 ⛔
You MUST satisfy ALL of these before completing. No exceptions.
| # | 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, run gh pr checks --json name,state,bucket or gh pr view --json statusCheckRollup to verify without dumping raw logs. Checks may be idle, queued, in_progress, or passing. Any failing/red check MUST be investigated and fixed before the session is complete. |
| 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. |
If you complete without satisfying ALL EIGHT requirements, you have failed this workflow.
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.
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
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)
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 2>/dev/null) || ; git merge-base --is-ancestor HEAD 2>/dev/null && ; ; | -nr | -1 | -d -f2)
[ -n ]; ;
-n || { ; 1; }
git fetch origin
git ..HEAD --oneline
gh view --json number -q .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" ] && [ != ];
)
-z || { ; ; 1; }
= || { ; 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
This step is NON-NEGOTIABLE. You MUST run the repo's quality gates and they 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
make lint
make test
just check
task check
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
This step is NON-NEGOTIABLE. You MUST fix commits, not just report on them.
PR_NUM=$(gh pr view --json number -q .number 2>/dev/null || echo "UNKNOWN")
echo "PR number: $PR_NUM"
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 request when the commit is empty
- ❌ Bad:
feat(mobile): add feature X on a non-empty commit
⛔ If ANY non-empty commit is missing the PR number, you MUST run the fix script:
~/.claude/skills/bossanova/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
Step 5: Squash and Tidy Commits
This step is NON-NEGOTIABLE. You MUST squash commits 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. Use drop in git rebase -i to 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 -i with fixup to squash, and reword to clean up messages
- Always use
--force-with-lease when 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
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
-n || { ; 1; }
git diff --name-only origin/..HEAD
-13 <( ) <(git diff --name-only origin/..HEAD | )
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
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)"
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
This step is NON-NEGOTIABLE. You MUST verify checks are not failing.
After pushing, wait a moment for checks to register, then scan the rollup with a --json filter
(keeps the raw check table out of the main thread — see the bulk-output discipline in Step 0):
gh pr checks --json name,state,bucket
Acceptable statuses (session can complete):
- ✅ pass — Check succeeded
- ⏳ pending / queued — Check hasn't started yet (OK, not a failure)
- 🔄 in_progress — Check is currently running (OK, not a failure)
- ⏸️ idle — Check is waiting to run (OK, not a failure)
Blocking statuses (MUST be fixed before completing):
- ❌ fail — Check has failed. You MUST investigate and fix it.
If any check is failing:
- Run
gh pr checks --json name,state,bucket to 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
This step is NON-NEGOTIABLE. You MUST mark the PR as 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
This step is NON-NEGOTIABLE. You MUST 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 1 then
test "$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
git remote prune origin
git status
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:
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/bossanova/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 |
Related Skills
| Skill | Relationship |
|---|
/boss-verify | Run verification before finalizing |
/boss-repair | Repair PR conflicts / failing checks |
/git-committing | Conventional commit format reference |