refactor(swipe): split swipe screen modules

Move swipe screen logic into hooks/services and extract
state-specific UI components to keep files small while
preserving existing behavior.

Reference: US-002
This commit is contained in:
jjbrunton committed 2026-01-25 21:05:54 +00:00
1 parent 209dc652d2
commit 8d4d9d8750
55 files changed
+11127 -1681

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
# Build
You are an autonomous coding agent. Your task is to complete the work for exactly one story and record the outcome.
## Paths
- PRD: {{PRD_PATH}}
- AGENTS (optional): {{AGENTS_PATH}}
- Progress Log: {{PROGRESS_PATH}}
- Guardrails: {{GUARDRAILS_PATH}}
- Guardrails Reference: {{GUARDRAILS_REF}}
- Context Reference: {{CONTEXT_REF}}
- Errors Log: {{ERRORS_LOG_PATH}}
- Activity Log: {{ACTIVITY_LOG_PATH}}
- Activity Logger: {{ACTIVITY_CMD}}
- No-commit: {{NO_COMMIT}}
- Repo Root: {{REPO_ROOT}}
- Run ID: {{RUN_ID}}
- Iteration: {{ITERATION}}
- Run Log: {{RUN_LOG_PATH}}
- Run Summary: {{RUN_META_PATH}}
## Global Quality Gates (apply to every story)
{{QUALITY_GATES}}
## Selected Story (Do not change scope)
ID: {{STORY_ID}}
Title: {{STORY_TITLE}}
Story details:
{{STORY_BLOCK}}
If the story details are empty or missing, STOP and report that the PRD story format could not be parsed.
## Rules (Non-Negotiable)
- Implement **only** the work required to complete the selected story.
- Complete all tasks associated with this story (and only this story).
- Do NOT ask the user questions.
- Do NOT change unrelated code.
- Do NOT assume something is unimplemented — confirm by reading code.
- Implement completely; no placeholders or stubs.
- If No-commit is true, do NOT commit or push changes.
- Do NOT edit the PRD JSON (status is handled by the loop).
- All changes made during the run must be committed (including updates to progress/logs).
- Before committing, perform a final **security**, **performance**, and **regression** review of your changes.
## Your Task (Do this in order)
1. Read {{GUARDRAILS_PATH}} before any code changes.
2. Read {{ERRORS_LOG_PATH}} for repeated failures to avoid.
3. Read {{PRD_PATH}} for global context (do not edit).
4. Fully audit and read all necessary files to understand the task end-to-end before implementing. Do not assume missing functionality.
5. If {{AGENTS_PATH}} exists, follow its build/test instructions.
6. Implement only the tasks that belong to {{STORY_ID}}.
7. Run verification commands listed in the story, the global quality gates, and in {{AGENTS_PATH}} (if required).
8. If the project has a build or dev workflow, run what applies:
- Build step (e.g., `npm run build`) if defined.
- Dev server (e.g., `npm run dev`, `wrangler dev`) if it is the normal validation path.
- Confirm no runtime/build errors in the console.
9. Perform a brief audit before committing:
- **Security:** check for obvious vulnerabilities or unsafe handling introduced by your changes.
- **Performance:** check for avoidable regressions (extra queries, heavy loops, unnecessary re-renders).
- **Regression:** verify existing behavior that could be impacted still works.
10. If No-commit is false, commit changes using the `$commit` skill.
- Stage everything: `git add -A`
- Confirm a clean working tree after commit: `git status --porcelain` should be empty.
- After committing, capture the commit hash and subject using:
`git show -s --format="%h %s" HEAD`.
11. Append a progress entry to {{PROGRESS_PATH}} with run/commit/test details (format below).
If No-commit is true, skip committing and note it in the progress entry.
## Progress Entry Format (Append Only)
```
## [Date/Time] - {{STORY_ID}}: {{STORY_TITLE}}
Thread: [codex exec session id if available, otherwise leave blank]
Run: {{RUN_ID}} (iteration {{ITERATION}})
Run log: {{RUN_LOG_PATH}}
Run summary: {{RUN_META_PATH}}
- Guardrails reviewed: yes
- No-commit run: {{NO_COMMIT}}
- Commit: <hash> <subject> (or `none` + reason)
- Post-commit status: `clean` or list remaining files
- Verification:
- Command: <exact command> -> PASS/FAIL
- Command: <exact command> -> PASS/FAIL
- Files changed:
- <file path>
- <file path>
- What was implemented
- **Learnings for future iterations:**
- Patterns discovered
- Gotchas encountered
- Useful context
---
```
## Completion Signal
Only output the completion signal when the **selected story** is fully complete and verified.
When the selected story is complete, output:
<promise>COMPLETE</promise>
Otherwise, end normally without the signal.
## Additional Guardrails
- When authoring documentation, capture the why (tests + implementation intent).
- If you learn how to run/build/test the project, update {{AGENTS_PATH}} briefly (operational only).
- Keep AGENTS operational only; progress notes belong in {{PROGRESS_PATH}}.
- If you hit repeated errors, log them in {{ERRORS_LOG_PATH}} and add a Sign to {{GUARDRAILS_PATH}} using {{GUARDRAILS_REF}} as the template.
## Activity Logging (Required)
Log major actions to {{ACTIVITY_LOG_PATH}} using the helper:
```
{{ACTIVITY_CMD}} "message"
```
Log at least:
- Start of work on the story
- After major code changes
- After tests/verification
- After updating progress log
## Browser Testing (Required for Frontend Stories)
If the selected story changes UI, you MUST verify it in the browser:
1. Load the `dev-browser` skill.
2. Navigate to the relevant page.
3. Verify the UI changes work as expected.
4. Take a screenshot if helpful for the progress log.
A frontend story is NOT complete until browser verification passes.
+190
View File
@@ -0,0 +1,190 @@
# Ralph
![Ralph](ralph.webp)
Ralph is a minimal, file‑based agent loop for autonomous coding. Each iteration starts fresh, reads the same on‑disk state, and commits work for one story at a time.
## How it works
Ralph treats **files and git** as memory, not the model context:
- **PRD (JSON)** defines stories, gates, and status
- **Loop** executes one story per iteration
- **State** persists in `.ralph/`
![Ralph architecture](diagram.svg)
## Global CLI (recommended)
Install and run Ralph from anywhere:
```bash
npm i -g @iannuttall/ralph
ralph prd # launches an interactive prompt
ralph build 1 # one Ralph run
```
### Template hierarchy
Ralph will look for templates in this order:
1. `.agents/ralph/` in the current project (if present)
2. Bundled defaults shipped with this repo
State and logs always go to `.ralph/` in the project.
### Install templates into a project (optional overrides)
```bash
ralph install
```
This creates `.agents/ralph/` in the current repo so you can customize prompts and loop behavior. During install, you’ll be asked if you want to add the required skills.
### Install required skills (optional)
```bash
ralph install --skills
```
You’ll be prompted for agent (codex/claude/droid/opencode) and local vs global install. Skills installed: **commit**, **dev-browser**, **prd**.
If you skipped skills during `ralph install`, you can run `ralph install --skills` anytime.
## Quick start (project)
1) Create your PRD (JSON) or generate one:
```
ralph prd
```
Requires the **prd** skill (install via `ralph install --skills`).
Example prompt text:
```
A lightweight uptime monitor (Hono app), deployed on Cloudflare, with email alerts via AWS SES
```
Default output (agent chooses a short filename in `.agents/tasks/`):
```
.agents/tasks/prd-<short>.json
```
2) Run one build iteration:
```
ralph build 1 # one Ralph run
```
No‑commit dry run:
```
ralph build 1 --no-commit # one Ralph run
```
Override PRD output for `ralph prd`:
```
ralph prd --out .agents/tasks/prd-api.json
```
Optional human overview (generated from JSON):
```
ralph overview
```
This writes a tiny overview alongside the PRD: `prd-<slug>.overview.md`.
PRD story status fields are updated automatically by the loop:
- `open` → selectable
- `in_progress` → locked by a running loop (with `startedAt`)
- `done` → completed (with `completedAt`)
If a loop crashes and a story stays `in_progress`, you can set `STALE_SECONDS` in `.agents/ralph/config.sh` to allow Ralph to automatically reopen stalled stories.
## Override PRD paths
You can point Ralph at a different PRD JSON file via CLI flags:
```bash
ralph build 1 --prd .agents/tasks/prd-api.json # one Ralph run
```
Optional progress override:
```bash
ralph build 1 --progress .ralph/progress-api.md # one Ralph run
```
If multiple PRD JSON files exist in `.agents/tasks/` and you omit `--prd`, Ralph will prompt you to choose.
Optional config file (if you installed templates):
```
.agents/ralph/config.sh
```
## Choose the agent runner
Set `AGENT_CMD` in `.agents/ralph/config.sh` to switch agents:
```
AGENT_CMD="codex exec --yolo -"
AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
AGENT_CMD="opencode run \"$(cat {prompt})\""
```
Or override per run:
```
ralph prd --agent=codex
ralph build 1 --agent=codex # one Ralph run
ralph build 1 --agent=claude # one Ralph run
ralph build 1 --agent=droid # one Ralph run
ralph build 1 --agent=opencode # one Ralph run
```
If the CLI isn’t installed, Ralph prints install hints:
```
codex -> npm i -g @openai/codex
claude -> curl -fsSL https://claude.ai/install.sh | bash
droid -> curl -fsSL https://app.factory.ai/cli | sh
opencode -> curl -fsSL https://opencode.ai/install.sh | bash
```
## State files (.ralph/)
- `progress.md` — append‑only progress log
- `guardrails.md` — “Signs” (lessons learned)
- `activity.log` — activity + timing log
- `errors.log` — repeated failures and notes
- `runs/` — raw run logs + summaries
## Notes
- `.agents/ralph` is portable and can be copied between repos.
- `.ralph` is per‑project state.
- Use `{prompt}` in `AGENT_CMD` when agent needs a file path instead of stdin.
- Examples: see `examples/commands.md`.
- **OpenCode server mode**: For faster performance with OpenCode, run `opencode serve` in a separate terminal and uncomment the `AGENT_OPENCODE_CMD` lines in `.agents/ralph/agents.sh` to use `--attach http://localhost:4096`. This avoids cold boot on every run.
## Tests
Dry-run smoke tests (no agent required):
```bash
npm test
```
Fast agent health check (real agent call, minimal output):
```bash
npm run test:ping
```
Optional integration test (requires agents installed):
```bash
RALPH_INTEGRATION=1 npm test
```
Full real-agent loop test:
```bash
npm run test:real
```
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Default agent command templates (used by loop.sh and CLI).
AGENT_CODEX_CMD="codex exec --yolo --skip-git-repo-check -"
AGENT_CODEX_INTERACTIVE_CMD="codex --yolo {prompt}"
AGENT_CLAUDE_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
AGENT_CLAUDE_INTERACTIVE_CMD="claude --dangerously-skip-permissions {prompt}"
AGENT_DROID_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
AGENT_DROID_INTERACTIVE_CMD="droid --skip-permissions-unsafe {prompt}"
AGENT_OPENCODE_CMD="opencode run \"\$(cat {prompt})\""
AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt}"
# Uncomment to use server mode (faster, avoids cold boot):
# AGENT_OPENCODE_CMD="opencode run --attach http://localhost:4096 \"\$(cat {prompt})\""
# AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt} --attach http://localhost:4096"
DEFAULT_AGENT="codex"
+25
View File
@@ -0,0 +1,25 @@
# Optional Ralph config overrides.
# All paths are relative to repo root unless absolute.
# Uncomment and edit as needed.
# PRD_PATH=".agents/tasks/prd.json"
# PROGRESS_PATH=".ralph/progress.md"
# GUARDRAILS_PATH=".ralph/guardrails.md"
# ERRORS_LOG_PATH=".ralph/errors.log"
# ACTIVITY_LOG_PATH=".ralph/activity.log"
# TMP_DIR=".ralph/.tmp"
# RUNS_DIR=".ralph/runs"
# GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md"
# CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md"
# ACTIVITY_CMD=".agents/ralph/log-activity.sh"
# AGENT_CMD defaults are defined in agents.sh. Override here if needed.
# AGENT_CMD="codex exec --yolo --skip-git-repo-check -"
# PRD_AGENT_CMD defaults are defined in agents.sh (interactive).
# PRD_AGENT_CMD="codex --yolo --skip-git-repo-check {prompt}"
# AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
# AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
# AGENTS_PATH="AGENTS.md"
# PROMPT_BUILD=".agents/ralph/PROMPT_build.md"
# NO_COMMIT=false
# MAX_ITERATIONS=25
# STALE_SECONDS=0
+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 520" role="img" aria-label="Ralph architecture diagram">
<style>
.bg { fill: #0f1218; }
.box { fill: #141a23; stroke: #e6e6e6; stroke-width: 1.5; }
.line { stroke: #e6e6e6; stroke-width: 1.5; }
.text { fill: #f3f3f3; font-family: Menlo, Consolas, Monaco, monospace; font-size: 16px; }
.small { font-size: 13px; opacity: 0.9; }
.muted { opacity: 0.7; }
</style>
<rect class="bg" x="0" y="0" width="1000" height="520" rx="0" />
<!-- Top boxes -->
<rect class="box" x="390" y="30" width="220" height="44" rx="0" />
<text class="text" x="500" y="58" text-anchor="middle">ralph CLI</text>
<rect class="box" x="240" y="114" width="520" height="48" rx="0" />
<text class="text small muted" x="500" y="144" text-anchor="middle">agent CLI: codex | claude | droid</text>
<!-- Arrow from top to agent (gap matches lower arrows) -->
<line class="line" x1="500" y1="74" x2="500" y2="96" />
<polygon points="496,96 500,104 504,96" fill="#e6e6e6" />
<!-- Branch lines (gap for heads) -->
<line class="line" x1="500" y1="162" x2="500" y2="200" />
<line class="line" x1="500" y1="200" x2="280" y2="200" />
<line class="line" x1="500" y1="200" x2="720" y2="200" />
<line class="line" x1="280" y1="200" x2="280" y2="214" />
<line class="line" x1="720" y1="200" x2="720" y2="214" />
<polygon points="276,214 280,222 284,214" fill="#e6e6e6" />
<polygon points="716,214 720,222 724,214" fill="#e6e6e6" />
<!-- Bottom boxes -->
<rect class="box" x="90" y="226" width="380" height="230" rx="0" />
<text class="text" x="110" y="256">.agents/ralph/</text>
<text class="text small muted" x="110" y="278">local overrides (optional)</text>
<text class="text small" x="110" y="306">loop.sh</text>
<text class="text small" x="110" y="328">PROMPT_build.md</text>
<text class="text small" x="110" y="350">references/</text>
<text class="text small" x="110" y="372">log-activity.sh</text>
<text class="text small" x="110" y="394">config.sh (optional)</text>
<rect class="box" x="530" y="226" width="380" height="230" rx="0" />
<text class="text" x="550" y="256">.ralph/</text>
<text class="text small muted" x="550" y="278">state + logs</text>
<text class="text small" x="550" y="306">errors.log</text>
<text class="text small" x="550" y="328">progress.md</text>
<text class="text small" x="550" y="350">guardrails.md</text>
<text class="text small" x="550" y="372">activity.log</text>
<text class="text small" x="550" y="394">runs/</text>
<text class="text small muted" x="500" y="500" text-anchor="middle">
Local templates override bundled defaults. State persists between runs.
</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
ACTIVITY_LOG="$ROOT_DIR/.ralph/activity.log"
if [ $# -lt 1 ]; then
echo "Usage: $0 \"message\""
exit 1
fi
mkdir -p "$(dirname "$ACTIVITY_LOG")"
TS=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TS] $*" >> "$ACTIVITY_LOG"
+955
View File
@@ -0,0 +1,955 @@
#!/bin/bash
# Ralph loop — simple, portable, single-agent
# Usage:
# ./.agents/ralph/loop.sh # build mode, default iterations
# ./.agents/ralph/loop.sh build # build mode
# ./.agents/ralph/loop.sh prd "request" # generate PRD via agent
# ./.agents/ralph/loop.sh 10 # build mode, 10 iterations
# ./.agents/ralph/loop.sh build 1 --no-commit
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${RALPH_ROOT:-${SCRIPT_DIR}/../..}" && pwd)"
CONFIG_FILE="${SCRIPT_DIR}/config.sh"
DEFAULT_PRD_PATH=".agents/tasks/prd.json"
DEFAULT_PROGRESS_PATH=".ralph/progress.md"
DEFAULT_AGENTS_PATH="AGENTS.md"
DEFAULT_PROMPT_BUILD=".agents/ralph/PROMPT_build.md"
DEFAULT_GUARDRAILS_PATH=".ralph/guardrails.md"
DEFAULT_ERRORS_LOG_PATH=".ralph/errors.log"
DEFAULT_ACTIVITY_LOG_PATH=".ralph/activity.log"
DEFAULT_TMP_DIR=".ralph/.tmp"
DEFAULT_RUNS_DIR=".ralph/runs"
DEFAULT_GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md"
DEFAULT_CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md"
DEFAULT_ACTIVITY_CMD=".agents/ralph/log-activity.sh"
if [[ -n "${RALPH_ROOT:-}" ]]; then
agents_path="$RALPH_ROOT/.agents/ralph/agents.sh"
else
agents_path="$SCRIPT_DIR/agents.sh"
fi
if [[ -f "$agents_path" ]]; then
# shellcheck source=/dev/null
source "$agents_path"
fi
DEFAULT_MAX_ITERATIONS=25
DEFAULT_NO_COMMIT=false
DEFAULT_STALE_SECONDS=0
PRD_REQUEST_PATH=""
PRD_INLINE=""
# Optional config overrides (simple shell vars)
if [ -f "$CONFIG_FILE" ]; then
# shellcheck source=/dev/null
. "$CONFIG_FILE"
fi
DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-codex}"
resolve_agent_cmd() {
local name="$1"
case "$name" in
claude)
echo "${AGENT_CLAUDE_CMD:-claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"}"
;;
droid)
echo "${AGENT_DROID_CMD:-droid exec --skip-permissions-unsafe -f {prompt}}"
;;
codex|"")
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
;;
*)
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
;;
esac
}
DEFAULT_AGENT_CMD="$(resolve_agent_cmd "$DEFAULT_AGENT_NAME")"
PRD_PATH="${PRD_PATH:-$DEFAULT_PRD_PATH}"
PROGRESS_PATH="${PROGRESS_PATH:-$DEFAULT_PROGRESS_PATH}"
AGENTS_PATH="${AGENTS_PATH:-$DEFAULT_AGENTS_PATH}"
PROMPT_BUILD="${PROMPT_BUILD:-$DEFAULT_PROMPT_BUILD}"
GUARDRAILS_PATH="${GUARDRAILS_PATH:-$DEFAULT_GUARDRAILS_PATH}"
ERRORS_LOG_PATH="${ERRORS_LOG_PATH:-$DEFAULT_ERRORS_LOG_PATH}"
ACTIVITY_LOG_PATH="${ACTIVITY_LOG_PATH:-$DEFAULT_ACTIVITY_LOG_PATH}"
TMP_DIR="${TMP_DIR:-$DEFAULT_TMP_DIR}"
RUNS_DIR="${RUNS_DIR:-$DEFAULT_RUNS_DIR}"
GUARDRAILS_REF="${GUARDRAILS_REF:-$DEFAULT_GUARDRAILS_REF}"
CONTEXT_REF="${CONTEXT_REF:-$DEFAULT_CONTEXT_REF}"
ACTIVITY_CMD="${ACTIVITY_CMD:-$DEFAULT_ACTIVITY_CMD}"
AGENT_CMD="${AGENT_CMD:-$DEFAULT_AGENT_CMD}"
MAX_ITERATIONS="${MAX_ITERATIONS:-$DEFAULT_MAX_ITERATIONS}"
NO_COMMIT="${NO_COMMIT:-$DEFAULT_NO_COMMIT}"
STALE_SECONDS="${STALE_SECONDS:-$DEFAULT_STALE_SECONDS}"
abs_path() {
local p="$1"
if [[ "$p" = /* ]]; then
echo "$p"
else
echo "$ROOT_DIR/$p"
fi
}
PRD_PATH="$(abs_path "$PRD_PATH")"
PROGRESS_PATH="$(abs_path "$PROGRESS_PATH")"
AGENTS_PATH="$(abs_path "$AGENTS_PATH")"
PROMPT_BUILD="$(abs_path "$PROMPT_BUILD")"
GUARDRAILS_PATH="$(abs_path "$GUARDRAILS_PATH")"
ERRORS_LOG_PATH="$(abs_path "$ERRORS_LOG_PATH")"
ACTIVITY_LOG_PATH="$(abs_path "$ACTIVITY_LOG_PATH")"
TMP_DIR="$(abs_path "$TMP_DIR")"
RUNS_DIR="$(abs_path "$RUNS_DIR")"
GUARDRAILS_REF="$(abs_path "$GUARDRAILS_REF")"
CONTEXT_REF="$(abs_path "$CONTEXT_REF")"
ACTIVITY_CMD="$(abs_path "$ACTIVITY_CMD")"
require_agent() {
local agent_cmd="${1:-$AGENT_CMD}"
local agent_bin
agent_bin="${agent_cmd%% *}"
if [ -z "$agent_bin" ]; then
echo "AGENT_CMD is empty. Set it in config.sh."
exit 1
fi
if ! command -v "$agent_bin" >/dev/null 2>&1; then
echo "Agent command not found: $agent_bin"
case "$agent_bin" in
codex)
echo "Install: npm i -g @openai/codex"
;;
claude)
echo "Install: curl -fsSL https://claude.ai/install.sh | bash"
;;
droid)
echo "Install: curl -fsSL https://app.factory.ai/cli | sh"
;;
opencode)
echo "Install: curl -fsSL https://opencode.ai/install.sh | bash"
;;
esac
echo "Then authenticate per the CLI's instructions."
exit 1
fi
}
run_agent() {
local prompt_file="$1"
if [[ "$AGENT_CMD" == *"{prompt}"* ]]; then
local escaped
escaped=$(printf '%q' "$prompt_file")
local cmd="${AGENT_CMD//\{prompt\}/$escaped}"
eval "$cmd"
else
cat "$prompt_file" | eval "$AGENT_CMD"
fi
}
run_agent_inline() {
local prompt_file="$1"
local prompt_content
prompt_content="$(cat "$prompt_file")"
local escaped
escaped=$(printf "%s" "$prompt_content" | sed "s/'/'\\\\''/g")
local cmd="${PRD_AGENT_CMD:-$AGENT_CMD}"
if [[ "$cmd" == *"{prompt}"* ]]; then
cmd="${cmd//\{prompt\}/'$escaped'}"
else
cmd="$cmd '$escaped'"
fi
eval "$cmd"
}
MODE="build"
while [ $# -gt 0 ]; do
case "$1" in
build|prd)
MODE="$1"
shift
;;
--prompt)
PRD_REQUEST_PATH="$2"
shift 2
;;
--no-commit)
NO_COMMIT=true
shift
;;
*)
if [ "$MODE" = "prd" ]; then
PRD_INLINE="${PRD_INLINE:+$PRD_INLINE }$1"
shift
elif [[ "$1" =~ ^[0-9]+$ ]]; then
MAX_ITERATIONS="$1"
shift
else
echo "Unknown arg: $1"
exit 1
fi
;;
esac
done
PROMPT_FILE="$PROMPT_BUILD"
if [ "$MODE" = "prd" ]; then
PRD_USE_INLINE=1
if [ -z "${PRD_AGENT_CMD:-}" ]; then
PRD_AGENT_CMD="$AGENT_CMD"
PRD_USE_INLINE=0
fi
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
require_agent "${PRD_AGENT_CMD:-$AGENT_CMD}"
fi
if [[ "$PRD_PATH" == *.json ]]; then
mkdir -p "$(dirname "$PRD_PATH")" "$TMP_DIR"
else
mkdir -p "$PRD_PATH" "$TMP_DIR"
fi
if [ -z "$PRD_REQUEST_PATH" ] && [ -n "$PRD_INLINE" ]; then
PRD_REQUEST_PATH="$TMP_DIR/prd-request-$(date +%Y%m%d-%H%M%S)-$$.txt"
printf '%s\n' "$PRD_INLINE" > "$PRD_REQUEST_PATH"
fi
if [ -z "$PRD_REQUEST_PATH" ] || [ ! -f "$PRD_REQUEST_PATH" ]; then
echo "PRD request missing. Provide a prompt string or --prompt <file>."
exit 1
fi
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
if [[ "$PRD_PATH" == *.json ]]; then
if [ ! -f "$PRD_PATH" ]; then
{
echo '{'
echo ' "version": 1,'
echo ' "project": "ralph",'
echo ' "qualityGates": [],'
echo ' "stories": []'
echo '}'
} > "$PRD_PATH"
fi
fi
exit 0
fi
PRD_PROMPT_FILE="$TMP_DIR/prd-prompt-$(date +%Y%m%d-%H%M%S)-$$.md"
{
echo "You are an autonomous coding agent."
echo "Use the \$prd skill to create a Product Requirements Document in JSON."
if [[ "$PRD_PATH" == *.json ]]; then
echo "Save the PRD to: $PRD_PATH"
else
echo "Save the PRD as JSON in directory: $PRD_PATH"
echo "Filename rules: prd-<short-slug>.json using 1-3 meaningful words."
echo "Examples: prd-workout-tracker.json, prd-usage-billing.json"
fi
echo "Do NOT implement anything."
echo "After creating the PRD, end with:"
echo "PRD JSON saved to <path>. Close this chat and run \`ralph build\`."
echo ""
echo "User request:"
cat "$PRD_REQUEST_PATH"
} > "$PRD_PROMPT_FILE"
if [ "$PRD_USE_INLINE" -eq 1 ]; then
run_agent_inline "$PRD_PROMPT_FILE"
else
run_agent "$PRD_PROMPT_FILE"
fi
exit 0
fi
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
require_agent
fi
if [ ! -f "$PROMPT_FILE" ]; then
echo "Prompt not found: $PROMPT_FILE"
exit 1
fi
if [ "$MODE" != "prd" ] && [ ! -f "$PRD_PATH" ]; then
echo "PRD not found: $PRD_PATH"
exit 1
fi
mkdir -p "$(dirname "$PROGRESS_PATH")" "$TMP_DIR" "$RUNS_DIR"
if [ ! -f "$PROGRESS_PATH" ]; then
{
echo "# Progress Log"
echo "Started: $(date)"
echo ""
echo "## Codebase Patterns"
echo "- (add reusable patterns here)"
echo ""
echo "---"
} > "$PROGRESS_PATH"
fi
if [ ! -f "$GUARDRAILS_PATH" ]; then
{
echo "# Guardrails (Signs)"
echo ""
echo "> Lessons learned from failures. Read before acting."
echo ""
echo "## Core Signs"
echo ""
echo "### Sign: Read Before Writing"
echo "- **Trigger**: Before modifying any file"
echo "- **Instruction**: Read the file first"
echo "- **Added after**: Core principle"
echo ""
echo "### Sign: Test Before Commit"
echo "- **Trigger**: Before committing changes"
echo "- **Instruction**: Run required tests and verify outputs"
echo "- **Added after**: Core principle"
echo ""
echo "---"
echo ""
echo "## Learned Signs"
echo ""
} > "$GUARDRAILS_PATH"
fi
if [ ! -f "$ERRORS_LOG_PATH" ]; then
{
echo "# Error Log"
echo ""
echo "> Failures and repeated issues. Use this to add guardrails."
echo ""
} > "$ERRORS_LOG_PATH"
fi
if [ ! -f "$ACTIVITY_LOG_PATH" ]; then
{
echo "# Activity Log"
echo ""
echo "## Run Summary"
echo ""
echo "## Events"
echo ""
} > "$ACTIVITY_LOG_PATH"
fi
RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$"
render_prompt() {
local src="$1"
local dst="$2"
local story_meta="$3"
local story_block="$4"
local run_id="$5"
local iter="$6"
local run_log="$7"
local run_meta="$8"
python3 - "$src" "$dst" "$PRD_PATH" "$AGENTS_PATH" "$PROGRESS_PATH" "$ROOT_DIR" "$GUARDRAILS_PATH" "$ERRORS_LOG_PATH" "$ACTIVITY_LOG_PATH" "$GUARDRAILS_REF" "$CONTEXT_REF" "$ACTIVITY_CMD" "$NO_COMMIT" "$story_meta" "$story_block" "$run_id" "$iter" "$run_log" "$run_meta" <<'PY'
import sys
from pathlib import Path
src = Path(sys.argv[1]).read_text()
prd, agents, progress, root = sys.argv[3:7]
guardrails = sys.argv[7]
errors_log = sys.argv[8]
activity_log = sys.argv[9]
guardrails_ref = sys.argv[10]
context_ref = sys.argv[11]
activity_cmd = sys.argv[12]
no_commit = sys.argv[13]
meta_path = sys.argv[14] if len(sys.argv) > 14 else ""
block_path = sys.argv[15] if len(sys.argv) > 15 else ""
run_id = sys.argv[16] if len(sys.argv) > 16 else ""
iteration = sys.argv[17] if len(sys.argv) > 17 else ""
run_log = sys.argv[18] if len(sys.argv) > 18 else ""
run_meta = sys.argv[19] if len(sys.argv) > 19 else ""
repl = {
"PRD_PATH": prd,
"AGENTS_PATH": agents,
"PROGRESS_PATH": progress,
"REPO_ROOT": root,
"GUARDRAILS_PATH": guardrails,
"ERRORS_LOG_PATH": errors_log,
"ACTIVITY_LOG_PATH": activity_log,
"GUARDRAILS_REF": guardrails_ref,
"CONTEXT_REF": context_ref,
"ACTIVITY_CMD": activity_cmd,
"NO_COMMIT": no_commit,
"RUN_ID": run_id,
"ITERATION": iteration,
"RUN_LOG_PATH": run_log,
"RUN_META_PATH": run_meta,
}
story = {"id": "", "title": "", "block": ""}
quality_gates = []
if meta_path:
try:
import json
meta = json.loads(Path(meta_path).read_text())
story["id"] = meta.get("id", "") or ""
story["title"] = meta.get("title", "") or ""
quality_gates = meta.get("quality_gates", []) or []
except Exception:
pass
if block_path and Path(block_path).exists():
story["block"] = Path(block_path).read_text()
repl["STORY_ID"] = story["id"]
repl["STORY_TITLE"] = story["title"]
repl["STORY_BLOCK"] = story["block"]
if quality_gates:
repl["QUALITY_GATES"] = "\n".join([f"- {g}" for g in quality_gates])
else:
repl["QUALITY_GATES"] = "- (none)"
for k, v in repl.items():
src = src.replace("{{" + k + "}}", v)
Path(sys.argv[2]).write_text(src)
PY
}
select_story() {
local meta_out="$1"
local block_out="$2"
python3 - "$PRD_PATH" "$meta_out" "$block_out" "$STALE_SECONDS" <<'PY'
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timezone
try:
import fcntl
except Exception:
fcntl = None
prd_path = Path(sys.argv[1])
meta_out = Path(sys.argv[2])
block_out = Path(sys.argv[3])
stale_seconds = 0
if len(sys.argv) > 4:
try:
stale_seconds = int(sys.argv[4])
except Exception:
stale_seconds = 0
if not prd_path.exists():
meta_out.write_text(json.dumps({"ok": False, "error": "PRD not found"}, indent=2) + "\n")
block_out.write_text("")
sys.exit(0)
def normalize_status(value):
if value is None:
return "open"
return str(value).strip().lower()
def parse_ts(value):
if not value:
return None
text = str(value).strip()
if text.endswith("Z"):
text = text[:-1] + "+00:00"
try:
return datetime.fromisoformat(text)
except Exception:
return None
def now_iso():
return datetime.now(timezone.utc).isoformat()
with prd_path.open("r+", encoding="utf-8") as fh:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
try:
try:
data = json.load(fh)
except Exception as exc:
meta_out.write_text(json.dumps({"ok": False, "error": f"Invalid PRD JSON: {exc}"}, indent=2) + "\n")
block_out.write_text("")
sys.exit(0)
stories = data.get("stories") if isinstance(data, dict) else None
if not isinstance(stories, list) or not stories:
meta_out.write_text(json.dumps({"ok": False, "error": "No stories found in PRD"}, indent=2) + "\n")
block_out.write_text("")
sys.exit(0)
story_index = {s.get("id"): s for s in stories if isinstance(s, dict)}
def is_done(story_id: str) -> bool:
target = story_index.get(story_id)
if not isinstance(target, dict):
return False
return normalize_status(target.get("status")) == "done"
if stale_seconds > 0:
now = datetime.now(timezone.utc)
for story in stories:
if not isinstance(story, dict):
continue
if normalize_status(story.get("status")) != "in_progress":
continue
started = parse_ts(story.get("startedAt"))
if started is None or (now - started).total_seconds() > stale_seconds:
story["status"] = "open"
story["startedAt"] = None
story["completedAt"] = None
story["updatedAt"] = now_iso()
candidate = None
for story in stories:
if not isinstance(story, dict):
continue
if normalize_status(story.get("status")) != "open":
continue
deps = story.get("dependsOn") or []
if not isinstance(deps, list):
deps = []
if all(is_done(dep) for dep in deps):
candidate = story
break
remaining = sum(
1 for story in stories
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
)
meta = {
"ok": True,
"total": len(stories),
"remaining": remaining,
"quality_gates": data.get("qualityGates", []) or [],
}
if candidate:
candidate["status"] = "in_progress"
if not candidate.get("startedAt"):
candidate["startedAt"] = now_iso()
candidate["completedAt"] = None
candidate["updatedAt"] = now_iso()
meta.update({
"id": candidate.get("id", ""),
"title": candidate.get("title", ""),
})
depends = candidate.get("dependsOn") or []
if not isinstance(depends, list):
depends = []
acceptance = candidate.get("acceptanceCriteria") or []
if not isinstance(acceptance, list):
acceptance = []
description = candidate.get("description") or ""
block_lines = []
block_lines.append(f"### {candidate.get('id', '')}: {candidate.get('title', '')}")
block_lines.append(f"Status: {candidate.get('status', 'open')}")
block_lines.append(
f"Depends on: {', '.join(depends) if depends else 'None'}"
)
block_lines.append("")
block_lines.append("Description:")
block_lines.append(description if description else "(none)")
block_lines.append("")
block_lines.append("Acceptance Criteria:")
if acceptance:
block_lines.extend([f"- [ ] {item}" for item in acceptance])
else:
block_lines.append("- (none)")
block_out.write_text("\n".join(block_lines).rstrip() + "\n")
else:
block_out.write_text("")
fh.seek(0)
fh.truncate()
json.dump(data, fh, indent=2)
fh.write("\n")
fh.flush()
os.fsync(fh.fileno())
finally:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
meta_out.write_text(json.dumps(meta, indent=2) + "\n")
PY
}
remaining_stories() {
local meta_file="$1"
python3 - "$meta_file" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text())
print(data.get("remaining", "unknown"))
PY
}
remaining_from_prd() {
python3 - "$PRD_PATH" <<'PY'
import json
import sys
from pathlib import Path
prd_path = Path(sys.argv[1])
if not prd_path.exists():
print("unknown")
sys.exit(0)
try:
data = json.loads(prd_path.read_text())
except Exception:
print("unknown")
sys.exit(0)
stories = data.get("stories") if isinstance(data, dict) else None
if not isinstance(stories, list):
print("unknown")
sys.exit(0)
def normalize_status(value):
if value is None:
return "open"
return str(value).strip().lower()
remaining = sum(
1 for story in stories
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
)
print(remaining)
PY
}
story_field() {
local meta_file="$1"
local field="$2"
python3 - "$meta_file" "$field" <<'PY'
import json
import sys
from pathlib import Path
data = json.loads(Path(sys.argv[1]).read_text())
field = sys.argv[2]
print(data.get(field, ""))
PY
}
update_story_status() {
local story_id="$1"
local new_status="$2"
python3 - "$PRD_PATH" "$story_id" "$new_status" <<'PY'
import json
import os
import sys
from pathlib import Path
from datetime import datetime, timezone
try:
import fcntl
except Exception:
fcntl = None
prd_path = Path(sys.argv[1])
story_id = sys.argv[2]
new_status = sys.argv[3]
if not story_id:
sys.exit(0)
if not prd_path.exists():
sys.exit(0)
def now_iso():
return datetime.now(timezone.utc).isoformat()
with prd_path.open("r+", encoding="utf-8") as fh:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
try:
data = json.load(fh)
stories = data.get("stories") if isinstance(data, dict) else None
if not isinstance(stories, list):
sys.exit(0)
for story in stories:
if isinstance(story, dict) and story.get("id") == story_id:
story["status"] = new_status
story["updatedAt"] = now_iso()
if new_status == "in_progress":
if not story.get("startedAt"):
story["startedAt"] = now_iso()
story["completedAt"] = None
elif new_status == "done":
story["completedAt"] = now_iso()
if not story.get("startedAt"):
story["startedAt"] = now_iso()
elif new_status == "open":
story["startedAt"] = None
story["completedAt"] = None
break
fh.seek(0)
fh.truncate()
json.dump(data, fh, indent=2)
fh.write("\n")
fh.flush()
os.fsync(fh.fileno())
finally:
if fcntl is not None:
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
PY
}
log_activity() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$ACTIVITY_LOG_PATH"
}
log_error() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" >> "$ERRORS_LOG_PATH"
}
append_run_summary() {
local line="$1"
python3 - "$ACTIVITY_LOG_PATH" "$line" <<'PY'
import sys
from pathlib import Path
path = Path(sys.argv[1])
line = sys.argv[2]
text = path.read_text().splitlines()
out = []
inserted = False
for l in text:
out.append(l)
if not inserted and l.strip() == "## Run Summary":
out.append(f"- {line}")
inserted = True
if not inserted:
out = [
"# Activity Log",
"",
"## Run Summary",
f"- {line}",
"",
"## Events",
"",
] + text
Path(path).write_text("\n".join(out).rstrip() + "\n")
PY
}
write_run_meta() {
local path="$1"
local mode="$2"
local iter="$3"
local run_id="$4"
local story_id="$5"
local story_title="$6"
local started="$7"
local ended="$8"
local duration="$9"
local status="${10}"
local log_file="${11}"
local head_before="${12}"
local head_after="${13}"
local commit_list="${14}"
local changed_files="${15}"
local dirty_files="${16}"
{
echo "# Ralph Run Summary"
echo ""
echo "- Run ID: $run_id"
echo "- Iteration: $iter"
echo "- Mode: $mode"
if [ -n "$story_id" ]; then
echo "- Story: $story_id: $story_title"
fi
echo "- Started: $started"
echo "- Ended: $ended"
echo "- Duration: ${duration}s"
echo "- Status: $status"
echo "- Log: $log_file"
echo ""
echo "## Git"
echo "- Head (before): ${head_before:-unknown}"
echo "- Head (after): ${head_after:-unknown}"
echo ""
echo "### Commits"
if [ -n "$commit_list" ]; then
echo "$commit_list"
else
echo "- (none)"
fi
echo ""
echo "### Changed Files (commits)"
if [ -n "$changed_files" ]; then
echo "$changed_files"
else
echo "- (none)"
fi
echo ""
echo "### Uncommitted Changes"
if [ -n "$dirty_files" ]; then
echo "$dirty_files"
else
echo "- (clean)"
fi
echo ""
} > "$path"
}
git_head() {
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true
else
echo ""
fi
}
git_commit_list() {
local before="$1"
local after="$2"
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
git -C "$ROOT_DIR" log --oneline "$before..$after" | sed 's/^/- /'
else
echo ""
fi
}
git_changed_files() {
local before="$1"
local after="$2"
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
git -C "$ROOT_DIR" diff --name-only "$before" "$after" | sed 's/^/- /'
else
echo ""
fi
}
git_dirty_files() {
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$ROOT_DIR" status --porcelain | awk '{print "- " $2}'
else
echo ""
fi
}
echo "Ralph mode: $MODE"
echo "Max iterations: $MAX_ITERATIONS"
echo "PRD: $PRD_PATH"
HAS_ERROR="false"
for i in $(seq 1 "$MAX_ITERATIONS"); do
echo ""
echo "═══════════════════════════════════════════════════════"
echo " Ralph Iteration $i of $MAX_ITERATIONS"
echo "═══════════════════════════════════════════════════════"
STORY_META=""
STORY_BLOCK=""
ITER_START=$(date +%s)
ITER_START_FMT=$(date '+%Y-%m-%d %H:%M:%S')
if [ "$MODE" = "build" ]; then
STORY_META="$TMP_DIR/story-$RUN_TAG-$i.json"
STORY_BLOCK="$TMP_DIR/story-$RUN_TAG-$i.md"
select_story "$STORY_META" "$STORY_BLOCK"
REMAINING="$(remaining_stories "$STORY_META")"
if [ "$REMAINING" = "unknown" ]; then
echo "Could not parse stories from PRD: $PRD_PATH"
exit 1
fi
if [ "$REMAINING" = "0" ]; then
echo "No remaining stories."
exit 0
fi
STORY_ID="$(story_field "$STORY_META" "id")"
STORY_TITLE="$(story_field "$STORY_META" "title")"
if [ -z "$STORY_ID" ]; then
echo "No actionable open stories (all blocked or in progress). Remaining: $REMAINING"
exit 0
fi
fi
HEAD_BEFORE="$(git_head)"
PROMPT_RENDERED="$TMP_DIR/prompt-$RUN_TAG-$i.md"
LOG_FILE="$RUNS_DIR/run-$RUN_TAG-iter-$i.log"
RUN_META="$RUNS_DIR/run-$RUN_TAG-iter-$i.md"
render_prompt "$PROMPT_FILE" "$PROMPT_RENDERED" "$STORY_META" "$STORY_BLOCK" "$RUN_TAG" "$i" "$LOG_FILE" "$RUN_META"
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
log_activity "ITERATION $i start (mode=$MODE story=$STORY_ID)"
else
log_activity "ITERATION $i start (mode=$MODE)"
fi
set +e
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
echo "[RALPH_DRY_RUN] Skipping agent execution." | tee "$LOG_FILE"
CMD_STATUS=0
else
run_agent "$PROMPT_RENDERED" 2>&1 | tee "$LOG_FILE"
CMD_STATUS=$?
fi
set -e
if [ "$CMD_STATUS" -eq 130 ] || [ "$CMD_STATUS" -eq 143 ]; then
echo "Interrupted."
exit "$CMD_STATUS"
fi
ITER_END=$(date +%s)
ITER_END_FMT=$(date '+%Y-%m-%d %H:%M:%S')
ITER_DURATION=$((ITER_END - ITER_START))
HEAD_AFTER="$(git_head)"
log_activity "ITERATION $i end (duration=${ITER_DURATION}s)"
if [ "$CMD_STATUS" -ne 0 ]; then
log_error "ITERATION $i command failed (status=$CMD_STATUS)"
HAS_ERROR="true"
fi
COMMIT_LIST="$(git_commit_list "$HEAD_BEFORE" "$HEAD_AFTER")"
CHANGED_FILES="$(git_changed_files "$HEAD_BEFORE" "$HEAD_AFTER")"
DIRTY_FILES="$(git_dirty_files)"
STATUS_LABEL="success"
if [ "$CMD_STATUS" -ne 0 ]; then
STATUS_LABEL="error"
fi
if [ "$MODE" = "build" ] && [ "$NO_COMMIT" = "false" ] && [ -n "$DIRTY_FILES" ]; then
log_error "ITERATION $i left uncommitted changes; review run summary at $RUN_META"
fi
write_run_meta "$RUN_META" "$MODE" "$i" "$RUN_TAG" "${STORY_ID:-}" "${STORY_TITLE:-}" "$ITER_START_FMT" "$ITER_END_FMT" "$ITER_DURATION" "$STATUS_LABEL" "$LOG_FILE" "$HEAD_BEFORE" "$HEAD_AFTER" "$COMMIT_LIST" "$CHANGED_FILES" "$DIRTY_FILES"
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | story=$STORY_ID | duration=${ITER_DURATION}s | status=$STATUS_LABEL"
else
append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | duration=${ITER_DURATION}s | status=$STATUS_LABEL"
fi
if [ "$MODE" = "build" ]; then
if [ "$CMD_STATUS" -ne 0 ]; then
log_error "ITERATION $i exited non-zero; review $LOG_FILE"
update_story_status "$STORY_ID" "open"
echo "Iteration failed; story reset to open."
elif grep -q "<promise>COMPLETE</promise>" "$LOG_FILE"; then
update_story_status "$STORY_ID" "done"
echo "Completion signal received; story marked done."
else
update_story_status "$STORY_ID" "open"
echo "No completion signal; story reset to open."
fi
REMAINING="$(remaining_from_prd)"
echo "Iteration $i complete. Remaining stories: $REMAINING"
if [ "$REMAINING" = "0" ]; then
echo "No remaining stories."
exit 0
fi
else
echo "Iteration $i complete."
fi
sleep 2
done
echo "Reached max iterations ($MAX_ITERATIONS)."
if [ "$HAS_ERROR" = "true" ]; then
exit 1
fi
exit 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

@@ -0,0 +1,126 @@
# Context Engineering Reference
This document explains the malloc/free metaphor for LLM context management that underlies the Ralph technique.
## The malloc() Metaphor
In traditional programming:
- `malloc()` allocates memory
- `free()` releases memory
- Memory leaks occur when you allocate without freeing
In LLM context:
- Reading files, receiving responses, tool outputs = `malloc()`
- **There is no `free()`** - context cannot be released
- The only way to "free" is to start a new conversation
## Why This Matters
### Context Pollution
When you work on multiple unrelated tasks in the same context:
```
Task 1: Build authentication → context contains auth code, JWT docs, security patterns
Task 2: Build UI components → context now ALSO contains auth stuff
Result: LLM might suggest auth-related patterns when building UI
or mix concerns inappropriately
```
### Autoregressive Failure
LLMs predict the next token based on ALL context. When context contains:
- Unrelated information
- Failed attempts
- Mixed concerns
The model can "spiral" into wrong territory, generating increasingly off-base responses.
### The Gutter Metaphor
> "If the bowling ball is in the gutter, there's no saving it."
Once context is polluted with failed attempts or mixed concerns, the model will keep referencing that pollution. Starting fresh is often faster than trying to correct course.
## Context Health Indicators
### 🟢 Healthy Context
- Single focused task
- Relevant files only
- Clear progress
- Under 60% capacity
### 🟡 Warning Signs
- Multiple unrelated topics discussed
- Several failed attempts in history
- Approaching 80% capacity
- Repeated similar errors
### 🔴 Critical / Gutter
- Mixed concerns throughout
- Circular failure patterns
- Over 90% capacity
- Model suggesting irrelevant solutions
## Best Practices
### 1. One Task Per Context
Don't ask "fix the auth bug AND add the new feature". Do them in separate conversations.
### 2. Fresh Start on Topic Change
Finished auth? Start a new conversation for the next feature.
### 3. Don't Redline
Stay under 80% of context capacity. Quality degrades as you approach limits.
### 4. Recognize the Gutter
If you're seeing:
- Same error 3+ times
- Solutions that don't match the problem
- Circular suggestions
Start fresh. Your progress is in the files.
### 5. State in Files, Not Context
Write progress to files. The next conversation can read them. Context is ephemeral; files are permanent.
## Ralph's Approach
The original Ralph technique (`while :; do cat PROMPT.md | agent ; done`) naturally implements these principles:
1. **Each iteration is a fresh process** - Context is freed
2. **State persists in files** - Progress survives context resets
3. **Same prompt each time** - Focused, single-task context
4. **Failures inform guardrails** - Learning without context pollution
This Cursor implementation aims to bring these benefits while working within Cursor's session model.
## Measuring Context
Rough estimates:
- 1 token ≈ 4 characters
- Average code file: 500-2000 tokens
- Large file: 5000+ tokens
- Conversation history: 100-500 tokens per exchange
Track allocations in `.ralph/context-log.md` to stay aware.
## When to Start Fresh
**Definitely start fresh when:**
- Switching to unrelated task
- Context over 90% full
- Same error 3+ times
- Model suggestions are off-topic
**Consider starting fresh when:**
- Context over 70% full
- Significant topic shift within task
- Feeling "stuck"
- Multiple failed approaches in history
+174
View File
@@ -0,0 +1,174 @@
# Guardrails Reference ("Signs")
This document explains how to create and use guardrails in Ralph.
## The Signs Metaphor
From Geoffrey Huntley:
> "Ralph is very good at making playgrounds, but he comes home bruised because he fell off the slide, so one then tunes Ralph by adding a sign next to the slide saying 'SLIDE DOWN, DON'T JUMP, LOOK AROUND,' and Ralph is more likely to look and see the sign."
Signs are explicit instructions added to prevent known failure modes.
## Anatomy of a Sign
```markdown
### Sign: [Descriptive Name]
- **Trigger**: When this situation occurs
- **Instruction**: What to do instead
- **Added after**: When/why this was added
- **Example**: Concrete example if helpful
```
## Types of Signs
### 1. Preventive Signs
Stop problems before they happen:
```markdown
### Sign: Validate Before Trust
- **Trigger**: When receiving external input
- **Instruction**: Always validate and sanitize input before using it
- **Added after**: Iteration 3 - SQL injection vulnerability
```
### 2. Corrective Signs
Fix recurring mistakes:
```markdown
### Sign: Check Return Values
- **Trigger**: When calling functions that can fail
- **Instruction**: Always check return values and handle errors
- **Added after**: Iteration 7 - Null pointer exception
```
### 3. Process Signs
Enforce good practices:
```markdown
### Sign: Test Before Commit
- **Trigger**: Before committing changes
- **Instruction**: Run the test suite and ensure all tests pass
- **Added after**: Iteration 2 - Broken tests committed
```
### 4. Architecture Signs
Guide design decisions:
```markdown
### Sign: Single Responsibility
- **Trigger**: When a function grows beyond 50 lines
- **Instruction**: Consider splitting into smaller, focused functions
- **Added after**: Iteration 12 - Unmaintainable god function
```
## When to Add Signs
Add a sign when:
1. **The same mistake happens twice** - Once is learning, twice is a pattern
2. **A subtle bug is found** - Prevent future occurrences
3. **A best practice is violated** - Reinforce good habits
4. **Context-specific knowledge is needed** - Project-specific conventions
## Sign Lifecycle
### Creation
```markdown
### Sign: [New Sign]
- **Trigger**: [When it applies]
- **Instruction**: [What to do]
- **Added after**: Iteration N - [What happened]
```
### Refinement
If a sign isn't working:
- Make the trigger more specific
- Make the instruction clearer
- Add examples
### Retirement
Signs can be removed when:
- The underlying issue is fixed at a deeper level
- The sign is no longer relevant
- The sign is causing more problems than it solves
## Example Signs Library
### Security
```markdown
### Sign: Sanitize All Input
- **Trigger**: Any user-provided data
- **Instruction**: Use parameterized queries, escape HTML, validate types
- **Example**: `db.query("SELECT * FROM users WHERE id = ?", [userId])`
```
### Error Handling
```markdown
### Sign: Graceful Degradation
- **Trigger**: External service calls
- **Instruction**: Always have a fallback for when services are unavailable
- **Example**: Cache results, provide default values, show friendly errors
```
### Testing
```markdown
### Sign: Test the Unhappy Path
- **Trigger**: Writing tests for new functionality
- **Instruction**: Include tests for error cases, edge cases, and invalid input
```
### Code Quality
```markdown
### Sign: Explain Why, Not What
- **Trigger**: Writing comments
- **Instruction**: Comments should explain reasoning, not describe obvious code
- **Example**: `// Using retry because API is flaky under load` not `// Call the API`
```
## Automatic Sign Detection
The Ralph hooks can automatically detect some patterns and suggest signs:
- **Thrashing**: Same file edited many times → "Step back and reconsider"
- **Repeated errors**: Same test failing → "Check the test assumptions"
- **Large changes**: Big diffs → "Consider smaller increments"
These are logged in `.ralph/failures.md` and can be promoted to guardrails.
## Using Signs Effectively
### Do
- Keep signs concise and actionable
- Include concrete examples
- Update signs when they're not working
- Remove outdated signs
### Don't
- Add signs for every minor issue
- Make signs too vague ("be careful")
- Ignore signs that keep triggering
- Let the guardrails file become overwhelming
## Integration with Ralph
Signs are:
1. Stored in `.ralph/guardrails.md`
2. Injected into context at the start of each iteration
3. Referenced when relevant situations arise
4. Updated based on observed failures
The goal is a self-improving system where each failure makes future iterations smarter.
+142
View File
@@ -0,0 +1,142 @@
{
"version": 1,
"project": "Mobile App File Size Refactor",
"overview": "Refactor the existing React Native (Expo) mobile app to keep most files within ~300-400 lines by splitting into smaller modules without changing functionality.",
"goals": [
"Reduce oversized files to a target range of ~300-400 lines where practical",
"Improve maintainability by splitting files into clearer layers (ui/components, hooks, services, utils)",
"Preserve existing behavior and UI"
],
"nonGoals": [
"No functional changes",
"No UI changes",
"No new product features"
],
"successMetrics": [
"All files are within target size or documented as justified exceptions",
"Refactor passes existing tests, lint, and typecheck"
],
"openQuestions": [
"Which specific files currently exceed 400 lines and need priority refactors?",
"Are there any existing architectural constraints that limit splitting (e.g., circular deps)?"
],
"stack": {
"framework": "React Native (Expo)",
"hosting": "App stores via Expo/EAS",
"database": "Existing app data layer (no changes)",
"auth": "Existing app auth (no changes)"
},
"routes": [
{
"path": "(existing)",
"name": "App Routes",
"purpose": "No route changes; refactor only"
}
],
"uiNotes": [
"No UI changes; keep visual output identical",
"Split view logic into smaller components without altering layout"
],
"dataModel": [
{
"entity": "N/A",
"fields": []
}
],
"importFormat": {
"description": "Not applicable",
"example": {}
},
"rules": [
"Target file size is ~300-400 lines; exceptions must include justification",
"Refactor should follow layer-based split: ui/components, hooks, services, utils",
"No behavior changes; identical inputs must yield identical outputs"
],
"qualityGates": [
"npm test",
"npm run lint",
"npm run typecheck"
],
"stories": [
{
"id": "US-001",
"title": "Inventory oversized files and refactor plan",
"status": "done",
"dependsOn": [],
"description": "As a developer, I want an inventory of oversized files and a planned split strategy so refactors are systematic and safe.",
"acceptanceCriteria": [
"Produce a list of files > 400 lines with line counts",
"For each file, define target split into layers (ui/components, hooks, services, utils)",
"Example: a 900-line screen file -> screen container + UI subcomponents + hooks + service module",
"Negative case: a file under 300 lines is not split unless a clear modularization need is documented"
],
"startedAt": "2026-01-25T20:40:12.238126+00:00",
"completedAt": "2026-01-25T20:47:05.362525+00:00",
"updatedAt": "2026-01-25T20:47:05.362425+00:00"
},
{
"id": "US-002",
"title": "Refactor screen-level files into layer-based modules",
"status": "in_progress",
"dependsOn": [
"US-001"
],
"description": "As a developer, I want oversized screen files split into smaller modules so each file stays around 300-400 lines.",
"acceptanceCriteria": [
"Split screen files into container + UI components + hooks + services as needed",
"Example: screen logic moves into `hooks/use<Screen>.ts` and data calls into `services/<screen>.ts`",
"Negative case: moving a function that creates circular dependencies is rejected and alternative split is used",
"All screens render identically before and after refactor"
],
"startedAt": "2026-01-25T20:47:07.436262+00:00",
"completedAt": null,
"updatedAt": "2026-01-25T20:47:07.436370+00:00"
},
{
"id": "US-003",
"title": "Refactor oversized shared components",
"status": "open",
"dependsOn": [
"US-001"
],
"description": "As a developer, I want oversized shared components split into smaller pieces to improve clarity and reusability.",
"acceptanceCriteria": [
"Split shared UI components into smaller subcomponents and helper modules",
"Example: extract nested UI blocks into `components/<Component>/SubComponent.tsx`",
"Negative case: do not extract one-off UI fragments that would reduce cohesion",
"No change to public component API unless required for split"
]
},
{
"id": "US-004",
"title": "Refactor oversized hooks/services/utils",
"status": "open",
"dependsOn": [
"US-001"
],
"description": "As a developer, I want oversized logic files split into smaller, focused modules for readability.",
"acceptanceCriteria": [
"Split large hooks into smaller hooks or helper functions in utils",
"Example: a 500-line hook becomes a main hook + `utils/<hook>-helpers.ts`",
"Negative case: do not split logic in a way that increases public API surface without justification",
"All previous exports remain available or are re-exported from the original entry point"
]
},
{
"id": "US-005",
"title": "Validate refactor quality gates",
"status": "open",
"dependsOn": [
"US-002",
"US-003",
"US-004"
],
"description": "As a developer, I want automated checks to pass to ensure the refactor preserved behavior.",
"acceptanceCriteria": [
"Run `npm test`, `npm run lint`, and `npm run typecheck` successfully",
"Example: all tests pass without modifications to test expectations",
"Negative case: if a check fails, refactor is incomplete and must be fixed before completion"
]
}
]
}