After the human approves the PR, finalize the work: merge the PR, verify the Railway deployment is healthy, move the Linear ticket to Done, record the outcome, and remove the worktree.
-
Read state outputs — pick up pr_number, pr_url, branch, worktree, ticket id, and any RAILWAY_* config from earlier phases.
-
Confirm PR is ready to merge:
gh pr view <pr-number> --json state,mergeable,mergeStateStatus
- If
state == "MERGED" already (rare — manual merge by human): skip to step 4
- If
mergeable != "MERGEABLE": surface to human with the reason, set phase to blocked
- Otherwise proceed
-
Merge the PR:
gh pr merge <pr-number> --squash --delete-branch
--squash keeps master history linear
--delete-branch removes the remote branch (the worktree's local branch is removed in step 8)
-
Sync the main repo to merged master — subsequent commits (e.g. cleanup section) land on up-to-date master, not on the now-stale feature branch.
The main repo is the common dir's parent of the worktree — git rev-parse --git-common-dir gives <repo>/.git, so its parent is the main repo root:
REPO_ROOT=$(cd "$WORKTREE_PATH" && dirname "$(git rev-parse --git-common-dir)")
git -C "$REPO_ROOT" fetch origin master
git -C "$REPO_ROOT" merge --ff-only origin/master
This is more robust than parsing git worktree list --porcelain (which assumed the first entry was the main repo — true today but not guaranteed if the worktree ordering ever changes).
-
Verify Railway deployment — poll API + Web healthchecks until 200 or 5-minute timeout:
API_HEALTH_URL="${RAILWAY_API_HEALTH_URL:-https://api.agentfleet.app/health}"
WEB_HEALTH_URL="${RAILWAY_WEB_HEALTH_URL:-https://app.agentfleet.app/}"
API_STATUS=000
for i in $(seq 1 30); do
API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_HEALTH_URL")
[ "$API_STATUS" = "200" ] && break
sleep 10
done
WEB_STATUS=000
for i in $(seq 1 30); do
WEB_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$WEB_HEALTH_URL")
[ "$WEB_STATUS" = "200" ] && break
sleep 10
done
- Both 200 within timeout → record
deploy_verified: true in state outputs with both URLs and statuses
- Either timeout / non-200 → surface to human with the URL and last status code, set phase to
blocked
- If
RAILWAY_API_HEALTH_URL / RAILWAY_WEB_HEALTH_URL are not configured for this repo, skip with a note in the conversation file
-
Update Linear ticket to "Done" — only on Railway success (or on skipped Railway with explicit note). Use mcp__plugin_linear_linear__save_issue with state: "Done". Skip entirely for plain-text tasks (no Linear ticket).
-
Record to conversation file — the conversation file is now on master (it was committed by harness-ship and then merged). Update the master copy in the main repo, NOT the worktree copy:
cd "$REPO_ROOT"
Insert before the ## Harness Issues marker in .harness/conversations/<task-id>.md (use Edit tool with ## Harness Issues as the anchor — do NOT literally append, that would land below the issues section):
## Cleanup
**Merged at:** <timestamp>
**Railway deploy:** verified (api: <api_status>, web: <web_status>) / skipped (no URLs configured) / failed
**Linear final status:** Done
- If you hit friction (merge blocked, deploy timeout, healthcheck flake, Linear update failed, worktree dirty), append an entry to the literal end of the file — it will land inside the
## Harness Issues section since that section is last.
-
Commit the cleanup section to master:
git add .harness/conversations/<task-id>.md
git commit -m "chore: record cleanup phase to <task-id> conversation log"
git push
If there are no changes, skip silently.
-
Remove worktree — only after every step above succeeded. Hard safety check first:
cd "$WORKTREE_PATH"
DIRTY=$(git status --porcelain)
if [ -n "$DIRTY" ]; then
echo "ERROR: worktree has uncommitted changes — refusing to remove."
echo "$DIRTY"
exit 1
fi
cd "$REPO_ROOT"
git worktree remove "$WORKTREE_PATH"
Never use --force. A dirty worktree at this point means something was missed in harness-ship step 1 (staging the conversation file) or in step 7 above (committing the cleanup section). Surface and investigate.