From 32ba940a7e5454a6a1ec0b42f04315ed7e403624 Mon Sep 17 00:00:00 2001 From: Dean Rie Date: Thu, 8 Oct 2026 18:25:46 +0300 Subject: [PATCH 1/3] fix(ralph-loop): address hook scripts via CURSOR_PLUGIN_ROOT; quote continual-learning hook path (cherry picked from commit 0cb8152ee6fa537169ab92def5da76c96fa26700) --- continual-learning/hooks/hooks.json | 2 +- ralph-loop/hooks/hooks.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/continual-learning/hooks/hooks.json b/continual-learning/hooks/hooks.json index 11a0f739f..d8856f5dc 100644 --- a/continual-learning/hooks/hooks.json +++ b/continual-learning/hooks/hooks.json @@ -3,7 +3,7 @@ "hooks": { "stop": [ { - "command": "bun run ${CURSOR_PLUGIN_ROOT}/hooks/continual-learning-stop.ts" + "command": "bun run \"${CURSOR_PLUGIN_ROOT}/hooks/continual-learning-stop.ts\"" } ] } diff --git a/ralph-loop/hooks/hooks.json b/ralph-loop/hooks/hooks.json index 04a996689..c007279f0 100644 --- a/ralph-loop/hooks/hooks.json +++ b/ralph-loop/hooks/hooks.json @@ -3,12 +3,12 @@ "hooks": { "afterAgentResponse": [ { - "command": "./hooks/capture-response.sh" + "command": "bash \"${CURSOR_PLUGIN_ROOT}/hooks/capture-response.sh\"" } ], "stop": [ { - "command": "./hooks/stop-hook.sh", + "command": "bash \"${CURSOR_PLUGIN_ROOT}/hooks/stop-hook.sh\"", "loop_limit": null } ] From f2dc6ed257cb620526c1be6fcb249ed2df2a8944 Mon Sep 17 00:00:00 2001 From: Dean Rie Date: Sun, 4 Oct 2026 12:03:52 +0300 Subject: [PATCH 2/3] fix(ralph-loop): fail loudly when jq or perl is missing instead of silently breaking the loop Both hooks hard-depend on jq (and capture-response on perl), but neither checks for them and the README never mentions them. jq is not installed by default on macOS or most Linux distributions, so a fresh install dies on the first turn: the stop hook increments the iteration counter, then hits "jq: command not found" (exit 127) and never emits a followup, so the loop just ends. capture-response fails the same way before it can detect a promise. - stop-hook.sh: check for jq before touching the state file; print a clear line to stderr and exit 0 without advancing the iteration - capture-response.sh: same for jq and perl - guards run only when a loop is active, so unrelated projects see no noise - README: Requirements section (bash, jq, perl) with install hints - tests/prerequisites.test.sh: runs the hooks with a PATH that lacks the tool and checks message, exit code, untouched state, no done flag; all 13 checks pass on this branch and 6 fail against the unguarded hooks - bump 1.0.0 -> 1.0.1 (cherry picked from commit 7cc76e2fc4d960a447ad1e748c40e664148d0d94) --- ralph-loop/.cursor-plugin/plugin.json | 2 +- ralph-loop/README.md | 10 +++ ralph-loop/hooks/capture-response.sh | 8 ++ ralph-loop/hooks/stop-hook.sh | 7 ++ ralph-loop/tests/prerequisites.test.sh | 111 +++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100755 ralph-loop/tests/prerequisites.test.sh diff --git a/ralph-loop/.cursor-plugin/plugin.json b/ralph-loop/.cursor-plugin/plugin.json index 529e02ea7..789e82638 100644 --- a/ralph-loop/.cursor-plugin/plugin.json +++ b/ralph-loop/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "ralph-loop", "displayName": "Ralph Loop", - "version": "1.0.0", + "version": "1.0.1", "description": "Continuous self-referential AI loops for iterative development, implementing the Ralph Wiggum technique. Run the agent in a while-true loop with the same prompt until task completion.", "author": { "name": "Cursor", diff --git a/ralph-loop/README.md b/ralph-loop/README.md index 890e184d7..31db34893 100644 --- a/ralph-loop/README.md +++ b/ralph-loop/README.md @@ -12,6 +12,16 @@ Two hooks drive the loop. An `afterAgentResponse` hook watches each response for /add-plugin ralph-loop ``` +## Requirements + +The hooks are Bash scripts and need `jq` and `perl` on the `PATH` of the shell Cursor uses to run hooks. `perl` ships with macOS and most Linux distributions; `jq` usually does not: + +- macOS: `brew install jq` +- Debian/Ubuntu: `sudo apt install jq` +- Fedora: `sudo dnf install jq` + +If `jq` is missing, the hooks print a `Ralph loop: jq is required` line to the hook output and stop without changing the loop state. + ## Quick start > Start a ralph loop: "Build a REST API for todos. CRUD operations, input validation, tests. Output COMPLETE when done." --completion-promise "COMPLETE" --max-iterations 50 diff --git a/ralph-loop/hooks/capture-response.sh b/ralph-loop/hooks/capture-response.sh index d8da50a2a..69db682ce 100755 --- a/ralph-loop/hooks/capture-response.sh +++ b/ralph-loop/hooks/capture-response.sh @@ -20,6 +20,14 @@ if [[ ! -f "$STATE_FILE" ]]; then exit 0 fi +# jq parses the hook input and perl extracts the promise. Fail loudly, not silently. +for dep in jq perl; do + if ! command -v "$dep" >/dev/null 2>&1; then + echo "Ralph loop: $dep is required but was not found on PATH. Completion promises cannot be detected until it is installed." >&2 + exit 0 + fi +done + # Extract completion promise from state file frontmatter FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE") COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/') diff --git a/ralph-loop/hooks/stop-hook.sh b/ralph-loop/hooks/stop-hook.sh index ba0ba3434..b1553f324 100755 --- a/ralph-loop/hooks/stop-hook.sh +++ b/ralph-loop/hooks/stop-hook.sh @@ -21,6 +21,13 @@ if [[ ! -f "$STATE_FILE" ]]; then exit 0 fi +# jq is required to emit the followup_message. Check before touching the state +# file so a missing dependency never advances the iteration counter. +if ! command -v jq >/dev/null 2>&1; then + echo "Ralph loop: jq is required but was not found on PATH (brew install jq / apt install jq). Loop paused; the state file is untouched." >&2 + exit 0 +fi + # Parse state file frontmatter FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE") ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//') diff --git a/ralph-loop/tests/prerequisites.test.sh b/ralph-loop/tests/prerequisites.test.sh new file mode 100755 index 000000000..323a36a8d --- /dev/null +++ b/ralph-loop/tests/prerequisites.test.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +# Tests for the dependency guards in ralph-loop/hooks/*.sh. +# Standalone (no test runner): bash ralph-loop/tests/prerequisites.test.sh +# +# Each case runs a hook with a PATH that contains the usual coreutils but +# deliberately omits jq (and, for one case, perl), then checks that the hook +# reports the missing tool and leaves the loop state untouched. + +set -uo pipefail + +HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/hooks" +FAILURES=0 + +make_project() { + local dir + dir="$(mktemp -d)" + mkdir -p "$dir/.cursor/ralph" + cat > "$dir/.cursor/ralph/scratchpad.md" <<'EOF' +--- +iteration: 1 +max_iterations: 5 +completion_promise: "ALL TESTS PASS" +--- +do the thing +EOF + echo "$dir" +} + +# Build a PATH with everything the hooks need except the tools named in $@. +make_path_without() { + local bindir + bindir="$(mktemp -d)" + local tool + for tool in bash sh cat sed grep awk mv rm touch mktemp dirname basename jq perl; do + case " $* " in *" $tool "*) continue ;; esac + if command -v "$tool" >/dev/null 2>&1; then + ln -s "$(command -v "$tool")" "$bindir/$tool" + fi + done + echo "$bindir" +} + +iteration_of() { + sed -n 's/^iteration: *//p' "$1/.cursor/ralph/scratchpad.md" +} + +check() { + local name="$1" ok="$2" + if [[ "$ok" == "1" ]]; then + echo "PASS: $name" + else + echo "FAIL: $name" + FAILURES=$((FAILURES + 1)) + fi +} + +# --- stop hook without jq: message, exit 0, iteration not advanced, no followup + +project="$(make_project)" +bindir="$(make_path_without jq)" +stdout="$(echo '{"conversation_id":"x","status":"completed","loop_count":0}' \ + | PATH="$bindir" CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/stop-hook.sh" 2>"$project/stderr")" +status=$? +check "stop-hook without jq exits 0" "$([[ $status -eq 0 ]] && echo 1 || echo 0)" +check "stop-hook without jq names jq on stderr" "$(grep -q 'jq is required' "$project/stderr" && echo 1 || echo 0)" +check "stop-hook without jq emits no followup" "$([[ -z "$stdout" ]] && echo 1 || echo 0)" +check "stop-hook without jq leaves iteration at 1" "$([[ "$(iteration_of "$project")" == "1" ]] && echo 1 || echo 0)" +rm -rf "$project" "$bindir" + +# --- stop hook with jq (control): followup emitted, iteration advanced + +if command -v jq >/dev/null 2>&1; then + project="$(make_project)" + stdout="$(echo '{"conversation_id":"x","status":"completed","loop_count":0}' \ + | CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/stop-hook.sh" 2>/dev/null)" + check "stop-hook with jq emits followup_message" "$(printf '%s' "$stdout" | jq -e '.followup_message | test("iteration 2")' >/dev/null 2>&1 && echo 1 || echo 0)" + check "stop-hook with jq advances iteration to 2" "$([[ "$(iteration_of "$project")" == "2" ]] && echo 1 || echo 0)" + rm -rf "$project" +else + echo "SKIP: control cases need jq on the host" +fi + +# --- capture-response without jq / without perl: message, exit 0, no done flag + +for missing in jq perl; do + project="$(make_project)" + bindir="$(make_path_without "$missing")" + echo '{"text":"ALL TESTS PASS"}' \ + | PATH="$bindir" CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/capture-response.sh" 2>"$project/stderr" + status=$? + check "capture-response without $missing exits 0" "$([[ $status -eq 0 ]] && echo 1 || echo 0)" + check "capture-response without $missing names $missing on stderr" "$(grep -q "$missing is required" "$project/stderr" && echo 1 || echo 0)" + check "capture-response without $missing does not create the done flag" "$([[ ! -f "$project/.cursor/ralph/done" ]] && echo 1 || echo 0)" + rm -rf "$project" "$bindir" +done + +# --- no active loop: guards must stay silent so unrelated projects see no noise + +project="$(mktemp -d)" +bindir="$(make_path_without jq)" +echo '{"conversation_id":"x","status":"completed","loop_count":0}' \ + | PATH="$bindir" CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/stop-hook.sh" 2>"$project/stderr" >/dev/null +check "stop-hook without jq is silent when no loop is active" "$([[ ! -s "$project/stderr" ]] && echo 1 || echo 0)" +rm -rf "$project" "$bindir" + +if [[ "$FAILURES" -gt 0 ]]; then + echo "$FAILURES check(s) failed." + exit 1 +fi +echo "All checks passed." From 5a8098fbac99a67c7eef139e085a2fc866d715fb Mon Sep 17 00:00:00 2001 From: Dean Rie Date: Sun, 4 Oct 2026 12:04:29 +0300 Subject: [PATCH 3/3] docs(continual-learning): document the Bun requirement for the stop hook hooks.json runs the stop hook with `bun run` and the script imports stdin from the "bun" module, but the README never says Bun is needed. Without it every stop event fails with "bun: command not found" (exit 127) and the learning cadence silently never fires. Add a Requirements section with install hints and bump to 1.0.1. (cherry picked from commit 1fc8491a2d1483b1806bab889c188e9e97e73391) --- continual-learning/.cursor-plugin/plugin.json | 2 +- continual-learning/README.md | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/continual-learning/.cursor-plugin/plugin.json b/continual-learning/.cursor-plugin/plugin.json index 3255db70a..09d6fa93f 100644 --- a/continual-learning/.cursor-plugin/plugin.json +++ b/continual-learning/.cursor-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "continual-learning", "displayName": "Continual Learning", - "version": "1.0.0", + "version": "1.0.1", "description": "Incrementally learns durable user preferences and workspace facts from transcript changes and keeps AGENTS.md up to date with plain bullet points.", "author": { "name": "Eric Zakariasson" diff --git a/continual-learning/README.md b/continual-learning/README.md index 410fe9252..1b992644f 100644 --- a/continual-learning/README.md +++ b/continual-learning/README.md @@ -20,6 +20,15 @@ It is designed to avoid noisy rewrites by: /add-plugin continual-learning ``` +## Requirements + +The `stop` hook is a TypeScript script executed with [Bun](https://bun.sh) (`bun run hooks/continual-learning-stop.ts`), so `bun` must be on the `PATH` of the shell Cursor uses to run hooks. Node is not enough: the script imports `stdin` from the `bun` module. + +- macOS / Linux: `curl -fsSL https://bun.sh/install | bash` +- Homebrew: `brew install oven-sh/bun/bun` + +Without Bun the hook exits with `bun: command not found` on every turn and the learning cadence never triggers. Check the hook output in Cursor if `AGENTS.md` is never updated. + ## How it works On eligible `stop` events, the hook may emit a `followup_message` that asks the agent to run the `continual-learning` skill.