ソースを参照

fix(ralph-loop): stop hook fails when last assistant block is tool_use

Claude Code writes each assistant content block (text/tool_use/thinking)
as its own JSONL line. The hook's `grep role:assistant | tail -1` would
grab whichever block happened to be last — often tool_use — then jq's
text filter returned empty string, triggering the 'no text content' path
which deletes the state file and exits without blocking.

Net effect: the loop silently never fires. In one observed session, 62%
of assistant lines were tool_use-only; the hook deleted state on the
very first Stop event every time.

Fix: slurp all assistant lines with jq -rs, flatten to text blocks only,
take the last. Empty result is now non-fatal — no text means no <promise>
tag, so the loop continues. Also absorbs jq parse errors (control chars
in text) via || fallback instead of aborting under set -e.
sfishman 6 ヶ月 前
コミット
adfc379663
1 ファイル変更13 行追加33 行削除
  1. 13 33
      plugins/ralph-loop/hooks/stop-hook.sh

+ 13 - 33
plugins/ralph-loop/hooks/stop-hook.sh

@@ -77,39 +77,19 @@ if ! grep -q '"role":"assistant"' "$TRANSCRIPT_PATH"; then
   exit 0
 fi
 
-# Extract last assistant message with explicit error handling
-LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
-if [[ -z "$LAST_LINE" ]]; then
-  echo "⚠️  Ralph loop: Failed to extract last assistant message" >&2
-  echo "   Ralph loop is stopping." >&2
-  rm "$RALPH_STATE_FILE"
-  exit 0
-fi
-
-# Parse JSON with error handling
-LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
-  .message.content |
-  map(select(.type == "text")) |
-  map(.text) |
-  join("\n")
-' 2>&1)
-
-# Check if jq succeeded
-if [[ $? -ne 0 ]]; then
-  echo "⚠️  Ralph loop: Failed to parse assistant message JSON" >&2
-  echo "   Error: $LAST_OUTPUT" >&2
-  echo "   This may indicate a transcript format issue" >&2
-  echo "   Ralph loop is stopping." >&2
-  rm "$RALPH_STATE_FILE"
-  exit 0
-fi
-
-if [[ -z "$LAST_OUTPUT" ]]; then
-  echo "⚠️  Ralph loop: Assistant message contained no text content" >&2
-  echo "   Ralph loop is stopping." >&2
-  rm "$RALPH_STATE_FILE"
-  exit 0
-fi
+# Extract the most recent assistant text block.
+#
+# Claude Code writes each content block (text/tool_use/thinking) as its own
+# JSONL line, all with role=assistant. `tail -1` alone would often grab a
+# tool_use or thinking block, leaving no text to check. Instead, slurp all
+# assistant lines, flatten to text blocks only, and take the last one.
+#
+# `last // ""` yields empty string when no text blocks exist (e.g. a turn
+# that is all tool calls). That's fine: empty text means no <promise> tag,
+# so the loop simply continues.
+LAST_OUTPUT=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | jq -rs '
+  map(.message.content[]? | select(.type == "text") | .text) | last // ""
+' 2>/dev/null) || LAST_OUTPUT=""
 
 # Check for completion promise (only if set)
 if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then