Ship: Handoff
SkillDev toolsShip completed work: verify locally, commit related changes, push, create or update the PR, watch CI/reviews, and fix until merge-ready or escalated. Use for "ship it", "create PR", "handoff", or finished code needing delivery.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Ship: Handoff skill
What this skill tells your AI
The instructions your AI receives, as published by heliohq/ship in skills/handoff/SKILL.md and read by ahel’s review.
Do not stop when the PR is created. Do not stop while any GitHub check is pending. If any GitHub check fails, fix the problem, push again, and wait again. If the PR is not merge-ready, sync with base or resolve conflicts inside the same fix loop.
This is a goal-directed loop, not a counted one. Keep looping while each round makes progress toward the completion conditions; escalate on evidence, never on a round counter — the specific evidence classes are in Loop Governance. Done means every condition in Completion is satisfied.
Process Flow
Phase index — the phases below own the detail:
- Pre-flight (resolve branch, base, and scope)
- Verify locally
- Update changelog / directly affected docs
- Push and create/update the PR
- Wait for GitHub checks
- Fix loop — governed by the round ledger, not a counter
Red Flag
Never:
- Stop when the PR is created — #1 failure mode
- Push code changes without re-running relevant local verification
- Force push without
--force-with-lease - Rewrite an already-pushed PR branch when there are human review, approval, or shared-branch signals
- Treat
pendingchecks as "good enough" - Treat green checks as sufficient when
mergeStateStatusis still blocked - Create the PR before local verification runs
- Use
git add -Awhen unrelated local changes are present - Forget to stage and commit changelog or doc edits before the first push
- Mark a thread or comment as resolved before the fix is actually pushed
- Resolve comments that still need product, security, or architecture judgment
- Silently ignore a comment — a decline is a visible reply with a reason
- Apply "further fixes add no value" to CI or merge-readiness — that judgment exists only for the comment decline classes
- Fix failures without reading the actual check logs or review comments
- Sync with base preemptively — only when drift, conflicts, or repo policy require it
- Re-attempt a fix for a failure signature that already survived a fix aimed at it — the second identical outcome is evidence the approach is wrong; escalate with the ledger instead of iterating on hope
- Count rounds as a stopping condition — progress, judgment, and external blockers are the only reasons to stop looping
- Leave doc debt implicit — carry it into the PR
Loop Governance
Modern harnesses run goal-directed loops natively — a fixed retry cap abandons hard-but-progressing PRs while adding no safety that progress detection doesn't provide better. The loop is governed by three things:
1. Progress detection (the round ledger). Every fix round appends one
block to <task_dir>/handoff.md (or tracks inline when no task dir):
Round <i>: trigger=<check name + error class, or "conflict"/"review">
action=<what was changed, one line>
result=<next terminal state: new signature | same signature | green>
Before starting a round, compare the current failure to the ledger:
- New failure signature (different check, or same check failing differently) → progress; loop.
- Same signature after a fix aimed at it → the approach is wrong. Escalate with the ledger as evidence — do not iterate on hope.
- The ledger survives context compaction; after a compaction, trust it over recollection.
2. Judgment boundaries. Product/security/architecture review comments, conflicts you cannot resolve confidently, and rebase-policy dead ends escalate immediately regardless of progress. A fix that would change the shipped behavior (not just repair its delivery) is scope drift — escalate; the fix loop repairs delivery, it does not redesign.
3. The harness's own goal net. Under /ship:auto, the stop gate
already blocks session exit until the PR is merge-ready. In a standalone
run on a harness with a native goal condition (e.g. Claude Code's
/goal), suggest the user arm one at the start — PR checks green and merge-ready, or stop after 25 turns — as the outer bound; the harness's
turn bound replaces any hand-rolled cap.
Progress Tracking
Track your progress with the harness's task/todo list. Create the items at the start based on what the repo actually needs. Not every repo has a CHANGELOG, CI, or docs to update — only include items for work that will actually happen.
Principle: one item per phase the user would wait on. Fix rounds are dynamic — add them only when a check fails.
Phase 1: Pre-flight
Resolve only the context needed to ship the PR:
- Determine the current branch.
- If HEAD is detached, create a feature branch before continuing.
- Determine the base branch:
- use the existing PR base if a PR already exists
- otherwise use the repo default branch
- If the current branch is the base branch, create a feature branch before continuing.
- Inspect the current scope with
git status --short,git diff <base>...HEAD --stat,git diff --cached --stat, andgit diff --stat. - Decide which local changes belong to this handoff.
- Do not use
git add -Aunless every dirty file belongs to this handoff. - If unrelated local changes cannot be separated safely, stop and escalate instead of guessing.
- If the caller already provides
task_dir, use it. Otherwise do not guess one here; resolve it only if a later phase needs to write artifacts.
Output a short start summary with the branch, base branch, and scope being shipped.
Phase 2: Verify Before PR
Before the first push in handoff, run the most relevant local verification available for this repo.
- Prefer the same commands the repo or CI already uses.
- Run only the checks that are relevant to the changed area: tests, lint, typecheck, build, or targeted smoke checks as applicable.
- If code changes during handoff, run the relevant verification again before the next push.
- If a task directory already exists, do not invent extra artifacts just for handoff.
- If verification fails, fix the issue before pushing.
Output a short summary of what was run and whether it passed.
Phase 3: Update CHANGELOG and Docs
Step A: CHANGELOG (auto-generate)
[ -f CHANGELOG.md ] || echo 'NO_CHANGELOG'
- No CHANGELOG.md → skip silently.
If CHANGELOG.md exists:
- Read header to learn the format
- Generate entry from:
git log <base>..HEAD --oneline - Categorize: Added / Changed / Fixed / Removed
- Insert after header, dated today
- Commit:
git add CHANGELOG.md && git commit -m "docs: update CHANGELOG"
After changelog handling, check whether the shipped changes also changed any user-facing or repo-facing documentation truths before the first push.
Use references/documentation.md for the documentation decision tree and
ownership rules.
- Only inspect docs that directly describe the changed commands, config, file paths, workflow, or behavior.
- Do not scan every markdown file in the repo.
- Route each changed truth to the narrowest correct document instead of defaulting to top-level docs.
- If a doc is mechanically stale, fix it in the same handoff loop.
- If the doc issue is semantic and you are not confident about the right wording, carry that note into the PR instead of inventing an explanation.
- Before moving on, explicitly record one of:
- docs updated
- docs checked, no update needed
- doc debt to note in the PR
Phase 4: Push and Create PR
When opening or updating the PR, keep the title and body concise.
Include only:
- what changed
- what local verification ran
- any known follow-up, risk, or skipped check
Do not invent a long template if the change is simple.
Push and create:
- Review the final diff, then stage all related changes that should ship now, including any changelog or doc edits made in this handoff.
- Commit the staged changes if anything new was staged in this phase.
git push -u origin HEAD- Create the PR if it does not exist.
- If the PR already exists, update the body or add a short comment with the latest verification summary.
- If
task_direxists, write or update<task_dir>/handoff.mdwith: PR URL, branch, base, verification commands/results, docs outcome, current check summary, currentmergeStateStatus, and the round ledger (Loop Governance format — one block per fix round). This file is the handoff evidence consumed by the stop gate, and the ledger is what makes progress detection survive context compaction.
Output: [Handoff] PR created: <url>
Phase 5: Wait for GitHub Checks
Inspect .github/workflows, branch protection signals, and the current PR
checks once so you understand what this repo expects to run. A repo can
have required checks from GitHub Apps even when it has no local workflow
files, so never skip this phase based on .github/workflows alone.
Let the harness wait — never agent-side sleep polling.
gh pr checks --watch blocks locally and polls GitHub itself every ~10s;
your job is to park on it with whatever waiting primitive your harness
has, so idle waiting costs nothing. The watch command in every case
below is timeout 3600 gh pr checks --watch:
-
Monitor available (Claude Code): arm one watch per wait cycle. A Monitor is single-use — it wakes you when output arrives and is re-armed fresh after each fix push. Use an explicit timeout, NOT
persistent: true(persistent overridestimeout_msentirely and needs a manual TaskStop; it is meant for dev servers, not bounded waits):Monitor( command: 'timeout 3600 gh pr checks --watch; echo "TERMINAL exit=$?"', description: "PR <number> checks settling", timeout_ms: 3600000 )Before arming, check whether a Monitor for this PR is already running — on resume the prior watch may still be alive. If so, wait for its event; do not arm a duplicate.
-
Background command support, no Monitor: run the watch command as a background command — the completion notification wakes you with the exit code.
-
Neither (e.g. Codex today): run the watch command as a plain blocking command; the harness waits on the process. If the harness offers a scheduled-wakeup loop instead, poll
gh pr checkson a few-minute interval.
When the watch terminates (TERMINAL exit=<code> event, background
completion, or command return), pull the authoritative state once:
# Full snapshot for interpretation
gh pr view --json state,statusCheckRollup,reviews,reviewDecision,mergeable,mergeStateStatus,comments
# Machine-readable check summary
gh pr checks --json name,state,bucket,link,workflow
# Read failing check logs if any. Prefer the failed run URL/check URL from
# the snapshot; use gh run view only after identifying the run id.
gh run view <run-id> --log-failed
Also inspect unresolved review threads when review comments may be
actionable. gh pr view --json comments,reviews is not enough because it
does not reliably expose thread resolution state.
OWNER=$(gh repo view --json owner --jq '.owner.login')
REPO=$(gh repo view --json name --jq '.name')
PR_NUMBER=$(gh pr view --json number --jq '.number')
gh api graphql -f owner="$OWNER" -f repo="$REPO" -F number="$PR_NUMBER" -f query='
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
reviewThreads(first: 100) {
nodes {
id
isResolved
comments(first: 20) {
nodes {
id
author { login }
body
path
line
outdated
url
}
}
}
}
}
}
}'
Interpret the snapshot:
- All relevant checks
SUCCESS,NEUTRAL, or intentionally ignored optionalSKIPPED/CANCELLED→ check gate green - Any relevant
FAILURE,ERROR,ACTION_REQUIRED, or failed check bucket → Phase 6 fix loop - Any relevant pending, queued, in-progress, expected, or waiting check → keep waiting
mergeStateStatusisDIRTY,BEHIND,BLOCKED,DRAFT, orUNKNOWNafter one re-query → Phase 6 fix loop or escalationmergeableisCONFLICTING→ Phase 6 fix loop- Any actionable unresolved review thread, review comment, or bot/workflow comment → Phase 6 fix loop
CANCELLEDchecks → informational only when they are optional and do not block normal CI/CD- Exit code non-zero but no concrete failure found in snapshot → re-query once, then escalate as ambiguous CI state
Wait timeout. If the watch hits its 1h bound with checks still pending, re-query once: checks that moved → re-arm the watch and keep waiting (slow CI is progress, not failure); checks frozen with no movement since the previous query → escalate as an external GitHub wait, not a code-fix failure. Record which checks were still pending so the user can investigate.
Re-entering the fix loop. When Phase 6 finishes pushing a fix, arm a fresh watch (the previous one terminated on its event) and loop back to the event-wait above.
Phase 6: Fix Loop
If CI failures, review comments, or merge conflicts exist, fix them. The loop is governed by progress, not a counter — see Loop Governance; step 0 below carries the in-loop ledger check.
Comment Triage
Review comments are not all action-required. Triage each one before spending a fix round on it — "addressed" means fixed OR declined with a visible reason, never silently ignored:
| Comment class | Action |
|---|---|
| Correctness, security, or spec violation (any author) | Fix it — this is what fix rounds are for |
| Human reviewer explicitly requests changes | Fix it, or escalate to the user — never self-judge a human's request away |
| Style, polish, or subjective preference (especially bot/AI reviews) | Fix only if trivial and local; otherwise reply with a one-line reasoned decline and move on |
| Out-of-scope suggestion (new feature, broader refactor) | Decline with a reply; note it as a follow-up in the PR body |
Diminishing returns is a stop signal for comments — never for CI or merge-readiness. When every remaining comment falls in the decline classes, the comment gate is satisfied: reply, minimize what's obsolete, and stop polishing. Checks green and merge-ready stay objective gates — "I think further fixes add no value" is never grounds to stop while a check is red or the PR is not merge-ready.
In each fix round:
-
Compare the current failure signature against the round ledger in
<task_dir>/handoff.md. Same signature as the previous round's target → stop and escalate with the ledger. New signature → append the new round entry and continue. -
Re-read the current PR status on GitHub, including checks,
mergeStateStatus, and unresolved review threads. -
If checks failed, inspect the failing check logs and fix the smallest real cause.
-
Triage review comments per Comment Triage above: fix the correctness/security/spec class, decline the polish and out-of-scope classes with a reasoned reply.
-
If a comment requires product, security, or architecture judgment, escalate instead of guessing.
-
If
mergeStateStatusreports conflicts, base drift, branch protection blockage, or a repo policy requires an update from base, sync with base and resolve it carefully. Use this strategy:- Always start with
git fetch origin <base-branch>. - Prefer
git rebase origin/<base-branch>when it can preserve a clean linear history without disrupting collaborators. This is always appropriate before the branch is pushed. - For an already-pushed PR branch, choose rebase only when all of
these safety gates pass: the branch is agent-owned, there are no
human approvals or unresolved human review threads, no other author
has pushed commits to the branch, and the repo appears to expect
linear history. Push the result with
git push --force-with-lease, never plain--force. - If any safety gate fails, prefer
git merge --no-ff origin/<base-branch>(or the repo's equivalent update-branch operation) so the fix can be pushed without rewriting review history. - If repo policy requires linear history but the rebase safety gates do not pass, escalate for user approval.
For an already-pushed PR branch, prove the safety gates before rebasing. Default to not safe if any command fails, returns ambiguous output, or shows collaboration:
BRANCH=$(git branch --show-current) BASE=$(gh pr view "$BRANCH" --json baseRefName --jq '.baseRefName') PR_AUTHOR=$(gh pr view "$BRANCH" --json author --jq '.author.login') ME=$(gh api user --jq '.login') export PR_AUTHOR ME # 1. Agent-owned branch naming convention. case "$BRANCH" in ship/*|codex/*) echo "agent-owned-name" ;; *) echo "NOT_SAFE: branch name is not agent-owned"; exit 1 ;; esac # 2. No human approvals, change requests, comments, or review threads. # gh --jq exits 0 even when the expression prints "false" — capture the # boolean and test the string, or the gate can never fire. NO_HUMAN_SIGNALS=$(gh pr view "$BRANCH" --json reviews,comments --jq ' [ .reviews[]?.author.login, .comments[]?.author.login ] | map(select(. != "github-actions[bot]" and . != "dependabot[bot]")) | map(select(. != env.PR_AUTHOR and . != env.ME)) | length == 0 ') [ "$NO_HUMAN_SIGNALS" = "true" ] || { echo "NOT_SAFE: human review/comment signal"; exit 1; } # 3. No unresolved review threads from humans. OWNER=$(gh repo view --json owner --jq '.owner.login') REPO=$(gh repo view --json name --jq '.name') PR_NUMBER=$(gh pr view "$BRANCH" --json number --jq '.number') NO_HUMAN_THREADS=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F number="$PR_NUMBER" -f query=' query($owner: String!, $repo: String!, $number: Int!) { repository(owner: $owner, name: $repo) { pullRequest(number: $number) { reviewThreads(first: 100) { nodes { isResolved comments(first: 20) { nodes { author { login } } } } } } } }' --jq ' [ .data.repository.pullRequest.reviewThreads.nodes[]? | select(.isResolved == false) | .comments.nodes[]?.author.login ] | map(select(. != "github-actions[bot]" and . != "dependabot[bot]")) | map(select(. != env.PR_AUTHOR and . != env.ME)) | length == 0 ') [ "$NO_HUMAN_THREADS" = "true" ] || { echo "NOT_SAFE: unresolved human review thread"; exit 1; } # 4. No other commit authors on this PR branch. git fetch origin "$BASE" MY_EMAIL=$(git config user.email) UNEXPECTED_AUTHORS=$(git log --format='%ae' "origin/$BASE..HEAD" | \ sort -u | grep -vxF "$MY_EMAIL" || true) [ -z "$UNEXPECTED_AUTHORS" ] || { echo "NOT_SAFE: unexpected commit authors: $UNEXPECTED_AUTHORS" exit 1 } # 5. Repo appears to prefer/require linear history. OWNER=$(gh repo view --json owner --jq '.owner.login') REPO=$(gh repo view --json name --jq '.name') LINEAR_HISTORY=$(gh api "repos/$OWNER/$REPO" --jq ' (.allow_rebase_merge == true or .allow_squash_merge == true) and (.allow_merge_commit == false) ') [ "$LINEAR_HISTORY" = "true" ] || { echo "NOT_SAFE: repo does not clearly require linear history"; exit 1; }Only when all gates are proven safe may the agent run:
git rebase "origin/$BASE" <relevant local verification command> git push --force-with-lease - Always start with
-
Do not resolve conflicts mechanically with
--oursor--theirsunless one side is clearly disposable. -
Read both sides of the conflict and preserve the behavior this PR is trying to ship. If both sides contain valid changes, merge them.
-
If you cannot resolve the conflict confidently, escalate instead of guessing.
-
After any code change, run the relevant local verification again.
-
Commit the fix and push it.
-
Update
<task_dir>/handoff.mdiftask_direxists. -
If the push fully addresses GitHub feedback, mark the addressed feedback as resolved:
- for review threads, resolve the thread
- for obsolete bot or workflow comments, hide/minimize the comment with
classifier
RESOLVED
-
Never resolve, hide, or minimize feedback that is only partially addressed or still needs user judgment.
-
Go back to Phase 5.
Use GitHub GraphQL when needed:
# Resolve a PR review thread
gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread { id isResolved }
}
}' -F threadId="<thread-id>"
# Hide/minimize an obsolete bot or workflow comment as resolved
gh api graphql -f query='
mutation($subjectId: ID!) {
minimizeComment(input: {subjectId: $subjectId, classifier: RESOLVED}) {
minimizedComment { isMinimized }
}
}' -F subjectId="<comment-node-id>"
Output: [Handoff] Fix round <i> — target: <failure signature> — <what was fixed>. Tests pass. Re-checking CI...
Execution Handoff
Output the report card — read the format from ../shared/report-card.md
(resolved against this skill's base directory, not the working directory):
## [Handoff] Report Card
| Field | Value |
|-------|-------|
| Status | <DONE / BLOCKED> |
| Summary | PR #<N> — checks <green / pending / failed>, merge <ready / blocked> |
### Metrics
| Metric | Value |
|--------|-------|
| PR URL | <url> |
| Check status | <green / N passing, M failed> |
| Merge state | <mergeStateStatus> |
| Fix rounds | <N> (each targeting a new failure signature) |
| Docs outcome | <updated / checked-no-update / debt-noted> |
### Artifacts
| File | Purpose |
|------|---------|
| PR on GitHub | Shipped code |
| .ship/tasks/<task_id>/handoff.md | PR URL, checks, merge state, verification, docs outcome |
| CHANGELOG.md | Updated changelog (if repo has one) |
Example Workflow
Condensed to show the loop shape. The full log would include the same verify/commit/push pattern after every fix round.
[Handoff] Start — branch feat/auth, base main, 4 files + 2 doc edits
[Handoff] Verify → npm test, npm run lint: PASS
[Handoff] CHANGELOG entry added, README updated
[Handoff] Push, PR created: https://github.com/org/repo/pull/123
[Handoff] Wait → ci/test FAILURE
[Handoff] Fix round 1 — target: ci/test nil deref — added nil guard, re-verify PASS, push
[Handoff] Wait → AI review: requested error-path coverage
[Handoff] Fix round 2 — target: review error-path coverage — added test, re-verify PASS, push
resolved review thread, minimized obsolete bot comment
(each round hit a NEW signature — progress; ledger updated)
[Handoff] Wait → all checks green
[Handoff] Merge state → CLEAN
[Handoff] DONE — PR #123 green and merge-ready
Completion
Done when:
- the PR exists
- relevant GitHub checks are green
- no relevant GitHub checks are pending
mergeStateStatusis merge-ready (CLEAN,HAS_HOOKS, orUNSTABLEonly when all failing checks are irrelevant/non-blocking)mergeableis notCONFLICTING, and there are no unresolved merge conflicts in the local worktree- the branch is not behind base in a way GitHub/repo policy requires updating before merge
- every review thread and bot/workflow comment is addressed: fixed, or declined with a reasoned reply per Comment Triage (a human "changes requested" review is never satisfied by an agent-side decline)
Escalate when:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 91
- Forks
- 7
- Last commit
- Jul 2026
- Hacker News mentions
- 20
Advanced
- Catalog kind
- skill
- Gateway key
handoff-heliohq- Source
- github.com/heliohq/ship