From 742b64e743cac352470bb472a724406e045e5df5 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 23 Mar 2026 09:56:49 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20stop=20hook=20should=20nudge=20Claude=20?= =?UTF-8?q?when=20PHASE=20file=20is=20empty=20=E2=80=94=20prevents=20silen?= =?UTF-8?q?t=20exit=20without=20PHASE:done=20(#585)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Claude finishes a response but hasn't written to the PHASE file, the stop hook now injects a nudge into the tmux session instead of just marking idle. This gives Claude another chance to complete the phase protocol before the monitor loop times out. Key changes: - on-idle-stop.sh: check phase file emptiness, nudge via tmux (max 2) - agent-session.sh: pass phase_file + session to stop hook, clean up nudge counter on session teardown Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/agent-session.sh | 6 ++++++ lib/hooks/on-idle-stop.sh | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/agent-session.sh b/lib/agent-session.sh index 5652838..d5cae19 100644 --- a/lib/agent-session.sh +++ b/lib/agent-session.sh @@ -66,6 +66,11 @@ create_agent_session() { local hook_script="${FACTORY_ROOT}/lib/hooks/on-idle-stop.sh" if [ -x "$hook_script" ]; then local hook_cmd="${hook_script} ${idle_marker}" + # When a phase file is available, pass it and the session name so the + # hook can nudge Claude if it returns to the prompt without signalling. + if [ -n "$phase_file" ]; then + hook_cmd="${hook_script} ${idle_marker} ${phase_file} ${session}" + fi if [ -f "$settings" ]; then # Append our Stop hook to existing project settings jq --arg cmd "$hook_cmd" ' @@ -443,6 +448,7 @@ agent_kill_session() { rm -f "/tmp/claude-idle-${session}.ts" rm -f "/tmp/phase-changed-${session}.marker" rm -f "/tmp/claude-exited-${session}.ts" + rm -f "/tmp/claude-nudge-${session}.count" } # Read the current phase from a phase file, stripped of whitespace. diff --git a/lib/hooks/on-idle-stop.sh b/lib/hooks/on-idle-stop.sh index f74f819..d614acd 100755 --- a/lib/hooks/on-idle-stop.sh +++ b/lib/hooks/on-idle-stop.sh @@ -5,10 +5,38 @@ # to a marker file so monitor_phase_loop can detect idle sessions # without fragile tmux pane scraping. # +# When a phase file is provided and exists but is empty, Claude likely +# returned to the prompt without following the phase protocol. Instead +# of marking idle, inject a nudge into the tmux session (up to 2 times). +# # Usage (in .claude/settings.json): -# {"type": "command", "command": "this-script /tmp/claude-idle-SESSION.ts"} +# {"type": "command", "command": "this-script /tmp/claude-idle-SESSION.ts [PHASE_FILE SESSION_NAME]"} # # Args: $1 = marker file path +# $2 = phase file path (optional) +# $3 = tmux session name (optional) cat > /dev/null # consume hook JSON from stdin -[ -n "${1:-}" ] && date +%s > "$1" + +MARKER="${1:-}" +[ -z "$MARKER" ] && exit 0 + +PHASE_FILE="${2:-}" +SESSION_NAME="${3:-}" + +# If phase file is provided, exists, and is empty — Claude forgot to signal. +# Nudge via tmux instead of marking idle (up to 2 attempts). +if [ -n "$PHASE_FILE" ] && [ -n "$SESSION_NAME" ] && [ -f "$PHASE_FILE" ] && [ ! -s "$PHASE_FILE" ]; then + NUDGE_FILE="/tmp/claude-nudge-${SESSION_NAME}.count" + NUDGE_COUNT=$(cat "$NUDGE_FILE" 2>/dev/null || echo 0) + if [ "$NUDGE_COUNT" -lt 2 ]; then + echo $(( NUDGE_COUNT + 1 )) > "$NUDGE_FILE" + tmux send-keys -t "$SESSION_NAME" \ + "You returned to the prompt without writing to the PHASE file. Checklist: (1) Did you complete the commit-and-pr step? (2) Did you write PHASE:done or PHASE:awaiting_ci to ${PHASE_FILE}? If no file changes were needed, write PHASE:done now." Enter + exit 0 + fi +fi + +# Normal idle mark — either no phase file, phase already has content, +# or nudge limit reached. +date +%s > "$MARKER"