feat: adopt Ralph pattern — pipe to claude (no --print), working Stop hook

This commit is contained in:
2026-03-27 13:24:13 -04:00
parent 1e7f7ea6ed
commit 994908aed2
2 changed files with 59 additions and 33 deletions

View File

@@ -1,19 +1,50 @@
#!/bin/bash
# Stop hook management for Claude Code loop continuation.
# Stop hook management for the agent loop.
#
# NOTE: Hooks are currently no-ops. The loop uses `claude --print` (non-interactive),
# which runs to completion and exits naturally — no Stop hook is needed to signal
# iteration boundaries. The install/remove interface is preserved so that a future
# interactive mode can be added without changing loop.sh's call sites.
# The Stop hook sends SIGINT to the loop's parent process when a claude session
# finishes. This is how claude signals "I'm done" back to the bash loop —
# the Ralph pattern (ghuntley.com/ralph).
#
# If interactive mode is added, the hook mechanism will need redesign: `kill -INT $PPID`
# targets the hook runner's parent (Claude Code), not loop.sh. A sentinel-file or
# named-pipe approach would be more reliable.
# Without this hook, claude would exit to an interactive prompt instead of
# returning control to the loop script.
SETTINGS_FILE="${PROJECT_ROOT}/.claude/settings.local.json"
install_hooks() {
: # no-op — see note above
if [ ! -f "$SETTINGS_FILE" ]; then
mkdir -p "$(dirname "$SETTINGS_FILE")"
echo '{}' > "$SETTINGS_FILE"
fi
if command -v jq &>/dev/null; then
jq '.hooks.Stop = [{"matcher": "", "hooks": [{"type": "command", "command": "kill -INT $PPID || true"}]}]' \
"$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
else
python3 -c "
import json, os
p = '$SETTINGS_FILE'
s = json.load(open(p)) if os.path.exists(p) else {}
s.setdefault('hooks', {})['Stop'] = [{'matcher': '', 'hooks': [{'type': 'command', 'command': 'kill -INT \$PPID || true'}]}]
json.dump(s, open(p, 'w'), indent=2)
"
fi
log "Stop hook installed"
}
remove_hooks() {
: # no-op — see note above
if [ -f "$SETTINGS_FILE" ]; then
if command -v jq &>/dev/null; then
jq 'del(.hooks.Stop)' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
jq 'if .hooks == {} then del(.hooks) else . end' "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
else
python3 -c "
import json
s = json.load(open('$SETTINGS_FILE'))
s.get('hooks', {}).pop('Stop', None)
if not s.get('hooks'): s.pop('hooks', None)
json.dump(s, open('$SETTINGS_FILE', 'w'), indent=2)
"
fi
log "Stop hook removed"
fi
}