| name | pause-resume |
| description | Resume companion to /pause; `/go-on` is the primary resume entry point and routes here. Explicitly clears the background-launch gate, reads parked and stopped-task recovery state, prints the current board, and re-arms selected work without duplicating live tasks. The refill gate is cleared only with --resume-refill. Triggers on "pause-resume", "resume from pause", "back from laptop", "restore parked work", "what did I park". |
| triggers | ["pause-resume","resume from pause","back from laptop","restore parked work","what did I park"] |
| argument-hint | [--resume-refill] (--resume-refill clears the refill pause; without it the pause stands and is reported) |
Thin restorer for /pause. Reads the pause state, prints the board as it is now (not as it was parked — it re-reads GitHub before printing), re-arms what was stopped, and reports what is waiting on you.
/go-on is the primary entry point for resuming. It classifies the stoppage from recorded evidence and routes here when the newest record is a /pause, forwarding --resume-refill verbatim — so nobody has to remember which stop happened (Issue #1397; ladder: .claude/reference/universal-resume.md). This command keeps working unchanged and stays the direct path when you already know the work was paused; it remains the executor, and /go-on never reimplements the restore below.
Running this when no pause state exists is a clean no-op: No parked session found — nothing to resume.
Step 0: Resolve helpers
/pause-resume is invocable from any thread — including one whose cwd is a different worktree than the one /pause ran in. The three-candidate resolution order is identical to every other stop-style command:
resolve_script() {
local name="$1" candidate
for candidate in \
"$HOME/.claude/skills-worktree/.claude/scripts/$name" \
"$HOME/.claude/scripts/$name"; do
if [[ -x "$candidate" ]]; then echo "$candidate"; return 0; fi
done
return 1
}
SESSION_STATE_SH=$(resolve_script session-state.sh) || SESSION_STATE_SH=""
EXECUTION_PAUSE_SH=$(resolve_script execution-pause.sh) || EXECUTION_PAUSE_SH=""
TASK_REGISTRY_SH=$(resolve_script background-task-registry.sh) || TASK_REGISTRY_SH=""
The current checkout is intentionally not a fallback: this resume command may
run from an unrelated or untrusted repository, and must execute only installed
helpers.
An unresolved session-state.sh in this skill is fatal for the state-read path but recoverable: fall back to the marker file in Step 1. Say which path is being used so the user knows.
Parse --resume-refill and the internal auto-wake generation token:
RESUME_REFILL=false
CALLER_GENERATION=""
EXPLICIT_MARKER=""
_NEXT_IS_GENERATION=false
_NEXT_IS_MARKER=false
for arg in $ARGUMENTS; do
if [[ "$_NEXT_IS_GENERATION" == true ]]; then
CALLER_GENERATION="$arg"
_NEXT_IS_GENERATION=false
continue
fi
if [[ "$_NEXT_IS_MARKER" == true ]]; then
EXPLICIT_MARKER="$arg"
_NEXT_IS_MARKER=false
continue
fi
case "$arg" in
--resume-refill) RESUME_REFILL=true ;;
--generation) _NEXT_IS_GENERATION=true ;;
--marker) _NEXT_IS_MARKER=true ;;
esac
done
[[ "$_NEXT_IS_GENERATION" == false ]] || \
{ echo "ERROR: --generation requires a value." >&2; exit 2; }
[[ "$_NEXT_IS_MARKER" == false ]] || \
{ echo "ERROR: --marker requires a value." >&2; exit 2; }
Before clearing any gate, validate a Monitor-supplied generation against the
current saved generation. A stale or unreadable generation terminates without
changing state, so an old wake cannot reopen execution or re-arm work:
if [[ -n "$CALLER_GENERATION" ]]; then
[[ -n "$SESSION_STATE_SH" ]] || \
{ echo "Cannot validate auto-wake generation; no gate was cleared." >&2; exit 1; }
REPO_KEY=$("$SESSION_STATE_SH" --repo-key 2>/dev/null) || REPO_KEY=""
[[ -n "$REPO_KEY" ]] || \
{ echo "Cannot identify the auto-wake repository; no gate was cleared." >&2; exit 1; }
STORED_GENERATION=$("$SESSION_STATE_SH" \
--get ".repos[\"$REPO_KEY\"].day.limit_resume_generation" 2>/dev/null) || \
{ echo "Cannot read the saved auto-wake generation; no gate was cleared." >&2; exit 1; }
if [[ -z "$STORED_GENERATION" || "$STORED_GENERATION" == "null" || \
"$CALLER_GENERATION" != "$STORED_GENERATION" ]]; then
echo "Stale auto-wake rejected; no gate was cleared or work re-armed."
exit 0
fi
fi
Step 1: Read pause state
Try the state file first; fall back to the newest repo-matching marker if it fails. The marker filename includes the repo key (set by /pause Step 7a) — validate it before choosing so a marker from another repo cannot be mistaken for this one's state:
PAUSE_STATE=""
USE_MARKER=false
MARKER_PATH=""
STATE_KEY="pause"
if [[ -n "$SESSION_STATE_SH" ]]; then
REPO_KEY=$("$SESSION_STATE_SH" --repo-key 2>/dev/null) || REPO_KEY=""
if [[ -n "$REPO_KEY" ]]; then
PAUSE_STATE=$("$SESSION_STATE_SH" --get ".repos[\"$REPO_KEY\"].pause" 2>/dev/null || echo "")
if [[ -z "$PAUSE_STATE" || "$PAUSE_STATE" == "null" ]]; then
PAUSE_STATE=$("$SESSION_STATE_SH" --get ".repos[\"$REPO_KEY\"].suspend" 2>/dev/null || echo "")
if [[ -n "$PAUSE_STATE" && "$PAUSE_STATE" != "null" ]]; then
STATE_KEY="suspend"
echo "(using legacy pre-Issue-1310 suspend state; new pauses use .pause)"
fi
fi
fi
fi
[[ -z || == ]];
[[ -n ]];
[[ -r ]] || \
{ >&2; 1; }
pause-*.md) STATE_KEY= ;;
-*.md) STATE_KEY= ;;
*) >&2; 1 ;;
MARKER_REPO=$(sed -n | -1)
[[ -n && != && \
-n && != ]];
>&2
1
MARKER_PATH=
[[ -z && -z ]];
0
[[ -z ]];
REPO_OWNER=
REPO_NAME=
REPO_KEY_SAFE=
LEGACY_REPO_KEY_SAFE=
IFS= -r candidate;
MARKER_NAME=
MARKER_REPO=$(sed -n 2>/dev/null | -1)
[[ == pause-** && \
== ]];
MARKER_PATH=
[[ == -** ]];
MARKER_PATH=
STATE_KEY=
< <( -t *.md \
*.md 2>/dev/null)
[[ -n ]];
USE_MARKER=
PAUSE_STATE=$(awk )
[[ -z && == ]];
0
Reading from session-state.json is the primary path. When that path is available, all later steps can use jq to parse $PAUSE_STATE as JSON. When the marker fallback is active (USE_MARKER=true), later steps read the marker file directly from $MARKER_PATH — they cannot assume $PAUSE_STATE is valid JSON, and should extract what they can from the human-readable sections.
The pause block is invisible to --session-view (that projection lifts only .prs and .root_repo). Always read it with an explicit --get .repos["<key>"].pause — never via --session-view.
The legacy .suspend read and suspend-*.md glob above are compatibility
inputs only. They preserve resumability for sessions parked before Issue #1310;
new /pause runs never write those names. STATE_KEY remembers which JSON
record was loaded so Step 7 closes that same record safely.
New marker auto-discovery requires the exact Repository: \owner/repo`field; the injective filename match is an index, not repository-identity authority. An_unknownmarker is resumable only through the explicit--markerpath printed by/pause`.
Step 2: Check if already resumed
For a JSON state read, check the active flag. For a marker-only read, the marker's existence implies an incomplete restore (a fully-resumed session writes active: false in the state file, which masks the state before this step runs):
if [[ "$USE_MARKER" == false ]]; then
ACTIVE=$(jq -r '.active // true' <<<"$PAUSE_STATE" 2>/dev/null || echo "true")
if [[ "$ACTIVE" == "false" ]]; then
if ! PENDING_REARMS=$(jq -er '
((.monitors_stopped // [])
| map(select((.rearmed // false) != true))
| length)
+ ((.background_tasks_stopped // [])
| map(select((.rearmed // false) != true))
| length)
' <<<"$PAUSE_STATE" 2>/dev/null); then
echo "Pause recovery state is unreadable; keeping the session active." >&2
PENDING_REARMS=-1
fi
if [[ "$PENDING_REARMS" -lt 0 ]]; then
exit 1
fi
if [[ "$PENDING_REARMS" -gt 0 ]]; then
echo "Pause session was partially resumed ($PENDING_REARMS re-arm(s) still pending). Continuing restore..."
else
echo "Pause state exists but is already marked resumed (active: false). Run /pause again to park a new session."
exit 0
fi
fi
fi
Idempotent on a fully-resumed session. On a partially-resumed session (some re-arms failed), the step continues so Step 5 can retry the incomplete entries.
Step 3: Re-read GitHub for each parked PR
Before printing the board, re-read GitHub for each PR listed in .pause.parked. A state that moved since pause (a review that landed, a merge that completed after the /wrap window, CI that finished) is reported as it is now, not as it was parked.
For each parked PR, run gh pr view <N> --json state,mergeStateStatus,mergeable,reviewDecision and update the parked entry's display. A PR whose state: MERGED is reported as landed (with a note that it merged after the window) rather than parked. A PR whose CI finished running is reported with its updated status.
This re-read is display-only: it does not change the persisted pause block. The block is a historical record of the parking point.
Step 4: Print the board
For JSON state, render the timestamp as
jq -r '.paused_at // .suspended_at // "unknown"'. The second field is the
legacy pre-Issue-1310 spelling; never print an empty timestamp merely because
the parked session predates the rename.
=== Resuming from pause at <paused_at> (window was <window_minutes>m) ===
Landed during pause:
merged PR #N (<at>)
<also merged after window: PR #M — merged after window expiry>
<nothing landed> if empty
Parked (<N> units) — current state:
PR #M [<current GitHub state>] — stopped at: <stopped_at> · next: <next_move> · waiting on: <waiting_on>
Subagent <kind> — handoff at <path>
<nothing parked> if empty
Monitors to re-arm:
babysit PR #N — will re-arm via /babysit-pr
PR fleet monitor — will re-arm via /pr-monitor-and-manage-wake
Day-mode loop — will re-arm via /pm day resume
<nothing to re-arm> if all monitors_stopped entries have no stopped entry
Refill pause:
<REFILL_PAUSED=true: "Refilling is paused (full_stop). To resume: tell Claude 'resume refilling' in this session, or /pause-resume --resume-refill to clear it now.">
<REFILL_PAUSED=false: "Refilling is not paused.">
The parked units show current GitHub state alongside the parking-point snapshot, so the user immediately sees what changed while the session was closed.
Step 4b: Open the execution gate for recovery
Only after Step 1 found state and Step 2 confirmed recovery is still active,
clear the session execution gate. This ordering keeps a missing or already
resumed pause as a clean no-op that does not mutate the gate. Clear before any
Monitor, Agent, Workflow, or background Bash is re-armed; if clearing fails,
stop without re-arming anything:
SESSION_ID="${CLAUDE_SESSION_ID:-default}"
if [[ -z "$EXECUTION_PAUSE_SH" ]] || \
! "$EXECUTION_PAUSE_SH" --clear --session "$SESSION_ID"; then
echo "Could not clear the pause execution gate; no work was re-armed." >&2
exit 1
fi
Step 5: Re-arm what was stopped
First inspect current-session stopped registry entries. Re-check runtime state
before every re-arm so an already-running identity is never duplicated. Resume
stopped agents by their exact runtime ID with SendMessage; for workflows,
background commands, and Monitors use the recorded recovery path and owning
skill, then mark the old entry rearmed. A missing recovery path is reported
as pending, not guessed. Preserve the stopped entry for audit history.
Before delegating to any re-arm skill, disarm the usage-limit auto-wake Monitor if one is armed. This prevents a double resume when the user runs /pause-resume manually while a limit-wake Monitor is still ticking (i.e. the rolling-window park from 2D.6 has not yet fired automatically). One registry covers both wake shapes: 2D.7's bounded probe Monitor (#1428) records its identity in these same fields, so the block below stops it too — clearing limit_probe_fires_remaining with the pair is what stops a later recovery re-arming a probe for a park the user has already resumed past. When /pause-resume is invoked by the Monitor itself (not manually), it carries --generation <id>; validate the generation before proceeding to reject stale or duplicate wakes:
LIMIT_WAKE_RESOLVED=false
if [[ -n "$SESSION_STATE_SH" && -n "$REPO_KEY" ]]; then
LIMIT_TASK_RC=0
LIMIT_TASK_ID=$("$SESSION_STATE_SH" --get ".repos[\"$REPO_KEY\"].day.limit_resume_task_id" 2>/dev/null) || LIMIT_TASK_RC=$?
if [[ "$LIMIT_TASK_RC" -ne 0 && "$LIMIT_TASK_RC" -ne 3 ]]; then
echo "(DEGRADED: could not read day.limit_resume_task_id (rc=$LIMIT_TASK_RC) — recovery remains active)"
elif [[ "$LIMIT_TASK_RC" -eq 0 && -n "$LIMIT_TASK_ID" && "$LIMIT_TASK_ID" != "null" ]]; then
if TaskStop "$LIMIT_TASK_ID" 2>/dev/null; then
if "$SESSION_STATE_SH" \
--set ".repos[\"$REPO_KEY\"].day.limit_resume_task_id=null" \
--set ".repos[\"$REPO_KEY\"].day.limit_resume_generation=null" \
--set ;
LIMIT_WAKE_RESOLVED=
LIMIT_WAKE_RESOLVED=
For each entry in monitors_stopped where stopped: true and rearmed is not
already true, delegate to the appropriate re-arm skill — never reimplement
their logic. Skip entries already confirmed rearmed so retries are idempotent.
Before runtime inspection or delegation, atomically claim the exact task ID in
the shared registry, using the same reservation as /end-resume:
"$TASK_REGISTRY_SH" --transition --session "$SESSION_ID" \
--task-id "$TASK_ID" --status rearming --from-status stopped
Exit 7 means another /pause-resume invocation already claimed or completed
the entry; re-read it and do not launch. A missing registry record or task ID
keeps the pause entry pending rather than falling back to an unlocked launch.
After the claim, re-check the execution gate immediately before delegation. A
blocked or failed launch rolls rearming -> stopped; a confirmed successor
rolls rearming -> rearmed, then (and only then) sets the pause array entry's
rearmed: true. The successor registers its own runtime ID through the normal
launch hook. This ordering makes concurrent invocations single-writer even
before either one persists the pause-state array:
- Babysit watcher for a PR — invoke
/babysit-pr <PR> for each entry with owner: "babysit".
- PR fleet monitor — invoke
/pr-monitor-and-manage-wake for any entry with owner: "pmm". The wake companion reads its own saved config (cadence, author, max-parallel, etc.) and re-arms at base cadence.
- Day-mode loop — invoke
/pm day resume for any entry with owner: "day". This re-arms the persistent Monitor and picks up from where the loop paused. After /pm day re-arms, it reads the current day.parked_until; if the value is still in the future (the limit window has not yet reopened), it will re-arm the auto-wake instead of the tick Monitor — the disarm above ensures only one wake Monitor runs at a time.
- Usage-limit auto-wake — entries with
owner: "day_limit_wake" are
handled by the disarm block above. Set rearmed: true only when
LIMIT_WAKE_RESOLVED=true; otherwise preserve rearmed: false and record
the read, stop, or state-clear error. Never close recovery around an
unconfirmed disarm.
Entries with stopped: false are listed as "not confirmed stopped at pause time — verify manually before re-arming."
If any re-arm delegation fails, report it and carry on — a partial re-arm is better than stopping entirely. Record a per-entry rearmed: true/false field in the state so Step 7 can set active=false only when all required entries are done, and Step 2 can detect a partially-resumed session and retry:
Apply the same filter and bookkeeping to background_tasks_stopped, keyed by
exact task_id: skip rearmed: true entries and process only entries still
requiring restoration. Use the same locked stopped -> rearming registry claim
before inspection or launch and the same rollback/finalize transitions. Set
rearmed: true only after runtime verification. Set or preserve
rearmed: false when recovery failed or required metadata is missing, and add
resume_error naming the missing path/action. Persist both updated arrays with
session-state.sh --set; never mutate the state file directly.
After those writes succeed, set
STATE_PATH=".repos[\"$REPO_KEY\"].$STATE_KEY" and re-read that exact block
into PAUSE_STATE. This matters for a legacy restore: reading .pause after
updating .suspend would evaluate stale or absent arrays. A failed refresh is
a partial restore: keep active=true, report the read failure, and do not run
Step 7's completion write. Step 7 must evaluate the freshly persisted arrays,
never the pre-rearm snapshot captured at command start.
Step 6: Clear the refill pause (--resume-refill only)
When --resume-refill was supplied:
if [[ "$RESUME_REFILL" == true && -n "$SESSION_STATE_SH" && -n "$REPO_KEY" ]]; then
"$SESSION_STATE_SH" \
--set ".repos[\"$REPO_KEY\"].refill={\"paused\":false,\"reason\":null,\"scope\":null,\"at\":null}" \
&& echo "Refill pause cleared — pipeline will refill on the next tick." \
|| echo "Could not clear the refill pause (session-state.sh --set failed) — lift it manually."
fi
Without --resume-refill, the pause stands and Step 4 has already stated plainly how to lift it.
Step 7: Mark the pause as resumed
Only set active=false after all required re-arms in Step 5 have been confirmed (rearmed: true). If any required re-arm is still pending, keep active=true so the next invocation's Step 2 detects the incomplete restore and retries:
if [[ -n "$SESSION_STATE_SH" && -n "$REPO_KEY" ]]; then
STATE_PATH=".repos[\"$REPO_KEY\"].$STATE_KEY"
ALL_REARMED=true
PENDING=-1
if PENDING_RESULT=$(jq -er '
((.monitors_stopped // [])
| map(select((.rearmed // false) != true))
| length)
+ ((.background_tasks_stopped // [])
| map(select((.rearmed // false) != true))
| length)
' <<<"$PAUSE_STATE" 2>/dev/null); then
PENDING="$PENDING_RESULT"
else
echo "Recovery state is unreadable; keeping pause active." >&2
fi
[[ "$PENDING" -ne 0 ]] && ALL_REARMED=false
NOW=$(date -u +%FT%TZ)
if [[ "$ALL_REARMED" == true ]]; then
"$SESSION_STATE_SH" \
--set "$STATE_PATH.active=false" \
--set "$STATE_PATH.resumed_at=\"$NOW\""
else
"$SESSION_STATE_SH" \
--set
[[ -lt 0 ]];
The active=false write is the idempotent guard from Step 2. The record is kept for history; landed and parked history remains, while both stopped arrays retain their per-entry recovery result.
Safety
- Never auto-clear the refill pause. It stays paused until the user supplies
--resume-refill or explicitly says "resume refilling" in chat. A pause that auto-cleared the pause on resume would defeat the purpose of pausing in the first place.
- Re-read GitHub before printing, not after. The board should reflect current state, not stale parking-point state, because the user is deciding what to work on next.
- Fail closed on the no-state check. An unreadable state file and an absent marker produce a clean no-op with a clear message, not an attempt to resume phantom state.
- Delegation, not reimplementation. The re-arm steps delegate to the existing companion skills (
/babysit-pr, /pr-monitor-and-manage-wake, /pm day resume) rather than reimplementing their logic. Those skills own their own Monitor-arming contracts and generation tracking; reimplementing them here creates a second code path with a high risk of divergence.