| name | pr-monitor-and-manage-wake |
| description | Resume companion to /pr-monitor-and-manage. Wakes a paused PR-fleet manager from its saved pause marker, stops any auto-wake Monitor, and re-arms the main Monitor. Also serves as the lightweight --auto-check target for that re-scan. Triggers on "/pr-monitor-and-manage-wake", "/pmm-wake", "wake PR monitor". |
| triggers | ["pr-monitor-and-manage-wake","pmm-wake","wake PR monitor","resume PR fleet"] |
| argument-hint | [--auto-check] (default: user-initiated resume; --auto-check = lightweight fleet scan from the auto-wake re-scan) |
Resume companion to /pr-monitor-and-manage. Wakes a paused PR-fleet manager: reads the pause marker, stops any auto-wake Monitor, clears the marker, and re-arms the main Monitor at base cadence.
Idempotent: running on a non-paused session is a clean no-op.
Post-merge symlink: after merge to main, symlink ~/.claude/skills/pr-monitor-and-manage-wake → ~/.claude/skills-worktree/.claude/skills/pr-monitor-and-manage-wake per skill-symlinks.md — never copy, never symlink to the root repo.
Resolve the state helper
Same three-candidate lookup as /babysit-pr-stop:
resolve_script() {
local name="$1" candidate
for candidate in \
"$HOME/.claude/skills-worktree/.claude/scripts/$name" \
"$HOME/.claude/scripts/$name" \
".claude/scripts/$name"; do
if [[ -x "$candidate" ]]; then echo "$candidate"; return 0; fi
done
return 1
}
SESSION_STATE_SH=$(resolve_script session-state.sh) || { echo "ERROR: session-state.sh not found" >&2; exit 1; }
Step 1: Parse mode
--auto-check — lightweight fleet scan (invoked by the auto-wake Monitor). Compare gh pr list against .pmm.fleet_at_pause; re-launch main skill only if changed. It must carry the runtime-only --monitor-generation <token> emitted by that Monitor.
- No flags (default) — user-initiated resume: clear marker, stop the re-scan, re-arm the main Monitor.
Before reading the pause marker in --auto-check mode, parse the supplied generation and compare it
with .pmm.auto_wake_monitor_generation. Missing or unequal means a queued event from an old or
unidentified re-scan: print [auto-check] Ignoring a stale Monitor generation. and exit 0 without
reading GitHub or mutating state. This gate is first so an event queued before exact TaskStop
cannot see a later pause marker and cancel or resume the replacement generation.
Execute that parser and generation gate explicitly before Step 2:
MODE="user-resume"
AUTO_CHECK_GENERATION=""
_AUTO_CHECK_EXPECT_GENERATION=false
for _WAKE_ARG in $ARGUMENTS; do
if [[ "$_AUTO_CHECK_EXPECT_GENERATION" == true ]]; then
AUTO_CHECK_GENERATION="$_WAKE_ARG"
_AUTO_CHECK_EXPECT_GENERATION=false
continue
fi
case "$_WAKE_ARG" in
--auto-check) MODE="auto-check" ;;
--monitor-generation) _AUTO_CHECK_EXPECT_GENERATION=true ;;
esac
done
if [[ "$_AUTO_CHECK_EXPECT_GENERATION" == true ]]; then
echo "ERROR: --monitor-generation requires a token." >&2
exit 2
fi
if [[ "$MODE" == "auto-check" ]]; then
RECORDED_AUTO_CHECK_GENERATION=$("$SESSION_STATE_SH" --get '.pmm.auto_wake_monitor_generation' 2>/dev/null || echo null)
if [[ -z "$AUTO_CHECK_GENERATION" || "$AUTO_CHECK_GENERATION" == "null" ||
"" != ]];
0
[[ -n ]];
>&2
2
Ignore --monitor-generation and its captured token in any later mode/config parsing. Never copy a
generation into the replacement main Monitor; Step 4b generates a fresh one for that arm.
Step 2: Check pause marker
PAUSED_AT=$("$SESSION_STATE_SH" --get '.pmm.paused_at' 2>/dev/null || echo null)
STOP_PENDING=$("$SESSION_STATE_SH" --get '.pmm.stop_requested' 2>/dev/null || echo false)
- If
$STOP_PENDING is true → report incomplete PMM teardown and exit non-zero; never resume or
auto-check until /pmm-stop completes exact task teardown.
- If
$PAUSED_AT is null/empty → print "PMM not paused — either not started or already running." and exit 0 (idempotent no-op).
- If present → route by mode:
--auto-check → Step 4a (compare fleet; the re-scan stays unless changed).
- User-initiated (default) → Step 3 (cancel re-scan) → Step 4b (resume).
Step 3: Cancel the auto-wake re-scan (user resume and auto-check-on-change only)
Do NOT run this step in --auto-check mode when the fleet is unchanged — the re-scan must keep running until a change is detected.
When $MODE is --auto-check, skip to Step 4a first; only cancel the re-scan from Step 4b after a fleet change is confirmed, or when $MODE is user-initiated resume (default).
Read both task IDs and their matching generations (.pmm.auto_wake_monitor_task_id plus
.pmm.auto_wake_monitor_generation, and .pmm_monitor_task_id plus
.pmm_monitor_generation) and stop each recorded task with exact TaskStop. The main-task ID is
normally null while paused, but a degraded pause retains it when teardown failed. Clear each
successfully stopped ID and generation immediately through one atomic
session-state.sh write while leaving the complete pause marker/config/fleet intact. If either
present ID cannot be stopped, abort the resume: retain that failed identity pair, clear only
identity pairs whose exact stops succeeded, and do not arm the replacement main Monitor. This prevents a later retry from
treating an already-stopped task as a live teardown failure. Nothing is removed from
polling_jobs[]; Issue #924 replaces the unreliable /loop re-scan with this recorded Monitor
task.
Step 4a: --auto-check branch (lightweight scan)
Do not remove the <!-- test-anchor: … --> comments below. They are how
.claude/scripts/tests/pmm-wake-step-4a.test.sh extracts and runs these
blocks rather than a copy, so the #871 fail-open cannot be re-opened by a
prose cleanup (issue #888). Reword and reorder freely — just keep each anchor
immediately above its fence. Changing the guards means re-running the
discrimination control documented in that test.
Read saved config and fleet snapshot:
CONFIG=$("$SESSION_STATE_SH" --get '.pmm.config_at_pause' 2>/dev/null || echo 'null')
FLEET_AT_PAUSE=$("$SESSION_STATE_SH" --get '.pmm.fleet_at_pause' 2>/dev/null || echo 'null')
AUTHOR=$(jq -r '.author // empty' <<<"$CONFIG" 2>/dev/null || echo '')
REPO=$(jq -r '.repo // empty' <<<"$CONFIG" 2>/dev/null || echo '')
REPO_FLAG=()
[[ -n "$REPO" ]] && REPO_FLAG=(--repo "$REPO")
scan_failed() {
echo "ERROR: [auto-check] fleet scan failed — $1. Pause marker and re-scan kept; retrying on the next re-scan tick." >&2
exit 1
}
[[ -n "$AUTHOR" ]] || scan_failed "no author in .pmm.config_at_pause — cannot scope the scan"
SCAN_RC=0
CURRENT=$(gh list --state open --author \
--json number,headRefOid,mergeStateStatus -- 500 2>/dev/null) || SCAN_RC=$?
[[ -eq 0 ]] || scan_failed
[[ -n ]] || scan_failed
jq -e <<< >/dev/null 2>&1 \
|| scan_failed
jq -e <<< >/dev/null 2>&1 \
|| scan_failed
CURRENT_FLEET=$(jq -c <<<) \
|| scan_failed
SAVED_FLEET=$(jq -c <<<) \
|| scan_failed
Only once every guard above has passed may the two snapshots be compared:
if [[ "$CURRENT_FLEET" == "$SAVED_FLEET" ]]; then
echo "[auto-check] Fleet unchanged — re-scan continues."
exit 0
fi
Step 4b: Resume (user wake or auto-check detected change)
Cancel the re-scan (Step 3) if not already done (user-initiated resume does it before this step; auto-check does it only after detecting a change).
Read config and reconstruct invocation flags:
CONFIG=$("$SESSION_STATE_SH" --get '.pmm.config_at_pause' 2>/dev/null || echo '{}')
CADENCE=$(jq -r '.cadence // "5m"' <<<"$CONFIG")
PMM_FLAGS=""
AUTHOR=$(jq -r '.author // empty' <<<"$CONFIG")
REPO=$(jq -r '.repo // empty' <<<"$CONFIG")
[ -n "$AUTHOR" ] && PMM_FLAGS="$PMM_FLAGS --author $AUTHOR"
[ -n "$REPO" ] && PMM_FLAGS="$PMM_FLAGS --repo $REPO"
PMM_FLAGS="$PMM_FLAGS --cadence $(jq -r '.cadence // "5m"' <<<"$CONFIG")"
PMM_FLAGS="$PMM_FLAGS --max-parallel $(jq -r '.max_parallel // 3' <<<"$CONFIG")"
PMM_FLAGS="$PMM_FLAGS --idle-pause-after $(jq -r '.idle_pause_after // 3' <<<"$CONFIG")"
jq -e '.auto_wake == true' <<<"$CONFIG" >/dev/null 2>&1 && PMM_FLAGS="$PMM_FLAGS --auto-wake"
PMM_FLAGS="$PMM_FLAGS --auto-wake-cadence $(jq -r '.auto_wake_cadence // ' <<<)"
jq -e <<< >/dev/null 2>&1 && PMM_FLAGS=
Arm the replacement at base cadence (not widened backoff) through a persistent Monitor:
MONITOR_GENERATION="$(date -u +%Y%m%dT%H%M%SZ)-$$-${RANDOM:-0}"
while sleep "<CADENCE in seconds>"; do
printf '%s\n' "/pr-monitor-and-manage --tick --monitor-generation $MONITOR_GENERATION <PMM_FLAGS>"
done
Call Monitor with persistent: true and description PR fleet monitor, then require a returned task ID. If arming fails or returns no ID, leave the pause marker/config/fleet intact, keep pmm_active=false, and report that resume failed (the auto-wake re-scan was already stopped in Step 3).
After a successful arm, clear the marker and publish the new task ID in one atomic write:
"$SESSION_STATE_SH" \
--set '.pmm.stop_requested=false' \
--set '.pmm.paused_at=null' \
--set '.pmm.fleet_at_pause=null' \
--set '.pmm.config_at_pause=null' \
--set '.pmm_idle_streak=0' \
--set '.pmm_active=true' \
--set ".pmm_monitor_task_id=$MONITOR_TASK_ID" \
--set ".pmm_monitor_generation=\"$MONITOR_GENERATION\"" \
--set '.pmm.auto_wake_monitor_task_id=null' \
--set '.pmm.auto_wake_monitor_generation=null' \
--set '.pmm_digest=null' \
--set '.pmm_row_digest=null' \
--set '.pmm_next_expected_tick_at=null'
If that ID+generation publication write fails, stop the newly armed task with TaskStop; the atomic failure leaves the pause
marker intact, so never claim resume. If the rollback stop also fails, report the exact new task ID
and generation and retain the paused state; best-effort set .pmm.stop_requested=true and .pmm_active=false so
the new task's internal --tick events are no-ops and no retry can arm a duplicate. An unrecorded
live task must never be described as cleaned up.
The next tick runs full discovery + per-PR state read. Print:
PMM resumed — Monitor re-armed at <CADENCE>. Fleet will be rediscovered on the next tick.
For --auto-check with detected change, add: [auto-check] Fleet changed — re-scan stopped, main Monitor re-launched.
Safety
- Never clear the pause marker on resume without also cancelling the auto-wake re-scan (Step 3) — except
--auto-check no-op exits without touching either.
- A present main or auto-wake task ID whose
TaskStop fails is a hard resume abort. Never arm the
replacement Monitor or discard that failed ID while an old task can still emit. IDs whose exact
stops succeeded have their matching generations cleared before the abort so a retry never wedges
on stale task identity.
- A failed or unverifiable scan is never a fleet change. If
gh pr list errors, returns empty, or either snapshot fails to parse as a JSON array, Step 4a keeps the pause marker and the re-scan and exits non-zero — it never falls through to the comparison. Only a scan that proves a difference may resume.
- Step 4b must null
.pmm_digest and .pmm_row_digest in the same --set batch that clears the marker. It clears .pmm.paused_at before the main skill runs, so the main skill's Step 0a reset cannot fire on this path; the digests are what make its Step 4 print the full table on the first post-resume tick.
--auto-check must not run full per-PR gate reads — only gh pr list + comparison.
- Re-running
/pr-monitor-and-manage-wake on a non-paused session is always a clean no-op (Step 2).