mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
62cc3b6ee0
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)
353 lines
12 KiB
Bash
Executable File
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"
|