| name | wrap-up |
| description | Drives a stalled session to a terminating end state - land the work, state the blocker, or take one step (dotfiles trial, manual-only) |
| disable-model-invocation | true |
Wrap Up
Overview
You have been woken by the hourly tmux-resume nudge, not by a human. Something in this session hit a rate limit and the scheduler is prompting you to resume.
Your job is to reach an end state, not to keep working. Every branch below terminates. If you finish this skill still working, you have used it wrong.
Announce at start: "Using the wrap-up skill to bring this session to a close."
Why this exists
The hourly nudge in config/tmux-resume-patterns.conf used to send a bare continue. continue has no exit branch, so sessions resume hourly forever and accumulate: 101 of 157 jobs on this machine were hourly continue nudges, and 56 sessions sat blocked on a human decision that was never surfaced. Fixing the inflow means the nudge must be able to end a session โ so the nudge now types /wrap-up instead.
Status: the nudge is wired, gated on opt-in, and its keystrokes are unconfirmed. tmux-resume types /wrap-up only into panes whose tmux window name starts with auto- (config/tmux-resume-patterns.conf; toggle with tauto / tnoauto). So you are reading this because a human named this window that way โ reaching this skill is itself evidence someone wanted this session wrapped up. Detection anchors on rate-limit wordings captured from real prompts, but the action โ 1 Enter, then /wrap-up, then two Enters โ has never been fired at a live prompt, because a sandboxed session cannot open a tmux socket. Both guesses are marked as such in the pattern file and are settled by one tmux-resume --dry-run against a real rate-limited pane.
disable-model-invocation: true stays on regardless. It makes this skill reachable only by someone typing /wrap-up โ and the nudge sending those keystrokes is that invocation, so wiring the nudge is not a reason to drop the flag. Omitting it would instead make the skill model-invoked (see claude/skills/writing-great-skills/SKILL.md ยง Invocation), letting any matching dotfiles prompt fire an autonomous commit-and-push path with no nudge involved at all. That is a different and much wider trigger surface than the one being trialled here. Discovery during the trial is via claude/skills/catalog/SKILL.md.
Step 0: Scope guard (required first)
Writing to git unattended โ staging, committing, pushing, opening a PR โ is trialled on dotfiles only. Before anything else:
basename "$(dirname "$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)")"
If that basename is not dotfiles, Step 2 is closed: stage nothing, commit nothing, push nothing. Still classify (Step 1), then take the blocked branch (Step 3) โ say where the work stands, name what is left uncommitted and in which checkout, and rename the session (Step 5). Then end your turn.
Out of scope terminates; it does not resume. An earlier version of this guard announced out-of-scope and then carried on with the prior task, exactly as a bare continue would. That was right while the nudge fired at every pane the scheduler could see: stopping dead would have stranded unrelated panes with no PR and no blocker stated, and since a resumed pane stops displaying a rate-limit banner, nothing would have matched on the next scan to retry. Opt-in removed that premise. The nudge now reaches only windows a human named auto-โฆ, so there are no bystander panes to strand, and resuming would rebuild the endless-continue pump this entire change exists to remove. The trial scope governs who may write to git, not whether the session may stop.
Use --git-common-dir, not --show-toplevel. In a linked worktree (cw, claude --worktree) --show-toplevel returns the worktree path, whose basename is the worktree's name โ agent-sprawl-spec, not dotfiles โ so a show-toplevel test excludes every branch worktree, which is where most sessions actually live. --git-common-dir resolves to the owning repo's .git in both a worktree and the root checkout, so its parent's basename is the repo name either way. Worktrees are in scope: they are the intended home for a wrap-up commit, since Step 2 forbids committing on main.
The opt-in prefix in config/tmux-resume-patterns.conf is the primary scope, and it lives in the sender, where it belongs โ by the time this guard runs the keystrokes have already landed, so a check here can never prevent an unwanted interruption. This guard is defence-in-depth for the git-writing branch only.
Step 1: Classify honestly
Pick exactly one. The classification decides everything downstream, so get it right before acting.
| State | Test | Go to |
|---|
| Done | The work the session was asked to do is complete, or complete enough to land behind review | Step 2 |
| Blocked | Progress requires a decision, credential, or approval only the owner can give | Step 3 |
| Mid-flight | There is a concrete next step you can take alone, right now, without a decision | Step 4 |
The honest classification is usually "blocked." If you find yourself reasoning toward "mid-flight" because it feels more productive, re-read the blocked test. A session that invents a next step to avoid stating a blocker is the exact failure this skill was written to stop.
Step 2: Done โ land it and exit
Precondition, before anything is staged: check the branch and the checkout.
git rev-parse --abbrev-ref HEAD
[ "$(git rev-parse --git-dir)" = "$(git rev-parse --git-common-dir)" ] && echo "root checkout" || echo "linked worktree"
If the branch is main or master, you may not commit. "Never push to main" suppresses only the push โ committing first still lands a direct local commit on main with no review path, and the nudge runs unattended in whatever checkout the pane happened to be in.
What you may do about it depends on which checkout you are in, because a branch switch is not a private act:
| Checkout | On main/master | On a feature branch |
|---|
Linked worktree (cw, claude --worktree) | git switch -c wrap-up/<topic> โ the worktree owns its own HEAD, so nobody else is affected | Commit here |
Root checkout (/home/yulong/code/dotfiles) | Classify blocked (Step 3). Do not switch | Commit here, but still do not switch branches |
Never git switch in the root checkout. A checkout has one HEAD shared by every pane, editor, and cron job pointed at it. Switching it to wrap-up/<topic> silently moves everyone else's working branch, so their next commit, pull, or deploy.sh runs against a branch they never chose โ from their side it looks like the repo changed under them for no reason. Owning the edits does not mean owning the checkout. If the work genuinely needs to land, that is a decision for the owner: state it in Step 3 and say the tree holds work that needs a worktree to land safely.
-
Run the tests first, if any exist and you have not run them. This has to happen before staging, not after: a test run can generate fixes or artifacts that belong in the commit, and the PR body created in item 6 is supposed to carry the result. A failing suite does not block landing a draft PR โ it blocks claiming the work is finished. Report the outcome plainly in the PR body and say which of the two it is.
-
List the paths this session touched, from your own transcript โ not from git status. A shared checkout can hold edits from the user, a runtime process, or another job running concurrently, and none of those are yours to commit.
-
Stage by name: git add <path> <path> โฆ, including new files this session created. Never git add -u (it sweeps up every tracked edit in the tree, including someone else's) and never git add -A (it also stages the sandbox's char-device masks). A blanket untracked ban is equally wrong in the other direction โ it silently drops the session's own new files and the run ends having landed nothing.
-
Verify before committing โ read the content, not just the names. git diff --cached --name-only must match your list exactly, nothing extra. That is necessary but not sufficient: a path is not an ownership boundary. In a shared checkout, another actor can have edited a different hunk of a file you also touched, and git add <path> stages their hunk alongside yours while the name-only check still passes cleanly. So also read git diff --cached and confirm every hunk is one you recognise. If a hunk is not yours, git restore --staged <path> and leave that file out. Anything modified that you cannot attribute to this session stays unstaged; name it in the PR body so the owner knows it is there. If you cannot attribute the changes at all, stop and go to Step 3 โ an unattributable diff is a decision for the owner, not a commit.
-
Commit with a real message that says why, not just what. No heredocs โ write to /tmp/claude/<job>-msg.txt and git commit -F.
-
If the branch is ahead of main, push and open a draft PR โ a dirty tree does not excuse skipping this. the staging rule above deliberately leaves unattributable edits unstaged, so a dirty tree is the expected end state, not an error; gating the push on cleanliness would mean the common case commits locally, exits "done", and leaves the work invisible on a machine nobody looks at. Push the commit; mention the leftover unstaged paths in the PR body. Never push to , never force-push, never merge.
Step 3: Blocked โ state the decision and exit
This is the branch that matters most. Produce, in this order:
- The decision, in one sentence, phrased as a question.
- The options, each with its real tradeoff โ not a strawman and a favourite.
- Your recommendation, with one sentence of reasoning.
- What it gates โ what stays stuck until this is answered.
Order matters: rename first, ask second. Do the Step 5 rename (blocked: <one-line decision>) and write the four items above into your narration before calling AskUserQuestion. AskUserQuestion is synchronous โ if the owner does not answer, it does not return, and every instruction after it is unreachable. Put the rename after the call and an unanswered question leaves the session sitting under its old name with the blocker recorded nowhere: exactly the invisible-stall state this skill exists to eliminate, now produced by the skill itself. Renaming first means the disposition survives regardless of whether an answer ever arrives.
Then surface the decision via AskUserQuestion, not prose. This session is a background job; prose questions do not notify the owner, and an unnotified question is indistinguishable from no question at all. See rules/background-job-questions.md.
Write needs input: on its own line. If the call returns with an answer, act on it. If it never returns, the session is already named and the blocker already stated โ that is the correct terminal state, not a failure.
Never choose on the owner's behalf in order to look finished. A stalled session is visibly stalled and costs an hour. A silently-wrong autonomous choice is invisible and lands in main. This constraint is not negotiable for the sake of a tidier job list โ if you are unsure whether a call is yours, it is not yours.
Step 4: Mid-flight โ one step, then land or state
Take one concrete step. Not a work session โ one step, the one you already knew you needed.
Then re-classify against Step 1 and follow Step 2 or Step 3. You may not return to Step 4 twice in a row: if the next nudge finds you mid-flight again, treat that as evidence you are actually blocked and go to Step 3.
Step 5: Rename so the list is readable
The jobs list shows nameSource: "auto" names that say nothing about outcome, which is why 157 entries are unreadable at a glance. Set the tmux window name to encode disposition:
tmux rename-window "done-<topic>"
tmux rename-window "blocked-<topic>"
Keep <topic> to two or three words. If tmux rename-window fails (no tmux, detached), skip it โ it is a legibility nicety, not a correctness step, and it must never abort a wrap-up.
Known limitation, stated rather than papered over: this renames the tmux window, which is what you see in a terminal. The name field in ~/.claude/jobs/<id>/state.json โ what the agents list reads โ is not writable from inside a session, and the sandbox denies writes under ~/.claude/jobs regardless. So this improves tmux legibility only. Closing the gap for the agents list needs a change outside this skill.
Anti-patterns
| Thought | Reality |
|---|
| "I'll just keep going, I'm close" | That is what continue did 101 times. Classify and terminate. |
| "I'll pick the sensible option and note it" | Step 3 exists precisely to stop this. Surface it. |
| "This repo isn't dotfiles but the work is obviously fine" | Step 0 closes Step 2 only. Say where the work stands, name the uncommitted paths and their checkout, rename the session, and end the turn. Do not resume โ out of scope means "do not write to git", never "keep going". |
| "The PR body should explain everything I did" | Under ~250 words, three sections, no narration. gh pr diff shows the files; the body says what works, what does not, and what was run. |
| "I'll commit everything to be safe" | Stage the paths you touched, by name. git add -u commits other people's work; git add -A also stages sandbox char-device masks. |
"I'm on main but the change is small" | Step 2's precondition is a hard stop. Unattended commits onto main are exactly what the draft-PR flow exists to prevent. |
| "I'll just switch the checkout to a branch first" | Only in a linked worktree. In the root checkout that moves HEAD for every other pane and cron job pointed at it. |
| "The file is on my list, so the staged diff is mine" | A path is not an ownership boundary. Read git diff --cached, not just --name-only. |
| "No blocker, so I'll invent a next step" | Re-read Step 1. Inventing work to avoid stating a blocker is the failure mode. |
| "I'll ask in prose, the owner will see it" | Background jobs do not notify on prose. AskUserQuestion or it did not happen. |