diff --git a/.github/workflows/ralph.yml b/.github/workflows/ralph.yml new file mode 100644 index 0000000..00a29ae --- /dev/null +++ b/.github/workflows/ralph.yml @@ -0,0 +1,63 @@ +name: Ralph Loop + +on: + workflow_dispatch: + push: + paths: + - "AGENTS.md" + - "scripts/ralph/**" + +permissions: + contents: write + +jobs: + ralph: + # 198982749 is the Copilot app actor id fallback when RALPH_BOT_ID is unset. + if: ${{ github.event_name == 'workflow_dispatch' || format('{0}', github.actor_id) == vars.RALPH_BOT_ID || github.actor_id == 198982749 }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.RALPH_PAT }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install jq + run: sudo apt-get update && sudo apt-get install -y jq + + - name: Install OpenCode + run: npm install -g @github/opencode + + - name: Validate state files + run: | + jq empty scripts/ralph/prd.json + jq empty scripts/ralph/constraints.json + jq empty scripts/ralph/failure.json + + - name: Ensure guard is executable + run: chmod +x scripts/ralph/guard.sh + + - name: Run OpenCode iteration + run: opencode run --config .opencode/opencode.json + + - name: Run guard + run: bash scripts/ralph/guard.sh scripts/ralph/constraints.json + + - name: Commit and push changes + env: + GIT_AUTHOR_NAME: ralph-bot + GIT_AUTHOR_EMAIL: ralph@example.com + GIT_COMMITTER_NAME: ralph-bot + GIT_COMMITTER_EMAIL: ralph@example.com + run: | + if [[ -z "$(git status --porcelain)" ]]; then + echo "No changes to commit." + exit 0 + fi + git add . + git commit -m "chore: ralph iteration" + git push diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..c51b4d2 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,16 @@ +{ + "workdir": ".", + "agents": [ + { + "id": "ralph", + "agentFile": "AGENTS.md", + "state": { + "prd": "scripts/ralph/prd.json", + "progress": "scripts/ralph/progress.txt", + "constraints": "scripts/ralph/constraints.json", + "failure": "scripts/ralph/failure.json" + }, + "maxIterations": 1 + } + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fc8ffa1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +# Ralph Agents + +- PAUSED: false +- MAX_FAILURE_RETRIES: 3 +- COMMIT_AUTHOR: ralph-bot + +## Loop checklist +1) Load AGENTS.md and scripts/ralph state files. +2) If paused, exit immediately. +3) Pick the first `"status": "todo"` story in prd.json and mark it doing before work. +4) Keep one story per run and make the smallest possible diff. +5) Run scripts/ralph/guard.sh before committing; never bypass it. +6) Update progress.txt with a short learning when a story is marked done. +7) If failures exceed MAX_FAILURE_RETRIES, set PAUSED: true and push state. + +## Working files +- scripts/ralph/prd.json - story queue (todo/doing/done) +- scripts/ralph/progress.txt - chronological learnings +- scripts/ralph/constraints.json - guard limits +- scripts/ralph/failure.json - consecutive failure bookkeeping + +## Footguns +- Do not touch build artifacts (dist, node_modules, .svelte-kit, build). +- Avoid changing more than 25 files or 400 total lines per iteration. +- Guard must pass before any commit or push. diff --git a/README.md b/README.md index 29e1293..55a472e 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,16 @@ npx create-remote-app my-app Then follow the setup guide in your new project's README. +## Ralph Loop Automation + +This repo includes a minimal Ralph loop to run one story at a time. + +1. Add a repo-scoped PAT secret named `RALPH_PAT` (contents + workflow). +2. Trigger the **Ralph Loop** workflow manually, or push changes to `AGENTS.md` or `scripts/ralph/**` (only bot pushes auto-run). +3. The loop reads `AGENTS.md` plus `scripts/ralph/{prd.json,progress.txt,constraints.json,failure.json}`, runs `scripts/ralph/guard.sh`, and commits if the guard passes. + +State files live under `scripts/ralph/` and `.opencode/opencode.json` wires them into OpenCode. + ## What This Repo Shows This project demonstrates: @@ -213,4 +223,4 @@ This is a pragmatic starting point for projects needing authenticated persistent ## Requirements - Node.js + Bun -- Cloudflare account (for deployment) \ No newline at end of file +- Cloudflare account (for deployment) diff --git a/scripts/ralph/constraints.json b/scripts/ralph/constraints.json new file mode 100644 index 0000000..9a1dfcd --- /dev/null +++ b/scripts/ralph/constraints.json @@ -0,0 +1,7 @@ +{ + "maxFilesChanged": 25, + "maxLinesChanged": 400, + "forbiddenPaths": ["node_modules", "dist", "build", ".svelte-kit", ".next"], + "requireProgressUpdate": true, + "maxFailureRetries": 3 +} diff --git a/scripts/ralph/failure.json b/scripts/ralph/failure.json new file mode 100644 index 0000000..c5a87e9 --- /dev/null +++ b/scripts/ralph/failure.json @@ -0,0 +1,6 @@ +{ + "consecutiveFailures": 0, + "lastRunUrl": "", + "lastError": "", + "lastTimestamp": "" +} diff --git a/scripts/ralph/guard.sh b/scripts/ralph/guard.sh new file mode 100755 index 0000000..24d407f --- /dev/null +++ b/scripts/ralph/guard.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -euo pipefail + +constraints_file="${1:-}" + +if [[ -z "${constraints_file}" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +if ! command -v jq >/dev/null 2>&1; then + echo "jq is required for guard checks" >&2 + exit 1 +fi + +if [[ ! -f "${constraints_file}" ]]; then + echo "Constraints file not found: ${constraints_file}" >&2 + exit 1 +fi + +if grep -Eq "^[[:space:]]*-[[:space:]]*PAUSED:[[:space:]]*true" AGENTS.md; then + echo "Guard blocked: AGENTS.md is paused" >&2 + exit 1 +fi + +max_files=$(jq -r '.maxFilesChanged // 0' "${constraints_file}") +max_lines=$(jq -r '.maxLinesChanged // 0' "${constraints_file}") +require_progress=$(jq -r '.requireProgressUpdate // false' "${constraints_file}") +readarray -t forbidden_paths < <(jq -r '.forbiddenPaths[]?' "${constraints_file}") +readarray -t changed_list < <(git diff HEAD --name-only) + +if [[ ${#changed_list[@]} -eq 0 ]]; then + echo "Guard note: no changes detected; nothing to validate." + exit 0 +fi + +file_count=${#changed_list[@]} +if [[ "${max_files}" -gt 0 && "${file_count}" -gt "${max_files}" ]]; then + echo "Guard failed: ${file_count} files changed (max ${max_files})." >&2 + exit 1 +fi + +line_total=$(git diff HEAD --numstat | awk '{add+=$1; del+=$2} END {total=add+del; if (NR==0) print 0; else print total}') +if [[ "${max_lines}" -gt 0 && "${line_total}" -gt "${max_lines}" ]]; then + echo "Guard failed: ${line_total} total line changes (max ${max_lines})." >&2 + exit 1 +fi + +if [[ ${#forbidden_paths[@]} -gt 0 ]]; then + for file in "${changed_list[@]}"; do + for path in "${forbidden_paths[@]}"; do + [[ -z "${path}" ]] && continue + case "${file}" in + "${path}"|"${path}"/*) + echo "Guard failed: forbidden path touched (${path})." >&2 + exit 1 + ;; + esac + done + done +fi + +if [[ "${require_progress}" == "true" ]]; then + prd_changed=0 + progress_changed=0 + for file in "${changed_list[@]}"; do + [[ "${file}" == "scripts/ralph/prd.json" ]] && prd_changed=$((prd_changed+1)) + [[ "${file}" == "scripts/ralph/progress.txt" ]] && progress_changed=$((progress_changed+1)) + done + if [[ "${prd_changed}" -gt 0 && "${progress_changed}" -eq 0 ]]; then + echo "Guard failed: prd.json changed without updating progress.txt." >&2 + exit 1 + fi +fi + +echo "Guard passed: constraints satisfied." diff --git a/scripts/ralph/prd.json b/scripts/ralph/prd.json new file mode 100644 index 0000000..ab9ad49 --- /dev/null +++ b/scripts/ralph/prd.json @@ -0,0 +1,32 @@ +[ + { + "id": "S1", + "title": "Boot Ralph automation scaffold", + "status": "todo", + "acceptanceCriteria": [ + "Required Ralph state files and OpenCode config exist in the repo", + "Guard script is available for later iterations" + ], + "notes": "" + }, + { + "id": "S2", + "title": "Add/verify diff guard in CI", + "status": "todo", + "acceptanceCriteria": [ + "Guard runs in the Ralph workflow", + "Guard fails on forbidden paths or oversized diffs" + ], + "notes": "" + }, + { + "id": "S3", + "title": "Update README with usage instructions", + "status": "todo", + "acceptanceCriteria": [ + "README documents the Ralph loop triggers", + "README lists required secrets and state files" + ], + "notes": "" + } +] diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt new file mode 100644 index 0000000..7a6a0d2 --- /dev/null +++ b/scripts/ralph/progress.txt @@ -0,0 +1,3 @@ +# Ralph Progress +- Initialized scaffold and baseline state. +- Clarified story naming and guard behavior.