diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index eedc4e946..e87f716ef 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -28,18 +28,6 @@ }, "category": "Productivity" }, - { - "name": "ralph", - "source": { - "source": "local", - "path": "./plugins/ralph" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Productivity" - }, { "name": "datadog", "source": { diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8e146219a..2e59d6e75 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -37,20 +37,6 @@ ], "version": "17.27.3" }, - { - "name": "ralph", - "description": "PRD-driven persistence loop — keeps Claude working until all user stories pass", - "source": "./plugins/ralph", - "category": "development", - "tags": [ - "ralph", - "loop", - "persistence", - "prd", - "automation" - ], - "version": "17.27.3" - }, { "name": "datadog", "description": "Datadog observability skills for Claude Code via the pup CLI", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f03170d84..0a3319f3f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,9 +5,9 @@ repos: - id: check-yaml - id: check-json - id: end-of-file-fixer - exclude: ^(tests/|\.ralph/) + exclude: ^tests/ - id: trailing-whitespace - exclude: ^(tests/|\.ralph/) + exclude: ^tests/ - repo: local hooks: diff --git a/README.md b/README.md index 6928d3fe1..a09a39969 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,6 @@ bstack/ │ ├── me/ # Core personal workflow plugin │ │ ├── hooks/ # Session hooks (git guard, handoff, LSP checks) │ │ └── skills/ # Personal skills -│ ├── ralph/ # Ralph Loop plugin -│ │ ├── hooks/ # Ralph persistence hooks -│ │ └── skills/ # ralph, ralph-cancel │ ├── jira/ # Jira integration plugin │ │ └── skills/ # Jira workflow skills │ ├── core/ # Shared agent definitions @@ -86,13 +83,6 @@ bstack/ |-------|-------------| | `autoresearch` | git 추적 실험으로 metric을 반복 최적화하는 자율 실험 루프 | -#### `ralph` plugin - -| Skill | Description | -|-------|-------------| -| `ralph` | PRD 기반 자동 반복 개발 루프 실행 | -| `ralph-cancel` | 실행 중인 Ralph 루프 취소 | - #### `jira` plugin | Skill | Description | diff --git a/plugins/me/skills/setup/settings.json b/plugins/me/skills/setup/settings.json index 9cef897ca..97c27b707 100644 --- a/plugins/me/skills/setup/settings.json +++ b/plugins/me/skills/setup/settings.json @@ -46,7 +46,6 @@ "bstack@bstack": true, "core@bstack": true, "jira@bstack": true, - "ralph@bstack": true, "ks-amplitude@search-claude-code-plugins": true, "ks-databricks@search-claude-code-plugins": true, "ks-git-guard@search-claude-code-plugins": true, diff --git a/plugins/ralph/.claude-plugin/plugin.json b/plugins/ralph/.claude-plugin/plugin.json deleted file mode 100644 index 5d1a47185..000000000 --- a/plugins/ralph/.claude-plugin/plugin.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "ralph", - "version": "17.27.3", - "description": "PRD-driven persistence loop — keeps Claude working until all user stories pass", - "author": { - "name": "baleen37", - "email": "git@baleen.me" - }, - "license": "MIT", - "keywords": [ - "ralph", - "loop", - "persistence", - "prd", - "automation" - ] -} diff --git a/plugins/ralph/.codex-plugin/plugin.json b/plugins/ralph/.codex-plugin/plugin.json deleted file mode 100644 index fee3c3517..000000000 --- a/plugins/ralph/.codex-plugin/plugin.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "ralph", - "version": "17.27.3", - "description": "PRD-driven persistence loop — keeps Claude working until all user stories pass", - "author": { - "name": "baleen37", - "email": "git@baleen.me" - }, - "license": "MIT", - "keywords": [ - "ralph", - "loop", - "persistence", - "prd", - "automation" - ], - "skills": "./skills/" -} diff --git a/plugins/ralph/hooks/hooks.json b/plugins/ralph/hooks/hooks.json deleted file mode 100644 index 037bf7994..000000000 --- a/plugins/ralph/hooks/hooks.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "hooks": { - "Stop": [ - { - "matcher": "*", - "hooks": [ - { - "type": "command", - "command": "bun run \"${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}/hooks/ralph-persist.ts\"", - "timeout": 10 - } - ] - } - ] - } -} diff --git a/plugins/ralph/hooks/ralph-persist.ts b/plugins/ralph/hooks/ralph-persist.ts deleted file mode 100644 index 8cf006b60..000000000 --- a/plugins/ralph/hooks/ralph-persist.ts +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bun -import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs"; -import { join } from "path"; - -const STALE_MS = 7_200_000; // 2h - -function allow(): never { process.exit(0); } -function block(i: number, m: number, p: string, cwd: string): never { - let progress = ""; - try { - const prd = JSON.parse(readFileSync(join(cwd, ".ralph", "prd.json"), "utf8")); - const stories = prd.userStories || []; - const done = stories.filter((s: any) => s.passes).length; - progress = ` (${done}/${stories.length} stories done)`; - } catch {} - let lastFail = ""; - try { - const lines = readFileSync(join(cwd, ".ralph", "progress.txt"), "utf8").trim().split("\n"); - lastFail = ` Last: ${lines[lines.length - 1]}`; - } catch {} - process.stdout.write(JSON.stringify({ decision: "block", reason: `[RALPH ${i}/${m}]${progress}${lastFail} Continue: ${p}` })); - process.exit(0); -} - -async function main() { - const input = JSON.parse(await Bun.stdin.text() || "{}"); - const cwd = input.cwd ?? process.cwd(); - const sid = input.session_id ?? ""; - const dir = join(cwd, ".ralph", "state"); - const sp = join(dir, "ralph-state.json"); - const save = (s: any) => writeFileSync(sp, JSON.stringify(s, null, 2)); - const off = (s: any) => { s.active = false; save(s); allow(); }; - - const sentinel = join(dir, "ralph-activating"); - if (existsSync(sentinel) && !existsSync(sp)) { - const p = readFileSync(sentinel, "utf8").trim(); - unlinkSync(sentinel); - const now = new Date().toISOString(); - mkdirSync(dir, { recursive: true }); - save({ active: true, session_id: sid, iteration: 1, max_iterations: 10, started_at: now, last_checked_at: now, prompt: p }); - block(1, 10, p, cwd); - } - - let s: any; - try { s = JSON.parse(readFileSync(sp, "utf8")); } catch { allow(); } - if (!s?.active) allow(); - if (s.session_id && sid && s.session_id !== sid) allow(); - if (existsSync(join(dir, "cancel-signal-state.json"))) { unlinkSync(join(dir, "cancel-signal-state.json")); off(s); } - if (Date.now() - new Date(s.last_checked_at).getTime() > STALE_MS) off(s); - - if (s.iteration >= s.max_iterations) s.max_iterations += 10; - s.iteration += 1; - s.last_checked_at = new Date().toISOString(); - save(s); - block(s.iteration, s.max_iterations, s.prompt, cwd); -} - -main().catch((e) => { process.stderr.write(`ralph-persist: ${e}\n`); process.exit(0); }); diff --git a/plugins/ralph/skills/ralph-cancel/SKILL.md b/plugins/ralph/skills/ralph-cancel/SKILL.md deleted file mode 100644 index 867fa8ca2..000000000 --- a/plugins/ralph/skills/ralph-cancel/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: ralph-cancel -description: Cancel an active Ralph loop — cleans up state files and exits the persistence loop ---- - -# Ralph Cancel - -Cancel the Ralph loop in the current project (CWD). - -1. If `.ralph/state/ralph-state.json` missing → "No active Ralph loop found." -2. If `active: false` → "Ralph loop is already inactive." -3. Write `.ralph/state/cancel-signal-state.json` (`{}`) — the Stop hook detects this and exits -4. Set `active: false` in `.ralph/state/ralph-state.json` -5. Report: "Ralph loop cancelled." diff --git a/plugins/ralph/skills/ralph/SKILL.md b/plugins/ralph/skills/ralph/SKILL.md deleted file mode 100644 index 9bfa6a535..000000000 --- a/plugins/ralph/skills/ralph/SKILL.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: ralph -description: PRD-driven persistence loop — keeps Claude working until all user stories pass ---- - -# Ralph Loop - -Stop hook keeps you running until you write the cancel signal. - -## Activation -1. `mkdir -p .ralph/state/` -2. Write `.ralph/state/ralph-activating` with the task description - -## PRD (`--no-prd` → auto-generate single-story prd) -Create `.ralph/prd.json`: `project`, `userStories[]` with `id`, `title`, `acceptanceCriteria[]`, `priority`, `passes:false`. Keep stories small and dependency-ordered. - -## Loop -1. Read `.ralph/progress.txt` if it exists -2. Find highest-priority `passes:false` story -3. All stories pass → go to Done -4. Implement one story at a time, run tests -5. Pass → set `passes:true`. Fail → append `[ITER N] US-XXX: ` to `.ralph/progress.txt` - -## Done -1. Review code quality (`--critic=none` to skip) -2. Remove slop: unnecessary comments, dead code, over-abstractions (`--no-deslop` to skip) -3. Run full test suite — must pass -4. Write `.ralph/state/cancel-signal-state.json` (`{}`) -5. Reply `COMPLETE` diff --git a/tests/codex_marketplace_json.bats b/tests/codex_marketplace_json.bats index 75d968330..ae7c3923f 100644 --- a/tests/codex_marketplace_json.bats +++ b/tests/codex_marketplace_json.bats @@ -36,7 +36,7 @@ setup() { @test "codex marketplace plugin entries use relative local paths and default policy" { local plugin_count plugin_count=$(jq -r '.plugins | length' "$MARKETPLACE_JSON") - [ "$plugin_count" -eq 5 ] + [ "$plugin_count" -eq 4 ] jq -e ' .plugins diff --git a/tests/ralph_hooks_json.bats b/tests/ralph_hooks_json.bats deleted file mode 100644 index 8fcd11862..000000000 --- a/tests/ralph_hooks_json.bats +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bats -# Test: ralph plugin hooks.json validation - -load helpers/bats_helper - -HOOKS_JSON="${PROJECT_ROOT}/plugins/ralph/hooks/hooks.json" - -setup() { - ensure_jq -} - -@test "ralph hooks.json is valid JSON" { - [ -f "$HOOKS_JSON" ] || skip "hooks.json not found" - validate_json "$HOOKS_JSON" -} - -@test "ralph hooks.json has Stop hook" { - [ -f "$HOOKS_JSON" ] || skip "hooks.json not found" - local has_stop - has_stop=$($JQ_BIN -e '.hooks.Stop' "$HOOKS_JSON") - [ -n "$has_stop" ] -} - -@test "ralph hooks.json only has Codex-supported top-level fields" { - [ -f "$HOOKS_JSON" ] || skip "hooks.json not found" - - local unsupported_fields - unsupported_fields=$($JQ_BIN -r 'keys - ["hooks"] | .[]' "$HOOKS_JSON") - [ -z "$unsupported_fields" ] -} - -@test "ralph hooks.json Stop hook uses plugin-root fallback" { - [ -f "$HOOKS_JSON" ] || skip "hooks.json not found" - local command - command=$($JQ_BIN -r '.hooks.Stop[0].hooks[0].command' "$HOOKS_JSON") - [[ "$command" == *'${PLUGIN_ROOT:-$CLAUDE_PLUGIN_ROOT}'* ]] -} diff --git a/tests/ralph_persist.bats b/tests/ralph_persist.bats deleted file mode 100644 index 6ea666132..000000000 --- a/tests/ralph_persist.bats +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bats -# Test: ralph-persist.ts Stop hook engine - -load helpers/bats_helper - -HOOK_SCRIPT="${PROJECT_ROOT}/plugins/ralph/hooks/ralph-persist.ts" -RALPH_DIR="" - -setup() { - RALPH_DIR="$(mktemp -d)/project" - mkdir -p "${RALPH_DIR}/.ralph/state" -} - -teardown() { - rm -rf "$(dirname "$RALPH_DIR")" -} - -# Helper: invoke the hook with given session_id and cwd -invoke_hook() { - local session_id="${1:-test-session}" - local cwd="${2:-$RALPH_DIR}" - echo "{\"session_id\": \"$session_id\", \"cwd\": \"$cwd\", \"hook_event_name\": \"Stop\"}" \ - | bun run "$HOOK_SCRIPT" -} - -# Helper: write state file -write_state() { - local state="$1" - echo "$state" > "${RALPH_DIR}/.ralph/state/ralph-state.json" -} - -@test "no state file: writes nothing and exits 0" { - run invoke_hook - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -@test "active false: writes nothing and exits 0" { - write_state '{"active":false,"session_id":"test-session","iteration":1,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z"}' - run invoke_hook - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -@test "session_id mismatch: writes nothing and exits 0" { - write_state '{"active":true,"session_id":"session-A","iteration":1,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z"}' - run invoke_hook "session-B" - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -@test "cancel signal exists: writes nothing, deletes signal, sets active false" { - write_state '{"active":true,"session_id":"test-session","iteration":1,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z"}' - touch "${RALPH_DIR}/.ralph/state/cancel-signal-state.json" - - run invoke_hook - [ "$status" -eq 0 ] - [ -z "$output" ] - [ ! -f "${RALPH_DIR}/.ralph/state/cancel-signal-state.json" ] - local active - active=$($JQ_BIN -r '.active' "${RALPH_DIR}/.ralph/state/ralph-state.json") - [ "$active" = "false" ] -} - -@test "stale state (>2h): writes nothing, sets active false" { - local stale_time - stale_time=$(date -u -v-3H '+%Y-%m-%dT%H:%M:%S.000Z' 2>/dev/null || date -u -d '3 hours ago' '+%Y-%m-%dT%H:%M:%S.000Z') - write_state "{\"active\":true,\"session_id\":\"test-session\",\"iteration\":1,\"max_iterations\":100,\"last_checked_at\":\"$stale_time\"}" - - run invoke_hook - [ "$status" -eq 0 ] - [ -z "$output" ] - local active - active=$($JQ_BIN -r '.active' "${RALPH_DIR}/.ralph/state/ralph-state.json") - [ "$active" = "false" ] -} - -@test "active state with matching session: returns decision block and increments iteration" { - write_state '{"active":true,"session_id":"test-session","iteration":2,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z","prompt":"build todo API"}' - - run invoke_hook "test-session" - [ "$status" -eq 0 ] - local decision - decision=$($JQ_BIN -r '.decision' <<< "$output") - [ "$decision" = "block" ] - local iteration - iteration=$($JQ_BIN -r '.iteration' "${RALPH_DIR}/.ralph/state/ralph-state.json") - [ "$iteration" = "3" ] -} - -@test "max_iterations reached: extends by 10 and blocks" { - write_state '{"active":true,"session_id":"test-session","iteration":100,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z","prompt":"build todo API"}' - - run invoke_hook "test-session" - [ "$status" -eq 0 ] - local decision - decision=$($JQ_BIN -r '.decision' <<< "$output") - [ "$decision" = "block" ] - local max_iter - max_iter=$($JQ_BIN -r '.max_iterations' "${RALPH_DIR}/.ralph/state/ralph-state.json") - [ "$max_iter" = "110" ] -} - -@test "happy path: full loop lifecycle" { - # Setup: active state - write_state '{"active":true,"session_id":"test-session","iteration":1,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z","prompt":"build todo API"}' - - # iteration 1 → block, becomes 2 - run invoke_hook "test-session" - [ "$status" -eq 0 ] - local decision - decision=$($JQ_BIN -r '.decision' <<< "$output") - [ "$decision" = "block" ] - - # iteration 2 → block, becomes 3 - run invoke_hook "test-session" - [ "$status" -eq 0 ] - decision=$($JQ_BIN -r '.decision' <<< "$output") - [ "$decision" = "block" ] - - # Create cancel signal - touch "${RALPH_DIR}/.ralph/state/cancel-signal-state.json" - - # Next stop → allow, cancel signal deleted, active false - run invoke_hook "test-session" - [ "$status" -eq 0 ] - [ -z "$output" ] - [ ! -f "${RALPH_DIR}/.ralph/state/cancel-signal-state.json" ] - local active - active=$($JQ_BIN -r '.active' "${RALPH_DIR}/.ralph/state/ralph-state.json") - [ "$active" = "false" ] -} - -@test "session isolation: orphaned state is ignored" { - write_state '{"active":true,"session_id":"session-A","iteration":1,"max_iterations":100,"last_checked_at":"2099-01-01T00:00:00.000Z","prompt":"build todo API"}' - - run invoke_hook "session-B" - [ "$status" -eq 0 ] - [ -z "$output" ] -} - -@test "activation sentinel: creates state and blocks on first stop" { - mkdir -p "${RALPH_DIR}/.ralph/state" - echo "build todo API" > "${RALPH_DIR}/.ralph/state/ralph-activating" - - run invoke_hook "test-session" - [ "$status" -eq 0 ] - local decision - decision=$($JQ_BIN -r '.decision' <<< "$output") - [ "$decision" = "block" ] - [ -f "${RALPH_DIR}/.ralph/state/ralph-state.json" ] - [ ! -f "${RALPH_DIR}/.ralph/state/ralph-activating" ] -}