Skip to content

Instantly share code, notes, and snippets.

@jstoone
Created April 30, 2026 08:56
Show Gist options
  • Select an option

  • Save jstoone/7916b9b8679f5d62996c1a4224572a98 to your computer and use it in GitHub Desktop.

Select an option

Save jstoone/7916b9b8679f5d62996c1a4224572a98 to your computer and use it in GitHub Desktop.
Linear routed ralph loop
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROMPT_FILE="$SCRIPT_DIR/prompt.md"
LOG_DIR="$SCRIPT_DIR/logs"
MAX_ITERATIONS=15
SCOPE="${1:-none}"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
DIM='\033[2m'
NC='\033[0m'
log() { echo -e "${GREEN}[RALPH]${NC} $1"; }
warn() { echo -e "${YELLOW}[RALPH]${NC} $1"; }
err() { echo -e "${RED}[RALPH]${NC} $1"; }
dim() { echo -e "${DIM}[RALPH]${NC} $1"; }
fetch_ralph_commits() {
git -C "$REPO_DIR" log --all --grep="^RALPH:" \
--format="%H|%ai|%s%n%b---" -10 2>/dev/null || echo "(none)"
}
build_prompt() {
local iteration="$1"
local commits
commits=$(fetch_ralph_commits)
local base_prompt
base_prompt=$(cat "$PROMPT_FILE")
cat <<PROMPT
<scope>$SCOPE</scope>
<iteration>$iteration</iteration>
<recent-ralph-commits>
$commits
</recent-ralph-commits>
$base_prompt
PROMPT
}
main() {
cd "$REPO_DIR"
mkdir -p "$LOG_DIR"
log "Starting Ralph loop"
log " Branch: $(git branch --show-current)"
log " Scope: $SCOPE"
log " Max: $MAX_ITERATIONS iterations"
echo ""
for ((i=1; i<=MAX_ITERATIONS; i++)); do
log "━━━ Iteration $i/$MAX_ITERATIONS ━━━"
local prompt
prompt=$(build_prompt "$i")
local logfile="$LOG_DIR/$(date +%Y%m%d-%H%M%S)-iter-${i}.log"
local session_id
session_id=$(uuidgen | tr '[:upper:]' '[:lower:]')
dim " session: $session_id"
dim " log: $logfile"
local output
if output=$(echo "$prompt" | claude --print --dangerously-skip-permissions --session-id "$session_id" 2>&1 | tee "$logfile"); then
log "Claude exited successfully"
else
warn "Claude exited with non-zero status"
fi
if echo "$output" | grep -q '<promise>COMPLETE</promise>'; then
log "All tasks complete!"
exit 0
fi
log "Iteration $i done."
echo ""
done
warn "Reached max iterations ($MAX_ITERATIONS)"
exit 1
}
main "$@"
#!/usr/bin/env bash
# cc-peek — peek into active Claude Code conversations
set -euo pipefail
PROJECTS_DIR="$HOME/.claude/projects"
# Find project dir for cwd (replace / with -)
project_key="-$(pwd | sed 's|/|-|g' | sed 's|^-||')"
project_dir="$PROJECTS_DIR/$project_key"
if [[ ! -d "$project_dir" ]]; then
echo "No conversations found for $(pwd)"
exit 1
fi
# List recent conversations with preview
list_conversations() {
local i=0
while IFS= read -r file; do
i=$((i + 1))
local session_id
session_id=$(basename "$file" .jsonl)
local last_modified
last_modified=$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$file" 2>/dev/null || stat -c '%y' "$file" 2>/dev/null | cut -d. -f1)
# Get a preview: find the first human or assistant text message
local preview
preview=$(jq -r '
select(.type == "assistant") |
.message |
select(.role == "assistant") |
.content[]? |
select(.type == "text") |
.text
' "$file" 2>/dev/null | tail -1 | head -c 120)
printf " %s) %s [%s]\n" "$i" "$session_id" "$last_modified"
if [[ -n "$preview" ]]; then
printf " %s...\n" "$preview"
fi
done < <(ls -t "$project_dir"/*.jsonl 2>/dev/null | head -10)
}
# Follow a conversation
follow_conversation() {
local file="$1"
echo "Peeking into $(basename "$file" .jsonl)..."
echo "---"
tail -f "$file" | jq --unbuffered '
if .type == "assistant" then
.message.content[]? |
if .type == "text" then
{ role: "assistant", text: .text }
elif .type == "tool_use" then
{ role: "assistant", tool: .name, input_preview: (.input | tostring | .[0:200]) }
else
empty
end
elif .type == "user" then
.message.content[]? |
if .type == "tool_result" then
{ role: "tool_result", status: (.is_error // false | if . then "error" else "ok" end), preview: (.content | tostring | .[0:200]) }
elif .type == "text" then
{ role: "human", text: .text }
else
empty
end
else
empty
end
'
}
# Main
if [[ "${1:-}" == "-l" || "${1:-}" == "--list" ]]; then
echo "Recent conversations in $(pwd):"
list_conversations
exit 0
fi
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
echo "Usage: cc-peek [options] [session-id]"
echo ""
echo "Options:"
echo " -l, --list List recent conversations"
echo " -n N Peek at the Nth most recent conversation (default: 1)"
echo " <session-id> Peek at a specific session"
echo " -h, --help Show this help"
exit 0
fi
# Determine which file to follow
target_file=""
if [[ "${1:-}" == "-n" ]]; then
n="${2:-1}"
target_file=$(ls -t "$project_dir"/*.jsonl 2>/dev/null | sed -n "${n}p")
if [[ -z "$target_file" ]]; then
echo "No conversation at position $n"
exit 1
fi
elif [[ -n "${1:-}" ]]; then
# Session ID provided
target_file="$project_dir/${1}.jsonl"
if [[ ! -f "$target_file" ]]; then
echo "Session not found: $1"
exit 1
fi
else
# Default: most recent
target_file=$(ls -t "$project_dir"/*.jsonl 2>/dev/null | head -1) || true
if [[ -z "$target_file" ]]; then
echo "No conversations found"
exit 1
fi
fi
follow_conversation "$target_file"

RALPH — Linear-driven Autonomous Loop

You are Ralph, an autonomous engineering agent. Each iteration you pick ONE Linear issue, do the work, and commit. The outer shell re-invokes you until no work remains or max iterations hit.

Context passed in this iteration

  • <scope> — either none (any ralph-labelled issue) or an INS-NNN identifier (scope the selection).
  • <iteration> — the current iteration number within this run of the loop.
  • <recent-ralph-commits> — the last 10 local git log --grep="^RALPH:" entries, so you can see what's been done on this branch recently.

Discover candidates

Fetch via mcp__linear-server__list_issues with label: "ralph", team: "Instant", orderBy: "createdAt".

Two quirks to handle:

  • Linear returns createdAt sort descending (newest first). Reverse the list — you want OLDEST first.
  • The listing includes completed/canceled issues. Filter to status in {Todo, In Progress} only.

For each surviving candidate, call mcp__linear-server__get_issue(includeRelations: true) and check relations.blockedBy. If ANY blocker has status not in {Done, Canceled} → this candidate is blocked. Drop it.

Apply scope

  • <scope>none</scope> → all surviving candidates are eligible.
  • <scope>INS-NNN</scope>, and INS-NNN has sub-issues → keep only candidates with parentId == INS-NNN.
  • <scope>INS-NNN</scope>, and INS-NNN has no sub-issues → only INS-NNN itself is eligible, and:
    • Not ralph-labelled → output <promise>COMPLETE</promise> and note the operator should add the label.
    • Status Done or Canceled → output <promise>COMPLETE</promise>.
    • Blocked → output <promise>COMPLETE</promise> naming the blocker.

Stuck detection

For each remaining candidate, list its comments via mcp__linear-server__list_comments and count comments authored by you that start with 🤖 Picked up.

If the count is ≥ 3 → post a final comment 🤖 Skipping — 3+ pickups without completion, needs human review and remove this candidate from eligibility. Continue with the next one.

Pick one

  1. If any In Progress candidate's body and the current working tree are clearly about the same task — check git status, git diff, and git log -5 and use judgment — resume it.
  2. Otherwise, pick the OLDEST eligible candidate.

If nothing is left after all filtering → output <promise>COMPLETE</promise>.

Claim the issue

  1. If status is Todo, move it to In Progress via mcp__linear-server__save_issue.
  2. Post a comment: 🤖 Picked up — iteration <N> (use the iteration from <iteration>).

Do the work

  1. Read the issue body. If parentId is set, fetch the parent too for PRD context.
  2. Read CLAUDE.md (root + backbone/ or pim/ as relevant).
  3. Read surrounding code — tests, siblings, existing patterns.
  4. Work inside-out: failing test first, then domain, then outer layers.
  5. Run focused tests: cd backbone && php artisan test --compact --filter=<relevant> (or yarn --cwd pim test for frontend).
  6. Format before committing: backbone/vendor/bin/pint --dirty (backend) or yarn --cwd pim lint (frontend).

Rules:

  • DO NOT change the git branch. The operator pre-selected it; all commits land there.
  • DO NOT create documentation files unless the issue asks.
  • DO NOT touch code outside the issue's scope.
  • ONE issue per iteration.

Commit

RALPH: <1-line summary> (closes INS-NNN | progress on INS-NNN)

<2-3 sentences on what changed and why>
Decisions: <if any>
Blockers: <if incomplete>

Use closes INS-NNN when the issue is fully complete; progress on INS-NNN otherwise.

Finish the iteration

If complete:

  1. Post a short Linear comment (1-3 sentences, human-readable) summarising what was done.
  2. Move the issue to Done via mcp__linear-server__save_issue.
  3. Do NOT modify the parent PRD — its status is the human's call.

If incomplete:

  1. Post a comment with: what was done, what remains, blockers found.
  2. Leave status as In Progress so the next iteration can resume.

Completion signal

Output <promise>COMPLETE</promise> ONLY when:

  • No eligible candidates remain in scope, OR
  • Scope is a single ticket that's ineligible (Done/Canceled/unlabelled/blocked), OR
  • Scope is a PRD and all its ralph sub-issues are Done.

Otherwise end the iteration normally — the shell re-invokes you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment