Files
Bo 62cc3b6ee0 Clear the operations-layer alignment residuals (#1054)
Closes out the six residual items #1051 disclosed: terminology residue
on non-authority surfaces, the eval command-surface fixture that failed
when executed (#{3,4} -> #{3,5}), the vacuous retrieval-quality canary
and the nightly job that ran it, the consumer-free dream config block
and its exclusive helpers, the remaining knowledge-shaped writers moved
to the scratch tier, and the MEMORY.md consumer audit.

bin/ralph still resumes legacy .agents/ralph/ checkpoints so the
documented backwards-compat contract holds without a migration; both
paths and the outside-both refusal are now tested.

Fresh author-distinct validation returned PASS with empty not_checked,
after an earlier revision failed on a dangling nightly invoker and a
back-compat test regression that were fixed and independently
re-verified.

Test-Removal-Reason: the dream config subsystem was deleted with its tests (operations-layer residuals)
2026-08-08 14:34:14 -04:00

353 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# ralph — Goal → Full RPI → PR
# "Ralph Wiggum" pattern: each phase gets a fresh context window.
#
# Usage:
# ralph "Add dark mode support"
# ralph --skip-premortem "Fix typo in README"
# ralph --branch feat/dark-mode "Add dark mode"
# ralph --spec spec.md "Add dark mode"
# ralph --dry-run "Add dark mode"
# ralph --resume .agents/scratch/ralph/<slug>.checkpoint
# ralph --phase-timeout 900 "Big feature"
set -euo pipefail
# ── Phase order (for checkpoint skip logic) ───────────────────────────────────
PHASES=(plan premortem branch crank vibe postmortem pr)
# ── Defaults ──────────────────────────────────────────────────────────────────
SKIP_PRE_MORTEM=false
DRY_RUN=false
BRANCH=""
GOAL=""
RESUME_FILE=""
RESUME_AFTER=""
SPEC_FILE=""
SPEC_CONTENT=""
PHASE_TIMEOUT=600
CODEX_BIN="${CODEX_BIN:-codex}"
# ── Parse args ────────────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do
case "$1" in
--skip-premortem) SKIP_PRE_MORTEM=true; shift ;;
--branch) BRANCH="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
--resume) RESUME_FILE="$2"; shift 2 ;;
--spec) SPEC_FILE="$2"; shift 2 ;;
--max-budget)
echo "ralph: --max-budget is no longer supported: Codex subscription execution cannot enforce dollar budgets." >&2
echo "Use --phase-timeout SECONDS for an enforceable wall-clock bound per phase." >&2
exit 2
;;
--phase-timeout) PHASE_TIMEOUT="$2"; shift 2 ;;
--help|-h)
sed -n '2,13p' "$0"
exit 0
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
GOAL="$1"; shift
;;
esac
done
# ── Resume from checkpoint ────────────────────────────────────────────────────
if [[ -n "$RESUME_FILE" ]]; then
if [[ ! -f "$RESUME_FILE" ]]; then
echo "Checkpoint file not found: $RESUME_FILE" >&2
exit 1
fi
# Validate the checkpoint lives in a ralph checkpoint dir and has the
# expected header. New checkpoints are written under .agents/scratch/ralph/;
# the legacy .agents/ralph/ path stays resumable so checkpoints written
# before the scratch-tier move still work (no migration required).
case "$RESUME_FILE" in
.agents/scratch/ralph/*.checkpoint|*/.agents/scratch/ralph/*.checkpoint) ;;
.agents/ralph/*.checkpoint|*/.agents/ralph/*.checkpoint) ;;
*) echo "Refusing to source checkpoint outside .agents/scratch/ralph/ (or the legacy .agents/ralph/): $RESUME_FILE" >&2; exit 1 ;;
esac
if ! head -1 "$RESUME_FILE" | grep -q '^# Ralph checkpoint'; then
echo "Invalid checkpoint file (missing header): $RESUME_FILE" >&2
exit 1
fi
# shellcheck source=/dev/null
source "$RESUME_FILE"
# LAST_PHASE is set by the checkpoint file
RESUME_AFTER="${LAST_PHASE:-}"
echo "Resuming after phase: $RESUME_AFTER"
fi
if [[ -z "$GOAL" ]]; then
echo "Usage: ralph [options] \"<goal>\"" >&2
exit 1
fi
# ── Load spec ─────────────────────────────────────────────────────────────────
if [[ -n "$SPEC_FILE" ]]; then
if [[ ! -f "$SPEC_FILE" ]]; then
echo "Spec file not found: $SPEC_FILE" >&2
exit 1
fi
SPEC_CONTENT=$(cat "$SPEC_FILE")
fi
# ── Derived values ────────────────────────────────────────────────────────────
BRANCH="${BRANCH:-feat/ralph-$(date +%s)}"
BASE_BRANCH="${BASE_BRANCH:-$(git branch --show-current)}"
WORKDIR="${WORKDIR:-$(pwd)}"
SLUG="${SLUG:-$(echo "$GOAL" | tr ' ' '-' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]//g' | head -c 40)}"
RALPH_DIR=".agents/scratch/ralph"
RALPH_LOG="${RALPH_LOG:-$RALPH_DIR/$(date +%Y-%m-%d)-$SLUG.log}"
CHECKPOINT_FILE="$RALPH_DIR/$SLUG.checkpoint"
mkdir -p "$RALPH_DIR"
# ── Helpers ───────────────────────────────────────────────────────────────────
log() { echo "=== $1 ===" | tee -a "$RALPH_LOG"; }
die() { echo "FATAL: $1" | tee -a "$RALPH_LOG" >&2; exit 1; }
should_skip() {
local phase="$1"
if [[ -z "$RESUME_AFTER" ]]; then
return 1 # no resume, don't skip
fi
for p in "${PHASES[@]}"; do
if [[ "$p" == "$phase" ]]; then
return 0 # haven't passed RESUME_AFTER yet, skip
fi
if [[ "$p" == "$RESUME_AFTER" ]]; then
return 1 # passed RESUME_AFTER, run this and all subsequent
fi
done
return 1
}
save_checkpoint() {
local last_phase="$1"
cat > "$CHECKPOINT_FILE" <<CKPT
# Ralph checkpoint — source this to resume
GOAL=$(printf '%q' "$GOAL")
BRANCH=$(printf '%q' "$BRANCH")
BASE_BRANCH=$(printf '%q' "$BASE_BRANCH")
WORKDIR=$(printf '%q' "$WORKDIR")
SLUG=$(printf '%q' "$SLUG")
SKIP_PRE_MORTEM=$(printf '%q' "$SKIP_PRE_MORTEM")
SPEC_FILE=$(printf '%q' "$SPEC_FILE")
RALPH_LOG=$(printf '%q' "$RALPH_LOG")
LAST_PHASE=$(printf '%q' "$last_phase")
CKPT
echo "Checkpoint saved: $CHECKPOINT_FILE" | tee -a "$RALPH_LOG"
}
spec_context() {
if [[ -n "$SPEC_CONTENT" ]]; then
printf '\n\nThe acceptance spec is:\n%s' "$SPEC_CONTENT"
fi
}
read_verdict() {
local file="$1"
if [[ -f "$file" ]]; then
cat "$file"
else
echo "UNKNOWN"
fi
}
run_phase() {
local phase_name="$1"
local prompt="$2"
if should_skip "$phase_name"; then
log "Skipping $phase_name (resuming)"
return 0
fi
log "Phase: $phase_name"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[dry-run] Would run codex exec with prompt:" | tee -a "$RALPH_LOG"
echo " $prompt" | tee -a "$RALPH_LOG"
echo "" | tee -a "$RALPH_LOG"
save_checkpoint "$phase_name"
return 0
fi
if ! command -v "$CODEX_BIN" >/dev/null 2>&1; then
die "Codex runtime not found: $CODEX_BIN"
fi
set +e
timeout "$PHASE_TIMEOUT" "$CODEX_BIN" exec \
--full-auto \
-C "$WORKDIR" \
"$prompt" 2>&1 | tee -a "$RALPH_LOG"
local exit_code=${PIPESTATUS[0]}
set -e
if [[ $exit_code -eq 124 ]]; then
save_checkpoint "$phase_name"
die "Phase $phase_name timed out after ${PHASE_TIMEOUT}s (gutter detected). Resume with: ralph --resume $CHECKPOINT_FILE"
fi
if [[ $exit_code -ne 0 ]]; then
save_checkpoint "$phase_name"
die "Phase $phase_name failed (exit $exit_code). Resume with: ralph --resume $CHECKPOINT_FILE"
fi
save_checkpoint "$phase_name"
}
# ── Banner ────────────────────────────────────────────────────────────────────
echo "" | tee -a "$RALPH_LOG"
log "Ralph Loop starting"
echo " Goal: $GOAL" | tee -a "$RALPH_LOG"
echo " Branch: $BRANCH" | tee -a "$RALPH_LOG"
echo " Base: $BASE_BRANCH" | tee -a "$RALPH_LOG"
echo " Skip PM: $SKIP_PRE_MORTEM" | tee -a "$RALPH_LOG"
echo " Dry run: $DRY_RUN" | tee -a "$RALPH_LOG"
echo " Spec: ${SPEC_FILE:-(none)}" | tee -a "$RALPH_LOG"
echo " Timeout: ${PHASE_TIMEOUT}s" | tee -a "$RALPH_LOG"
echo " Resume: ${RESUME_AFTER:-(fresh)}" | tee -a "$RALPH_LOG"
echo " Log: $RALPH_LOG" | tee -a "$RALPH_LOG"
echo "" | tee -a "$RALPH_LOG"
# ── Phase 1: Plan ─────────────────────────────────────────────────────────────
run_phase "plan" \
"You are working in $WORKDIR. The goal is: $GOAL$(spec_context)
Run /plan to decompose this goal into trackable issues.
Create a plan document under .agents/plans/.
When done, write the path to the plan file into $RALPH_DIR/plan-path.txt (just the path, nothing else)."
PLAN_PATH="$RALPH_DIR/plan-path.txt"
# ── Phase 2: Pre-mortem (optional) ────────────────────────────────────────────
if [[ "$SKIP_PRE_MORTEM" == "true" ]]; then
if ! should_skip "premortem"; then
log "Skipping premortem (--skip-premortem)"
echo "SKIP" > "$RALPH_DIR/premortem-verdict.txt"
save_checkpoint "premortem"
fi
else
PLAN_FILE=""
if [[ -f "$PLAN_PATH" ]]; then
PLAN_FILE=$(cat "$PLAN_PATH")
fi
run_phase "premortem" \
"You are working in $WORKDIR.
Run /premortem on the plan${PLAN_FILE:+ at $PLAN_FILE}.$(spec_context)
After the council finishes, write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/premortem-verdict.txt."
if ! should_skip "premortem"; then
PRE_MORTEM=$(read_verdict "$RALPH_DIR/premortem-verdict.txt")
if [[ "$PRE_MORTEM" == "FAIL" ]]; then
die "Pre-mortem verdict: FAIL — aborting. See $RALPH_LOG for details."
fi
log "Pre-mortem verdict: $PRE_MORTEM"
fi
fi
# ── Create branch ─────────────────────────────────────────────────────────────
if ! should_skip "branch"; then
if [[ "$DRY_RUN" != "true" ]]; then
log "Creating branch: $BRANCH"
git checkout -b "$BRANCH" 2>/dev/null || git checkout "$BRANCH"
fi
save_checkpoint "branch"
fi
# ── Phase 3: Crank ────────────────────────────────────────────────────────────
PLAN_FILE=""
if [[ -f "$PLAN_PATH" ]]; then
PLAN_FILE=$(cat "$PLAN_PATH")
fi
run_phase "crank" \
"You are working in $WORKDIR on branch $BRANCH. The goal is: $GOAL.
${PLAN_FILE:+The plan is at $PLAN_FILE.}$(spec_context)
Run /crank to implement all planned work. Commit all changes when done."
# ── Phase 4: Vibe ─────────────────────────────────────────────────────────────
run_phase "vibe" \
"You are working in $WORKDIR on branch $BRANCH.
Run /vibe to validate the implementation.$(spec_context)
After the council finishes, write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/vibe-verdict.txt."
if ! should_skip "vibe"; then
VIBE=$(read_verdict "$RALPH_DIR/vibe-verdict.txt")
if [[ "$VIBE" == "FAIL" ]]; then
die "Vibe verdict: FAIL — aborting before PR. See $RALPH_LOG for details."
fi
if [[ "$VIBE" == "WARN" ]]; then
log "Vibe verdict: WARN — continuing with warnings"
fi
fi
# ── Phase 5: Post-mortem ──────────────────────────────────────────────────────
run_phase "postmortem" \
"You are working in $WORKDIR on branch $BRANCH.
Run /postmortem to wrap up. Extract learnings.
Write exactly one word — PASS, WARN, or FAIL — into $RALPH_DIR/postmortem-verdict.txt."
# ── Phase 6: PR ───────────────────────────────────────────────────────────────
if should_skip "pr"; then
log "Skipping PR (resuming)"
else
log "Creating PR"
if [[ "$DRY_RUN" == "true" ]]; then
echo "[dry-run] Would create PR with title: $(echo "$GOAL" | head -c 70)"
echo "[dry-run] Done."
exit 0
fi
# Push branch
git push -u origin "$BRANCH"
# Build PR body
PLAN_FILE=""
if [[ -f "$PLAN_PATH" ]]; then
PLAN_FILE=$(cat "$PLAN_PATH")
fi
PR_BODY="## Goal
$GOAL
## Council Verdicts
- Pre-mortem: $(read_verdict "$RALPH_DIR/premortem-verdict.txt")
- Vibe: $(read_verdict "$RALPH_DIR/vibe-verdict.txt")
- Post-mortem: $(read_verdict "$RALPH_DIR/postmortem-verdict.txt")
"
if [[ -n "$SPEC_CONTENT" ]]; then
PR_BODY+="
## Spec
$SPEC_CONTENT
"
fi
PR_BODY+="
## Plan
$(cat "$PLAN_FILE" 2>/dev/null || echo "See .agents/plans/")
## Reports
See \`.agents/council/\` for full council reports."
PR_URL=$(gh pr create \
--base "$BASE_BRANCH" \
--title "$(echo "$GOAL" | head -c 70)" \
--body "$PR_BODY")
save_checkpoint "pr"
log "Done"
echo ""
echo "PR: $PR_URL"
fi
echo "Log: $RALPH_LOG"